Now In Android with Koin — part 4

Arnaud Giuliani
ProAndroidDev
Published in
6 min readApr 4, 2023

--

Koined from original banner — https://github.com/android/nowinandroid

Now In Android is an open-source Android application that covers Modern Android Development best practices. The project is maintained by the Google Android team.

I propose to continue our tour with the version built with the Koin dependency injection framework. This is a good time to refresh practices, from standard components structure to more advanced cases.

For this article, I propose now to use Koin Annotations instead of Koin DSL to configure all the app’s components injection. This is interesting to see how much it can improve the experience in terms of writing.

Prepare yourself, we have many things to see together 👍 🚀

This version uses Koin Annotations 1.2

Previously in part 3, we saw how to setup and use Koin annotations with Koin dependency injection framework. It’s really easy as the Koin annotation processor can detect many cases, and generate your dependency injection configuration really quickly. It’s now time to dive into more components of the NowInAndroid project.

You will have the reference for all the content to browse the code. Also, everything is available online on the Github repo: https://github.com/InsertKoinIO/nowinandroid/

Common Data Layers

Following the part 2 article, we will now go through the common core components that will be used by the features later. But this time, the configuration will be done with annotations.

As a reminder, the Nia app is developed using Jetpack Compose and uses repository & use-case components:

  • Repository to access data (network, database …)
  • Usecase to handle business logic

The module that is gathering all those common components is theDataKoinModule.kt module:

@Module(includes = [DaosKoinModule::class, DataStoreKoinModule::class, NetworkKoinModule::class, DispatchersKoinModule::class, DataUtilModule::class])
@ComponentScan("com.google.samples.apps.nowinandroid.core.data.repository")
class DataKoinModule

This module is making several things:

  • scans all repository classes defined in @ComponentScan
  • includes modules that are declaring sub-data layers components

Each repository class is simply tagged with @Single annotation like this:

@Single
class OfflineFirstAuthorsRepository(
private val authorDao: AuthorDao,
private val network: NiaNetworkDataSource,
)

You can find all the repository classes in the source code data package.

Database Storage

For the database storage layer, we need to declare our Room database instance via a function using the Room API builder like this:

@Module
class DatabaseKoinModule {

@Single
fun database(context: Context) =
Room.databaseBuilder(context, NiaDatabase::class.java, "nia-database")
.build()
}

The context parameter here is the Android Context instance from Koin.

In a second module, we can reuse our NiaDatabase instance below in DAOs:

@Module(includes = [DatabaseKoinModule::class])
class DaosKoinModule {

@Single
fun authorDao(niaDatabase: NiaDatabase) = niaDatabase.authorDao()

@Single
fun topicDao(niaDatabase: NiaDatabase) = niaDatabase.topicDao()

@Single
fun newsResourcesDao(niaDatabase: NiaDatabase) = niaDatabase.newsResourceDao()
}

That’s it! Our Database Layer is ready to be injected.

Datasource Components — Datastore & Networking

This layer defines Datasources which are components that abstract the calls to different sources of data. E.g: remote web service, local data storage, and so on. Therefore, the UI doesn’t need to know where the data comes from. It just calls the interface defined here.

We are defining severasl kind of usages with NiaNetworkDatasource:

interface NiaNetworkDataSource {
suspend fun getTopics(ids: List<String>? = null): List<NetworkTopic>

suspend fun getAuthors(ids: List<String>? = null): List<NetworkAuthor>

suspend fun getNewsResources(ids: List<String>? = null): List<NetworkNewsResource>

suspend fun getTopicChangeList(after: Int? = null): List<NetworkChangeList>

suspend fun getAuthorChangeList(after: Int? = null): List<NetworkChangeList>

suspend fun getNewsResourceChangeList(after: Int? = null): List<NetworkChangeList>
}

First, we need to declare a default Coroutine dispatcher directly in a module:

@Module
class DispatchersKoinModule{

@Single
fun dispatcher() = Dispatchers.IO
}

In a test environment, you simply have to redefine a CoroutineDispatcher type to specify your needed one. Just add the new definition and it will override the existing one.

The network module is declaring NiaNetworkDatasource, and is organized into 2 flavors:

  • demo — with local data
  • prod — for online data

The NetworkKoinModule includes the right flavour implementation:

