Kotlin
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Null Safety
-
Section 4: Object-Oriented Kotlin
-
Section 5: Functional Kotlin
-
Section 6: Coroutines
-
Section 7: Collections Deep Dive
-
Section 8: Type System Deep Dive
-
Section 9: Interop and Tooling
-
Section 10: Kotlin DSLs and Patterns
-
Section 11: Testing and Quality
-
Section 12: Server-Side Kotlin
-
Section 13: Practical Projects
-
Section 14: Interview Practice
-
Section 15: More Practice Exercises
-
Section 16: More Standard Library
-
Section 17: Multiplatform Kotlin
-
Section 18: kotlin.collections In Depth
-
Section 19: kotlin.text In Depth
-
Section 20: kotlin.ranges and kotlin.sequences
-
Section 21: kotlin.io and File Handling
-
Section 22: kotlinx.coroutines Deep Dive
-
Section 23: kotlin.reflect
-
Section 24: Android Development with Kotlin Overview
-
Section 25: Kotlin Multiplatform Deep Dive
-
Section 26: Kotlin for Backend Deep Dive
-
Section 27: Kotlin Design Patterns
-
Section 28: Advanced Language Features
-
Section 29: More Practice Exercises
-
Section 30: More Interview Practice
-
Section 31: Kotlin Type System Deep Dive
-
Section 32: Kotlin Null Safety Advanced
-
Section 33: Kotlin Testing Deep Dive
-
Section 34: Kotlin Build Tooling Deep Dive
-
Section 35: Kotlin Serialization
-
Section 36: Kotlin Performance Considerations
-
Section 37: Kotlin Native Overview
-
Section 38: Kotlin for Data and Scripting
-
Section 39: More Coroutines Practice
-
Section 40: More Android-Adjacent Patterns
-
Section 41: More Practical Projects
-
Section 42: More Design and Architecture Practice
-
Section 43: Kotlin Language Evolution
-
Section 44: More Interview and Review
-
Section 45: Kotlin Delegation Patterns Deep Dive
-
Section 46: Kotlin Annotations Deep Dive
-
Section 47: Kotlin for Gradle Plugin Development
-
Section 48: Kotlin Concurrency Beyond Coroutines
-
Section 49: Kotlin Compiler Internals
-
Section 50: Real-World Kotlin Case Studies
-
Section 51: Final Practice Projects
-
Section 52: Kotlin for Server-Side Reactive Programming
-
Section 53: More Practice and Drills
-
Section 54: Kotlin Security Practices
185: MVI Architecture Pattern in Kotlin
By the time you get to MVI (Model-View-Intent), you've probably already wrestled with MVVM. While MVVM is great, it can sometimes lead to a "state explosion" where you have five different LiveData or StateFlow objects in your ViewModel, and you're left guessing which one updated first. MVI is my preferred way to stop that chaos. It's all about Unidirectional Data Flow (UDF).
Wait, how is MVI actually different from MVVM?
In MVVM, the View usually calls a function in the ViewModel, and the ViewModel updates one or more observables. It's a bit of a free-for-all. In MVI, we treat everything as a cycle. The View doesn't just "call a function"; it emits an Intent. The ViewModel takes that Intent, processes it, and spits out a single, immutable State object. The View then simply renders whatever that state tells it to.
Think of it like a vending machine. You don't go inside and move the gears yourself (MVVM). You press a button (Intent), the machine processes the request, and it drops a soda (State). You just react to the soda appearing.
How do I actually structure the Intents and State in Kotlin?
I always recommend using sealed classes or sealed interfaces here. It makes your code type-safe and forces you to handle every possible action in a when expression. Let's use a Crypto Price Tracker as an example.
// The "What happened" - User actions
sealed class CryptoIntent {
object RefreshPrices : CryptoIntent()
data class SearchCoin(val query: String) : CryptoIntent()
data class ToggleFavorite(val coinId: String) : CryptoIntent()
}
// The "What to show" - Single source of truth
data class CryptoState(
val coins: List<Coin> = emptyList(),
val isLoading: Boolean = false,
val errorMessage: String? = null,
val searchQuery: String = ""
)
Notice how CryptoState is a data class. We never modify it directly; we use .copy() to create a new version of the state. This is key for debugging—if you log your state changes, you can see exactly how the app evolved step-by-step.
Where does the logic go to turn an Intent into a State?
This happens in your ViewModel. You'll typically have a MutableStateFlow that holds the current state and a function to "handle" the intents. I like to keep the "reducer" logic (the part that decides the new state) separate from the business logic (the API calls).
class CryptoViewModel : ViewModel() {
private val _state = MutableStateFlow(CryptoState())
val state: StateFlow<CryptoState> = _state.asStateFlow()
fun handleIntent(intent: CryptoIntent) {
when (intent) {
is CryptoIntent.RefreshPrices -> fetchCoins()
is CryptoIntent.SearchCoin -> {
_state.update { it.copy(searchQuery = intent.query) }
filterCoins()
}
is CryptoIntent.ToggleFavorite -> toggleCoin(intent.coinId)
}
}
private fun fetchCoins() {
viewModelScope.launch {
_state.update { it.copy(isLoading = true) }
try {
val coins = repository.getLatestPrices()
_state.update { it.copy(coins = coins, isLoading = false) }
} catch (e: Exception) {
_state.update { it.copy(errorMessage = e.message, isLoading = false) }
}
}
}
}
What about "one-time" events, like showing a Toast or navigating?
This is the biggest "gotcha" in MVI. If you put showToast = true in your state, the toast will pop up again every time the screen rotates or the state updates for some other reason. We don't want that.
The fix is to introduce Side Effects (sometimes called Events). These are things that happen once and are then forgotten. I usually implement this using a Channel.
sealed class CryptoEffect {
data class ShowSnackbar(val message: String) : CryptoEffect()
object NavigateToDetails : CryptoEffect()
}
// Inside ViewModel...
private val _effect = Channel<CryptoEffect>()
val effect = _effect.receiveAsFlow()
private fun handlePaymentError() {
viewModelScope.launch {
_effect.send(CryptoEffect.ShowSnackbar("Payment Failed!"))
}
}
Now your View observes the state for the UI layout and the effect flow for those "fire-and-forget" actions. It keeps the state clean and the behavior predictable.
📋 Practical Task
Exercise: Implementing a Real-time User Filter with MVI
You are building a contact list app. Your goal is to implement a search filter using the MVI pattern. Follow these requirements:
- Define the State: Create a
UserStatedata class containing a list ofUserobjects, aStringfor the current search query, and aBooleanfor the loading status. - Define the Intents: Create a
UserIntentsealed class with two actions:UpdateQuery(val query: String)andClearFilter. - Build the ViewModel:
- Use a
MutableStateFlowto manage theUserState. - Implement a
handleIntent(intent: UserIntent)function. - When
UpdateQueryis received, update the state's query and filter the user list based on that query. - When
ClearFilteris received, reset the query to an empty string and restore the full user list.
- Use a
- Side Effect: Add a
UserEffectsealed class and aChannel. If a user searches for a name that doesn't exist in the list, send aShowNoResultsFoundeffect.
There are no comments for now.