Skip to main content
Version: 4.2

Android Library Integration

This page gives complete Koin setups for libraries most Android apps use: Retrofit with OkHttp, Room and a JSON serializer, then a layered app structure. Library classes come from builders, so they are declared with provider functions. How that works (create(::function), annotated functions) is explained in Definitions; this page only shows the Android setups.

Retrofit and OkHttp​

interface ApiService {
@GET("users/{id}")
suspend fun getUser(@Path("id") id: String): User
}
import org.koin.plugin.module.dsl.create

fun okHttpClient(): OkHttpClient =
OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.addInterceptor(HttpLoggingInterceptor().apply {
level = if (BuildConfig.DEBUG) HttpLoggingInterceptor.Level.BODY else HttpLoggingInterceptor.Level.NONE
})
.build()

fun retrofit(client: OkHttpClient, json: Json): Retrofit =
Retrofit.Builder()
.baseUrl(BuildConfig.API_URL)
.client(client)
.addConverterFactory(json.asConverterFactory("application/json".toMediaType()))
.build()

fun apiService(retrofit: Retrofit): ApiService = retrofit.create(ApiService::class.java)

val networkModule = module {
single { create(::okHttpClient) }
single { create(::retrofit) }
single { create(::apiService) }
}

Declare OkHttpClient and Retrofit as single: each one holds a connection pool and thread pools, and creating several wastes them. BuildConfig.API_URL is a buildConfigField you define per build type. The Json instance comes from the next section.

JSON serialization​

fun json(): Json = Json {
ignoreUnknownKeys = true
explicitNulls = false
}

val serializationModule = module {
single { create(::json) }
}

The same pattern works for Gson (single { create(::gson) } with fun gson(): Gson = GsonBuilder().create()) or Moshi. One shared instance keeps the serializer configuration in one place.

Room​

@Database(entities = [User::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
}
fun appDatabase(context: Context): AppDatabase =
Room.databaseBuilder(context, AppDatabase::class.java, "app-database").build()

fun userDao(database: AppDatabase): UserDao = database.userDao()

val databaseModule = module {
single { create(::appDatabase) }
single { create(::userDao) }
}

Declare the database as a single: a RoomDatabase instance is costly, and the Room documentation asks for one instance per database in a process. Context here is the Application, given by androidContext() at start (see Starting Koin on Android). DAOs are cheap and Room returns the same instance for each call, so single or factory both work.

WorkManager​

Workers get their dependencies through a dedicated WorkerFactory. Setup and the worker definition are in WorkManager.

Layered architecture​

A typical Android app has a data layer (network, database, repositories), a domain layer (use cases) and a UI layer (ViewModels). Each layer uses the definition type that matches its lifetime:

LayerClassesDefinition
DataOkHttpClient, Retrofit, AppDatabase, data sources, repositoriessingle
DomainUse cases, with no statefactory
UIViewModelsviewModel
val dataModule = module {
includes(networkModule, databaseModule, serializationModule)
single<UserRemoteDataSource>()
single<UserLocalDataSource>()
single<UserRepositoryImpl>() bind UserRepository::class
}

val domainModule = module {
factory<GetUserUseCase>()
}

val uiModule = module {
viewModel<UserViewModel>()
}

The ViewModel depends on the use case, the use case on the UserRepository interface, and only the data module knows UserRepositoryImpl. To swap the data source in tests, override the UserRepository binding (see Unit Testing). When each layer lives in its own Gradle module, see Multi-Module Apps.