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 👍 🚀
⚠️ The version used below is not yet public. @KoinWorker is coming soon in next release
This article series covers several parts:
Part 1 — Koin setup, application verification, and a first module tour
Part 2 — Common Modules components and feature modules
Part 3 — Get started with Koin Annotations
Part 4 — Core & Features Components with Koin Annotations
… more to come 🙂
Features picture from https://github.com/android/nowinandroid
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> }
Job Offers
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 aViewModel
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.
This article was previously published on proandroiddev.com