Download as pdf or txt
Download as pdf or txt
You are on page 1of 34

Koined from original banner — https://github.

com/android/nowinandroid

Modern Android Development


with Koin
The Simple Dependency Injection
Solution
Abstract
The "Now in Android" app (Nia), developed by the Google Android team, serves as a showcase for
best practices in Android app development. It models Modern Android Development best
practices.

As you may be aware, Nia is a fully functional Android app built with Kotlin and Jetpack Compose.
One of the key aspects of the app is its efficient use of dependency injection (DI), which we propose
can be implemented using the Koin framework.

In this white paper, we delve into a world where we have a version of the Nia app built using Koin
for DI.

We will first explore the benefits of using Koin for DI in Android apps and then take a step-by-step
process of setting up and configuring Koin in the Nia app.
The Nia App: Setting the Standard for Best
Practices in Android Development
The app aims to provide a comprehensive and engaging user experience, offering a wide
range of features and functionalities that exemplify best practices in Android app design,
architecture, and performance optimization.

From its sleek and intuitive user interface to its robust and scalable architecture, the Nia
app sets a high standard for quality and innovation in the Android development
community.

Features picture from https://github.com/android/nowinandroid

As a showcase for best practices, the Nia app is an invaluable resource for developers,
organizations, and stakeholders in the Android app development ecosystem. It serves as
a source of inspiration and learning, providing real-world examples of how to implement
industry-standard practices in Android app development.

The importance of the Nia app cannot be overstated. It offers insights into modern
architecture and design patterns, such as MVVM, or Clean Architecture, and
demonstrates how these patterns can be effectively applied in an Android app. It also
showcases the utilization of popular libraries and technologies, such as Room, and Glide,
and provides practical examples of how these tools can enhance the functionality and
performance of an Android app.
The Simple Power of Koin in Android
Development
The Nia app serves as a prime example of how Koin can be used as a powerful and flexible
DI framework in Android app development. We demonstrate how Koin can be integrated
into an app's architecture to manage dependencies efficiently, improve code organization,
and enhance testability and modularity.

Koin, a Popular Dependency Injection Framework

DI is a fundamental concept in modern software development, enabling the separation of


concerns and promoting modular, maintainable, and testable code. In the realm of Android
app development, DI frameworks play a crucial role in managing dependencies and
facilitating loose coupling between components, leading to highly scalable and
maintainable apps.

Koin, a lightweight and pragmatic DI framework, has gained significant popularity among
Android developers due to its simplicity, flexibility, and powerful features. Developed by
the Kotzilla team and many open-source contributors, Koin has emerged as a go-to choice
for many Android developers seeking an effective and efficient DI solution.

One of the key advantages of Koin is its Kotlin-first approach, making it seamlessly
compatible with Kotlin, the officially supported language for Android app development.
Koin leverages Kotlin's expressive and concise syntax, making it intuitive and
straightforward to use, even for developers new to DI concepts.

Koin follows a declarative approach to DI, where dependencies are defined and configured
using simple and concise DSL (Domain-Specific Language) statements, eliminating the need
for complex annotations or configurations. This approach promotes clean and readable
code, reducing boilerplate and improving code maintainability.
Koin offers a wide range of features that make it highly suitable for Android app
development. It supports both constructor and property injection, allowing developers to
choose the most appropriate approach based on their specific use cases. Koin also provides
support for qualified dependencies, making it easy to differentiate between multiple
implementations of the same interface or class.

Koin's modular and extensible architecture allows developers to define and organize their
dependencies in a fine-grained manner, enabling efficient management of complex
dependency graphs. It also offers seamless integration with popular Android libraries, such as
Room, & ViewModel, making it a natural fit for modern Android app development.

Another notable feature of Koin is its robust support for unit testing. Koin allows for easy
mocking and substitution of dependencies during testing, facilitating effective unit testing of
Android components without the need for complex frameworks or tools.
Advantages of using Koin
Koin offers several advantages for developers seeking efficient dependency management. In
summary, these benefits include:

