Using Kotlin Flows for Exposing Observable Screen UI state in android

Mubarak Native
8 min readOct 12, 2024

--

In this article we are going to learn how to expose the Screen UI state by observable type using kotlin flows, and we are going to explore reactive programming paradigm in general, and. Lots more. Let’s begin.

Exposing Screen UI state in android

As I mentioned in my previous article, Implementing the right architecture for your app in android in the Expose UI state section, we usually expose the screen UI state in ViewModel, because viewmodel is the place where we place our business logic and create the app UI state from combining data from the repository data layer, even UI element state like Textfield state can be place here, if the business logic read or write needs it.

Let’s come to our topic. I even mentioned in my previous article, the UI needs to be reactive, meaning, it shouldn’t directly pull data from the ViewModel, instead, it collects it. Whenever the data changes, it instantly notifies our collector. In this case, the UI, this is the exact work of the Kotlin Flows. We will see this type of programming and its android specific Kotlin flows implementation in detail in a moment.

The Reactive Programming Paradigm

Reactive programming is a programming paradigm used in many modern GUI application nowadays, for mobile, web, that focuses on creating software that responds to data changes and events in real time. It’s a key component of modern software development.

Reactive programming uses asynchronous programming logic to handle data streams and change propagation. It’s based on the concepts of streams and observables, which are:

Streams

  • A sequence of ongoing data, such as sending ongoing location info for map, video playback, or stock market updates.

Observables

A type of stream that can be observed, allowing you to react to and listen for incoming data without need to pull the data manually.

Implementation of Reactive Paradigm

ReactiveX — ReactiveX (Rx, also known as Reactive Extensions) is a library originally created by Microsoft for composing asynchronous and event-based programs using observable sequences.

It is an implementation of reactive programming and provides a blueprint for the tools to be implemented in multiple programming languages, such as: RxJava, RxJS, Rx.NET, and others.

How to Implement this Reactive Approach for Android UI

For this exact use case, We have an Asynchronous Flow as a part of kotlin coroutines, flows is a type that emits a streams of data sequencially unlike suspend function in coroutine that returns a single value.

Just like a iterator that traverse over the collection element sequencially, but the flow also produces and consumes the value asynchronously, this ensure while performing network operation or doing heavy jobs in background thread, without blocking the main thread.

There are three entities involved in streams of data.

  • A producer produces data from the datasource, that is added to the stream. As i mentioned previously the flow is backed by coroutines, so that it can produce data asynchronously.
  • (Optional) Intermediaries can modify each value emitted into the stream or the stream itself without collecting it ex: filter, map, take, and others.
  • A consumer consumes the values from the stream it usually the UI.

There are multiple benefits when using Kotlin flows compared to other observable data holders such as, Live Data, RxJava. The main benefit of using flows is that, by using intermediary operators, from the basic one to complex ones, like basic: map, filter, take, to complex: flattening flows. So, if you are a beginner in flow in general, I would recommend you to get a basic familiarity from these docs and also, in this article I am not going to compare their benefits and features. There are a lot of great articles about that.

Lets see how to expose Screen UI state by using flows

Before we go to learn about exposing the screen UI state, first we need to understand the flow types and differences in its characters.

Types of flows

  • Flow
  • StateFlow
  • SharedFlow

These are the three types of flow. We can classify this type of flow into two types. These are:

  1. Cold Flows

2. Hot Flows

  1. Cold Flows are similar to sequences. The code in producer block or flow builder doesn’t run until the flow is collected by using a terminal operator such as collect.

Here’s the kotlin sample code to demonstrate:


fun simple(): Flow<Int> = flow { // flow builder
println("Flow started")
for (i in 1..3) {
delay(100)
emit(i)
}
}

fun main() = runBlocking<Unit> {
println("Calling simple function...")
val flow = simple() // the function is called, but print statement is not executed becuase the flow is not collect yet.
println("Calling collect...")
flow.collect { value -> println(value) }
println("Calling collect again...")
flow.collect { value -> println(value) }
}

Output:

Calling simple function… 
Calling collect…
Flow started 1 2 3
Calling collect again…
Flow started 1 2 3

Flows are cold and lazy unless specified by other intermediate operators such as: .stateIn or .sharedIn

2. Hot flow

Hot flows are those flows that don’t consider the observer collects its value or not. They produce the stream of data even if no collector is actively present.

StateFlow and SharedFlow are an example of hot flows.

Usually, most of the time, you don’t need to be a producer for the flow. Most popular jetpack libraries do this work. For example:

Room, DataStore, Retrofit, WorkManager

For Room

You just need to return flow as a observable data holder like in our above code example, Where we return Flow<List<Note>> for getAllNotes() and getSearchNote function

For Datastore

Now, we have understand the basic type lets move further to our main topic to know which flow are used to expose screen UI state

Traversing the Data from the Data Source to UI by using Flows

Generally we need to expose flow from our data sources like in Repository classes, like this:

class DefaultOsbRepository @Inject constructor(
private val osbDatabase: OsbDatabase,
private val dispatcher: CoroutineDispatcher = Dispatchers.IO
):OsbRepository {

// ..

override fun getAllNote(): Flow<List<Note>> { // expose Flow from the repository
return osbDatabase.getDao().getAllNotes()
}

override fun getNoteBySearch(searchQuery: String): Flow<List<Note>> {
return osbDatabase.getDao().getSearchedNote(searchQuery)
}

override suspend fun deleteAllNotes() {
osbDatabase.getDao().deleteAllNotes()
}

override fun getNoteStreamById(noteId: Long): Flow<Note> {
return osbDatabase.getDao().getNoteStream(noteId)
}

// ...
}

