Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

value of list in model is updated but it does not reflect on composable function

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

like image 742
Kushal Dave Avatar asked Dec 01 '19 12:12

Kushal Dave


1 Answers

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.

like image 199
Sean McQuillan Avatar answered Oct 06 '22 00:10

Sean McQuillan