I am working on an Android App using the MVVM pattern along LiveData (possibly Transformations) and DataBinding between View and ViewModel. Since the app is "growing", now ViewModels contain lots of data, and most of the latter are kept as LiveData to have Views subscribe to them (of course, this data is needed for the UI, be it a Two-Way Binding as per EditTexts or a One-Way Binding). I heard (and googled) about keeping data that represents the UI state in the ViewModel. However, the results I found were just simple and generic. I would like to know if anyone has hints or could share some knowledge on best practices for this case. In simple words, What could be the best way to store the state of an UI (View) in a ViewModel considering LiveData and DataBinding available? Thanks in advance for any answer!
I struggled with the same problem at work and can share what is working for us. We're developing 100% in Kotlin so the following code samples will be as well.
UI state
To prevent the ViewModel from getting bloated with lots of LiveData properties, expose a single ViewState for views (Activity or Fragment) to observe. It may contain the data previously exposed by the multiple LiveData and any other info the view might need to display correctly:
data class LoginViewState (
val user: String = "",
val password: String = "",
val checking: Boolean = false
)
Note, that I'm using a Data class with immutable properties for the state and deliberately don't use any Android resources. This is not something specific to MVVM, but an immutable view state prevents UI inconsistencies and threading problems.
Inside the ViewModel create a LiveData property to expose the state and initialize it:
class LoginViewModel : ViewModel() {
private val _state = MutableLiveData<LoginViewState>()
val state : LiveData<LoginViewState> get() = _state
init {
_state.value = LoginViewState()
}
}
To then emit a new state, use the copy function provided by Kotlin's Data class from anywhere inside the ViewModel:
_state.value = _state.value!!.copy(checking = true)
In the view, observe the state as you would any other LiveData and update the layout accordingly. In the View layer you can translate the state's properties to actual view visibilities and use resources with full access to the Context:
viewModel.state.observe(this, Observer {
it?.let {
userTextView.text = it.user
passwordTextView.text = it.password
checkingImageView.setImageResource(
if (it.checking) R.drawable.checking else R.drawable.waiting
)
}
})
Conflating multiple data sources
Since you probably previously exposed results and data from database or network calls in the ViewModel, you may use a MediatorLiveData to conflate these into the single state:
private val _state = MediatorLiveData<LoginViewState>()
val state : LiveData<LoginViewState> get() = _state
_state.addSource(databaseUserLiveData, { name ->
_state.value = _state.value!!.copy(user = name)
})
...
Data binding
Since a unified, immutable ViewState essentially breaks the notification mechanism of the Data binding library, we're using a mutable BindingState that extends BaseObservable to selectively notify the layout of changes. It provides a refresh function that receives the corresponding ViewState:
Update: Removed the if statements checking for changed values since the Data binding library already takes care of only rendering actually changed values. Thanks to #CarsonHolzheimer
class LoginBindingState : BaseObservable() {
#get:Bindable
var user = ""
private set(value) {
field = value
notifyPropertyChanged(BR.user)
}
#get:Bindable
var password = ""
private set(value) {
field = value
notifyPropertyChanged(BR.password)
}
#get:Bindable
var checkingResId = R.drawable.waiting
private set(value) {
field = value
notifyPropertyChanged(BR.checking)
}
fun refresh(state: AngryCatViewState) {
user = state.user
password = state.password
checking = if (it.checking) R.drawable.checking else R.drawable.waiting
}
}
Create a property in the observing view for the BindingState and call refresh from the Observer:
private val state = LoginBindingState()
...
viewModel.state.observe(this, Observer { it?.let { state.refresh(it) } })
binding.state = state
Then, use the state as any other variable in your layout:
<layout ...>
<data>
<variable name="state" type=".LoginBindingState"/>
</data>
...
<TextView
...
android:text="#{state.user}"/>
<TextView
...
android:text="#{state.password}"/>
<ImageView
...
app:imageResource="#{state.checkingResId}"/>
...
</layout>
Advanced info
Some of the boilerplate would definitely benefit from extension functions and Delegated properties like updating the ViewState and notifying changes in the BindingState.
If you want more info on state and status handling with Architecture Components using a "clean" architecture you may checkout Eiffel on GitHub.
It's a library I created specifically for handling immutable view states and data binding with ViewModel and LiveData as well as glueing it together with Android system operations and business use cases.
The documentation goes more in depth than what I'm able to provide here.
Android Unidirectional Data Flow (UDF) 2.0
Update 12/18/2019: Android Unidirectional Data Flow with LiveData — 2.0
I've designed a pattern based on the Unidirectional Data Flow using Kotlin with LiveData.
UDF 1.0
Check out the full Medium post or YouTube talk for an in-depth explanation.
Medium - Android Unidirectional Data Flow with LiveData
YouTube - Unidirectional Data Flow - Adam Hurwitz - Medellín Android Meetup
Code Overview
Step 1 of 6 — Define Models
ViewState.kt
// Immutable ViewState attributes.
data class ViewState(val contentList:LiveData<PagedList<Content>>, ...)
// View sends to business logic.
sealed class ViewEvent {
data class ScreenLoad(...) : ViewEvent()
...
}
// Business logic sends to UI.
sealed class ViewEffect {
class UpdateAds : ViewEffect()
...
}
Step 2 of 6 — Pass events to ViewModel
Fragment.kt
private val viewEvent: LiveData<Event<ViewEvent>> get() = _viewEvent
private val _viewEvent = MutableLiveData<Event<ViewEvent>>()
override fun onCreate(savedInstanceState: Bundle?) {
...
if (savedInstanceState == null)
_viewEvent.value = Event(ScreenLoad(...))
}
override fun onResume() {
super.onResume()
viewEvent.observe(viewLifecycleOwner, EventObserver { event ->
contentViewModel.processEvent(event)
})
}
Step 3 of 6 — Process events
ViewModel.kt
val viewState: LiveData<ViewState> get() = _viewState
val viewEffect: LiveData<Event<ViewEffect>> get() = _viewEffect
private val _viewState = MutableLiveData<ViewState>()
private val _viewEffect = MutableLiveData<Event<ViewEffect>>()
fun processEvent(event: ViewEvent) {
when (event) {
is ViewEvent.ScreenLoad -> {
// Populate view state based on network request response.
_viewState.value = ContentViewState(getMainFeed(...),...)
_viewEffect.value = Event(UpdateAds())
}
...
}
Step 4 of 6 — Manage Network Requests with LCE Pattern
LCE.kt
sealed class Lce<T> {
class Loading<T> : Lce<T>()
data class Content<T>(val packet: T) : Lce<T>()
data class Error<T>(val packet: T) : Lce<T>()
}
Result.kt
sealed class Result {
data class PagedListResult(
val pagedList: LiveData<PagedList<Content>>?,
val errorMessage: String): ContentResult()
...
}
Repository.kt
fun getMainFeed(...)= MutableLiveData<Lce<Result.PagedListResult>>().also { lce ->
lce.value = Lce.Loading()
/* Firestore request here. */.addOnCompleteListener {
// Save data.
lce.value = Lce.Content(ContentResult.PagedListResult(...))
}.addOnFailureListener {
lce.value = Lce.Error(ContentResult.PagedListResult(...))
}
}
Step 5 of 6 — Handle LCE States
ViewModel.kt
private fun getMainFeed(...) = Transformations.switchMap(repository.getFeed(...)) {
lce -> when (lce) {
// SwitchMap must be observed for data to be emitted in ViewModel.
is Lce.Loading -> Transformations.switchMap(/*Get data from Room Db.*/) {
pagedList -> MutableLiveData<PagedList<Content>>().apply {
this.value = pagedList
}
}
is Lce.Content -> Transformations.switchMap(lce.packet.pagedList!!) {
pagedList -> MutableLiveData<PagedList<Content>>().apply {
this.value = pagedList
}
}
is Lce.Error -> {
_viewEffect.value = Event(SnackBar(...))
Transformations.switchMap(/*Get data from Room Db.*/) {
pagedList -> MutableLiveData<PagedList<Content>>().apply {
this.value = pagedList
}
}
}
Step 6 of 6 — Observe State Change!
Fragment.kt
contentViewModel.viewState.observe(viewLifecycleOwner, Observer { viewState ->
viewState.contentList.observe(viewLifecycleOwner, Observer { contentList ->
adapter.submitList(contentList)
})
...
}
Related
In my application I want update data with SharedFlow and my application architecture is MVI .
I write below code, but just update one of data!
I have 2 spinners and this spinners data fill in viewmodel.
ViewModel code :
class MyViewModel #Inject constructor(private val repository: DetailRepository) : ViewModel() {
private val _state = MutableStateFlow<MyState>(MyState.Idle)
val state: StateFlow<MyState> get() = _state
fun handleIntent(intent: MyIntent) {
when (intent) {
is MyIntent.CategoriesList -> fetchingCategoriesList()
is MyIntent.PriorityList -> fetchingPrioritiesList()
}
}
private fun fetchingCategoriesList() {
val data = mutableListOf(Car, Animal, Color, Food)
_state.value = DetailState.CategoriesData(data)
}
private fun fetchingPrioritiesList() {
val data = mutableListOf(Low, Normal, High)
_state.value = DetailState.PriorityData(data)
}
}
With below codes I filled spinners in fragment :
lifecycleScope.launch {
//Send
viewModel.handleIntent(MyIntent.CategoriesList)
viewModel.handleIntent(MyIntent.PriorityList)
//Get
viewModel.state.collect { state ->
when (state) {
is DetailState.Idle -> {}
is DetailState.CategoriesData -> {
categoriesList.addAll(state.categoriesData)
categorySpinner.setupListWithAdapter(state.categoriesData) { itItem ->
category = itItem
}
Log.e("DetailLog","1")
}
is DetailState.PriorityData -> {
prioritiesList.addAll(state.prioritiesData)
prioritySpinner.setupListWithAdapter(state.prioritiesData) { itItem ->
priority = itItem
}
Log.e("DetailLog","2")
}
}
When run application not show me number 1 in logcat, just show number 2.
Not call this line : is DetailState.CategoriesData
But when comment this line viewModel.handleIntent(MyIntent.PriorityList) show me number 1 in logcat!
Why when use this code viewModel.handleIntent(MyIntent.CategoriesList) viewModel.handleIntent(MyIntent.PriorityList) not show number 1 and 2 in logcat ?
The problem is that a StateFlow is conflated, meaning if you rapidly change its value faster than collectors can collect it, old values are dropped without ever being collected. Therefore, StateFlow is not suited for an event-like system like this. After all, it’s in the name that it is for states rather than events.
It’s hard to suggest an alternative because your current code looks like you shouldn’t be using Flows at all. You could simply call a function that synchronously returns data that you use synchronously. I don’t know if your current code is a stepping stone towards something more complicated that really would be suitable for flows.
I started building my app using Room, Flow, LiveData and Coroutines, and have come across something odd: what I'm expecting to be a value flow actually has one null item in it.
My setup is as follows:
#Dao
interface BookDao {
#Query("SELECT * FROM books WHERE id = :id")
fun getBook(id: Long): Flow<Book>
}
#Singleton
class BookRepository #Inject constructor(
private val bookDao: BookDao
) {
fun getBook(id: Long) = bookDao.getBook(id).filterNotNull()
}
#HiltViewModel
class BookDetailViewModel #Inject internal constructor(
savedStateHandle: SavedStateHandle,
private val bookRepository: BookRepository,
private val chapterRepository: ChapterRepository,
) : ViewModel() {
val bookID: Long = savedStateHandle.get<Long>(BOOK_ID_SAVED_STATE_KEY)!!
val book = bookRepository.getBook(bookID).asLiveData()
fun getChapters(): LiveData<PagingData<Chapter>> {
val lastChapterID = book.value.let { book ->
book?.lastChapterID ?: 0L
}
val chapters = chapterRepository.getChapters(bookID, lastChapterID)
return chapters.asLiveData()
}
companion object {
private const val BOOK_ID_SAVED_STATE_KEY = "bookID"
}
}
#AndroidEntryPoint
class BookDetailFragment : Fragment() {
private var queryJob: Job? = null
private val viewModel: BookDetailViewModel by viewModels()
override fun onResume() {
super.onResume()
load()
}
private fun load() {
queryJob?.cancel()
queryJob = lifecycleScope.launch() {
val bookName = viewModel.book.value.let { book ->
book?.name
}
binding.toolbar.title = bookName
Log.i(TAG, "value: $bookName")
}
viewModel.book.observe(viewLifecycleOwner) { book ->
binding.toolbar.title = book.name
Log.i(TAG, "observe: ${book.name}")
}
}
}
Then I get a null value in lifecycleScope.launch while observe(viewLifecycleOwner) gets a normal value.
I think it might be because of sync and async issues, but I don't know the exact reason, and how can I use LiveData<T>.value to get the value?
Because I want to use it in BookDetailViewModel.getChapters method.
APPEND: In the best practice example of Android Jetpack (Sunflower), LiveData.value (createShareIntent method of PlantDetailFragment) works fine.
APPEND 2: The getChapters method returns a paged data (Flow<PagingData<Chapter>>). If the book triggers an update, it will cause the page to be refreshed again, confusing the UI logic.
APPEND 3: I found that when I bind BookDetailViewModel with DataBinding, BookDetailViewModel.book works fine and can get book.value.
LiveData.value has extremely limited usefulness because you might be reading it when no value is available yet.
You’re checking the value of your LiveData before it’s source Flow can emit its first value, and the initial value of a LiveData before it emits anything is null.
If you want getChapters to be based on the book LiveData, you should do a transformation on the book LiveData. This creates a LiveData that under the hood observes the other LiveData and uses that to determine what it publishes. In this case, since the return value is another LiveData, switchMap is appropriate. Then if the source book Flow emits another version of the book, the LiveData previously retrieved from getChapters will continue to emit, but it will be emitting values that are up to date with the current book.
fun getChapters(): LiveData<PagingData<Chapter>> =
Transformations.switchMap(book) { book ->
val lastChapterID = book.lastChapterID
val chapters = chapterRepository.getChapters(bookID, lastChapterID)
chapters.asLiveData()
}
Based on your comment, you can call take(1) on the Flow so it will not change the LiveData book value when the repo changes.
val book = bookRepository.getBook(bookID).take(1).asLiveData()
But maybe you want the Book in that LiveData to be able to be changed when the repo changes, and what you want is that the Chapters LiveData retrieved previously does not change? So you need to manually get it again if you want it to be based on the latest Book? If that's the case, you don't want to be using take(1) there which would prevent the book from appearing updated in the book LiveData.
I would personally in that case use a SharedFlow instead of LiveData, so you could avoid retrieving the values twice, but since you're currently working with LiveData, here's a possible solution that doesn't require you to learn those yet. You could use a temporary Flow of your LiveData to easily get its current or first value, and then use that in a liveData builder function in the getChapters() function.
fun getChapters(): LiveData<PagingData<Chapter>> = liveData {
val singleBook = book.asFlow().first()
val lastChapterID = singleBook.lastChapterID
val chapters = chapterRepository.getChapters(bookID, lastChapterID)
emitSource(chapters)
}
I'm a complete novice when it comes to LiveData and MVVM architecture. I'm trying to figure out how to observe a LiveData<List> in the ViewModel to update another variable depending on if it is empty or not.
I'm getting the LiveData from my Room database with this:
class MealsViewModel #Inject constructor(
private val mealDao : MealDao
) : ViewModel() {
...
private val currentDay: MutableLiveData<Date> = MutableLiveData(Date())
val meals = Transformations.switchMap(currentDay){ date -> mealDao.getMeals(date).asLiveData() }
I would like for another variable in the ViewModel, private val empty: Boolean, to update anytime the list is returned empty (null). This will be used in updating an ImageView in the Fragment from Visible.GONE to Visible.VISIBLE.
How do I check if val meals is empty synchronously?
I've read around and saw some people said to useobserveForever, but the architecture guide explicitly advises against any observers in ViewModels.
I could probably observe the LiveData in the Fragment, but that would require business logic in the Fragment, ie:
viewModel.meals.observe(viewLifecycleOwner) {
if meals.value.isEmpty() imageView.visibility = View.VISIBLE else imageView.visibility = View.GONE
}
And I'd like to keep the Fragment as 'dumb' as possible, so I'd prefer to have that logic in the ViewModel. Is that possible?
You can check live data meal to see if it's empty or null and then trigger with your live data empty like this:
In the viewmodel, you create a livedata isEmptyMeals. This live data variable will always trigger when meals value change and will check if your meals value are empty or null.
MealsViewModel.kt
class MealsViewModel #Inject constructor(
private val mealDao : MealDao
) : ViewModel() {
...
private val currentDay: MutableLiveData<Date> = MutableLiveData(Date())
val meals = Transformations.switchMap(currentDay){ date -> mealDao.getMeals(date).asLiveData() }
val isEmptyMeals = meals.map {
it.isNullOrEmpty()
}
}
And in the fragment, you will listen to observe the livedata isEmptyMeals and perform the logic to hide or show the image view you want.
Fragment.kt
viewModel.isEmptyMeals.observe(viewLifecycleOwner) {
imageView.visibility = if (it) View.VISIBLE else View.GONE
}
I don't know exactly how your code is set up but you can do something like below
Add variable to ViewModel
val empty = MutableLiveData<Boolean>()
In meals observer
viewModel.meals.observe(viewLifecycleOwner) {
viewModel.empty,postValue(meals.value.isEmpty())
Then observe from empty
Using MediatorLiveData
In your ViewModel class, create
val empty = MediatorLiveData<Boolean>()
Then
empty.addSource(meals) {
empty.value = it.isEmpty()
}
I saw all of the following scenarios in different example projects from Google's Codelabs and other sources and do not fully understand where the values from the LiveData object are retrieved from.
Scenario 1 - Current Understanding:
According to https://developer.android.com/.../viewmodel one reason to use a ViewModel is to store/cache UI related data that I want to re-use after the corresponding UI has been rebuild after a configuration change.
Given the following simplified ViewModel and Repository: After updateName() is called the first time, the LiveData object of _currentName contains a String. If the UI is then rebuild after a screen rotation, the view that needs to display the current name requests it by observing currentName which in turn returns the value of the LiveData object that is contained in the field of the _currentName property. Am I correct?
ViewModel
class NamesViewModel(): ViewModel() {
private val respository = NamesRepository()
private val _currentName: MutableLivedata<String?> = MutableLiveData(null)
val currentName: LiveData<String?> get() = this._currentName
...
// Called as UI event listener.
fun updateName() {
this._currentName.value = this.repository.updateName()
}
}
Repository
class NamesRepository() {
fun updateName(): String {
val nextName: String
...
return nextName
}
}
Scenario 2:
What happens if the UI is rebuild after a screen rotation in the following case? _currentName in the ViewModel 'observes' currentName in the repository, but it still is a property and therefore stores its own LiveData object in its field. When the view then requests currentName from the ViewModel, the value is retrieved from the LiveData object that is contained in the field of the _currentName property in the ViewModel. Is this correct?
ViewModel
class NamesViewModel(): ViewModel() {
private val respository = NamesRepository()
private val _currentName: LiveData<String?> = this.repository.currentName
val currentName: LiveData<String?> get() = this._currentName
...
// Called as UI event listener.
fun updateName() {
this.repository.updateName()
}
}
Repository
class NamesRepository() {
private val _currentName: MutableLivedata<String?> = MutableLiveData(null)
val currentName: LiveData<String?> get() = this._currentName
fun updateName() {
val nextName: String
...
this._currentName.value = nextName
}
}
Scenario 3:
In the following scenario, if the UI is rebuild and a view requests currentNam from the ViewModel, where is the requested value stored? My current understanding is, that currentName falls back to the field of the property _currentName in the repository. Isn't that against the idea of the ViewModel to store relevant UI data to be re-used after a configuration change? In the case below, it might be no problem to retrieve the value from the repository instead of the viewModel, but what if the repository itself retrieves the value directly from a LiveData object that comes from a Room database? Wouldn't a database access take place every time a view requests _currentName from the viewModel?
I hope somebody can clarify the situation more, in order to understand how to cache UI related data in the viewModel the correct way (or at least to understand what are the incorrect ways).
ViewModel
class NamesViewModel(): ViewModel() {
private val respository = NamesRepository()
val currentName: LiveData<String?> get() = this.repository.currentName
...
// Called as UI event listener.
fun updateName() {
this.repository.updateName()
}
}
Repository
class NamesRepository() {
private val _currentName: MutableLivedata<String?> = MutableLiveData(null)
val currentName: LiveData<String?> get() = this._currentName
fun updateName() {
val nextName: String
...
this._currentName.value = nextName
}
}
To answer your question scenario#1 is correct usage of LiveData.
Firstly, LiveData is not responsible for caching, it is just LifeCycleAware Observable, given that caching is done at ViewModel, when your activity recreates due to any configuration changes, android will try to retrieve the existing instance of ViewModel, if found then it's state and data are retained as is else it will create a new instance of ViewModel.
Second, using LiveData in repository is a bad idea at many levels, repository instances are held by ViewModel and LiveData are part of Android Framework which makes repositories rely on Android Framework thus creating problems in Unit Testing. Always use LiveData only in ViewModels.
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 */
}
}