Search code examples
androidandroid-jetpack-composekotlin-coroutineskotlin-stateflow

Collect transformed StateFlow in Composable


There is function collectAsState() applicable to a StateFlow property in order to observe it in a Composable.

A composable requires a StateFlow because StateFlow guarantees an initial value. A Flow doesn't come with that guarantee.

Now, what is the way to go if I have a StateFlow property but I want to apply an operator (like map) before collecting the Flow in the Composable?

Here an example:

Let's say a repository exposes a StateFlow<MyClass>

val myClassStateFlow: StateFlow<MyClass>

data class MyClass(val a: String)

... and a view model has a dependency on the repository and wants to expose only the property a to its Composable...

val aFlow = myClassState.Flow.map { it.a } // <- this is of type Flow<String>

The map operator changes the type from StateFlow<MyClass> to Flow<String>.

  1. Is it semantically justified that aFlow has no initial value anymore? After all its first emission is derived from the initial value of myClassStateFlow.
  2. It's required to convert Flow back into StateFlow at some point. Which is the more idiomatic place for this?
    1. In the view model using stateIn()? How would the code look like?
    2. In the composable using collectAsState(initial: MyClass) and come up with an initial value (although myClassStateFlow had an initial value)?

Solution

  • See this issue on GitHub

    Currently there is no built-in way to transform StateFlows, only Flows. But you can write your own.

    Way I ended up solving was to use the example in that post.

    First create a notion of a DerivedStateFlow.

    class DerivedStateFlow<T>(
        private val getValue: () -> T,
        private val flow: Flow<T>
    ) : StateFlow<T> {
    
        override val replayCache: List<T>
            get () = listOf(value)
    
        override val value: T
            get () = getValue()
    
        @InternalCoroutinesApi
        override suspend fun collect(collector: FlowCollector<T>) {
            flow.collect(collector)
        }
    }
    
    

    Then have an extension on StateFlow like the current map extension on Flow

    fun <T1, R> StateFlow<T1>.mapState(transform: (a: T1) -> R): StateFlow<R> {
        return DerivedStateFlow(
            getValue = { transform(this.value) },
            flow = this.map { a -> transform(a) }
        )
    }
    

    Now in your Repository or ViewModel, you can use it as below.

    
    class MyViewModel( ... ) {
        private val originalStateFlow:StateFlow<SomeT>  = ...
        
        val someStateFlowtoExposeToCompose = 
            originalStateFlow
            .mapState { item -> 
                yourTransform(item)
            }
    }
    

    Now you can consume it as you expect in Compose without any special work, since it returns a StateFlow.