android compose: remember failed with destructuring pattern - android

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.

Related

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

How to update properties of data class show compose ui can observe the changes

I have a CounterScreenUiState data class with a single property called counterVal (integer). If I am updating the value of my counter from viewModel which of the following is the correct approach?
Approach A:
data class CounterUiState(
val counterVal: Int = 0,
)
class CounterViewModel : ViewModel() {
var uiState by mutableStateOf(CounterUiState())
private set
fun inc() {
uiState = uiState.copy(counterVal = uiState.counterVal + 1)
}
fun dec() {
uiState = uiState.copy(counterVal = uiState.counterVal - 1)
}
}
or
Approach B:
data class CounterUiState(
var counterVal: MutableState<Int> = mutableStateOf(0)
)
class CounterViewModel : ViewModel() {
var uiState by mutableStateOf(CounterUiState())
private set
fun inc() {
uiState.counterVal.value = uiState.counterVal.value + 1
}
fun dec() {
uiState.counterVal.value = uiState.counterVal.value - 1
}
}
For the record, I tried both approach and both works well without unnecessary re-compositions.
Thanks in Advance!!!
So to summarize, "implementation" and "performance" wise, your'e only
choice is A.
This is not true. It's a common pattern that is used other Google's sample apps, JetSnack for instance, and default functions like rememberScrollable or Animatable are the ones that come to my mind. And in that article it's also shared as
#Stable
class MyStateHolder {
var isLoading by mutableStateOf(false)
}
or
#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
}
Animatable class
class Animatable<T, V : AnimationVector>(
initialValue: T,
val typeConverter: TwoWayConverter<T, V>,
private val visibilityThreshold: T? = null,
val label: String = "Animatable"
) {
internal val internalState = AnimationState(
typeConverter = typeConverter,
initialValue = initialValue
)
/**
* Current value of the animation.
*/
val value: T
get() = internalState.value
/**
* 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
}
Omitted some code from Animatable for simplicity but as can be seen it's a common pattern to use a class that hold one or multiple MutableStates. Even type AnimationState hold its own MutableState.
You can create state holder classes and since these are not e not variables but states without them changing you won't have recompositions unless these states change. The thing needs to be changed with option B is instead of using
data class CounterUiState(
var counterVal: MutableState<Int> = mutableStateOf(0)
)
You should change it to
class CounterUiState(
var counterVal by mutableStateOf(0)
)
since you don't need to set new instance of State itself but only the value.
And since you already wrap your states inside your uiState there is no need to use
var uiState by mutableStateOf(CounterUiState())
private set
you can have this inside your ViewModel as
val uiState = CounterUiState()
or inside your Composable after wrapping with remember
#Composable
fun rememberCounterUiState(): CounterUiState = remember {
CounterUiState()
}
With this pattern you can store States in one class and hold variables that should not trigger recomposition as part of internal calculations and it's up to developer expose these non-state variables based on the design.
https://github.com/android/compose-samples/blob/main/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/search/Search.kt
#Stable
class SearchState(
query: TextFieldValue,
focused: Boolean,
searching: Boolean,
categories: List<SearchCategoryCollection>,
suggestions: List<SearchSuggestionGroup>,
filters: List<Filter>,
searchResults: List<Snack>
) {
var query by mutableStateOf(query)
var focused by mutableStateOf(focused)
var searching by mutableStateOf(searching)
var categories by mutableStateOf(categories)
var suggestions by mutableStateOf(suggestions)
var filters by mutableStateOf(filters)
var searchResults by mutableStateOf(searchResults)
val searchDisplay: SearchDisplay
get() = when {
!focused && query.text.isEmpty() -> SearchDisplay.Categories
focused && query.text.isEmpty() -> SearchDisplay.Suggestions
searchResults.isEmpty() -> SearchDisplay.NoResults
else -> SearchDisplay.Results
}
}
Also for skippibility
Compose will treat your CounterUiState as unstable and down the road
it will definitely cause you headaches because what ever you do,
This is misleading. Most of the time optimizing for skippability is premature optimization as mentioned in that article and the one shared by originally Chris Banes.
Should every Composable be skippable? No.
Chasing complete skippability for every composable in your app is a
premature optimization. Being skippable actually adds a small overhead
of its own which may not be worth it, you can even annotate your
composable to be non-restartable in cases where you determine that
being restartable is more overhead than it’s worth. There are many
other situations where being skippable won’t have any real benefit and
will just lead to hard to maintain code. For example:
A composable that is not recomposed often, or at all.

Koltin Flow flatMapLatest to combineTransform Using Multiple StateFlows

I have the below working code which uses a dropdown to update the satusFilterFlow to allow for the filtering of characters through the getCharacterList call. The getCharacterList call uses the jetpack paging and returns Flow<PagerData<Character>>.
private val statusFilterFlow = MutableStateFlow<StatusFilter>(NoStatusFilter)
// private val searchFilterFlow = MutableStateFlow<SearchFilter>(NoSearchFilter)
val listData: LiveData<PagingData<Character>> =
statusFilterFlow.flatMapLatest{ statusFilter ->
characterRepository.getCharacterList(null, statusFilter.status)
.cachedIn(viewModelScope)
.flowOn(Dispatchers.IO)
}.asLiveData()
Given the above working solution, what is the correct flow extension to allow for me to add multiple StateFlows as I build out additional filters (e.g. SearchFilter).
I have tried combineTransorm as follows:
private val statusFilterFlow = MutableStateFlow<StatusFilter>(NoStatusFilter)
private val searchFilterFlow = MutableStateFlow<SearchFilter>(NoSearchFilter)
val listData: LiveData<PagingData<Character>> =
statusFilterFlow.combineTransform(searchFilterFlow) { statusFilter, searchFilter ->
characterRepository.getCharacterList(searchFilter.search, statusFilter.status)
.flowOn(Dispatchers.IO)
.cachedIn(viewModelScope)
}.asLiveData()
However, this gives me a "Not enough information to infer type variable R" error.
The usual way to understand and/or fix those errors is to specify types explicitly in the function call:
statusFilterFlow.combineTransform<StatusFilter, SearchFilter, PagingData<Character>>(searchFilterFlow) { ... }
This is orthogonal to the problem at hand, but I'd also suggest using the top-level combineTransform overload that takes all source flows as argument (instead of having the first one as receiver), so there is a better symmetry. Since I believe there is no reason one of the filters is more special than the other.
All in all, this gives:
val listData: LiveData<PagingData<Character>> =
combineTransform<StatusFilter, SearchFilter, PagingData<Character>>(statusFilterFlow, searchFilterFlow) { statusFilter, searchFilter ->
characterRepository.getCharacterList(searchFilter.search, statusFilter.status)
.flowOn(Dispatchers.IO)
.cachedIn(viewModelScope)
}.asLiveData()
For anymore else, this is too complex or doesn't work out for you ... Use Combine then flatMap latest on the top of that.
private val _selectionLocation: MutableStateFlow<Location?> = MutableStateFlow(null)
val searchKeyword: MutableStateFlow<String> = MutableStateFlow("")
val unassignedJobs: LiveData<List<Job>> =
combine(_selectionLocation, searchKeyword) { location: Location?, keyword: String ->
Log.e("HomeViewModel", "$location -- $keyword")
location to keyword
}.flatMapLatest { pair ->
_repo.getJob(Status.UNASSIGNED, pair.first).map {
Log.e("HomeViewModel", "size ${it.size}")
it.filter { it.desc.contains(pair.second) }
}
}.flowOn(Dispatchers.IO).asLiveData(Dispatchers.Main)

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

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 }
}
}
}

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.

Categories

Resources