I have a data class.
data class ServiceInfoState(
val availableServices : List<Services> = emptyList(),
val loadingState : AvailableServicesState = AvailableServicesState.Loading
)
In my view model I keep the state as such:
private val _uiState = mutableStateOf(ServiceInfoState())
val uiState : State<ServiceInfoState> = _uiState
Later I create a new updated list of services and want to update the state.
_uiState.value = uiState.value.copy(availableServices = updated)
It is my understanding that this will create a new shallow copy of uiState.value, update the available services list, and assign this to the private property _uiState.value. This change to the value property should cause the UI which is watching the publicly exposed version uiState to recompose. This does not happen.
I then check the code like this:
println("List Equality: ${_uiState.value.availableServices === updated}")
val before = _uiState.value
_uiState.value = uiState.value.copy(availableServices = updated)
val after = _uiState.value
println("State Equality: ${before === after}")
Which prints out:
List Equality: false
State Equality: true
I agree that the list is not equal. That was my intention. To have a distinct list just in case. What surprises me is that the value property is equal before and after the copy. Doesn't copy return a new shallow reference? Is this not how this update should occur?
So I see why I am not getting a recomposition. Compose does not see the value property change because it did not change. How do I get it to change?
You can use SnapshotMutationPolicy to control how the result of mutableStateOf report and merge changes to the state object. Take a look at my answer here:
https://stackoverflow.com/a/74301717/8389251
Related
I think I haven't quite wrapped my head around how compose states work yet. I'm not able to trigger a recomposition when an item in the uiState changes.
I'm building an app that need notification access, so for that I'm navigating the user to the settings and after the user has granted permission they have to navigate back to the app. That's where I want to trigger the recomposition.
I have the permission check in onResume working and the variable in the uiState changes, but the recomposition doesn't get called. What am I missing here?
Composable
#Composable
private fun MainLayout(viewModel: SetupViewModel){
val uiState = viewModel.uiState.collectAsState()
SetupItem(
title = "Notification access",
summary = if(uiState.value.hasNotificationPermission) stringResource(R.string.granted) else stringResource(R.string.not_granted){}
}
SetupUiState.kt
data class SetupUiState(
var hasNotificationPermission: Boolean = false
)
I know for a fact that hasNotificationPermission gets set to true, but the summary in the SetupItem does not update. How do I accomplish that?
The problem here is that the hasNotificationPermission field is mutable (var and not val). Compose is not tracking inner fields for change. You have two options here:
Modify the SetupUiState as a whole, assuming you are using StateFlow in your ViewModel, it can look like this:
fun setHasNotificationPermission(value: Boolean) {
uiState.update { it.copy(hasNotificationPermission = value) }
}
You should also change hasNotificationPermission from var to val.
You can make use of compose's State and do something like this:
class SetupUiState(
initialHasPermission: Boolean = false
) {
var hasNotificationPermission: Boolean by mutableStateOf(initialHasPermission)
}
With this you can then simply do uiState.hasNotificationPermission = value and composition will be notified, since it's tracking State instances automatically.
#Composable
fun Screen() {
val viewModel: MainViewModel = viewModel()
val listOfID = viewModel.list.observeAsState()
val liveScores = viewModel.liveScores.observeAsState()
LazyColumn {
items(listOfID.value.orEmpty()) { id ->
AnotherComposable(id = id, liveScores = liveScores)
}
}
}
liveScores is a HashMap that maps an ID to something else. Its value gets reassigned in the VM every 5 seconds. The IDs are the exact same, but the properties of the other object could change.
In AnotherComposable, the ID is used to access the object, and that object's properties are used as inputs for more Composables.
My question is, when liveScores.value is reassigned inside the view model, how does compose know to only recompose the AnotherComposable that use the changed data? Since liveScores is being reassigned, shouldn't all the AnotherComposable be recomposed, as they all take liveScores as input?
I got a StateFlow of type UserStateModel (data class) in my app.
private val _userStateFlow: MutableStateFlow<UserStateModel?> = MutableStateFlow(UserStateModel())
val userStateFlow: StateFlow<UserStateModel?> = _userStateFlow
here is the UserStateModel
data class UserStateModel(
val uid: String? = null,
val username: String? = null,
val profileImageUrl: String? = null,
var isLoggedIn: Boolean = false,
val isPremiumUser: Boolean = false,
val posts: List<Post>? = listOf()
)
When I update the StateFlow with a new Username it emits the change to the collectors and the UI updates.
But when I change a property inside the posts: List? list it doesnt emit the changes.
When I change the size of the list it does, when I change the name property of the Post at index 0 it doesnt.
How can I detect changes to the child properties of the Data class?
Right now I use an ugly workaround, I add
val updateErrorWorkaround: Int = 0
to the UserStateModel data class and increase it by one so the collectors get notified
P.s I'm using MVVM + Clean Architecture and Jeptack Compose
EDIT
Thats my Post Model:
data class Post(
val id: Int,
val name: String,
val tags: MutableList<Tag>? = null
)
Here is how I update the MutableList:
val posts = userStateFlow.value?.posts
posts.get(index).tags?.add(myNewTag)
_userStateFlow.value = userStateFlow.value?.copy(posts = posts)
Those changes are not emitted to the collectors
StateFlow emits only if it detects changes to the value, it ignores replacing the value with the same data. To do this, it compares the previous value with the new one. For this reason, we shouldn't modify the data that we already provided to the StateFlow, because it won't be able to detect changes.
For example, we set value to a User(name=John). Then we mutate the same user object by modifying its name to James and we set the value to this "new" user object. StateFlow compares "new" User(name=James) with its stored value, which is now also User(name=James), so it doesn't see any changes.
In your example you created a copy of UserStateModel, but inside you re-use the same objects and you mutate them. In this case you added a new item to tags and this change affected old UserStateModel as well, so StateFlow doesn't detect the change.
To fix the problem, you need to copy all the data that was changed and do not mutate anything in-place. It is safer to make all the data immutable, so val and List - this way you are forced to make copies. I changed tags to val tags: List<Tag> = listOf(), then your code could look like the following:
val posts = userStateFlow.value?.posts!!.toMutableList()
posts[index] = posts[index].copy(tags = posts[index].tags + myNewTag)
userStateFlow.value = userStateFlow.value?.copy(posts = posts)
Here we create a copy of not only UserStateModel. We also copy posts list, the Post that we modify and we also copy the list of tags.
Alternatively, if this behavior of StateFlow is more annoying to you than helpful, you can use SharedFlow which doesn't compare values, but just emits.
I have a ViewModel class that looks like this:
class EditUserViewModel(
private val initUser: User,
) : ViewModel() {
private val _user = MutableLiveData(initUser)
val user: LiveData<User>
get() = _user
fun hasUserChanged() = initUser != _user.value
}
User can update some properties of the User data class instance through the UI.
To check if there are any changes when navigating from the fragment I use hasUserChanged method.
The problem is that is always false. I checked and it seems that the initialUser changes every time I change the _user MutableLiveData.
Why is that? Is the initial value of MutableLiveData passed by reference? I always thought that Kotlin is a "pass-by-value" type of language.
Update:
The problem seems to disappear when copying initUser before putting it inside the MutableLiveData.
private val _user = MutableLiveData(initUser.copy())
But it still doesn't make sense to me why I have to do that.
Kotlin is like java and they are pass-by-value. If you implement the equals function in User class, or make it as data class (which implements the equals function implicitly), it makes you sure that the content of the user objects is checked by != operator.
Update
If you are changing the value of LiveData directly, for example like this:
_user.value.name = "some name"
it means that you are changing the name property of the initUser, because _user.value exactly refers to the object that the initUser does. Consequently, the != operator always returns false, because we have one object with two references to it.
Now, when you are doing so:
private val _user = MutableLiveData(initUser.copy())
you are creating a deep copy of initUser (let's call it X) which is a new object in memory with the same property values of initUser.
Thus, by changing its properties like: _user.value.name = "some name", in fact, you are making this change on X, not initUser. It leads to preserving the initial values in initUser, meaning do not changing them, and solving the issue.
I am creating demo project for using jetpack compose with mvvm , i have created model class that holds the list of users.. those users are displayed in list and there is a button at top which adds new user to the list when clicked...
when user clicks on the button an the lambda updates activity about it and activity calls viewmodel which adds data to list and updates back to activity using livedata, now after the model receives the new data it does not update composable function about it and hence ui of list is not updated..
here is the code
#Model
data class UsersState(var users: ArrayList<UserModel> = ArrayList())
Activity
class MainActivity : AppCompatActivity() {
private val usersState: UsersState = UsersState()
private val usersListViewModel: UsersListViewModel = UsersListViewModel()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
usersListViewModel.getUsers().observe(this, Observer {
usersState.users.addAll(it)
})
usersListViewModel.addUsers()
setContent {
UsersListUi.addList(
usersState,
onAddClick = { usersListViewModel.addNewUser() },
onRemoveClick = { usersListViewModel.removeFirstUser() })
}
}
}
ViewModel
class UsersListViewModel {
private val usersList: MutableLiveData<ArrayList<UserModel>> by lazy {
MutableLiveData<ArrayList<UserModel>>()
}
private val users: ArrayList<UserModel> = ArrayList()
fun addUsers() {
users.add(UserModel("jon", "doe", "android developer"))
users.add(UserModel("john", "doe", "flutter developer"))
users.add(UserModel("jonn", "dove", "ios developer"))
usersList.value = users
}
fun getUsers(): MutableLiveData<ArrayList<UserModel>> {
return usersList
}
fun addNewUser() {
users.add(UserModel("jony", "dove", "ruby developer"))
usersList.value = users
}
fun removeFirstUser() {
if (!users.isNullOrEmpty()) {
users.removeAt(0)
usersList.value = users
}
}
}
composable function
#Composable
fun addList(state: UsersState, onAddClick: () -> Unit, onRemoveClick: () -> Unit) {
MaterialTheme {
FlexColumn {
inflexible {
// Item height will be equal content height
TopAppBar( // App Bar with title
title = { Text("Users") }
)
FlexRow() {
expanded(flex = 1f) {
Button(
text = "add",
onClick = { onAddClick.invoke() },
style = OutlinedButtonStyle()
)
}
expanded(flex = 1f) {
Button(
text = "sub",
onClick = { onRemoveClick.invoke() },
style = OutlinedButtonStyle()
)
}
}
VerticalScroller {
Column {
state.users.forEach {
Column {
Row {
Text(text = it.userName)
WidthSpacer(width = 2.dp)
Text(text = it.userSurName)
}
Text(text = it.userJob)
}
Divider(color = Color.Black, height = 1.dp)
}
}
}
}
}
}
}
the whole source code is available here
I am not sure if i am doing something wrong or is it because jetpack compose is still in developers preview , so would appreciate any help..
thank you
Ahoy!
Sean from Android Devrel here. The main reason this isn't updating is the ArrayList in UserState.users is not observable – it's just a regular ArrayList so mutating it won't update compose.
Model makes all properties of the model class observable
It seems like this might work because UserState is annotated #Model, which makes things automatically observable by Compose. However, the observability only applies one level deep. Here's an example that would never trigger recomposition:
class ModelState(var username: String, var email: String)
#Model
class MyImmutableModel(val state: ModelState())
Since the state variable is immutable (val), Compose will never trigger recompositions when you change the email or username. This is because #Model only applies to the properties of the class annotated. In this example state is observable in Compose, but username and email are just regular strings.
Fix Option #0: You don't need #Model
In this case you already have a LiveData from getUsers() – you can observe that in compose. We haven't shipped a Compose observation yet in the dev releases, but it's possible to write one using effects until we ship a observation method. Just remember to remove the observer in onDispose {}.
This is also true if you're using any other observable type, like Flow, Flowable, etc. You can pass them directly into #Composable functions and observe them with effects without introducing an intermediate #Model class.
Fix Option #1: Using immutable types in #Model
A lot of developers prefer immutable data types for UI state (patterns like MVI encourage this). You can update your example to use immutable lists, then in order to change the list you'll have to assign to the users property which will be observable by Compose.
#Model
class UsersState(var users: List<UserModel> = listOf())
Then when you want to update it you have to assign the users variable:
val usersState = UsersState()
// ...
fun addUsers(newUsers: List<UserModel>) {
usersState.users = usersState.users + newUsers
// performance note: note this allocates a new list every time on the main thread
// which may be OK if this is rarely called and lists are small
// it's too expensive for large lists or if this is called often
}
This will always trigger recomposition any time a new List<UserModel is assigned to users, and since there's no way to edit the list after it's been assigned the UI will always show the current state.
In this case, since the data structure is a List that you're concatenating the performance of immutable types may not be acceptable. However, if you're holding an immutable data class this option is a good one so I included it for completeness.
Fix Option #2: Using ModelList
Compose has a special observable list type for exactly this use case. You can use instead of an ArrayList and any changes to the list will be observable by compose.
#Model
class UsersState(val users: ModelList<UserModel> = ModelList())
If you use ModelList the rest of the code you've written in the Activity will work correctly and Compose will be able to observe changes to users directly.
Related: Nesting #Model classes
It's worth noting that you can nest #Model classes, which is how the ModelList version works. Going back to the example at the beginning, if you annotate both classes as #Model, then all of the properties will be observable in Compose.
#Model
class ModelState(var username: String, var email: String)
#Model
class MyModel(var state: ModelState())
Note: This version adds #Model to ModelState, and also allows reassignment of state in MyModel
Since #Model makes all of the properties of the class that is annotated observable by compose, state, username, and email will all be observable.
TL;DR which option to choose
Avoiding #Model (Option #0) completely in this code will avoid introducing a duplicate model layer just for Compose. Since you're already holding state in a ViewModel and exposing it via LiveData you can just pass the LiveData directly to compose and observe it there. This would be my first choice.
If you do want to use #Model to represent a mutable list, then use ModelList from Option #2.
You'll probably want to change the ViewModel to hold a MutableLiveData reference as well. Currently the list held by the ViewModel is not observable. For an introduction to ViewModel and LiveData from Android Architecture components check out the Android Basics course.
Your model is not observed so changes won't be reflected.
In this article under the section 'Putting it all together' the List is added.
val list = +memo{ calculation: () -> T}
Example for your list:
#Composable
fun test(supplier: UserState) {
val list = +memo{supplier.users}
ListConsumer(list){
/* Do other stuff for your usecase */
}
}