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.
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.
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.
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.
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.
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With