StateFlow: Modernizing Android State Management
Understand the evolution of state management for cleaner, more efficient apps.
Background
Android development often involves complex state management and repetitive UI updates. ViewModels offer such structure and testability to address these challenge and many others.
With LiveData, UI changes automatically synchronizes with ViewModel data.
But isn't livedata,
overly reliant on a lifecycle-centric approach? How can we handle situations with mismatched sender and observer lifecycles?
support additional transformation capabilities to enhance the incoming data's suitability for UI consumption?
still shows stale data after resuming from the background? This might be because it doesn't automatically refresh to fetch the latest items from the data layer.
handle cases where new data arrives faster than it can be consumed, with potential delays in new data detection?
Cant throw NullPointerException if I assign null to a non-nullable LiveData variable?
To avoid above limitations, we need a robust yet elegant approach to state management.
Say goodbye to complex workarounds and hello to effortless UI updates.
Introducing StateFlow
StateFlow is a state-holder observable flow that emits the current and new state updates to its collectors. The current state value can also be read through its value property. To update state and send it to the flow, assign a new value to the value property of the MutableStateFlow class.
How I tried to understand this:
Imagined StateFlow as a whiteboard in a classroom, where teacher writes down an important piece of information for my app. For example, "Loading..." or "User Name: Androidplay".
This special whiteboard keeps track of both the current information on the board AND any changes made.
Any other student (subscriber) can "collect" updates from the whiteboard. It's like those parts subscribing to any changes in the message.
To change the message, teacher simply erases the old one and write a new message on the whiteboard. All the "subscribers" know about the update and the latest value.
Let's understand via ViewModel code example.
Start by adding latest kotlin versions:
plugins {
// For build.gradle.kts (Kotlin DSL)
kotlin("jvm") version "1.9.21"
// For build.gradle (Groovy DSL)
id "org.jetbrains.kotlin.jvm" version "1.9.21"
}
Add coroutines dependencies, and sync your project:
dependencies {
def coroutinesVersion = "1.8.0" // replace with latest version
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion")
// Optional
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion")
}
Let's consider that the Room DB is our source of truth for data i.e list of runs,
@Query("SELECT * FROM running_table")
fun getRun(): Flow<List<Run>> // or LiveData<List<Run>>
In our repository,
class MainRepository @Inject constructor(private val runDAO: RunDAO) {
fun getRun() = runDAO.getRun()
}
For simplicity we will write code for RunViewModel
which is responsible to fetch list of runs and update StateFlow for UI to consume,
@HiltViewModel
class RunViewModel @Inject constructor(
private val repository: RunRepository,
dispatcher: Dispatchers,
) : ViewModel() {
// code here...
}
Declare StateFlow variables inside RunViewModel
which is responsible for holding states,
/**
* Represents the current state of the UI. This is stored internally as a MutableStateFlow,
* allowing for updates to the UI state to be observed by other components.
*/
private val _uiState = MutableStateFlow<UIState>(UIState.Loading)
/**
* A read-only StateFlow exposing the current UI state. This is ideal for components
* that need to react to changes in the UI state.
*/
val uiState: StateFlow<UIState> = _uiState
Now inside init
block of RunViewModel
we will add logic to get list of runs.
Collect data from repository
Update StateFlow with latest data. Initial value is
loading
Surrounded the code with
try-catch
to handle errorUIState
Represents the possible states of a UI component or screen. This is used to model loading, success, and error states.
init {
viewModelScope.launch(dispatcher.IO) {
try {
mainRepository.getRun().collect { listOfRuns ->
_uiState.value = UIState.Success(listOfRuns)
}
} catch (e: Exception) {
_uiState.value = UIState.Error(e)
}
}
}
Finally in our compose screen, we will consume these states,
@Composable
fun RunScreen(viewModel: RunViewModel = hiltViewModel()) {
val runListState: UIState by viewModel.uiState.collectAsStateWithLifecycle()
when (runListState) {
is UIState.Loading -> {
TODO("Show loading indicator")
}
is UIState.Success -> {
TODO("Show the list of runs")
}
is UIState.Error -> {
TODO("Show an error message")
}
}
}
In can of activity or fragment, we can consume these states,
@AndroidEntryPoint
class RunFragment : Fragment() {
val viewModel: MainViewModel = // get ViewModel
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewLifecycleOwner.lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect { uiState ->
when (uiState) {
is UIState.Loading -> {
// Show loading
}
is UIState.Success -> {
// Show success
}
is UIState.Error -> {
// Show error
}
}
}
}
}
}
}
Conclusion
I hope this guide shed some light on the power of StateFlow! Got any tricky Flow situations you've encountered recently? Share them below – let's learn from each other.
Loved the post? Hit ♡ below and show some support! It fuels my motivation to keep sharing knowledge
And if you're into reactive programming, check out my other blog, 'Reactive Programming with Flow,' where we dive deeper into its potential.
Code hard, code strong – it's the way to go!