How to safely (lifecycle aware) .collectAsState() a StateFlow? - android

I'm trying to follow the official guidelines to migrate from LiveData to Flow/StateFlow with Compose, as per these articles:
A safer way to collect flows from Android UIs
Migrating from LiveData to Kotlin’s Flow
I am trying to follow what is recommended in the first article, in the Safe Flow collection in Jetpack Compose section near the end.
In Compose, side effects must be performed in a controlled
environment. For that, use LaunchedEffect to create a coroutine that
follows the composable’s lifecycle. In its block, you could call the
suspend Lifecycle.repeatOnLifecycle if you need it to re-launch a
block of code when the host lifecycle is in a certain State.
I have managed to use .flowWithLifecycle() in this way to make sure the flow is not emmiting when the app goes to the background:
#Composable
fun MyScreen() {
val lifecycleOwner = LocalLifecycleOwner.current
val someState = remember(viewModel.someFlow, lifecycleOwner) {
viewModel.someFlow
.flowWithLifecycle(lifecycleOwner.lifecycle, Lifecycle.State.STARTED)
.stateIn(
scope = viewModel.viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = null
)
}.collectAsState()
}
I find this very "boilerplatey" -there must be something better. I would like to have StateFlow in the ViewModel, instead of Flow that gets converted to StateFLow in the #Composable, and use .repeatOnLifeCycle(), so I can use multiple .collectAsState() with less boilerplate.
When I try to use .collectAsState() inside a coroutine (LaunchedEffect), I obviously get an error about .collectAsState() having to be called from the context of #Composable function.
How can I achieve similar functionality as with .collectAsState(), but inside .repeatOnLifecycle(). Do I have to use .collect() on the StateFlow and then wrap the value with State? Isn't there anything with less boilerplate than that?

From "androidx.lifecycle:lifecycle-runtime-compose:2.6.0-alpha01" you can use the collectAsStateWithLifecycle() extension function to collect from flow/stateflow and represents its latest value as Compose State in a lifecycle-aware manner.
import androidx.lifecycle.compose.collectAsStateWithLifecycle
#Composable
fun MyScreen() {
val state by viewModel.state.collectAsStateWithLifecycle()
}
Source: Android Lifecycle release

After reading a few more articles, including
Things to know about Flow’s shareIn and stateIn operators
repeatOnLifecycle API design story
and eventually realising that I wanted to have the StateFlow in the ViewModel instead of within the composable, I came up with these two solutions:
1. What I ended up using, which is better for multiple StateFlows residing in the ViewModel that need to be collected in the background while there is a subscriber from the UI (in this case, plus 5000ms delay to deal with configuration changes, like screen rotation, where the UI is still interested in the data, so we don't want to restart the StateFlow collecting routine). In my case, the original Flow is coming from Room, and been made a StateFlow in the VM so other parts of the app can have access to the latest data.
class MyViewModel: ViewModel() {
//...
val someStateFlow = someFlow.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = Result.Loading()
)
val anotherStateFlow = anotherFlow.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = Result.Loading()
)
//...
}
Then collected in the UI:
#Composable
fun SomeScreen() {
var someUIState: Any? by remember { mutableStateOf(null)}
var anotherUIState: Any? by remember { mutableStateOf(null)}
LaunchedEffect(true) {
lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
launch {
viewModel.someStateFlow.collectLatest {
someUIState = it
}
}
launch {
viewModel.anotherStateFlow.collectLatest {
anotherUIState = it
}
}
}
}
}
2. An extension function to alleviate the boilerplate when collecting a single StateFlow as State within a #Composable. This is useful only when we have an individual HOT flow that won't be shared with other Screens/parts of the UI, but still needs the latest data at any given time (hot flows like this one created with the .stateIn operator will keep collecting in the background, with some differences in behaviour depending on the started parameter). If a cold flow is enough for our needs, we can drop the .stateIn operator together with the initial and scope parameters, but in that case there's not so much boilerplate and we probably don't need this extension function.
#Composable
fun <T> Flow<T>.flowWithLifecycleStateInAndCollectAsState(
scope: CoroutineScope,
initial: T? = null,
context: CoroutineContext = EmptyCoroutineContext,
): State<T?> {
val lifecycleOwner = LocalLifecycleOwner.current
return remember(this, lifecycleOwner) {
this
.flowWithLifecycle(
lifecycleOwner.lifecycle,
Lifecycle.State.STARTED
).stateIn(
scope = scope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = initial
)
}.collectAsState(context)
}
This would then be used like this in a #Composable:
#Composable
fun SomeScreen() {
//...
val someState = viewModel.someFlow
.flowWithLifecycleStateInAndCollectAsState(
scope = viewModel.viewModelScope //or the composable's scope
)
//...
}

