Skip to Content
Course content

109: Kotlin-Specific Android Patterns

Click on the "Edit" button in the top corner of the screen to edit your slide content.

A few years ago, I was reviewing a PR for a junior dev who was building a complex search screen. He had about six different Boolean flags in his ViewModel: isLoading, isError, isEmpty, isSearching, and so on. During the demo, we hit a weird edge case where the app showed both a loading spinner and an error message simultaneously. He'd forgotten to set isLoading = false when the error occurred. It was a classic case of "impossible states"β€”the UI was trying to be two things at once because the state was fragmented across multiple variables.

This is where Kotlin's specific language features allow us to move past the "Java way" of doing Android. We can actually make those impossible states unrepresentable in the code.

Replacing Boolean Soup with Sealed UI States

Instead of managing a handful of flags, the professional pattern in Kotlin is to use a sealed interface to represent the entire state of a screen. This forces the UI to handle only one state at a time. If the state is Error, it physically cannot be Loading.

sealed interface SearchUiState {
    object Loading : SearchUiState
    data class Success(val results: List<SearchResult>) : SearchUiState
    data class Error(val message: String) : SearchUiState
    object Empty : SearchUiState
}

In your ViewModel, you expose this as a StateFlow. When the UI collects this state, you use a when expression. Because it's a sealed interface, the compiler will scream at you if you forget to handle the Error or Empty states. I've found that this single change eliminates about 90% of the "weird" UI bugs I used to see in legacy Android projects.

Leveraging Property Delegates for Android Boilerplate

You've likely seen by lazy, but in Android, we use delegates to solve a specific problem: the Android Lifecycle. You can't just instantiate a ViewModel in a Fragment because the Fragment is destroyed and recreated during configuration changes. If you did, you'd lose your data every time the user rotated the screen.

The by viewModels() delegate (from the fragment-ktx library) is the gold standard here. It handles the ViewModelStoreOwner logic under the hood, ensuring you get the same instance of the ViewModel across rotations. I also highly recommend using by lazy for things like adapter initialization or heavy dependency lookups that only need to happen once the Activity has reached a specific state. It keeps your onCreate clean and ensures you aren't wasting memory on objects that might never be accessed if the user leaves the screen immediately.

Cleaning up Context with Extension Functions

Android's Context is everywhere, and it's often clunky. I hate seeing Toast.makeText(context, "Message", Toast.LENGTH_SHORT).show() repeated twenty times in a codebase. It's noise. Since Kotlin allows us to add functionality to existing classes without inheriting from them, we can turn these into one-liners.

fun Context.toast(message: String, duration: Int = Toast.LENGTH_SHORT) {
    Toast.makeText(this, message, duration).show()
}

// Now, inside an Activity or Fragment:
toast("Connection lost!")

I usually keep a ContextExt.kt file in my projects. By moving the boilerplate into extensions, your actual business logic becomes readable. You stop focusing on the Android API's verbosity and start focusing on what the app is actually doing.




πŸ“‹ Practical Task

Building a State-Driven Weather Dashboard

Your task is to refactor a fragmented weather screen into a professional Kotlin-Android pattern. Currently, the screen uses three separate LiveData Booleans to track loading, error, and data presence.

Requirements:

  • Create a WeatherUiState sealed interface with four states: Loading, Success (containing a WeatherReport data class), Error (containing an error message), and Empty.
  • Implement a WeatherViewModel that uses a MutableStateFlow to manage this state.
  • Create a function in the ViewModel called fetchWeather() that simulates a network call (using delay()) and transitions the state from Loading to either Success or Error.
  • Write a render(state: WeatherUiState) function in a mock Activity/Fragment that uses a when expression to handle every possible state, ensuring the compiler verifies that the state handling is exhaustive.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.