Simplicity: Koin's DSL approach for defining and configuring dependencies makes it easy to
integrate and use in apps. It's minimalistic syntax and straightforward API provides a low
learning curve, facilitating the quick implementation of DI in the app's architecture.

Lightweight nature: Koin is designed to have a small footprint on the app's codebase and
runtime performance. It avoids unnecessary complexities and overheads, making it suitable
for resource-constrained mobile environments, without impacting app performance and
startup times.

Flexibility: Koin offers high flexibility, supporting various scopes such as singleton, factory,
and prototype, providing fine-grained control over dependency lifecycles and behaviors.
Koin also supports the module-based organization, enabling modularization of app
dependencies and easy switching between different implementations for improved code
modularity and maintainability.

Multiplatform Ready: Koin is fully and seamlessly KotlinMultiplatform(KMM)ready, which


means it can be used to share code between Android and iOS platforms. Developers can
write code once and deploy it on multiple platforms, with KMM as the main cross-platform
technology, and Koin as the DI framework. While using KMM to develop your app, Koin
offers a fully Kotlin-centric approach, leveraging the language's features and syntax to
provide a simpler and more intuitive DI experience.

Koin Annotations: Koin annotations provide a simpler alternative to achieve similar


functionality as Dagger while offering a more concise syntax. Koin's annotations like
@Module serve similar purposes as Dagger's @Module annotation, allowing developers to
define modules and provide dependencies but with a simpler approach (automatic binding,
constructor kind detection…). Koin's approach eliminates the need for extensive boilerplate
code, resulting in a more straightforward and readable codebase. This simplification in
writing and understanding DI code is one of the advantages of using Koin over Dagger.

-50%code needed: Compared to other DI frameworks, Koin requires approximately 50%


less code to write the DI configuration. Koin's Kotlin DSL and annotations provide a concise
and intuitive way to declare components, eliminating the need for boilerplate code. This
results in a more streamlined codebase.

-50% to -75% compilation time: Koin significantly reduces compilation time, up to 75%
faster compared to Dagger. By leveraging Kotlin's features and optimizations, Koin
minimizes the impact on the compilation process. This faster compilation time improves
developer productivity, allowing for quicker iterations and a smoother development
experience.
Step-by-Step Setup and Configuration of Koin
in the Nia App
In this section, we’ll take you through a detailed four-part walkthrough of the setup
and configuration process of Koin in the Nia app

1 Koin setup, application verification, and a first module


tour
We will start by delving into the essential aspects of setting up Koin, and uncover the
process of application verification, as we take a tour through the first module.

Architecture & Modules Map


The project presents an overview of the existing modules. Please refer to the
modularization document for more details. Here is a brief overview of the module
organization:

Module organization — https://github.com/android/nowinandroid

The project consists of the following parts:

App Module: The main module and application entry point


Common modules: Contains common components such as database, repository,
network, domain, etc.
Feature modules: Implements each part/screen of the app
Sync module: Dedicated to data resyncing
Gradle Setup & Build logic for Koin
We begin by setting up the project. We utilize the following Koin dependencies:

koin-android — Android features (common Android parts)

koin-androidx-compose — features related to Jetpack Compose (feature module)

koin-androidx-workmanager — WorkManager features (common data sync module)

koin-core — pure Kotlin components (for Kotlin only common module)

koin-test — verification and testing parts

Throughout this white paper, we will be using the Koin 3.3 version. For detailed setup instructions,
please refer to the setup page

The Gradle configuration has been updated to directly use the Koin package with the established
version catalog. Check out the libs.versions.toml file for information on Koin artifacts.

The AndroidFeatureConventionPlugin file has also been updated to include koin-androidx-compose


for each feature module.

Starting Koin
We’ll start by opening the NiaApplication class, which serves as the application entry
point, and examine the onCreate method responsible for initializing Koin using the
startKoin function:

Starting Koin
We utilize the following options:

androidLogger — Enables Koin logging on an Android

