Photo by Joseph James: A walk through the streets of Zoo, Berlin

Choosing the Right Flow in Android Development: A Deep Dive into The Flow, The StateFlow, and The SharedFlow

Joseph James (JJ)
4 min readJul 22, 2023

--

Greetings, Android developers! Today, we’re delving into the world of Kotlin Flows, breaking down the differences and best uses of StateFlow, SharedFlow, and standard Flow within Android.

Kotlin Flows: A Brief Introduction

Kotlin Flows, part of the kotlinx.coroutines library, provide an efficient way to handle streams of data asynchronously, ensuring our Android applications are reactive, performant, and maintainable.

There are three primary types of Flows in Kotlin:

  • Flow
  • StateFlow
  • SharedFlow

Flow

A Flow is a stream that allows sequences of data to be received and processed asynchronously, utilizing the builder pattern. Flows are cold, meaning data is produced only when collected. No collector, no computation.

Use-case: Fetching and Processing Data

For instance, if we want to fetch users from a local database or network API, we could use Flow like this:

fun fetchUsers(): Flow<List<User>> {
return flow {
val users = userService.getUsers() // Fetch users from network or database
emit(users) // Emit the users to the collector
}
}

Then in our ViewModel, we would collect the Flow:

viewModelScope.launch {
userRepository.fetchUsers().collect { users ->
// Use users data in the UI
}
}

Use-case: Listening to Text Changes on an EditText

A standard Flow can be used to handle sequences of data. Here's how to create an extension function for an EditText that emits text changes as a Flow:

fun EditText.textChanges(): Flow<String> = callbackFlow {
val watcher = this@textChanges.doOnTextChanged { text, _, _, _ ->
trySend(text.toString())
}
awaitClose { this@textChanges.removeTextChangedListener(watcher) }
}
// Usage in ViewModel:
val searchQuery: Flow<String> = someEditText.textChanges()

Then, you can use transform operator on this Flow to make API requests based on the search query:

val searchResults: Flow<List<SearchResult>> = searchQuery
.debounce(300) // to skip rapid changes
.distinctUntilChanged() // to skip duplicates
.transform { query ->
if (query.isNotBlank()) {
emit(apiService.search(query)) // emits API search results
}
}

StateFlow

StateFlow is a state-holder observable flow that emits the current and future state updates to its collectors. It’s hot, meaning it produces values irrespective of the presence of collectors, and maintains state. Any new collector receives the most recent value immediately.

Use-case: Updating UI Based on State Changes

Imagine an app that displays network status. We could use a StateFlow in our ViewModel:

val _networkStatus = MutableStateFlow<NetworkStatus>(NetworkStatus.Loading)
val networkStatus: StateFlow<NetworkStatus> = _networkStatus
fun fetchNetworkStatus() {
viewModelScope.launch {
val status = networkService.getStatus() // Fetch status from a network service
_networkStatus.emit(status) // Emit the status to the StateFlow
}
}

In our UI, we can collect this StateFlow to update the network status:

lifecycleScope.launchWhenStarted {
viewModel.networkStatus.collect { status ->
// Update UI based on network status
}
}

Use-case: User Authentication State Management

Suppose you want to keep track of a user’s authentication status in your app. You can use StateFlow to maintain this state and observe changes:

sealed class AuthenticationState {
object Unauthenticated : AuthenticationState()
data class Authenticated(val user: User) : AuthenticationState()
}
class AuthViewModel(private val authRepository: AuthRepository) : ViewModel() {
private val _authState = MutableStateFlow<AuthenticationState>(AuthenticationState.Unauthenticated)
val authState: StateFlow<AuthenticationState> = _authState
fun signIn(username: String, password: String) {
viewModelScope.launch {
val user = authRepository.signIn(username, password)
_authState.value = AuthenticationState.Authenticated(user)
}
}
fun signOut() {
_authState.value = AuthenticationState.Unauthenticated
}
}

// In UI:
lifecycleScope.launchWhenStarted {
viewModel.authState.collect { authState ->
// Update UI based on authState
}
}

SharedFlow

SharedFlow, similar to StateFlow, is hot, but it’s not confined to holding a state. It can emit values to multiple collectors simultaneously. The emission can be replayed based on the replay parameter during SharedFlow initialization.

Use-case: Showing SnackBars or Toasts

SharedFlow is perfect for displaying Snackbar messages or Toast messages from our ViewModel:

val _snackbarEvents = MutableSharedFlow<String>()
val snackbarEvents: SharedFlow<String> = _snackbarEvents

fun doSomething() {
viewModelScope.launch {
try {
someService.doSomething() // Do something that might throw an exception
} catch (e: Exception) {
_snackbarEvents.emit("An error occurred") // Emit a message to the SharedFlow
}
}
}

In our UI, we can collect this SharedFlow to show Snackbar messages:

lifecycleScope.launchWhenStarted {
viewModel.snackbarEvents.collect { message ->
Snackbar.make(view, message, Snackbar.LENGTH_SHORT).show() // Show the Snackbar message
}
}

Use-case: Communication between different Fragments

Suppose you have a BottomSheetDialogFragment where a user selects a color. You can use SharedFlow to communicate the selected color back to your Fragment or Activity:

class ColorPickerDialog : BottomSheetDialogFragment() {
private val _colorSelected = MutableSharedFlow<Int>()
val colorSelected: SharedFlow<Int> = _colorSelected
// Some function that gets called when a color is picked
fun onColorPicked(color: Int) {
_colorSelected.tryEmit(color)
}
}
// In your Fragment/Activity
colorPickerDialog.colorSelected.onEach { color ->
// Use the selected color in your UI
}.launchIn(lifecycleScope)

Conclusion

In summary, Flow, StateFlow, and SharedFlow are powerful tools for creating asynchronous and reactive applications. Understanding how and when to use each one will improve the robustness and readability of your code, leading to better overall apps and a happier user base. Happy coding!

--

--

Joseph James (JJ)
Joseph James (JJ)

No responses yet