Here, we deal with different types of lifecycle for UI, usually process recreation (Configuration change) can happen when the device is rotated. But our ViewModel can handle this when producing the screen UI state.

The nature of flow here is when the flow is cancelled when we collect data from UI, due to config changes, it recreate a new flow pipe if we collect it again. There is a better way to handle this. That is by using StateFlow. A stateflow is a hot flow that doesn’t cancel and recreate if the collector or observer stops collecting from it, also Stateflow emits the current and new state updates to its collectors. The current state value can also be read through its value property.

Here, the StateFlow is ideal for providing streams of data from ViewModel to UI. Any flow can be converted to stateflow by using the following extension function

            .stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = NewsFeedUiState.Loading,
)

scope = specifies the coroutine scope in which sharing is started usually viewmodelScope in ViewModel.

started = the strategy that controls when sharing is started and stopped. (The SharingStarted.WhileSubscribed() start policy keeps the upstream producer active while there are active subscribers. Other start policies are available, such as SharingStarted.Eagerly to start the producer immediately or SharingStarted.Lazily to start sharing after the first subscriber appears and keep the flow active forever. )

initialValue = the initial value of the state flow.

@HiltViewModel
class HomeViewModel @Inject constructor(
private val newsRepository: NewsRepository
) : ViewModel() {

val newsUiState: StateFlow<NewsFeedState> =
newsRepository.getNewsFeed() // returns Flow<List<News>>
.map(NewsFeedUiState::Success)
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = NewsFeedUiState.Loading,
)
}

sealed interface HomeUiState {
data class Success(val newsList: List<News> = emptyList()) : HomeUiState
data object Error : HomeUiState
data object Loading : HomeUiState
}

Collecting UI from ViewModel (Compose)

    val newsUiState by viewModel.newsUiState.collectAsStateWithLifecycle()

Collecting UI State from ViewModel (Views)

    lifecycleScope.launch {
// repeatOnLifecycle launches the block in a new coroutine every time the
// lifecycle is in the STARTED state (or above) and cancels it when it's STOPPED.
repeatOnLifecycle(Lifecycle.State.STARTED) {
// Trigger the flow and start listening for values.
// Note that this happens when lifecycle is STARTED and stops collecting when the lifecycle is STOPPED
viewModel.newsUiState.collect { uiState ->

}
}
}

collectAsStateWithLifecycle() collects values from a Flow in a lifecycle-aware manner, allowing your app to conserve app resources. It represents the latest emitted value from the Compose State. Use this API as the recommended way to collect flow on Android apps.

For collecting UI State from other types of observable data holder, see Other supported type section

In a case when you have no way to use Flow as an observable data holder, you can use this approach instead.

   sealed interface HomeUiState {
data class Success(val newsFeed: NewsFeed) : HomeUiState
data object Error : HomeUiState
data object Loading : HomeUiState
}

@HiltViewModel
class HomeViewModel @Inject constructor(
private val newsRepository: NewsRepository
) : ViewModel() {

var newsUiState: HomeUiState by mutableStateOf(HomeUiState.Loading)
private set

init {
getNewsFeed()
}

private fun getNewsFeed() {
viewModelScope.launch {
newsUiState = HomeUiState.Loading
newsUiState = try {
HomeUiState.Success(newsRepository.getNewsFeed())
} catch (e: IOException) {
HomeUiState.Error
} catch (e: Exception) {
HomeUiState.Error
} catch (e: HttpException) {
HomeUiState.Error
}
}
}

}

Here, I would use a Compose State directly to represent Screen UI State because my newsRepository.getNewsFeed() returns all in one wrapper class like this.

interface NewsRepository {
suspend fun getNewsFeed(): NewsFeed
suspend fun getSearchNews(searchQuery:String): SearchNews
}

@Serializable
data class NewsFeed(

@SerialName("tfa")
val todayFeaturedArticle: Tfa? = null,

@SerialName("mostread")
val mostRead: MostRead? = null, // news

@SerialName("news")
val news: List<News>?= null,

@SerialName("onthisday")
val onThisDay: List<Onthisday>? = null,

)

Here, i need to show all type of content not only news, So i will need to return just a NewsFeed other wise here it not match the same type

    data class Success(val newsFeed: NewsFeed /** Needs to be a whole wrapper class insteas of List<Flow> */) : HomeUiState

SharedFlow

SharedFlow are used to broadcasting event, data streams to multiple collectors, Unlike StateFlow the SharedFlow doesn’t contain latest state in it, But it is highly configurable via replay property (i.e: keeping a certain number of past event)

Key Differences between StateFlow and SharedFlow

StateFlow:

  • Holds the latest state value.
  • Ideal for exposing UI state that needs to persist through configuration changes.
  • Only emits new values to collectors when the state changes.

SharedFlow:

  • Does not hold any state.
  • Useful for broadcasting events to multiple collectors (e.g., navigation events).
  • Can be configured with replay options to retain a limited history of events.

Conclusion

In Android, using Kotlin Flows like StateFlow and SharedFlow offers a reactive way to expose observable UI states and events. While StateFlow is excellent for keeping track of the current UI state, SharedFlow is perfect for broadcasting events. Both tools, backed by Kotlin coroutines, help create responsive, non-blocking, and reactive UIs.

--

--

Mubarak Native
Mubarak Native

Written by Mubarak Native

Mubarak Basha a.k.a (Mubarak Native) Mobile App Developer, Embedded Systems Developer, Analog,... driven by curiosity and not afraid to explore new worlds.

No responses yet