Android - Jetpack Compose
This tutorial lets you write an Android application with Jetpack Compose UI and use Koin dependency injection to retrieve your components. You need around 10 min to do the tutorial.
update - 2024-11-28
Get the code
Gradle Setup
Add the Koin Android and Koin Compose dependencies like below:
dependencies {
// Koin for Android
implementation("io.insert-koin:koin-android:$koin_version")
// Koin for Jetpack Compose
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 ViewModel and Jetpack Compose UI:
Users -> UserRepository -> UserService -> UserViewModel -> MainActivity (Compose UI)
The "User" Data
We will manage a collection of Users. Here is the data class:
data class User(val name: String, val email: 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 findUserOrNull(name: String): User?
fun addUsers(users: List<User>)
}
class UserRepositoryImpl : UserRepository {
private val _users = arrayListOf<User>()
override fun findUserOrNull(name: String): User? {
return _users.firstOrNull { it.name == name }
}
override fun addUsers(users: List<User>) {
_users.addAll(users)
}
}
The UserService Component
Let's write a service component to manage user operations:
interface UserService {
fun getUserOrNull(name: String): User?
fun loadUsers()
fun prepareHelloMessage(user: User?): String
}
class UserServiceImpl(
private val userRepository: UserRepository
) : UserService {
override fun getUserOrNull(name: String): User? = userRepository.findUserOrNull(name)
override fun loadUsers() {
userRepository.addUsers(listOf(
User("Alice", "alice@example.com"),
User("Bob", "bob@example.com"),
User("Charlie", "charlie@example.com")
))
}
override fun prepareHelloMessage(user: User?): String {
return user?.let { "Hello '${user.name}' (${user.email})! 👋" } ?: "❌ User not found"
}
}
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 components. We want singletons of UserRepository and UserService:
val appModule = module {
singleOf(::UserRepositoryImpl) { bind<UserRepository>() }
singleOf(::UserServiceImpl) { bind<UserService>() }
}
Displaying User with ViewModel
Let's write a ViewModel component to display a user:
class UserViewModel(private val userService: UserService) : ViewModel() {
fun sayHello(name: String): String {
val user = userService.getUserOrNull(name)
val message = userService.prepareHelloMessage(user)
return "[UserViewModel] $message"
}
}
UserService 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>() }
singleOf(::UserServiceImpl) { bind<UserService>() }
viewModelOf(::UserViewModel)
}
Injecting ViewModel in Jetpack Compose
With Jetpack Compose, we use ComponentActivity instead of AppCompatActivity, and we build our UI with composable functions instead of XML layouts.
The UserViewModel component will be created, resolving the UserService instance with it. To get it into our Compose UI, we use the koinViewModel() function:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MaterialTheme {
MainScreen()
}
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MainScreen(
viewModel: UserViewModel = koinViewModel()
) {
var nameInput by remember { mutableStateOf("") }
var greetingMessage by remember { mutableStateOf("") }
Scaffold(
topBar = {
TopAppBar(
title = { Text("Koin Sample") }
)
}
) { paddingValues ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
OutlinedTextField(
value = nameInput,
onValueChange = { nameInput = it },
label = { Text("Enter name") },
modifier = Modifier.fillMaxWidth()
)
Button(
onClick = {
val userName = nameInput.trim().ifEmpty { "Alice" }
greetingMessage = viewModel.sayHello(userName)
},
modifier = Modifier.fillMaxWidth()
) {
Text("Say Hello")
}
if (greetingMessage.isNotEmpty()) {
Text(
text = greetingMessage,
style = MaterialTheme.typography.bodyLarge,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth()
)
}
}
}
}
That's it, your Compose app is ready!
The koinViewModel() function retrieves a ViewModel instance from Koin and automatically binds it to the Compose lifecycle. This is the Compose-specific way to inject ViewModels, replacing the by viewModel() delegate used in traditional Android Views.
Key Compose Concepts
- ComponentActivity: Base class for Compose apps (instead of AppCompatActivity)
- setContent: Sets the Composable content as the activity's UI
- @Composable: Functions that build UI declaratively
- remember & mutableStateOf: Compose state management for reactive UI updates
- koinViewModel(): Koin's Compose integration for ViewModel injection
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
Koin module: classic or constructor DSL?
Here is the Koin module declaration for our app:
val appModule = module {
single<UserRepository> { UserRepositoryImpl() }
single<UserService> { UserServiceImpl(get()) }
viewModel { UserViewModel(get()) }
}
We can write it in a more compact way, by using constructors:
val appModule = module {
singleOf(::UserRepositoryImpl) { bind<UserRepository>() }
singleOf(::UserServiceImpl) { bind<UserService>() }
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 test dependency like below:
// Add Maven Central to your repositories if needed
repositories {
mavenCentral()
}
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!
Compose vs XML Views
This tutorial demonstrates the same functionality as the Android ViewModel tutorial, but using Jetpack Compose instead of XML layouts:
| Aspect | XML Views | Jetpack Compose |
|---|---|---|
| Activity Base | AppCompatActivity | ComponentActivity |
| UI Definition | XML layout files | @Composable functions |
| ViewModel Injection | by viewModel() delegate | koinViewModel() function |
| State Management | LiveData/StateFlow | remember + mutableStateOf |
| UI Updates | View binding + observers | Automatic recomposition |
For a version using Koin Annotations with Compose, see the Compose Multiplatform Annotations tutorial.