androidContext— References the Android Application context

workManagerFactory— Starts WorkManager components declared in Koin

We load Koin modules using the modules() function. In this case we load the niaAppModule.

The call to Sync.initialize() initializes the WorkManager for data resynchronization.

We will explore the details of this later on.

The Nia Application Module


The first step is to create a main module that includes all other sub-modules. The
NiaAppModule.kt file declares the main Koin module as follows:

The main Koin module

The includes function is used to load a list of modules within the current module. This helps flatten
all module graphs and optimize application startup.

In the code snippet below, we declare MainActivityViewModel as a ViewModel in Koin using


the viewModelOf() function. This function directly targets the class constructor for building
the ViewModel.

The use of "::" identifies the constructor of the MainActivityViewModel class.


We recommend using the includes() function to ensure that all app modules are gathered into
the main module. In addition to organizing modules and sub-modules, the
includes function allows you to globally verify your Koin configuration. Let's take a look:

Verifying the Koin Configuration — verify() your module


One important feature introduced in Koin 3.3 is the ability to verify a Koin configuration within
milliseconds using a JUnit test. To access this testing feature, the koin-test Gradle package is
required.

How does it work?


Simply use the verify() extension function on a Koin Module. That's it! Under the hood,
this function verifies all constructor classes and crosschecks them with the Koin
configuration to ensure that a component is declared for each dependency. If a failure
occurs, the function throws a MissingKoinDefinitionException.

Let's examine the NiaAppModuleCheck.kt file:

verify() on the main Koin module

Now launch the JUnit test, and you're all set!

As you can see, we use the extraTypes parameter to list types used in the Koin configuration
but not directly declared. This includes types like SavedStateHandle and WorkerParameters,
which are used as injected parameters. The Context is declared using the androidContext()
function at the start.

The verify() API is lightweight and does not require any mocks or stubs to run on your
configuration.
First module & injection from an Activity
Let's continue exploring the project, still within the app module. There is a small module that
allows you to build JankStats instances for an Activity: the JankStatsModule.

Here is the original version using Dagger:

JankStatsModule in Dagger version

The idea is to create a JankStats object that enables/disables performance tracking in an


Activity.

Lazy Dagger Property for JankStats


Using lazyStats

In the Koin version, we can simplify this part. Let's open the JankStatsKoinModule file:

JankStats Koin module

In this module, we define a factory that builds the JankStats object instance from an
incoming Activity.

To pass an Activity to a definition, we use injected parameters to declare that we will use
an Activity as a parameter:

factory { (activity: Activity) ->


JankStats.createAndTrack(activity.window, createOnFrameListener()) }

We use a function (a lambda block behind the factory keyword) to write a Kotlin function that
builds our component.

In our MainActivity, we can simply declare JankStats with a call to the inject function as follows:
The parametersOf expression passes arguments to your Koin definition.

Lazy inject in Activity with Koin & injected parameters

The lazyStats property is a true Kotlin lazy type and can be used directly.

Using lazyStats
2 Common Modules Components and Feature
Modules
In this section, we will focus on building the common blocks of the Nia app using
Koin.

These common blocks include repository and usecase components, which play a
crucial role in accessing data and handling business logic. We will explore the
definitions of these components, including database, DataStore, network, and sync
worker modules.

Additionally, we will learn how to inject ViewModels in the Jetpack Compose


composable function. Let's dive in!

Building the common blocks with Koin


The screens of the Nia app are developed using Jetpack Compose and utilize repository
and use case components:

Repository: Provides access to data sources such as networks and databases.

Use case : Handles the business logic.

Now let's open the DataKoinModule.kt file to see the definitions of our repository
components:

[Common Data Components]

Common Data Components

All the repository components are declared using the singleOf keyword, followed by a bind()
section to specify the bound type. This ensures that each component is created as a singleton
instance.

It's considered a good practice to use the includes() function to explicitly list any Koin
modules that are required for the definitions in the current module. This establishes a strong
link that can be used by the verify() API to validate our Koin configuration.

