Multi-module projects

In a project split into several Gradle modules, each feature can own its dependency-injection module, and the application assembles them.

Running the processor in every Gradle module

KSP only processes the sources of the Gradle module it is applied to. Every Gradle module that declares a @DIModule or a @DIFactory therefore needs the processor, as shown in Setup.

Gradle modules that only use modules declared elsewhere only need inara-runtime.

One module per feature

Each feature exposes a public module interface, describing what it offers to the rest of the application:

feature-users/src/commonMain/kotlin/UsersModule.kt
@DIModule
interface UsersModule {
    fun usersRepository(): UsersRepository
}

fun usersModule(httpClient: HttpClient): UsersModule = UsersModule( (1)
    usersRepository = Binding.single { HttpUsersRepository(httpClient) },
)
1 An optional convenience function, which keeps the feature’s wiring inside the feature.
feature-billing/src/commonMain/kotlin/BillingModule.kt
@DIModule
interface BillingModule {
    fun invoices(): InvoiceService
}

Assembling the application

The application module extends the feature modules, and composes their built instances:

app/src/commonMain/kotlin/AppModule.kt
@DIModule(composed = true)
interface AppModule : UsersModule, BillingModule {
    val appName: String
}

val httpClient = HttpClient()

val app = AppModule(
    usersModule = usersModule(httpClient),
    billingModule = BillingModule(invoices = Binding.single { InvoiceService(httpClient) }),
    appName = Binding.value("My App"),
)

Parent modules generated by Inara keep their bindings as they are, even when they were generated in another Gradle module: a single that was already built in a feature module is not built again in the application module.

An AppModule is a UsersModule and a BillingModule, so it can be handed to any code of either feature.

Keeping wiring private

A module interface that is internal cannot be seen from other Gradle modules, and neither can its builder. Use it for wiring that is an implementation detail of a feature:

@DIModule
internal interface UsersInternalModule {
    fun cache(): UsersCache
}

A @DIFactory object can only be public when every type of its generated signature is public, and a type that is internal to another Gradle module can never be passed through. See the visibility rules of factories.