@Module(includes = [FlavoredNetworkKoinModule::class])
class NetworkKoinModule {

@Single
fun json() = Json { ignoreUnknownKeys = true }
}
Demo flavor in Network module

The demo flavour uses Datastore API and Protobuff API is used to store local data to display as an offline-first architecture.

@Module(includes = [DispatchersKoinModule::class])
@ComponentScan("com.google.samples.apps.nowinandroid.core.network.fake")
class FlavoredNetworkKoinModule{

@Single
fun assetManager(context: Context) = FakeAssetManager(context.assets::open)
}

Below is the demo datasource implementation declared as a singleton:

@Single
class FakeNiaNetworkDataSource(
private val ioDispatcher: CoroutineDispatcher,
private val networkJson: Json,
private val assets: FakeAssetManager = JvmUnitTestFakeAssetManager,
) : NiaNetworkDataSource

The online version is declared with the following module:

@Module(includes = [DispatchersKoinModule::class])
@ComponentScan("com.google.samples.apps.nowinandroid.core.network.retrofit")
class FlavoredNetworkKoinModule

This module will scan the Retrofit implementation:

@Single
class RetrofitNiaNetwork(
networkJson: Json
) : NiaNetworkDataSource

One last part is about Datastore persistence API, used to declare local data storage. Check the Datastore Persistence Module that is declaring the required components for NiaPreferencesDatasource.

Domain & Features Modules

Before running our features, we have some use-case components using the DataKoinModule. Those use-cases components are reusable business logic components. They are defined from the DomainKoinModule:

@Module(includes = [DataKoinModule::class])
@ComponentScan
class DomainKoinModule

You can note that we don’t specify what package to scan. This means that the module will scan in the current package and sub-packages for annotated components:

Each usecase component is declared with @Factory annotation. This asks Koin to create a new instance each time we need it.

@Factory
class GetFollowableTopicsStreamUseCase(
private val topicsRepository: TopicsRepository,
private val userDataRepository: UserDataRepository
)

Why not a singleton instance? Because those usecase components will be used with a ViewModel, following the Android lifecycle. Making them as a singleton, we would take a risk to have references to a ViewModel that are destroyed by the application.

Finally, we are ready to use all of this in our Feature module. Each will then include DomainKoinModule or DataKoinModule to benefit from the common components:

@Module(includes = [DomainKoinModule::class,StringDecoderKoinModule::class])
@ComponentScan("com.google.samples.apps.nowinandroid.feature.author")
class AuthorKoinModule

By scanning the right package in our module, we will be able to declare our ViewModel instances like this:

@KoinViewModel
class AuthorViewModel(
savedStateHandle: SavedStateHandle,
stringDecoder: StringDecoder,
private val userDataRepository: UserDataRepository,
authorsRepository: AuthorsRepository,
getSaveableNewsResourcesStream: GetSaveableNewsResourcesStreamUseCase
) : ViewModel()

Sync Worker — Offline data sync with WorkManager

Finally, we need to declare our SyncWorker components, to asynchronously prepare offline content. This consists of a module:

@Module
@ComponentScan
class SyncWorkerKoinModule

The following definitions will be scanned by the module.

@Single
class WorkManagerSyncStatusMonitor(
context: Context
) : SyncStatusMonitor

And the SyncWorker component declared with @KoinWorker annotation. This will generate the equivalent of worker { } DSL:

@KoinWorker
class SyncWorker (
private val appContext: Context,
workerParams: WorkerParameters,
private val niaPreferences: NiaPreferencesDataSource,
private val topicRepository: TopicsRepository,
private val newsRepository: NewsRepository,
private val authorsRepository: AuthorsRepository,
private val ioDispatcher: CoroutineDispatcher,
) : CoroutineWorker(appContext, workerParams), Synchronizer

SyncWorker will be declared with Workmanager Koin factory. This one has to be activated at the start like this:

Koin start in NiaApplication class

Koin Annotations — Cheat Sheet

Hope you enjoyed the walkthrough NowInAndroid application with Koin dependency injection and annotations. You will find below the last cheat sheet we’ve made.

--

--

Creator/Maintainer of Koin framework -- Kotlin Google Dev Expert -- Cofounder @ Kotzilla.io