Reactive programming with Kotlin flow

Hey developers! Super excited to share my first-ever post with you. I’d love your feedback in the comments below so I can learn and improve!

In this post I will talk about:

  • Intro about reactive programming.

  • Theoretical difference between traditional & reactive programming.

  • Case study with code example using kotlin flow.

What is Reactive programming ?

Reactive programming is an asynchronous, declarative programming paradigm concerned with the orchestration of data streams and the propagation of change via dynamically constructed dependency graphs. In this paradigm, time is treated as a first-class citizen, where computations are not merely sequential steps of imperative instruction but an intricate dance of values and events evolving across an abstract temporal dimension.

In simple words, it lets your focus on managing flow of data from generators to the consumers who are observing it, thus ease out complex implementation of writing code to bind data with UI states, making consumers reactive only to the data changes over time.

Each of these consumers collectively work together independently to form a system, or a module (android SDK).

Traditional vs Reactive programming

Imperative Approach

In a traditional or imperative approach, updating the UI based on new data from the server requires a focus on the specific steps to achieve this outcome. You would need to:

  • Define a data source: Initiate a network request which returns a list of data.

  • Transform data: Parse the server’s response into a POJO.

  • Connect UI: Explicitly update the UI elements with the new data.

  • Handle states: Handle loading and error states manually for every scenario.

Reactive Approach

A reactive approach simplifies this process by establishing a declarative data flow. Here’s how it works:

  • Define a data source: Establish a data flow that emits new lists of data as they become available from the server.

  • Transform data: Apply transformations to the data flow as needed to reshape the data according to your application’s requirements.

  • Connect UI: Create observers (e.g: using collectAsState in Jetpack Compose) that automatically update corresponding UI elements whenever new data is emitted.

  • Handle states: The data flow inherently manages loading and error states, allowing the UI to represent these conditions without additional complexity.

RxKotlin, Reactor-Kotlin, and Arrow-kt offer powerful reactive capabilities. However, we’ll focus on the simplicity of Kotlin Flows (by JetBrains), diving into code examples to bring these theories to life.

Case Study

Use-case: Filterable search with live results.

Details: Implement a search feature for an e-commerce app where results needs to meet below conditions:

  • Update in realtime as the user types.

  • Potentially fetch results either from local database, or network API.

  • Apply filtering or sorting logic as selected by the user.

First let’s add sealed interface which represents 3 states:

sealed interface SearchResultUiStates {
    data object Loading : SearchResultUiStates

    data class Error(val message: String = "") : SearchResultUiStates {
        fun errorMessage(): String {
            return "Found error due to: $message"
        }
    }

    data class Success(val data: List<Product> = emptyList()) : SearchResultUiStates
}

Followed by adding logic to ViewModel

class SearchViewModel {
    private val _searchQuery = MutableStateFlow("")
    private val _searchResultUiStates = MutableStateFlow(SearchResultUiStates.Loading)
    val searchResultUiStates = _searchResultUiStates.asStateFlow()

    init {
        searchQueryFlow
        .debounce(300)
        .flatMapLatest { query -> 
            combine(localDataSource.search(query), remoteDataSource.search(query)) 
                { localResults, networkResults ->
                    // apply logic on both results. Emit single data flow.
                }
        }
        .collect { error -> 
                // collecting the end reuslt data list
                _searchResultUiStates.update { SearchResultUiStates.Success(data = result) }
            }
        .catch { result ->
                // collecting the end reuslt data list
                _searchResultUiStates.update {
                    SearchResultUiStates.Error(message = e.message ?: "Unknown error")
                }
            }
    }

    fun setSearchQuery(query: String) {
        _searchQuery.value = query
    }
}
  • To improve performance we have used debounce of 300ms with search query. This limits excessive requests when the user is still type in search box.

  • We used flatMapLatest & combine to transform each debounced incoming query into a separate flow.
    When we speak separate transformed flow, as you can see in the code above we are using the query as argument to make local & network requests, and getting individual results as another flow.
    We are merging the flow results and on top business logic can be added.

  • Finally we are converting the result flow into StateFlow, which can be collected by UI layer.

  • With the help of SearchResultUiStates, we are able to model various state of search operation.

Finally, collect the state in UI

@Composable
fun SearchBox(searchViewModel: SearchViewModel = hiltViewModel()) {
    val searchResultUiState by searchViewModel.searchResultUiState.collectAsStateWithLifecycle()
    // handle searchResultUiState
}

I hope this lights a fire for your reactive programming adventures! The potential is huge, let’s explore what you can create.

Do share your experiences, questions, and let’s learn together!

Thank you for reading! ✨

Did you find this article valuable?

Support Ankush Bose by becoming a sponsor. Any amount is appreciated!