8. Flows

In Kotlin, a Flow is an asynchronous stream of values. Values will flow down the stream, and there are collectors that can observe these values. Flow is probably best explained by example, so let's go over some common use-cases for Flows in Android Development

Example 0: Pure flow example

class NewsRemoteDataSource(
    private val newsApi: NewsApi,
    private val refreshIntervalMs: Long = 5000
) {
    val latestNews: Flow<List<ArticleHeadline>> = flow {
        while(true) {
            val latestNews = newsApi.fetchLatestNews()
            emit(latestNews) // Emits the result of the request to the flow
            delay(refreshIntervalMs) // Suspends the coroutine for some time
        }
    }
}

Here we have a very pure example of how a Flow can be used. I took this from creating a flow on the Android developer documentation since it is a very nice first flow usage example. Here we use flow , which is a flow builder that allows us to run suspend functions inside and emit their results to the flow. We could observe this flow using latestNews.collect , so then whenever the emit function is called, the lambda we pass in to latestNews.collect is also called. Let's see how we could observe this Flow in the UI.

val latestNewsState: State<List<String>> =
            newsRemoteDataSource.latestNews.collectAsState(initial = listOf())

        Column(Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) {
            LazyColumn {
                items(latestNewsState.value) {
                    Text(text = it)
                }
            }
        }

We can use the collectAsState method. collect is a method on a Flow that suspends the code block until the FlowCollector finishes emitting values. In this case, our FlowCollector is the while(true) block from the first code snippet, so collect block suspends indefinitely. However, collectAsState runs collect in a coroutine and transforms the results into a State<List<String>>, and as we know whenever the State value changes, our UI recomposes and automatically updates. This is one of the benefits of reactive UI. So even though running a lambda on each flow value emission is a more of an imperative idea, we can still use it in a reactive (declarative) context.

Example 1: Account creation validation

Let's say that the user is typing in their account information, and we want the UI to update live based on whether their information is valid. This could be if their email is valid, if their password is secure enough, etc.. But for this demo, we're going to do a basic version with just username and password. Let's walk through the ViewModel code to see how it uses Flow to streamline this idea.

MainScreenViewModel.kt
data class UiState(
    val username: String = "",
    val password: String = "",
    val isValid: Boolean = false,
)

@HiltViewModel
class MainScreenViewModel @Inject constructor(
) : ViewModel() {
    private val password = MutableStateFlow("")
    private val username = MutableStateFlow("")

    private val _uiState = MutableStateFlow(UiState())
    val uiState = _uiState.asStateFlow()

    init {
        combine(username, password) { username, password ->
            val isValid = username.isNotBlank() && password.length > 8
            _uiState.update {
                UiState(
                    username,
                    password,
                    isValid,
                )
            }
        }.launchIn(viewModelScope)
    }

    fun updatePassword(newPassword: String) {
        password.update { newPassword }
    }

    fun updateUsername(newUsername: String) {
        username.update { newUsername }
    }
}

Let's walk through this code. You'll first notice that password and username are MutableStateFlows. A MutableStateFlow is a type of Flow that holds a state, and it emits new values whenever its state is updated. You may be wondering, why not just use a state to represent these values? The main reason is that we want to be able to launch an operation whenever either of these values updates. States are automatically observed by the UI, since whenever a state's value changes the UI recomposes. But we don't have a good way of observing state updates ourselves. Flow emissions however are easily observed through the combine function. Whenever the username or password flows have an emission, the combine function is called, and we update the UiState accordingly.

So we use combine to create a new Flow that is a result of applying the transform functions to the values from the emissions of username and password. The thing is, the combine method alone just initializes this new Flow, but it doesn't actually collect it, so the transform lambda we wrote won't be called. That's why we use launchIn, which collects the flow in a certain CoroutineScope. We use viewModelScope , so when our ViewModel gets disposed, our coroutine will get disposed with it. and we don't have a memory leak where we are using resources to constantly observe this Flow.

Some other design decisions for this ViewModel include making all the MutableStateFlows private. This was done for separation of concerns, so we don't have to worry about the UI updating our ViewModel flows however they want to. The only functions that mutate the state that we expose to the UI are updatePassword and updateUsername , so now we know exactly where updates to this ViewModel's state will be coming from: usages of those functions. We expose the uiState with asStateFlow, which makes it read-only.

Last updated