Building upon OP's answer, it can be a bit more light-weight by not going through StateFlow internally, if you don't care about the WhileSubscribed(5000) behavior.
#Composable
fun <T> Flow<T>.toStateWhenStarted(initialValue: T): State<T> {
val lifecycleOwner = LocalLifecycleOwner.current
return produceState(initialValue = initialValue, this, lifecycleOwner) {
lifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
collect { value = it }
}
}
}

Related

android compose: remember failed with destructuring pattern

I'm trying to get specific behavior with focus and so use something like this :
val (focusA, focusB) = remember { FocusRequester.createRefs() }
And since i didn't get the correct behavior, start to investigate and the destructuring pattern with remember is the problem.
If you try this (this is what is it done under the hood of FocusRequester.createRefs()):
` class MyClass
object MyClassFactory{
operator fun component1() = MyClass()
operator fun component2() = MyClass()
}
fun createRefs() = MyClassFactory
#Composable
private fun ContentBody() {
val (a, b) = remember {
createRefs()
}
Log.d(">>:a", "${a.hashCode()}")
Log.d(">>:b", "${b.hashCode()}")
}
`
You will realise that a and b are new instance each time there is a recomposition.
Does any one have some information about that? Why remember fail with destructuring pattern. We can see many time this pattern (i use it with constraint layout for example), and according to that, it is a complete failure because each time a new instance are created...
What I'm doing wrong? I solved all my problem by using a remember without destructuring.
Thank.

Jetpack compose api call with viewModel in infinite loop

