Kotlin Multiplatform MVI Architecture with Koin DI

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.

Why This Combo Works

MVI and Koin together act like a GPS and fuel system for your app:

  • MVI keeps your UI state predictable
  • Koin manages dependencies without ceremony
  • Both work seamlessly across Android, iOS, and other platforms

Here’s a quick comparison of state management solutions:

AspectMVIMVVMMVC
State FlowUnidirectionalBi-directionalChaotic
TestabilityExcellentGoodPoor
BoilerplateModerateLowNone
Platform SuitabilityCross-platformAndroid-focusedWeb-era

MVI Core Components

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) }
    }
}

Koin Setup Made Simple

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 }
}

Testing the Combo

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-Specific DI Handling

PlatformDI ConsiderationKoin Solution
AndroidContext accessandroidContext()
iOSNSObject managementFactory providers
DesktopLong-lived servicesSingleton scope
WebLightweight instancesScoped modules

Performance Pro Tips

  1. Scope your Koin modules – Don’t make everything global
  2. Use state flattening – Keep MVI state objects lean
  3. Lazy-load dependencies – Especially for platform services
  4. Dispose resources – Coroutine scopes are your friends
class AnalyticsService : Closeable {
    override fun close() {
        <em>// Cleanup tracking resources</em>
    }
}

val analyticsModule = module {
    single { AnalyticsService() }
}

<em>// Cleanup when Koin stops</em>
stopKoin().close()

Common Pitfalls and Fixes

IssueSymptomSolution
Memory leaksApp slows over timeUse Koin’s scope API
State bloatSlow recompositionsSplit nested states
DI conflictsRuntime crashesQualifier annotations
Platform leaksiOS crashesExpect/actual cleanup

Migration Strategy

Already using another architecture? Here’s how to transition:

  1. Start with isolated features
  2. Wrap existing ViewModels in MVI contracts
  3. Gradually replace manual DI with Koin
  4. Migrate platform-specific code last

Final Checklist

Before shipping your MVI/Koin app:

  1. All business logic tested in shared module
  2. Koin modules organized by feature
  3. State objects are immutable
  4. Effects channel properly closed
  5. Platform resources properly released

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.

Saiful Alam Rifan

Mobile Application Developer with over 12 years of experience crafting exceptional digital experiences. I specialize in delivering high-quality, user-friendly mobile applications across diverse domains including EdTech, Ride Sharing, Telemedicine, Blockchain Wallets, and Payment Gateway integration. My approach combines technical expertise with collaborative leadership, working seamlessly with analysts, QA teams, and engineers to create scalable, bug-free solutions that exceed expectations. Let's connect and transform your ideas into remarkable mobile experiences.

Recent Posts

What’s New in Jetpack Compose 1.8: Autofill, Text, Visibility & More (2025)

Jetpack Compose 1.8 rolls out handy features like Autofill integration, slick Text enhancements including auto-sizing… Read More

2 weeks ago

Reified Keyword in Kotlin Explained: Unlock Type Safety

 Reified Keyword in Kotlin: Simplify Your Generic Functions Kotlin's reified keyword lets your generic functions know the… Read More

2 weeks ago

Android Studio Cloud: Develop Android Apps Anywhere (2025)

Android Studio Cloud: Ditch the Setup, Code Anywhere (Seriously!) Alright, fellow Android devs, gather 'round… Read More

2 weeks ago

Firebase Studio & Google’s AI Dev Tools Guide

Firebase Studio is a new cloud-based platform for building AI-powered apps, launched at Google Cloud… Read More

3 weeks ago

Kotlin Multiplatform Future: Trends, Use Cases & Ecosystem Growth

1. Emerging Trends in Kotlin Multiplatform 1.1 Expansion to New Platforms KMP is branching beyond… Read More

3 weeks ago

Clean Kotlin Multiplatform Code: Best Practices for Maintainable Apps

Why Clean Code Matters in KMP Poorly structured Kotlin Multiplatform projects often face: 80% longer… Read More

4 weeks ago