Android - Jetpack Compose
This tutorial lets you write an Android application and use Koin dependency injection to retrieve your components. You need around 10 min to do the tutorial.
update - 2024-10-21
Get the code
Gradle Setup
Add the Koin Android dependency like below:
dependencies {
// Koin for Android
implementation "io.insert-koin:koin-androidx-compose:$koin_version"
}
Application Overview
The idea of the application is to manage a list of users, and display it in our MainActivity
class with a Presenter or a ViewModel:
Users -> UserRepository -> (Presenter or ViewModel) -> Composable
The "User" Data
We will manage a collection of Users. Here is the data class:
data class User(val name : String)
We create a "Repository" component to manage the list of users (add users or find one by name). Here below, the UserRepository
interface and its implementation:
interface UserRepository {
fun findUser(name : String): User?
fun addUsers(users : List<User>)
}
class UserRepositoryImpl : UserRepository {
private val _users = arrayListOf<User>()
override fun findUser(name: String): User? {
return _users.firstOrNull { it.name == name }
}
override fun addUsers(users : List<User>) {
_users.addAll(users)
}
}
The Koin module
Use the module
function to declare a Koin module. A Koin module is the place where we define all our components to be injected.
val appModule = module {
}
Let's declare our first component. We want a singleton of UserRepository
, by creating an instance of UserRepositoryImpl
val appModule = module {
singleOf(::UserRepositoryImpl) bind UserRepository::class
}
Displaying User with UserViewModel
The UserViewModel
class
Let's write a ViewModel component to display a user:
class UserViewModel(private val repository: UserRepository) : ViewModel() {
fun sayHello(name : String) : String{
val foundUser = repository.findUser(name)
return foundUser?.let { "Hello '$it' from $this" } ?: "User '$name' not found!"
}
}
UserRepository is referenced in UserViewModel's constructor
We declare UserViewModel
in our Koin module. We declare it as a viewModelOf
definition, to not keep any instance in memory (avoid any leak with Android lifecycle):
val appModule = module {
singleOf(::UserRepositoryImpl) { bind<UserRepository>() }
viewModelOf(::UserViewModel)
}
The
get()
function allow to ask Koin to resolve the needed dependency.
Injecting ViewModel in Compose
The UserViewModel
component will be created, resolving the UserRepository
instance with it. To get it into our Activity, let's inject it with the koinViewModel()
function:
@Composable
fun ViewModelInject(userName : String, viewModel: UserViewModel = koinViewModel()){
Text(text = viewModel.sayHello(userName), modifier = Modifier.padding(8.dp))
}
The koinViewModel
function allows us to retrieve a ViewModel instances, create the associated ViewModel Factory for you and bind it to the lifecycle
Displaying User with UserStateHolder
The UserStateHolder
class
Let's write a State holder component to display a user:
class UserStateHolder(private val repository: UserRepository) {
fun sayHello(name : String) : String{
val foundUser = repository.findUser(name)
return foundUser?.let { "Hello '$it' from $this" } ?: "User '$name' not found!"
}
}
UserRepository is referenced in UserViewModel's constructor
We declare UserStateHolder
in our Koin module. We declare it as a factoryOf
definition, to not keep any instance in memory (avoid any leak with Android lifecycle):
val appModule = module {
singleOf(::UserRepositoryImpl) { bind<UserRepository>() }
factoryOf(::UserStateHolder)
}
Injecting UserStateHolder in Compose
The UserStateHolder
component will be created, resolving the UserRepository
instance with it. To get it into our Activity, let's inject it with the koinInject()
function:
@Composable
fun FactoryInject(userName : String, presenter: UserStateHolder = koinInject()){
Text(text = presenter.sayHello(userName), modifier = Modifier.padding(8.dp))
}
The koinInject
function allows us to retrieve a ViewModel instances, create the associated ViewModel Factory for you and bind it to the lifecycle
Start Koin
We need to start Koin with our Android application. Just call the startKoin()
function in the application's main entry point, our MainApplication
class:
class MainApplication : Application(){
override fun onCreate() {
super.onCreate()
startKoin{
androidLogger()
androidContext(this@MainApplication)
modules(appModule)
}
}
}
The modules()
function in startKoin
load the given list of modules
While starting the Compose application we need to link Koin to our current Compose application, with KoinAndroidContext
:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MaterialTheme {
KoinAndroidContext {
App()
}
}
}
}
}
Koin module: classic or constructor DSL?
Here is the Koin moduel declaration for our app:
val appModule = module {
single<HelloRepository> { HelloRepositoryImpl() }
viewModel { MyViewModel(get()) }
}
We can write it in a more compact way, by using constructors:
val appModule = module {
singleOf(::UserRepositoryImpl) { bind<UserRepository>() }
viewModelOf(::UserViewModel)
}
Verifying your App!
We can ensure that our Koin configuration is good before launching our app, by verifying our Koin configuration with a simple JUnit Test.
Gradle Setup
Add the Koin Android dependency like below:
dependencies {
// Koin for Tests
testImplementation "io.insert-koin:koin-test-junit4:$koin_version"
}
Checking your modules
The verify()
function allow to verify the given Koin modules:
class CheckModulesTest : KoinTest {
@Test
fun checkAllModules() {
appModule.verify()
}
}
With just a JUnit test, you can ensure your definitions configuration are not missing anything!