Introduction to flows

The Kotlin version of the Home APIs uses flows, a feature of the Kotlin language that provides powerful capabilities for handling asynchronous data streams, including transforming, filtering, mapping, converting to and from collections, and so forth.

Coroutines are closely associated with flows, and provide an elegant abstraction for writing asynchronous code and free the developer from having to explicitly write callback routines. Flows work hand-in-hand with coroutines, serving as a way to retrieve data from coroutines asynchronously, without having to wait for the completion of a function or a thread.

Data is retrieved from a flow by the consumer in a process called collecting. Kotlin flows have a collect() function that may be used for this purpose. What happens in a flow during collection depends on the type of flow.

Kotlin flows (Flow<T>) are cold by default, meaning that the flow builder code produces data only when collect() is called. A hot flow, by contrast, produces data immediately, buffering it in memory like a queue for whenever the data is consumed. Because it maintains data in memory, a hot flow is considered to be stateful.

Flows may be converted from cold to hot using the shareIn operator (see Making cold flows hot using shareIn ). You can also use the SharedFlow or StateFlow to turn a cold flow into a hot flow.

How the sample app uses flows

The sample app uses view models in Jetpack Compose connected to the flows in the Home APIs. Many of the sample app UI elements are driven by state, but can be interacted with. The sample app also blends state from devices with an 'optimistic' state in the UI for a realtime user experience. The term optimistic means that the the app assumes that a given action succeeded and immediately updates the UI to reflect the expected outcome without waiting for confirmation. If it turns out that the action failed, the UI is updated to reflect the true state.

In the sample app, flows are created for each layer of the view model (structures, rooms, devices). For example, it does this for structures with the following call in GlobalViewModel.kt:

  fun getStructuresState(): StateFlow<List<StructureModel>?> =
    homeClient
      .structures()
      .map { structures -> structures.map { StructureModel(it.name, it) }.sortedBy { it.name } }
      .handleErrors()
      .flowOn(Dispatchers.IO)
      .stateIn(scope = viewModelScope, started = SharingStarted.WhileSubscribed(), null)

emitAll() collects all the values from the given flow and emits them to the collector. stateIn() shares the most recently emitted value from a single running instance of the upstream flow with multiple downstream subscribers. See the kotlinx.coroutines.flow reference for more information.

Jetpack Compose

To store flow objects in local memory and prevent them from being terminated, use the Kotlin remember API.

In Jetpack Compose, if you use this with collectAsStateWithLifecycle(), Jetpack automatically manages subscribing and unsubscribing from flows based on whether the application UI showing that state is actually in the foreground or not.

A simple call in the sample app does this. Using the getStructuresState() function shown earlier:

val structuresList by
    remember(globalViewModel) { globalViewModel.getStructuresState() }.collectAsStateWithLifecycle()

Now, when any state for the structure changes (such as the name), the Composable function that uses it will reflect that updated state automatically. In the sample app, this is the HomeActivityContent() function.

Resources

For more information on Kotlin, flows, coroutines, and Jetpack Compose, see the following resources: