Android Jetpack Compose: VM not updating data structure when modified - android

I’ve got a problem with a LazyColumn of elements that have a favourite button: basically when I tap the favourite button, the item that is being favourited (a document in my case) is changed in the underlying data structure in the VM, but the view isn’t updated, so I never see any change in the button state.
class MainViewModel(private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO) : ViewModel() {
var documentList = emptyList<PDFDocument>().toMutableStateList()
....
fun toggleFavoriteDocument(pdfDocument: PDFDocument) {
documentList.find {
it == pdfDocument
}?.let {
it.favorite = !it.favorite
}
}
}
The composables are:
#Composable
fun DocumentRow(
document: PDFDocument,
onDocumentClicked: (String, Boolean) -> Unit,
onFavoriteValueChange: (Uri) -> Unit
) {
HeartIcon(
isFavorite = document.favorite,
onValueChanged = { onFavoriteValueChange(document.uri) }
)
}
#Composable
fun HeartIcon(
isFavorite: Boolean,
color: Color = Color(0xffE91E63),
onValueChanged: (Boolean) -> Unit
) {
IconToggleButton(
checked = isFavorite,
onCheckedChange = {
onValueChanged()
}
) {
Icon(
tint = color,
imageVector = if (isFavorite) {
Icons.Filled.Favorite
} else {
Icons.Default.FavoriteBorder
},
contentDescription = null
)
}
}
Am I doing something wrong? because when I call the toggleFavouriteDocument in the ViewModel, I see it’s marked or unmarked as favorite but there is no recomposition at all anywhere.

I might be missing it because you didn't post the rest of your code, but your documentList in the VM isn't observable, so how would the Composable know that it got changed? It needs to be something like Flow or LiveData, and it needs to be observed in the Composable. Something like this:
in ViewModel:
val documentList = MutableLiveData<List<PDFDocument>>()
in Composable:
val documentList by viewModel.documentList.observeAsState(List<PDFDocument>())
And you'll probably have to change the way you modify items in documentList. LiveData is weird about mutable collections inside MutableLiveData, and modifying individual items doesn't trigger a state change. You have to create a copy of the list with the modified items, and then re-port the whole list to the LiveData variable:
fun toggleFavoriteDocument(pdfDocument: PDFDocument) {
documentList.value?.let { oldList ->
// create a copy of existing list
val newList = mutableListOf<PDFDocument>()
newList.addAll(oldList)
// modify the item in the new list
newList.find {
it == pdfDocument
}?.let {
it.favorite = !it.favorite
}
// update the observable
documentList.postValue(newList)
}
}
Edit: There's also a potential problem with the way that you're trying to update the favorite value in the existing list. Without knowing how PDFDocument is implemented, I don't know if you can use the = operator. You should test that to make sure that newList.find { it == pdfDocument } actually finds the document

Related

Jetpack compose when to create a new lambda instance in Composable?

Please see my code:
#Composable
fun RecomposeLambdaTest() {
var state by remember {
mutableStateOf("1")
}
val stateHolder = remember {
StateHolder()
}
Column {
Button(onClick = {
state += "1"
}) {
Text(text = "change the state")
}
OuterComposable(state = state) {
stateHolder// just a reference to the instance outer the scope
}
}
}
#Composable
fun OuterComposable(state: String, onClick: () -> Unit) {
LogUtil.d("lambda hashcode: ${onClick.hashCode()}")
Column {
Text(text = state)
Button(onClick = onClick) {
Log.d("Jeck", "compose 2")
Text(text = "Text")
}
}
}
//#Stable
class StateHolder{
private var b = 2
}
Every time I click button, OuterComposable recompose, and log the lambda hashcode——always different! It means that a new lambda instance is created when recompose, everytime
and I uncomment the code in StateHolder and make it look like:
#Stable
class StateHolder{
private var b = 2
}
Every time I click button, OuterComposable recompose, and log the lambda hashcode——always the same! It means that when recompose, Composer reuse the lambda
So what' s under the hood?
Edit:
Ok, make it easier, Let's change the code like this:
val stateHolder = remember {
2
}
the result is lambda is reused.
make val to var, the lambda is created when every recompose.
So I think I know that: If the lambda refenence a valuable outer scope and the valuable is not stable, recreate lambda every time.
So the question is:
Why Compose compiler do this?
Why Compiler think the StateHolder before is not stable, it only contains a private var!?
An author met the same question, here is his article——6 Jetpack Compose Guidelines to Optimize Your App Performance
He said, private property still affact stability, it seems it is a Google team's choice.

Compose not refreshing view on change of mutableStateOf value

I have a ViewModel with the property
var uiState by mutableStateOf(UiState())
I have a composable that accesses this value like so
fun AppView(viewModel: BridgeViewModel) {
val uiState = viewModel.uiState
when {
uiState.isLoading -> {
LoadingView()
}
uiState.data != null -> {
TextToShareView(to = uiState.data)
}
}
}
After the composable is created I trigger a function in my ViewModel that changes the state like so
uiState = UiState(isLoading = true)
The problem is, the composable is not being redrawn when I change the value of state.
Any idea? I can't see how this is different to the official sample in the docs.

Jetpack Compose recompostion of property change in list of objects

I am quite new to Jetpack compose and have an issue that my list is not recomposing when a property of an object in the list changes. In my composable I get a list of available appointments from my view model and it is collected as a state.
// AppointmentsScreen.kt
#Composable
internal fun AppointmentScreen(
navController: NavHostController
) {
val appointmentsViewModel = hiltViewModel<AppointmentViewModel>()
val availableAppointments= appointmentsViewModel.appointmentList.collectAsState()
AppointmentContent(appointments = availableAppointments, navController = navController)
}
In my view model I get the data from a dummy repository which returns a flow.
// AppointmentViewModel.kt
private val _appointmentList = MutableStateFlow(emptyList<Appointment>())
val appointmentList : StateFlow<List<Appointment>> = _appointmentList.asStateFlow()
init {
getAppointmentsFromRepository()
}
// Get the data from the dummy repository
private fun getAppointmentsFromRepository() {
viewModelScope.launch(Dispatchers.IO) {
dummyRepository.getAllAppointments()
.distinctUntilChanged()
.collect { listOfAppointments ->
if (listOfAppointments.isNullOrEmpty()) {
Log.d(TAG, "Init: Empty Appointment List")
} else {
_appointmentList.value = listOfAppointments
}
}
}
}
// dummy function for demonstration, this is called from a UI button
fun setAllStatesToPaused() {
dummyRepository.setSatesInAllObjects(AppointmentState.Finished)
// Get the new data
getAppointmentsFromRepository()
}
Here is the data class for appointments
// Appointment data class
data class Appointment(
val uuid: String,
var state: AppointmentState = AppointmentState.NotStarted,
val title: String,
val timeStart: LocalTime,
val estimatedDuration: Duration? = null,
val timeEnd: LocalTime? = null
)
My question: If a property of one of the appointment objects (in the view models variable appointmentList) changes then there is no recomposition. I guess it is because the objects are still the same and only the properties have changed. What do I have to do that the if one of the properties changes also a recomposition of the screen is fired?
For example if you have realtime app that display stocks/shares with share prices then you will probably also have a list with stock objects and the share price updates every few seconds. The share price is a property of the stock object so this quite a similiar situation.

Using viewModel as the single source of truth in jetpack compose

