First time user of kotlin-inject-anvil, it’s been ...
# kotlin-inject
a
First time user of kotlin-inject-anvil, it’s been awesome so far! I do have a question: what is the correct approach regarding multi-module projects?
Let’s say I have the following dependency chain between modules:
:network:http
:network:graphql
:data
:app
With the following components:
Copy code
// :network:http
@ContributesTo(AppScope::class)
@SingleIn(AppScope::class)
interface NetworkHttpComponent {
    @Provides
    fun provideHttpClient(): HttpClient = HttpClient()
}
// :network:graphql
@ContributesTo(AppScope::class)
@SingleIn(AppScope::class)
interface CoreNetworkGraphqlComponent {
    @Provides
    fun graphqlClient(httpClient: HttpClient): ApolloClient =
        ApolloClient.Builder()
            .serverUrl("<http://example.com/graphql>")
            .ktorClient(httpClient)
            .build()
}
// :data
interface AuthRepository {
    fun getWelcomeMessage(): Flow<String>
}
@Inject
@SingleIn(AppScope::class)
@ContributesBinding(AppScope::class)
class DefaultAuthRepository(val graphqlClient: ApolloClient) : AuthRepository {
    override fun getWelcomeMessage(): Flow<String> = {…}
}
// :app
@MergeComponent(AppScope::class)
@SingleIn(AppScope::class)
abstract class AppComponent() {…}
Since
:app
uses
@MergeComponent
, it only works if every module depend on eachother via
api()
instead of
implementation()
. So that’s definitely not correct. Looking at the generated code in
:app
, it makes sense to need
api()
right now because every single dependency appears in it:
Copy code
public class InjectKotlinInjectAppComponent(
  appDelegate: Application,
) : KotlinInjectAppComponent(appDelegate),
    ScopedComponent {
  override val _scoped: LazyMap = LazyMap()

  override val viewModelFactory: ViewModelProvider.Factory
    get() = _scoped.get("@ForScope(scope=AppScope)" + "androidx.lifecycle.ViewModelProvider.Factory") {
      provideViewModelFactory(
        viewModelFactory = KotlinInjectViewModelFactory(
          viewModelMap = mapOf(
            provideOnboardingViewModel(
              factory = {
                OnboardingViewModel(
                  authRepository = provideDefaultAuthRepositoryAuthRepository(
                    defaultAuthRepository = _scoped.get("com.example.`data`.DefaultAuthRepository") {
                      DefaultAuthRepository(
                        graphqlClient = graphqlClient(
                          httpClient = provideHttpClient()
                        )
                      // ...
I took a look at the sample app, but since there’s only one
:lib
module it does not help here. There’s probably something simple that I am missing, but since I don’t know much about Anvil in the first place I’m at loss. Do I need other scopes than
AppScope
? Any help will be appreciated!
b
This is how I've been using it. Even at Square we included every module in our app module with api rather than implementation.
I have mine split into two primary types though, impl modules and public modules. Impl includes the anvil bindings and public are just the interfaces that I include in whatever modules that need those dependencies
I'm loosely following this in addition to what I learned at Square https://github.com/android/nowinandroid/blob/main/docs%2FModularizationLearningJourney.md
a
That’s super insightful, thank you. Indeed, I was wondering how you limit the api surface of your modules then, and api/impl split might be the way.
I heavily inspire myself from NowInAndroid too (but multiplatform), and since you don't necessarily have this issue with Dagger/Hilt I was wondering if I was missing something from understanding Anvil. Thanks 🙂
b
I need to write an article about what I learned at Square but my setup is even more robust. A given feature module might look like this
Copy code
:feature:feature-name:public
:feature:feature-name:impl
:feature:feature-name:impl-robots
:feature:feature-name:test
:feature:feature-name:demo
:feature:feature-name:fake
public
can be included in other feature modules and is just the exposed api for other modules to use. No anvil here and it’s usually just plain old kotlin objects. public only imports other public
impl
provides the actual implementations for public
impl-robots
test robots for app, demo apps, and whatever other modules want to use functionality
test
testing utils for other modules (not the tests themselves)
demo
an isolated demo app useful for just testing a specific features. I might have just one feature tested through this. Having a demo app means I don’t need to do a whole bunch of other things to get to the screen I want to test. I can just load it up directly in the demo app with whatever conditions I want
fake
fakes that can be used for testing. goes great in demo apps or the main app tests. These are the fake implementations of the public module There’s a few more we used but for a project that isn’t 4500 modules or whatever Square is at now, a few will suffice. A lot of this might be unnecessary depending on the setup, but having a
public
and
impl
module at minimum has saved me a ton of headache.
The
impl
modules are all included in my top level app and I just need to include
public
everywhere.
a
Thank you for taking the time to write this up—I'm learning a lot. It definitely makes sense for large-scale projects, and it might even be beneficial for a smaller project like mine. I suppose build times benefit from this separation either way, so it might be worth the extra setup anyway.
b
With having all of these modules it would probably be good to setup some gradle convention plugins. I ended up making my own gradle extensions and gradle plugins which was very very difficult to do with the available information on the internet. It’s a little off topic for this thread but you can dm me if you want more info.
🙏 1
r
Hot off the press: https://amzn.github.io/app-platform/ take a look at the sample too. It covers this module structure (I’m the author of the module structure at Square and brought he same to Amazon now) and uses
kotlin-inject-anvil
. The sample in this project is larger and shows all of this in practice.
❤️ 2
a
That’s truly a blessing—I skimmed through it, and I couldn’t have imagined anything better. Thank you too! (even "App Platform" it self, loved reading the comparison with Circuit but that's off-topic too)
So turned out that the App Platform sample is exactly what I needed to better understand why splitting modules into public/imp is truly beneficial for DI. One thing I cannot wrap my head around though is “that no other module but the final application module is allowed to depend on
:impl
modules”. Why is the
:app
module allowed to have
:impl
at all? Is it solely for generating the object graph? Can’t sub object graphs be generated into submodules and then merged? Why is it not needed in Dagger/Hilt in NowInAndroid for example?
b
Yeah they will get merged but if an impl module isn't included in the app module then nothing is depending on it... It's the same if I just make any other module and don't include it anywhere. There would be unused source code. If I don't include a demo module in the app module (don't do this btw) then it just remains a standalone demo app. One other thing. Impl modules can also be included in demo apps. Usually I use fakes but there's nothing wrong with impl
🙏 1
a
Okay it clicked! And it actually makes perfect sense. I was thinking too much about
:impl
as transitive somehow (which is wrong), and not the fact that yeah they would actually be leftovers otherwise