How to force jetpack compose to recompose? - android

Say that, I'm building a custom compose layout and populating that list as below
val list = remember { dataList.toMutableStateList()}
MyCustomLayout{
list.forEach { item ->
key(item){
listItemCompose( data = item,
onChange = { index1,index2 -> Collections.swap(list, index1,index2)})
}
}
This code is working fine and the screen gets recomposed whenever onChange lambda function is called, but when it comes to any small change in any item's property, it does not recompose, to elaborate that let's change the above lambda functions to do the following
{index1,index2 -> list[index1].propertyName = true}
Having that lambda changing list item's property won't trigger the screen to recompose. I don't know whether this is a bug in jetpack compose or I'm just following the wrong approach to tackle this issue and I would like to know the right way to do it from Android Developers Team. That's what makes me ask if there is a way to force-recomposing the whole screen.

You can't force a composable function to recompose, this is all handled by the compose framework itself, there are optimizations to determine when something has changed that would invalidate the composable and to trigger a recomposition, of only those elements that are affected by the change.
The problem with your approach is that you are not using immutable classes to represent your state. If your state changes, instead of mutating some deep variable in your state class you should create a new instance of your state class (using Kotin's data class), that way (by virtue of using the equals in the class that gets autogenerated) the composable will be notified of a state change and trigger a recomposition.
Compose works best when you use UDF (Unidirectional Data Flow) and immutable classes to represent the state.
This is no different than, say, using a LiveData<List<Foo>> from the view system and mutating the Foos in the list, the observable for this LiveData would not be notified, you would have to assign a new list to the LiveData object. The same principle applies to compose state.

you can recreate an entire composition using this
val text = remember { mutableStateOf("foo") }
key(text.value) {
YourComposableFun(
onClick = {
text.value = "bar"
}
) {
}
}

call this
currentComposer.composition.recompose()

Related

Jetpack Compose: How to prevent first composition when screen should be created by a certain asynchronous state

I am still new to compose and I am curious how people treat this kind of thing.
Let's imagine that we have a screen that has two variants, one variant with some views, another variant with other views. That variant should be dictated by a persisted flag, which I have stored using DataStore (the new SharedPrefs). The only issues is that unlike SharedPrefs, DataStore is asynchronous and is made to work with coroutines. So here's what happens, the screen gets rendered in default state (variant A) for just a split second, atfer about 100-200ms the viewModel successfully reads the value from DataStore on a coroutine and posts it on a mutableStateOf(), which as a result triggers recomposition with the variant B of the screen that is saved in the prefs. This transition is visible and the entire behavior looks glitchy. How do you fix this? I don't want the screen to compose the default state before the viewModel has time to read the stored value, I want the screen to await those 100-200ms without doing anything and composing the views only after the reading from DataStore.
The code looks like:
#Composable
fun MyScreen(){
val viewModel = hiltViewModel<ScreenViewModel>()
val state = viewModel.uiState
if(state == MyScreenState.A){
[...] // some view here
} else {
[...] // other view here
}
}
#HiltViewModel
class ScreenViewModel #Inject constructor(
private val dataStoreService: DataStoreService
) : BaseViewModel() {
var uiState by mutableStateOf(MyScreenState.A)
init {
viewModelScope.launch {
dataStoreService.flag().collect { flag ->
uiState = if(flag) MyScreenState.A else MyScreenState.B
}
}
}
}
For simplicity, MyScreenState is just a simple enum in this case. One of the things I thought about is defaulting the uiState to null instead of variant A and in my screen check if the state is null and if so returning a Unit (basically rendering nothing). If the state is not null, render the screen accordingly. But the truth is that I don't feel like making that uiState nullable, I avoid working with nulls at all cost because they make the code just a little less readable and needs extra handling. What's your solution on this? Thanks.
Instead of creating nullable state create another state that represents nothing being happening. I generally use Idle state to set as initial or a UI state when nothing should happen. I also use this approach for fire once events after event is invoked and processed.
var uiState by mutableStateOf(MyScreenState.Idle)
it will be a loading or a blank screen depending on your UI

Activity Launcher(File picker) is loading multiple times in single event - Jetpack compose

I am using a file picker inside a HorizontalPager in jetpack compose. When the corresponding screen is loaded while tapping the button, the launcher is triggered 2 times.
Code snippet
var openFileManager by remember {
mutableStateOf(false)
}
if (openFileManager) {
launcher.launch("*/*")
}
Button(text = "Upload",
onClick = {
openFileManager = true
})
Edited: First of all Ian's point is valid why not just launch it in the onClick directly? I also assumed that maybe you want to do something more with your true false value. If you want nothing but launch then all these are useless.
The screen can draw multiple times when you click and make openFileManager true so using only condition won't prevent it from calling multiple times.
You can wrap your code with LaunchedEffect with openFileManager as a key. The LaunchedEffect block will run only when your openFileManager change.
if (openFileManager) {
LaunchedEffect(openFileManager) {
launcher.launch("*/*")
}
}
You should NEVER store such important state inside a #Composable. Such important business logic is meant to be stored in a more robust holder like the ViewModel.
ViewModel{
var launch by mutableStateOf (false)
private set
fun updateLaunchValue(newValue: Boolean){
launch = newValue
}
}
Pass these to the Composable from the main activity
MyComposable(
launchValue = viewModel.launch
updateLaunchValue = viewModel::updateLaunchValue
)
Create the parameters in the Composable as necessary
#Comoosable
fun Uploader(launchValue: Boolean, onUpdateLaunchValue: (Boolean) -> Unit){
LaunchedEffect (launchValue){
if (launchValue)
launcher.launch(...)
}
Button { // This is onClick
onUpdateLaunchValue(true) // makes the value true in the vm, updating state
}
}
If you think it is overcomplicated, you're in the wrong paradigm. This is the recommended AND CORRECT way of handling state in Compose, or any declarative paradigm, really afaik. This keeps the code clean, while completely separating UI and data layers, allowing controlled interaction between UI and state to achieve just the perfect behaviour for the app.

Why Compose remembers a state of an object rather than the object itself?

I learned that Compose remembers a state in a way such as:
var text by remember { mutableStateOf("") }
So in this case, it remembers a MutableState of a String. My question is why it wants to remember a thing called "MutableState" rather than just the String itself, and why it requires an extra layer?
I know its late, but here's what I understand with remember.
I have a simple Todo app where a list of todos are hoisted in a viewwmodelusing a SnapshotStatelist, this list is rendered by a LazyColumn where each todo model has its own remembered state where I do some pretty basic UI functionality (e.g card elevation, visibility of some icons). Any changes I make to a todo should propagate back up to the mutableStateList (e.g deleting a todo), SnapshotStateList will then notify the LazyColumn to perform a recomposition, however when I edit a Todo (e.g, modifying the title), I also have to update the item composable that holds this todo(some UI changes), then I got stuck up as I can't figure out why the item composable is not recomposing even if I was able to verify that the SnapShotStateList item is modified by using the code below
val todoList = viewModel.todos
val snapshot = Snapshot.takeMutableSnapshot()
snapshot.enter {
for (todo in todoList) {
Log.e("TodoModel", todo.title)
}
}
Code that modifies the list
val index = todos.indexOfFirst { it.id == modifiedModel.id }
todos[index] = todos[index].copy(//..some properties to be copied)
I verified that any modification I make on a todo reflects back to its host list, but the item composable that renders a todo item doesn't trigger a re-composition. I kept reading some posts and carefully thinking object references and trying to understand my code based on this object reference thought, I thought that there must be something that holds the previous state of the item and the changes is not applied to the remember, until I found that you can supply a key to a remember where it will be the thing that will decide if remember needs to re-calculate. Now I found out that remember only remembers a state (by state I dont mean compose state, but state in general) on initial composition, it will keep that initial structure/state for as long as the entire composable it is part of is still running, in this case all the way up to the parent composable (i.e my DashboardScreen), what made my remember re-calculate is I supplied the key with the todo object itself
val itemState: ItemCardState = remember(key1 = todoModel) {
ItemCardState(todoModel = todoModel)
}
This way, when a change happens to the SnapShotStateList, an item's remember will see the same object reference (data class) but with changes applied to it. remember caches the initial state and will hold it forever unless you supply a key that you think might change and will let remember re-calculate a new initial state to be remembered, in my case I supplied the key as the todo object itself and when remember sees the same object reference but with a different value, it will re-calculate.
Having this understanding now, I can't imagine a way when there is no layer that will hold an object (remember) and prevent unnecessary re-composition when the state of an object changes.
Just sharing what I learned, also open for discussion that I may have said in a wrongful way.
remember is used for storing objects to have it when a recomposition happens. Mutable state is used for triggering recomopsition, you can check this answer for more details.
by is delegation that is a feature of Kotlin which translates the code
var text = remember { mutableStateOf("") }
text.value = "newString"
you basically store a trigger and value inside a remember. when you change MutableState.value new recomposition occurs and in this new recomposition you get the latest value of MutableState.
There are also usecases of remember without needing MutableState like a Paint or custom object when something else triggers the recomposition like canvas touch position for instance.
you remember object since you won't instantiate it.
val paint = remember {Paint()}
var offset by remember {mutableStateOf(Offset.Zero)
then when offset changes with user touching screen you trigger recomposition but since and you don't need to instantiate Paint object again.
remember only and remember with MutableState has different usecases.
Mutable state is needed for two reasons:
Saving mutable state between recompositions. remember is gonna save result of lambda calculation, but if you change a variable later - remember cannot save and track it. The solution is to have a state holder - an object that's created by mutableStateOf, saved by remember, will always be the same, but you can mutate it properties, in this case value (which is hidden when you're using delegation with by).
Triggering recomposition. If you just create a class, save it with remember and update a property, Compose won't know that it was changed, and that view update it needed - that's why a special Compose State was created, which notifies a view that it needs to be recomposed.
You can continue deepening your knowledge with state in Compose documentation and Thinking in Compose.

Jetpack Compose: Explain to me why the list content is not updated?

I know that in Jetpack Compose you have to change the state of the passed in data in order to trigger a recomposition of the UI to update the UI with any changes. I have also read the documentation about Jetpack Compose state and ViewModels here. But that's a very simple example and does not cover the use case below.
Below is a conceptual scenario where I want to update the state of the list, by updating just one item's state that I wish to be reflected in the Jetpack Compose rendered part. I know I must assign a new list as data, which should trigger the recomposition and below I am using toMutableList() to try to achieve this. But this does not work. When I run this kind of code, recomposition does not happen and the single item's state is not updated in the list.
Could someone please explain to me why this does not work and how should I approach this?
I already know of mutableStateListOf(), but how should I approach this if I want to keep my view models compatible with other non-Jetpack Compose parts of my app, and thus I only want to use LiveData in my view models?
class Model : ViewModel() {
private val _items = MutableLiveData(listOf<Something>())
val items: LiveData<String> = _items
fun update(item: Something) {
_items.value = _items.value!!.toMutableList().map {
if (it == item) {
// Update item. But it's not reflected in Jetpack Compose
}
}
}
}
#Composable
fun ListComponent(model: Model) {
val items by model.items.observeAsState(emptyList())
LazyColumn {
items(items) { item ->
...
}
}
}
I think it's because you are mutating array instead of copying it. Compose needs stable equality when talking about recomposition avoidance, here i believe it can only use reference. Try copying array and then mutating the new one. I believe if you do map without toMutableList() it will create a copy and do exactly what you want

Using mutableStateOf instead of observeAsState

I'm working with Jetpack Compose in an Android app and had the problem that my uiState (LiveData) was set to its initial value on every recomposition, since I've initialized it like
val authUiState: AuthUIState by authenticationViewModel.uiState.observeAsState(AuthUIState.Loading)
It was set to Loading on every recomposition before it was set to the correct value.
When I tried to Remember the value, I learned that we can't use observeAsState within the remember block and finally changed it to
val authUiState = remember{ mutableStateOf(authenticationViewModel.uiState.value) }.value
This works, but I'm not quite sure, if this is the common and good way to solve this.
What do you think? Should I do it differently? Do you need more information?
See if the uiState inside your viewmodel is something like a LiveData Object, (which is kinda what it seems like from the code), the recommended way is to store it in the viewmodel itself as mutable state.
var uiState by mutableStateOf (initialValue)
private set //Do not allow external modifications to maintain consistency of state
fun onUiStateChange(newValue: Any){
uiState = newValue
}
You just need to initialise it as a MutableState, in the rest of the code, to update, delete or whatever you want to do with it, just treat it as a regular variable. Compose will trigger recomposition every time the value is updated.
The following code snippet below will almost certainly not work, because here, the state is whatever you wrap inside mutableStateOf(), which is just a simple value which will be fetched once from the viewmodel and then remembered throughout recompositions, so no code change will be triggered here
val authUiState by remember{ mutableStateOf(authenticationViewModel.uiState.value) }
Storing state in the viewmodel as mutableState, is as far as my knowledge extends, the best practice in compose. You will see the same in the 'State Codelab' from Android developers
Good luck

Categories

Resources