Concurrency with Kotlin Flow

Jun 5 2024 · Kotlin 1.9.20, Android 14, Android Studio Iguana

Lesson 01: Learn Reactive Programming

Reactive Demo

Episode complete

Play next episode

Next
Transcript

In this demo, you’ll take a quick look at the difference between imperative programming and reactive programming. Start Android Studio and open the 01-learn-reactive-programming/Starter folder.

Before making any changes, get familiar with the app you’ll build. Run the app and check what’s been prepared for you. You’ll work on a streaming app. It currently displays six movie categories that you can scroll through. At the top, you’ll also see category tags that let you quickly access your favorite categories. And in the top-right corner, you have a button that takes you to a movie ratings screen. You’ll work on tags and the movie ratings screen in later lessons. In this lesson, you’ll focus on the Home screen.

Open HomeViewModel.kt. HomeViewModel has two dependencies: MovieRepository and HomeView. It uses MovieRepository to fetch data and HomeView to render it. Usually, in the MVVM architecture, a ViewModel doesn’t have a reference to the View. In this case, HomeView is used to make it easier to explain the concept of imperative programming.

Get a better look at renderView():

class HomeViewModel {

  // ...

  private fun renderView() {
    viewModelScope.launch {
      homeView.showLoading()

      val favoriteCategories = movieRepository.favoriteCategories()

      // Filter/Map categories if needed

      val moviesByCategories = movieRepository.fetchMoviesByCategorySuspending()

      // Filter/Map movies if needed

      homeView.hideLoading()
      homeView.renderData(favoriteCategories, moviesByCategories)
    }
  }
}

You can probably see a resemblance to the cooking app code snippet shown previously. There are clear steps written in an imperative way on what needs to happen to display the data. You need to:

  1. Show loading indicator
  2. Fetch categories
  3. Fetch movies
  4. Hide loading indicator
  5. Finally, render the data

This whole operation is being triggered from init() by explicitly calling renderView().

Now, you’ll convert this ViewModel to be more reactive. The ViewModel’s main task is to get the data to the View. The View will get the data by subscribing to a data stream in the ViewModel. So, the first thing you’ll do is expose the data for the View.

Add category and movie data streams:

private val _categories: MutableStateFlow<List<MovieCategory>> = MutableStateFlow(emptyList())
val categories: Flow<List<MovieCategory>> = _categories

private val _moviesByCategories: MutableStateFlow<Map<String, List<Movie>>> = MutableStateFlow(emptyMap())
val moviesByCategories: Flow<Map<String, List<Movie>>> = _moviesByCategories

Here, you used MutableStateFlow to create a reactive data stream that you can subscribe to. For now, don’t bother too much with the details behind it. It’s enough to know that this creates a reactive data stream that you can emit data to. For both data streams, you also created a public property that allows your View to subscribe to those reactive streams.

Next, you need to add logic that will push some data to those streams. Add two functions, one for categories and one for movies:

private fun fetchMoviesByCategories() {
  viewModelScope.launch {
    val moviesByCategories = movieRepository.fetchMoviesByCategorySuspending()
    _moviesByCategories.emit(moviesByCategories)
  }
}

private fun fetchFavoriteCategories() {
  viewModelScope.launch {
    val favoriteCategories = movieRepository.favoriteCategories()
    _categories.emit(favoriteCategories)
  }
}

These functions, when invoked, will fetch movies and categories and push them to mutable streams you created in the previous step.

Notice you had to launch a coroutine to emit to Flow. Kotlin Flow is built on top of Kotlin coroutines. It uses coroutines for its asynchronous operations, making it a natural extension for handling streams of data that can emit multiple values over time within the coroutine framework.

The last thing to do in HomeViewModel.kt is to remove the old imperative code:

  1. Remove HomeView from the ViewModel’s dependencies.
  2. Remove renderView().
  3. Replace init() with the following code:
init {
  fetchMoviesByCategories()
  fetchFavoriteCategories()
}

ViewModel now does the same thing as before — but in a reactive way. The remaining work now consists of removing the rest of the old code before you can test the app.

Next, open FlixFlowApplication.kt and update the Koin definition for HomeViewModel() so it contains one less dependency:

override fun onCreate() {
  super.onCreate()

  startKoin {
    androidContext(this@FlixFlowApplication)
    modules(
      module {
        // ...
        viewModel { HomeViewModel(get()) } // Changed
        // ...
      }
    )
  }
}

Next, open MainActivity.kt, remove the homeView property, and also remove the homeView parameter passed to the HomeScreen composable. And finally, in HomeScreen.kt, remove HomeView from its dependencies and update the code that will subscribe the View to the ViewModel:

@Composable
fun HomeScreen(
  onRatingsClicked: () -> Unit,
  onCategoryClicked: (String) -> Unit,
  onMovieClicked: (String) -> Unit
) {
  val viewModel = koinViewModel<HomeViewModel>()

  val categories by viewModel.categories.collectAsState(initial = emptyList())
  val moviesByCategories by viewModel.moviesByCategories.collectAsState(initial = emptyMap())

  // The rest of the function
  // ...
}

Excellent. Run the app again; you’ll see it behaves the same way as before, but your View and ViewModel are implemented in a reactive way.

That ends this demo. Continue with the lesson for a summary.

See forum comments
Cinema mode Download course materials from Github
Previous: The Fundamentals of Reactive Programming Next: Conclusion