Components from the Data layer can be declared as singleton instances. The Data layer is
independent of the UI layer, so there is no need to associate them with a specific lifecycle.
Next, we will delve into the details of Usecase domain components:

Architecture layers from https://developer.android.com/topic/architecture

We’ll start by looking at the database components in the daosKoinModule.

DAO & Database Layer


The project uses Room for database operations. To declare a Room database instance, we
use the Room.databaseBuilder() builder function. This instance is registered as a
singleton and referenced by the DAO components.

In the databaseKoinModule below, we define the Room database instance as follows:

Declaring Room Database

We simply use a single definition followed by a function to be executed. Here, we use the
androidContext() function to retrieve the Android context from Koin.

In the daosKoinModule, we simply declare each DAO by referencing them from the
NiaDatabase interface.
We can reference each DAO by using the get<NiaDatabase>() expression to retrieve our
database instance, and use it to call our DAO instance like this:
Declaring DAOs

Each DAO is defined with the single keyword, indicating that they are singleton instances. We
include the database definition module.

In Dagger Hilt, on the other hand, the approach is similar but more verbose. Here are the
corresponding declarations in Hilt:

Hilt NiaDatabase Declaration

Hilt DAOs declaration


Next, let's move on to the DataStore components in the section below.

Datastore Layer
In this part, we need to prepare the creation of the DataStoreFactory instance to read offline data. Let's
open the dataStoreKoinModule to see these components:

DataStore Module

We need several singletons here:


UserPreferencesSerializer: Used to serialize data from UserPreferences and passed
to our DataStore.
DataStoreFactory: The DataStore instance itself.
NiaPreferencesDataSource: A DataSource component that uses the DataStore to
read local data.

In this module, we also need to inject the Kotlin coroutines "default dispatcher". It is
included in our module and can be defined as follows:

Declaring Default Coroutines Dispatcher


Note that we can easily override this definition in a test environment by providing a new
definition that overrides the default one. If there are multiple definitions of the same type,
qualifiers can be added.

Now, let's move on to the network layer.

Network Layer
The network module presents an interesting case where we need to load different
implementations depending on the flavor of the module. There are two flavors: demo (static
demo content) and prod (content requested over the network).

To dynamically use the appropriate flavor implementation, we first write the


networkKoinModule file, which includes child implementations:

The network module

Here, we declare all the common definitions used by the included modules, such as the JSON
serializer instance.

The call to includes (networkFlavoredKoinModule) loads the appropriate Koin module. Let's
write the networkFlavoredKoinModule for each flavor. The project will automatically link and
compile

The demo flavor folder:

Demo Flavor
In the demo flavor folder, we have the networkFlavoredKoinModule file, which declares a
fake implementation:

Demo Network Module

The Prod Flavor folder:

Prod flavor

In the prod flavor folder, we have the networkFlavoredKoinModule file, which declares a
Retrofit implementation:

Prod NiaNetworkDataSource Implementation

The networkFlavoredKoinModule file in the prod flavor folder declares a Retrofit


implementation, specifically for the NiaNetworkDataSource.
Usecase Domain Layer
Now that we have core components to facilitate working with our data, we can focus on
components dedicated to business logic, enabling us to reuse them in the UI layer.

Let’s open the domainKoinModule file to see our Usecases definitions:

Use-case definitions

In the domainKoinModule file, we can find the definitions for our use-case components.
Each use-case component is defined as a "factory" to ensure they remain "stateless" and
independent of UI-related memory. These use-case components launch Coroutines Flows
to listen for incoming data updates, without retaining Flow references between screens.

GetFollowableTopicsStream Usecase

Using factoryOf guarantees that a new instance is created each time it is requested, allowing
previously used instances to be garbage collected.
Sync Worker - Offline Data Sync with WorkManager
A crucial component in this project is the SyncWorker class, responsible for
synchronizing data with repositories. It supports the "offline first" strategy, where
locally available data is displayed while fetching new data remotely, thereby
preventing empty content from being shown to the user. The SyncWorker class relies
heavily on common components.

