Professional Documents
Culture Documents
White Paper Now in Android With Koin - PDF 2
White Paper Now in Android With Koin - PDF 2
com/android/nowinandroid
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.
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 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.
-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
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.
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:
We load Koin modules using the modules() function. In this case we load the niaAppModule.
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.
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.
In the Koin version, we can simplify this part. Let's open the JankStatsKoinModule file:
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:
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.
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.
Now let's open the DataKoinModule.kt file to see the definitions of our repository
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:
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:
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
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:
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).
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
Demo Flavor
In the demo flavor folder, we have the networkFlavoredKoinModule file, which declares a
fake implementation:
Prod flavor
In the prod flavor folder, we have the networkFlavoredKoinModule file, which declares a
Retrofit implementation:
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.
To set up WorkManager with Koin, remember to start the WorkManager Koin factory at the
application start using the workManagerFactory() function.
Declaring AuthorViewModel
With this keyword, your ViewModel can be automatically injected with the SavedStateHandle
parameter if needed.
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.
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 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.
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:
The plugin analyzes the constructor and inherited types, and it can detect nullable types and
generate nullable dependency access using the question mark operator.
The plugin also recognizes List and Lazy types and uses the appropriate Koin function to
retrieve components.
Just add an annotation to your class and there you go. It’s as simple as that 👍
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:
If you want to declare a definition from a function, you can do so by annotating the function:
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.
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.
The module that gathers all these common components is the DataKoinModule.kt
module:
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 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:
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.
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:
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:
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:
In conclusion, we hope you found the walkthrough of the Nia application with Koin
dependency injection and annotations enjoyable and informative.