i'am trying to build an app with jetpack compose but when it's come to api call with view model i get an infinite loop. the app keep calling the api and i don't get why. here is my viewmodel :
class LibraryViewModel() : ViewModel() {
var library: ArrayList<PKIssue> = arrayListOf()
var loadLibrary by mutableStateOf(false)
init {
getLibrary()
}
fun getLibrary(){
viewModelScope.launch {
Press.issues(
result = object : result<ArrayList<Issue>, Error> {
override fun succeed(result: ArrayList<Issue>?) {
loadLibrary = true
if (result != null) {
library = result
}
}
override fun failed(error: Error?) {
loadLibrary = false
}
})
}
}
But as soon as i init my viewModel i get infinite call to my api, here is how i try to declare it :
#SuppressLint("StateFlowValueCalledInComposition")
#Destination
#Composable
fun HomeScreen(
navigator: DestinationsNavigator,
libraryViewModel: LibraryViewModel = LibraryViewModel()
) {
or inside the composable : val libraryViewModel = LibraryViewModel() but i get the same problem, i am i missing something ? it seem that it wait the end of the api call to put loadLibrary at true but in the mean time it keep call getLibrary() in loop. Thanks for helping
When you use
libraryViewModel: LibraryViewModel = LibraryViewModel()
You are directly constructing a brand new instance of your LibraryViewModel every time that method recomposes. Since you probably read your loadLibrary value once getLibrary returns, that causes a recomposition of that method, hence the infinite loop (as the recomposition again causes another brand new instance to be created...which kicks off a load...which causes another recomposition).
Instead, you should be following the documentation on using ViewModels with Compose:
Add the androidx.lifecycle:lifecycle-viewmodel-compose:2.5.1 dependency to your build.gradle file
Use the viewModel() method to instantiate your ViewModel.
fun HomeScreen(
navigator: DestinationsNavigator,
libraryViewModel: LibraryViewModel = viewModel()
) {
The viewModel() method is what actually causes your ViewModel to be cached and stored across recompositions, navigating to a different screen, and across configuration changes. Since by using that method you'll only have a single instance of that ViewModel, you won't run into the same infinite loop.

What/Where is the best way to handle with states on Jetpack Compose?

I've seen some Jetpack Compose projects and I've seen two types of managing states, not realizing which one is better.
For example, let's assume: the input state. I've seen people manage this state in the UI, using remember to save the state of the value.
Another way I've seen is to create this mutableState in the ViewModel and store/use it from there. What's the best way to do this?
In addition to #Thracian's answer.
Let me share my thought process based on my current level of experience in Jetpack Compose. Just a disclaimer, I'm still in the learning curve.
IMO, theres no such thing as "best", things in our field evolves, what might be considered "best" today may become obsolete tomorrow, but there are certain practices that are "recommended", approved and adopted by the community which might save you from dealing with some pitfalls (e.g unwanted re-compositions, infinite navhost calls( you already dealt with this) etc..), but its up to you if you will follow it or not.
So what your'e trying to understand is called State Hoisting. The way I could explain this is by just simply sampling a scenario (again this is based on my own experience with how I apply my knowledge in Jetpack Compose).
Consider a Login use-case with 3 different levels of complexity
A Login UI prototype : — Just showcasing your potential Login Screen design and user interaction
Login UI Mock-up : — With a bit of validation and some toast showing a negative scenario, just an advance version of the prototype
A fully working Login module — where you have to construct view models, bind things to lifecycles, perform concurrent operations etc..
At this point, you already have an idea the different levels of state management based on the use-case above.
For a Login prototype, I won't be needing a state class or a view model, since its just a prototype
#Composable
fun LoginScreen() {
val userName by remember { <mutable string state username> }
val password by remember { <mutable string state password> }
Column {
Text(text = username)
Text(text = password)
Button("Login")
}
}
and because its a very simple UI(composable), I only need to specify basic structure of a composable using remember + state, showcasing an input is happening.
For the Login mock-up with simple validation, we utilized the recommended state hoisting using a class,
class LoginState {
var event;
var mutableUserNameState;
var mutablePasswordState;
fun onUserNameInput() {...}
fun onPasswordInput() {...}
fun onValidate() {
if (not valid) {
event.emit(ShowToast("Not Valid"))
} else {
event.emit(ShowToast("Valid"))
}
}
}
#Composable
fun LoginScreen() {
val loginState by remember { LoginState }
LaunchedEffect() {
event.observe {
it.ShowToast()
}
}
Column {
Text(text = loginState.mutableUserNameState, onInput = { loginState.onUserNameInput()} )
Text(text = loginState.mutablePasswordState, onInput = { loginState.onPasswordInput()} )
Button(loginState.onValidate)
}
}
Now for a full blown Login Module, where your'e also taking lifecylce scopes into consideration
class LoginViewModel(
val userRepository: UserRepository // injected by your D.I framework
): ViewModel {
var event;
var mutableUserNameState;
var mutablePasswordState;
fun onUserNameInput() {...}
fun onPasswordInput() {...}
fun onValidateViaNetwork() {
// do a non-blocking call to a server
viewModelScope.launch {
var isUserValid = userRepository.validate(username, password)
if (isUserValid) {
event.emit(ShowToast("Valid"))
} else {
event.emit(ShowToast("Not Valid"))
}
}
}
}
#Composable
fun LoginScreen() {
val userNameState by viewModel.mutableUserNameState
val passwordState by viewModel.mutablePasswordState
LaunchedEffect() {
event.observe {
it.ShowToast()
}
}
Column {
Text(text = userNameState, onInput = { viewModel.onUserNameInput()} )
Text(text = passwordState, onInput = { viewModel.onPasswordInput()} )
Button(viewModel.onValidateViaNetwork)
}
}
Again, this is just based on my experience and how I decide on hoisting my states. As for the snippets I included, I tried to make them as pseudo as possible without making them look out of context so they are not compilable. Also mock and prototype are considered the same, I just used them in conjunction to put things into context.
It depends on your preference. Using states inside a Composable if you are building a standalone Composable or a library is preferred. Any class you see with rememberXState() keeps state variable. For instance scrollState()
#Composable
fun rememberScrollState(initial: Int = 0): ScrollState {
return rememberSaveable(saver = ScrollState.Saver) {
ScrollState(initial = initial)
}
}
#Stable
class ScrollState(initial: Int) : ScrollableState {
/**
* current scroll position value in pixels
*/
var value: Int by mutableStateOf(initial, structuralEqualityPolicy())
private set
// rest of the code
}
This is a common approach in Jetpack Compose. I use this approach in libraries i build, for instance in this image crop library, i keep state and Animatable. Animatable which is low level default animation class also has hold its own states.
#Suppress("NotCloseable")
class Animatable<T, V : AnimationVector>(
initialValue: T,
val typeConverter: TwoWayConverter<T, V>,
private val visibilityThreshold: T? = null
) {
internal val internalState = AnimationState(
typeConverter = typeConverter,
initialValue = initialValue
)
/**
* Current value of the animation.
*/
val value: T
get() = internalState.value
/**
* Velocity vector of the animation (in the form of [AnimationVector].
*/
val velocityVector: V
get() = internalState.velocityVector
/**
* Returns the velocity, converted from [velocityVector].
*/
val velocity: T
get() = typeConverter.convertFromVector(velocityVector)
/**
* Indicates whether the animation is running.
*/
var isRunning: Boolean by mutableStateOf(false)
private set
/**
* The target of the current animation. If the animation finishes un-interrupted, it will
* reach this target value.
*/
var targetValue: T by mutableStateOf(initialValue)
private set
}
and so on. This approach is doing for ui components that don't involve business logic but Ui logic.
When you need to update your Ui based on business logic like search or getting results from an API you should use a Presenter class which can be ViewModel too.
Last but least people are now questioning whether there should be a ViewModel with Jetpack Compose since we can use states with an AAC ViewModel. And cashapp introduced molecule library, you can check it out either.
Also this link about state holders is good source to read

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

Jetpack Compose State Hoisting, Previews, and ViewModels best practices

So it seems like the recommended thing in Jetpack Compose is to hoist state out of your composables, to make them stateless, reusable, and testable, and allow using them in previews easily.
So instead of having something like
#Composable
fun MyInputField() {
var text by remember { mutableStateOf("") }
TextField(value = text, onValueChange = { text = it })
}
You'd hoist the state, like this
#Composable
fun MyInputField(text: String, onTextChange: (String) -> Unit) {
TextField(value = text, onValueChange = onTextChange)
}
This is fine, however what of some more complex uses?
Let's pretend I have a screen represented by a composable, with multiple interactions between the View and the ViewModel. This screen is split into multiple inner composable (think for instance one for a header, one for the body, which in turn is split into several smaller composables)
You can't create a ViewModel (with viewModel() at least, you can instantiate one manually) inside a composable and use this composable in a Preview (previews don't support creating viewmodel like this)
Using a ViewModel inside the inner composables would make them stateful, wouldn't it ?
So the "cleanest" solution I see, would be to instantiate my viewmodel only at the highest composable level, and then pass to the children composables only vals representing the state, and callbacks to the ViewModel functions.
But that's wild, I'm not passing down all my ViewModel state and functions through individual parameters to all composables needing them.
Grouping them in a data class for example could be a solution
data class UiState(
val textInput: String,
val numberPicked: Int,
……
and maybe create another one for callbacks ?
But that's still creating a whole new class just to mimic what the viewmodel already has.
I don't actually see what the best way of doing this could be, and I find nothing about that anywhere
A good way to manage complex states is to encapsulate required complex behavior into a class and use remember function while having stateless widgets as most as you can and change any properties of state whenever it's required.
SearchTextField is a component that uses only state hoisting, SearchBar has back arrow and SearchTextField and also itself is a stateless composable. Communication between these two and parent of Searchbar is handled via callback functions only which makes both SearchTextField re-suable and easy to preview with a default state in preview. HomeScreen contains this state and where you manage changes.
Full implementation is posted here.
#Composable
fun <R, S> rememberSearchState(
query: TextFieldValue = TextFieldValue(""),
focused: Boolean = false,
searching: Boolean = false,
suggestions: List<S> = emptyList(),
searchResults: List<R> = emptyList()
): SearchState<R, S> {
return remember {
SearchState(
query = query,
focused = focused,
searching = searching,
suggestions = suggestions,
searchResults = searchResults
)
}
}
remember function to keep state for this only to be evaluated during the composition.
class SearchState<R, S>(
query: TextFieldValue,
focused: Boolean,
searching: Boolean,
suggestions: List<S>,
searchResults: List<R>
) {
var query by mutableStateOf(query)
var focused by mutableStateOf(focused)
var searching by mutableStateOf(searching)
var suggestions by mutableStateOf(suggestions)
var searchResults by mutableStateOf(searchResults)
val searchDisplay: SearchDisplay
get() = when {
!focused && query.text.isEmpty() -> SearchDisplay.InitialResults
focused && query.text.isEmpty() -> SearchDisplay.Suggestions
searchResults.isEmpty() -> SearchDisplay.NoResults
else -> SearchDisplay.Results
}
}
And change state in any part of UI by passing state to other composable or by ViewModel as
fun HomeScreen(
modifier: Modifier = Modifier,
viewModel: HomeViewModel,
navigateToTutorial: (String) -> Unit,
state: SearchState<TutorialSectionModel, SuggestionModel> = rememberSearchState()
) {
Column(
modifier = modifier.fillMaxSize()
) {
SearchBar(
query = state.query,
onQueryChange = { state.query = it },
onSearchFocusChange = { state.focused = it },
onClearQuery = { state.query = TextFieldValue("") },
onBack = { state.query = TextFieldValue("") },
searching = state.searching,
focused = state.focused,
modifier = modifier
)
LaunchedEffect(state.query.text) {
state.searching = true
delay(100)
state.searchResults = viewModel.getTutorials(state.query.text)
state.searching = false
}
when (state.searchDisplay) {
SearchDisplay.InitialResults -> {
}
SearchDisplay.NoResults -> {
}
SearchDisplay.Suggestions -> {
}
SearchDisplay.Results -> {
}
}
}
}
Jetmagic is an open source framework that deals exactly with this issue while also solving other major issues that Google neglected when developing Compose. Concerning your request, you don't pass in viewmodels at all as parameters. Jetmagic follows the "hoisted state" pattern, but it manages the viewmodels for you and keeps them associated with your composables. It treats composables as resources in a way that is similar to how the older view system treats xml layouts. Instead of directly calling a composable function, you ask Jetmagic's framework to provide you with an "instance" of the composable that best matches the device's configuration. Keep in mind, under the older xml-based system, you could effectively have multiple layouts for the same screen (such as one for portrait mode and another for landscape mode). Jetmagic picks the correct one for you. When it does this, it provides you with an object that it uses to manage the state of the composable and it's related viewmodel.
You can easily access the viewmodel anywhere within your screen's hierarchy without the need to pass the viewmodel down the hierarchy as parameters. This is done in part using CompositionLocalProvider.
Jetmagic is designed to handle the top-level composables that make up your screen. Within your composable hierarchy, you still call composables as you normally do but using state hoisting where it makes sense.
The best thing is to download Jetmagic and try it out. It has a great demo that illustrates the solution you are looking for:
https://github.com/JohannBlake/Jetmagic

Categories

Resources