SyncWorker Class- to help sync repositories

SyncWorker Class - Data Repository Synchronization


The SyncWorker class facilitates data synchronization with repositories.

Declaring Sync Worker

In the syncWorkerKoinModule file, we declare our WorkManager component using the


workerOf keyword.
WorkManager Setup with Koin

WorkManager Setup with Koin

To set up WorkManager with Koin, remember to start the WorkManager Koin factory at the
application start using the workManagerFactory() function.

The call to Sync.initialize() initializes data synchronization for offline content.

For more details, refer to the documentation.

Injecting ViewModels in Features


We are now ready to inject all components into a Jetpack Compose composable function. In
the AuthorRoute screen composable, we use the koinViewModel() function to obtain the
ViewModel.

Injecting ViewModel in Compose


To declare a ViewModel component, we simply use the viewModelOf keyword followed by
the class constructor.

Declaring AuthorViewModel

With this keyword, your ViewModel can be automatically injected with the SavedStateHandle
parameter if needed.

AuthorViewModel Class Constructor


3 Getting Started with Koin Annotations
Why Use Annotations with Koin?

The Koin DI framework provides a Kotlin DSL as the default way to declare application
components and perform injections. However, annotations offer an alternative approach
for declaring components within your code. By simply adding annotations to classes, you
can automatically declare them inside Koin without manually modifying the
configuration file. This approach aims to enhance the developer experience and provide
a seamless Kotlin development workflow.

The idea is not to reinvent the wheel like existing solutions such as Dagger Hilt but
to offer a new great Kotlin developer experience.

It's important to note that Koin Annotations do not replace the Koin DSL but rather
serve as a complementary way to define components within your Kotlin code. The Koin
Annotations project includes an annotation processor powered by Google KSP (Kotlin
Symbol Processing), which generates the Koin configuration DSL for you.

DSL vs. Annotations? Ultimately, it comes down to personal preference and workflow
choices.

Google KSP & Koin Annotations — Setup


Google KSP & Koin Annotations — Setup
The Koin Annotations project has been available since 2022 and provides stability and
convenience for integration.

To use Koin Annotations and compiler you need to set up the project with the Google
KSP Kotlin plugin.

For the Nia project, the KSP plugin is already configured. You can utilize the
alias(libs.plugins.ksp) expression:

Gradle KSP Plugin


To enable KSP and Koin Annotations, add the following Gradle dependencies:

Gradle dependencies

If you need to set up the KSP plugin from scratch, you can follow the instructions on our
setup page.

The code generated by the Koin KSP Plugin is minimal and easily debuggable. It consists of
one line per declared component and follows the pure Kotlin/Koin DSL configuration. This
design ensures minimal compilation impact, which was one of our primary requirements.

Let's dive into the code and start using annotations for our Koin configuration.

Declaring Components with Annotations


To declare a class as a Koin component, simply add a Koin annotation to the class. For
example, you can use the @Single annotation to make a class a singleton.

That's it! No additional specifications are required.

All bound types are automatically detected. The @Single annotation in the example is equivalent to
the following Koin DSL declaration:
Annotations in Koin follow a ‘semantic’ similar to the DSL. You'll find similar keywords between
the two:

@Single: Declare as a singleton, equivalent to the "single" keyword in the DSL.

@Factory: Declare as a factory, equivalent to the "factory" keyword in the DSL.

@KoinViewModel: Declare as an Android ViewModel, equivalent to the "viewModel"

keyword in the DSL.

The plugin analyzes the constructor and inherited types, and it can detect nullable types and
generate nullable dependency access using the question mark operator.

Detecting nullable types in constructors

The plugin also recognizes List and Lazy types and uses the appropriate Koin function to
retrieve components.

For more details, refer to the Koin documentation.

Just add an annotation to your class and there you go. It’s as simple as that 👍

Modules & Component Scan


