kotlin multiplatform mvi koin
Building Rock-Solid Apps with MVI and Koin in Kotlin Multiplatform
Architecting a Kotlin Multiplatform app without solid patterns is like trying to assemble IKEA furniture without instructions – possible, but you’ll likely end up with extra parts and wobbly results. Let’s explore how combining MVI (Model-View-Intent) architecture with Koin dependency injection creates maintainable, testable apps that work across platforms.
MVI and Koin together act like a GPS and fuel system for your app:
Here’s a quick comparison of state management solutions:
Aspect | MVI | MVVM | MVC |
---|---|---|---|
State Flow | Unidirectional | Bi-directional | Chaotic |
Testability | Excellent | Good | Poor |
Boilerplate | Moderate | Low | None |
Platform Suitability | Cross-platform | Android-focused | Web-era |
Let’s break down a login feature implementation:
<em>// Contract</em>
interface LoginContract {
data class State(
val email: String = "",
val isLoading: Boolean = false,
val error: String? = null
)
sealed class Intent {
data class EmailUpdated(val text: String) : Intent()
object Submit : Intent()
}
sealed class Effect {
object NavigateHome : Effect()
data class ShowToast(val message: String) : Effect()
}
}
<em>// ViewModel</em>
class LoginViewModel(
private val authRepo: AuthRepository
) : ViewModel() {
private val _state = MutableStateFlow(LoginContract.State())
val state: StateFlow<LoginContract.State> = _state
private val _effects = Channel<LoginContract.Effect>()
val effects: Flow<LoginContract.Effect> = _effects.receiveAsFlow()
fun process(intent: LoginContract.Intent) {
when (intent) {
is LoginContract.Intent.EmailUpdated -> updateEmail(intent.text)
LoginContract.Intent.Submit -> performLogin()
}
}
private fun performLogin() = viewModelScope.launch {
_state.update { it.copy(isLoading = true) }
when (val result = authRepo.login(_state.value.email)) {
is Success -> _effects.send(LoginContract.Effect.NavigateHome)
is Failure -> _effects.send(LoginContract.Effect.ShowToast("Login failed"))
}
_state.update { it.copy(isLoading = false) }
}
}
Configure dependencies in shared module:
val appModule = module {
single<AuthRepository> { AuthRepositoryImpl(get()) }
viewModel { LoginViewModel(get()) }
}
val networkModule = module {
single { HttpClient() }
}
<em>// Platform-specific modules</em>
expect val platformModule: Module
Android implementation:
actual val platformModule = module {
single { AndroidContext.getApplicationContext() }
}
iOS implementation:
actual val platformModule = module {
single { NSBundle.mainBundle }
}
Verify your architecture works as intended:
class LoginViewModelTest {
@Test
fun `login shows loading state`() = runTest {
val mockRepo = mockk<AuthRepository> {
coEvery { login(any()) } coAnswers { delay(100); Success }
}
val vm = LoginViewModel(mockRepo)
vm.process(LoginContract.Intent.Submit)
assertTrue(vm.state.value.isLoading)
coVerify { mockRepo.login(any()) }
}
}
class KoinTest : KoinTest {
@Test
fun `verify dependency graph`() {
startKoin { modules(appModule, networkModule) }
get<HttpClient>() shouldNotBe null
get<AuthRepository>() shouldBeInstanceOf<AuthRepositoryImpl>()
}
}
Platform | DI Consideration | Koin Solution |
---|---|---|
Android | Context access | androidContext() |
iOS | NSObject management | Factory providers |
Desktop | Long-lived services | Singleton scope |
Web | Lightweight instances | Scoped modules |
class AnalyticsService : Closeable {
override fun close() {
<em>// Cleanup tracking resources</em>
}
}
val analyticsModule = module {
single { AnalyticsService() }
}
<em>// Cleanup when Koin stops</em>
stopKoin().close()
Issue | Symptom | Solution |
---|---|---|
Memory leaks | App slows over time | Use Koin’s scope API |
State bloat | Slow recompositions | Split nested states |
DI conflicts | Runtime crashes | Qualifier annotations |
Platform leaks | iOS crashes | Expect/actual cleanup |
Already using another architecture? Here’s how to transition:
Before shipping your MVI/Koin app:
This combination has powered production apps handling millions of users across platforms. It takes some upfront discipline, but pays off in long-term maintainability and happiness for developers and users alike.
Jetpack Compose 1.8 rolls out handy features like Autofill integration, slick Text enhancements including auto-sizing… Read More
Reified Keyword in Kotlin: Simplify Your Generic Functions Kotlin's reified keyword lets your generic functions know the… Read More
Android Studio Cloud: Ditch the Setup, Code Anywhere (Seriously!) Alright, fellow Android devs, gather 'round… Read More
Why Clean Code Matters in KMP Poorly structured Kotlin Multiplatform projects often face: 80% longer… Read More