Alexandre Gressier
04/17/2025, 1:02 PMAlexandre Gressier
04/17/2025, 1:02 PM:network:http ← :network:graphql ← :data ← :app
With the following components:
// :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:
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!blakelee
04/17/2025, 3:34 PMblakelee
04/17/2025, 3:36 PMblakelee
04/17/2025, 3:38 PMAlexandre Gressier
04/17/2025, 3:39 PMAlexandre Gressier
04/17/2025, 3:42 PMblakelee
04/17/2025, 3:58 PM: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.blakelee
04/17/2025, 3:58 PMimpl modules are all included in my top level app and I just need to include public everywhere.Alexandre Gressier
04/17/2025, 4:05 PMblakelee
04/17/2025, 4:06 PMralf
04/17/2025, 4:16 PMkotlin-inject-anvil. The sample in this project is larger and shows all of this in practice.Alexandre Gressier
04/17/2025, 4:22 PMAlexandre Gressier
04/18/2025, 6:55 PM: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?blakelee
04/18/2025, 7:25 PMAlexandre Gressier
04/18/2025, 7:29 PM:impl as transitive somehow (which is wrong), and not the fact that yeah they would actually be leftovers otherwiseElijah Dangerfield
11/13/2025, 9:21 PMkotlin-inject-anvil. Each feature is split into api and impl.
I am having a problem tho. I have 2 different feature impl modules binding into a set. The app module is supposed to grab that set and use it to build the nav graph.
That works for Android but breaks for iOS 😕
No idea why but the KotlinInjectAppComponentMerged is empty for iOSblakelee
11/13/2025, 9:24 PM/**
* The final iOS app component. Note that [uiApplication] is an iOS specific type and classes living
* in the iOS source folder can therefore inject [UIApplication].
*
* [rootScopeProvider] is provided in the [IosAppComponent] and can be injected.
*/
@MergeComponent(AppScope::class)
@SingleIn(AppScope::class)
abstract class IosAppComponent(
@get:Provides val uiApplication: UIApplication,
@get:Provides val rootScopeProvider: RootScopeProvider
) : AppComponent {
abstract val deepLinkHandler: IosDeepLinkHandler
}
/**
* Factory function to instantiate the component. This is necessary, because `iosMain` is a shared
* source folder and generated components live in the x64, arm64 and simulatorArm64 source folders.
*/
@Suppress("NO_ACTUAL_FOR_EXPECT")
@MergeComponent.CreateComponent
expect fun KClass<IosAppComponent>.createComponent(
uiApplication: UIApplication,
rootScopeProvider: RootScopeProvider,
): IosAppComponent
/** This function is called from Swift to create a new component instance. */
@Suppress("unused")
fun createIosAppComponent(
application: UIApplication,
rootScopeProvider: RootScopeProvider,
): AppComponent {
return IosAppComponent::class.createComponent(application, rootScopeProvider)
}
fun getIosAppComponent(rootScopeProvider: RootScopeProvider): IosAppComponent {
return rootScopeProvider.rootScope.diComponent()
}Elijah Dangerfield
11/13/2025, 9:27 PMcommonMain . Im pretty new to KMP but that should be okay right?
EX:
app
-> commonMain
---> MyComponent
feature1
-> commonMain
---> MyImpl1
feature2
-> commonMain
---> MyImpl2
So each feature is binding into the set in its commonMain and the component I have is created in commonMain tooElijah Dangerfield
11/13/2025, 9:29 PMblakelee
11/13/2025, 9:30 PMabstract class IosAppComponent : AppComponent
I know there’s been some updates since then, but the docs were slightly wrong but there was a workaroundElijah Dangerfield
11/13/2025, 9:30 PMElijah Dangerfield
11/13/2025, 10:01 PMCannot find an @Inject constructor or provider for: Set<com.dangerfield.ante.libraries.navigation.FeatureEntryPoint>
commonMain:
interface AppComponent {
val featureEntryPoints: Set<FeatureEntryPoint>
}
expect fun createAppComponent(): AppComponent
iosMain:
@MergeComponent(AppScope::class)
@SingleIn(AppScope::class)
abstract class IosAppComponent : AppComponent
@Suppress("NO_ACTUAL_FOR_EXPECT")
@MergeComponent.CreateComponent
expect fun KClass<IosAppComponent>.createiOSComponent(): IosAppComponent
actual fun createAppComponent(): AppComponent {
return IosAppComponent::class.createiOSComponent()
}
androidMain:
@MergeComponent(AppScope::class)
@SingleIn(AppScope::class)
abstract class AndroidAppComponent : AppComponent
actual fun createAppComponent(): AppComponent {
return AndroidAppComponent::class.create()
}Elijah Dangerfield
11/13/2025, 10:03 PMInjectKotlinInjectIosAppComponent doesnt have a override val featureEntryPoints like the Android one does:blakelee
11/13/2025, 10:04 PMElijah Dangerfield
11/13/2025, 10:04 PMblakelee
11/13/2025, 10:07 PMblakelee
11/13/2025, 10:09 PM@SingleIn(AppScope::class)
interface AppComponent {
/** All [Scoped] instances part of the app scope. */
@ForScope(AppScope::class)
val appScopedInstances: Set<Scoped>
@Provides
@IntoSet
@ForScope(AppScope::class)
fun provideEmptyScoped(): Scoped = Scoped.NO_OP
}Elijah Dangerfield
11/13/2025, 10:17 PMElijah Dangerfield
11/13/2025, 10:17 PMblakelee
11/13/2025, 10:17 PM