let's say we have a viewModel that has a value called apiKey inside. Contents of this value is received from DataStore in form of a Flow and then, it is exposed as LiveData.
On the other hand we have a Fragment called SettingsFragment, and we are trying to display that apiKey inside a TextField, let the user modify it and save it in DataStore right away.
The solution that I'm currently using is down below, but the issue is that the UI gets very laggy and slow when changes are being made to the text.
My question is that what is the best way to implement this and still have a single source of truth for our apiKey?
class SettingsViewModel() : ViewModel() {
val apiKey = readOutFromDataStore.asLiveData()
fun saveApiKey(apiKey: String) {
viewModelScope.launch(Dispatchers.IO) {
saveToDataStore("KEY", apiKey)
}
}
}
/** SettingsFragment **/
...
#Composable
fun ContentView() {
var text = mViewModel.apiKey.observeAsState().value?.apiKey ?: ""
Column() {
OutlinedTextField(
label = { Text(text = "API Key") },
value = text,
onValueChange = {
text = it
mViewModel.saveApiKey(it)
})
}
}
Don't save the TextField's value in the onValueChange event to the data store on every key press - which is almost certainly slowing you down - especially if you are using the same thread. Use a local state variable and only update the data store when the user either moves the focus elsewhere or they save what's on the screen through some button press. You also need to avoid mixing UI threading with data storage threading which should be on the IO thread. Here is one possible solution:
#Composable
fun ContentViewHandler() {
ContentView(
initialText = viewmodel.getApiKey(),
onTextChange = { text ->
viewmodel.updateApiKey(text)
}
)
}
#Composable
fun ContentView(
initialText: String,
onTextChange: (text: String) -> Unit
) {
var text by remember { mutableStateOf(initialText) }
Column() {
OutlinedTextField(
label = { Text(text = "API Key") },
value = text,
onValueChange = {
text = it
},
modifier = Modifier.onFocusChanged {
onTextChange(text)
}
)
// Or add a button and save the text when clicked.
}
}

Composable reparenting in Jetpack Compose

Is there a way to reparent a Composable without it losing the state? The androidx.compose.runtime.key seems to not support this use case.
For example, after transitioning from:
// This function is in the external library, you can not
// modify it!
#Composable
fun FooBar() {
val uid = remember { UUID.randomUUID().toString() }
Text(uid)
}
Box {
Box {
FooBar()
}
}
to
Box {
Row {
FooBar()
}
}
the Text will show a different message.
I'm not asking for ways to actually remember the randomly generated ID, as I could obviously just move it up the hierarchy. What I want to archive is the composable keeping its internal state.
Is this possible to do without modifying the FooBar function?
The Flutter has GlobalKey specifically for this purpose. Speaking Compose that might look something like this:
val key = GlobalKey.create()
Box {
Box {
globalKey(key) {
FooBar()
}
}
}
Box {
Row {
globalKey(key) {
FooBar()
}
}
}
This is now possible with
movableContentOf
See this example:
val boxes = remember {
movableContentOf {
LetterBox(letter = 'A')
LetterBox(letter = 'B')
}
}
Column(
horizontalAlignment = Alignment.CenterHorizontally
) {
Button(onClick = { isRow = !isRow }) {
Text(text = "Switch")
}
if (isRow) {
Row(
Modifier.weight(1f),
verticalAlignment = Alignment.CenterVertically
) {
boxes()
}
} else {
Column(
Modifier.weight(1f),
verticalArrangement = Arrangement.Center
) {
boxes()
}
}
}
remember will store only one value in the same view. The key in Compose has a very different purpose: if the key passed to remember has a different value from the last recomposition, it means that the old value is no longer relevant and must be recomputed.
There is no direct equivalent of Flutter keys in Compose.
You can simply declare a global variable. In case you need to change it, wrap it with a mutable state, so changes will update your view.
var state by mutableStateOf(UUID.randomUUID().toString())
I'm not sure if that the same what GlobalKey does, in any case it's not the best practice, just like any other global variable.
If you need to share some data between views, it is much cleaner to use view models.
#Composable
fun TestScreen() {
val viewModel = viewModel<SomeViewModel>()
Column {
Text("TestScreen text: ${viewModel.state}")
OtherView()
}
}
#Composable
fun OtherView() {
val viewModel = viewModel<SomeViewModel>()
Text("OtherScreen text: ${viewModel.state}")
}
class SomeViewModel: ViewModel() {
var state by mutableStateOf(UUID.randomUUID().toString())
}
The hierarchy topmost viewModel call creates a view model - in my case inside TestScreen. All children that call viewModel of the same class will get the same object. The exception to this is different destinations of Compose Navigation, see how to handle this case in this answer.
You can update the mutable state value, and it will be reflected on all views using that model. Check out more about state in Compose.
When the view that created the view model is removed from the view hierarchy, the view model is also freed, so a new one will be created next time.

Categories

Resources