Koin definitions are organized within Koin modules. To define a Koin module, create a
class and annotate it with @Module:

You can use the includes parameter to specify dependencies on other modules.
To associate components with your module, you have two options:

Scan for annotated classes: This approach looks for any annotated class in the specified
package.
Annotated module class methods: Any function within an annotated module class is
considered a component.

To scan components, use the @ComponentScan annotation on your module. This scans all
definitions within the current package and sub-packages:

You can also specify the package you want to scan:

If you want to declare a definition from a function, you can do so by annotating the function:

Then, what to choose? An annotated class or an annotated function? 🤔

Adding an annotation on an existing component is super easy and straightforward. However,


you may need to declare a component within a:

The instance is created with an API (like Room API builder)or needs an expression.
Class is not accessible to be annotated.

Also, it depends on how you prefer to write things. You can see how easy it is to organize
yourself with your components.

Check out the Koin documentation for more details 🤘🏾.


Compatible with Graph Verification API
The Koin Annotations generate your Koin configuration, allowing you to focus more on
your app and less on your tools. The verify API is also available for any generated module
by using the verify() extension on a generated module extension.

Mixing DSL & Annotations — Choose the best for you


You can choose between DSL or Annotations, and even mix them depending
on your needs and usage. You are not constrained to working with just one
solution.

Below is an example of a DSL module that includes annotated class modules and one DSL
module:
4 Core & Features Components with Koin
Annotations
Common Data Layers
We will now explore the common core components that will be utilized by the
features later, with the configuration being done using annotations.

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

Repository: to access data from sources such as the network and database.

Use-case: to handle the business logic.

The module that gathers all these common components is the DataKoinModule.kt
module:

This module serves several purposes:

Scans all repository classes defined in @ComponentScan.

Includes modules that declare sub-data layers components.


Each repository class is simply tagged with the @Single annotation, as shown in
the example below:

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 using a
function with the Room API builder, as demonstrated below:

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

In a separate module, we can reuse our NiaDatabase instance in the DAOs:

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


Datasource Components - Datastore & Networking:
This layer defines Datasources, which are components that abstract the calls to different sources
of data, such as remote web services and local data storage. The UI doesn't need to know where
the data comes from; it just calls the interface defined here.

We define various usages with NiaNetworkDataSource:

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

In a test environment, you can simply redefine the CoroutineDispatcher type to specify the
one you need.

The network module declares NiaNetworkDataSource and is organized into two flavors:

"demo"with local data,


and "prod" for online data.

The NetworkKoinModule includes the appropriate flavor implementation:


Demo flavor in Network module

The demo flavor utilizes the Datastore API and Protobuff API for storing local data,
enabling an offline-first architecture.

The below code represents the implementation of the demo data source as a singleton:

On the other hand, the online version is declared in the following module:
This module scans and includes the Retrofit implementation:

Additionally, there is a section about the Datastore persistence API, which is used for local
data storage. The Datastore Persistence Module declares the necessary components for
NiaPreferencesDatasource.

Domain & Features Modules


Moving on to the Domain & Features Modules, they contain the use-case components
that utilize the DataKoinModule. These use-case components are reusable business logic
components defined in the DomainKoinModule:

Note that the module does not specify a package to scan. It will scan the current package
and its sub-packages for annotated components.
Each use-case component is declared with the @Factory annotation, indicating that Koin should
create a new instance each time it is needed:

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, in the Feature module, we can utilize the common components by including
either the DomainKoinModule or DataKoinModule:

By scanning the appropriate package in our module, we can declare our ViewModel
instances as follows:

Sync Worker — Offline data sync with WorkManager


Lastly, we need to declare our SyncWorker components to prepare offline content
asynchronously. This consists of a module:
The module will scan the following definitions:

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

The SyncWorker will be declared with the Workmanager Koin factory. It needs to be activated at
the start like this:

Koin start in NiaApplication class

In conclusion, we hope you found the walkthrough of the Nia application with Koin
dependency injection and annotations enjoyable and informative.

You might also like