Updating Immutable View State Values in Android Unidirectional Data Flow - android

Question
I'm looking to refactor an immutable view state's values in the Android ViewModel (VM) in order to do the following:
Update the view state in the VM cleanly without copying the entire view state
Keep the view state data immutable to the view observing updates
I've built an Android Unidirectional Data Flow (UDF) pattern using LiveData to update the view state changes in the VM that are observed in the view.
See: Android Unidirectional Data Flow with LiveData — 2.0
Full sample code: Coinverse Open App
Implementation
The existing implementation uses nested LiveData.
One LiveData val to store the view state in the VM
Nested LiveData for the view state attributes as immutable vals
// Stored as viewState LiveData val in VM
data class FeedViewState(
val contentList: LiveData<PagedList<Content>>
val anotherAttribute: LiveData<Int>)
The view state is created in the VM's init{...}.
Then, in order to update the view state it must be copied and updated with the given attribute because it is an immutable val. If the attribute were to be mutable, it could be reassigned cleanly without the copy in the VM. However, being immutable is important to make sure the view cannot unintentionally change the val.
class ViewModel: ViewModel() {
val viewState: LiveData<FeedViewState> get() = _viewState
private val _viewState = MutableLiveData<FeedViewState>()
init {
_viewState.value = FeedViewState(
contentList = getContentList(...)
anotherAttribute = ...)
}
override fun swipeToRefresh(event: SwipeToRefresh) {
_viewState.value = _viewState.value?.copy(contentList = getContentList(...))
}
}

I am not sure if having "nested LiveData" is okay. When we work with any event-driven design implementation (LiveData, RxJava, Flow) we usually required to assume that the discrete data events are immutable and operations on these events are purely functional. Being immutable is NOT synonymous with being read-only(val). Immutable means immutable. It should be time-invariant and should work exactly the same way under any circumstances. That is one reason why I feel strange to have LiveData or ArrayList members in the data class, regardless of whether they are defined read-only or not.
Another, technical reason why one should avoid nested streams: it is almost impossible to observe them correctly. Every time there is a new data event emitted through the outer stream, the developers must make sure to remove inner subscriptions before observing the new inner stream, otherwise it can cause all sorts of problems. What's the point of having life-cycle aware observers, when the developers need to manually unsubscribe them?
In almost all scenarios, nested streams can be converted to one layer of stream. In your case:
class ViewModel: ViewModel() {
val contentList: LiveData<PagedList<Content>>
val anotherAttribute: LiveData<Int>
private val swipeToRefreshTrigger = MutableLiveData<Boolean>(true)
init {
contentList = Transformations.switchMap(swipeToRefreshTrigger) {
getContentList(...)
}
anotherAttribute = ...
}
override fun swipeToRefresh(event: SwipeToRefresh) {
swipeToRefreshTrigger.postValue(true)
}
}
Notes on PagedList:
PagedList is also mutable, but I guess it is something we just have to live with. PagedList usage is another topic so I won't be discussing it here.

Use Kotlin StateFlow - 7/21/20 Update
Rather than having two state classes with LiveData, one private and mutable, the other public and immutable, with the Kotlin coroutines 1.3.6 release a StateFlow value can be updated in the ViewModel, and rendered in the view's activity/fragment through an interface method.
See: Android Model-View-Intent with Kotlin Flow
Remove Nested LiveData, Create State Classes - 2/11/20
Approach: Store immutable LiveData state and effects in a view state and view effect class inside the ViewModel that is publicly accessible.
The view state and view effects attributes could be LiveData values directly in the VM. However, I'd like to organize the view state and effects into separate classes in order for the view to know whether it observing a view state or a view effect.
class FeedViewState(
_contentList: MutableLiveData<PagedList<Content>>,
_anotherAttribute: MutableLiveData<Int>
) {
val contentList: LiveData<PagedList<Content>> = _contentList
val anotherAttribute: LiveData<Int> = _anotherAttribute
}
The view state is created in the VM.
class ViewModel: ViewModel() {
val feedViewState: FeedViewState
private val _contentList = MutableLiveData<PagedList<Content>>()
private val _anotherAttribute = MutableLiveData<Int>()
init {
feedViewState = FeedViewState(_contentList, _anotherAttribute)
}
...
fun updateContent(){
_contentList.value = ...
}
fun updateAnotherAttribute(){
_anotherAttribute.value = ...
}
}
Then, the view state attributes would be observed in the activity/fragment.
class Fragment: Fragment() {
private fun observeViewState() {
feedViewModel.feedViewState.contentList(viewLifecycleOwner){ pagedList: PagedList<Content> ->
adapter.submitList(pagedList)
}
feedViewModel.feedViewState.anotherAttribute(viewLifecycleOwner){ anotherAttribute: Int ->
//TODO: Do something with other attribute.
}
}
}

Related

How should I go about implementing MVVM architecture pattern in my project?

I know this is a very documented topic, but I couldn't find a way to implement it in my project, even after spending hours trying to figure it out.
My root problem is that I have a RecyclerView with an Adapter whose content isn't updating as I'd like. I'm a beginner in Android, so I didn't implement any MVVM or such architecture, and my project only contains a repository, fetching data from Firebase Database, and passing it to a list of ShowModel, a copy of said list being used in my Adapter to display my shows (In order to filter/sort them without modifying the list with all shows).
However, when adding a show to the database from another Activity, my Adapter isn't displaying the newly added show (as detailed here)
I was told to use LiveData and ViewModel, but even though I started understanding how it works after spending time researching it, I don't fully get how I should use it in order to implement it in my project.
Currently I have the following classes:
The Adapter:
class ShowAdapter(private val context: MainActivity, private val layoutId: Int, private val textNoResult: TextView?) : RecyclerView.Adapter<ShowAdapter.ViewHolder>(), Filterable {
var displayList = ArrayList(showList)
class ViewHolder(view : View) : RecyclerView.ViewHolder(view){
val showName: TextView = view.findViewById(R.id.show_name)
val showMenuIcon: ImageView = view.findViewById(R.id.menu_icon)
}
#SuppressLint("NewApi")
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context).inflate(layoutId, parent, false)
return ViewHolder(view)
}
#SuppressLint("NewApi", "WeekBasedYear")
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val currentShow = displayList[position]
val index = holder.adapterPosition
holder.showName.text = currentShow.name
holder.itemView.setOnClickListener{ // Display show content
val intent = Intent(context, DetailsActivity::class.java)
intent.putExtra("position", index)
startActivity(context, intent, null)
}
holder.showMenuIcon.setOnClickListener{
val popupMenu = PopupMenu(context, it)
popupMenu.menuInflater.inflate(R.menu.show_management_menu, popupMenu.menu)
popupMenu.show()
popupMenu.setOnMenuItemClickListener {
when(it.itemId){
R.id.edit -> { // Edit show
val intent = Intent(context, AddShowActivity::class.java)
intent.putExtra("position", index)
startActivity(context, intent, null)
return#setOnMenuItemClickListener true
}
R.id.delete -> { // Delete show
val repo = ShowRepository()
repo.deleteShow(currentShow)
displayList.remove(currentShow)
notifyItemRemoved(index)
return#setOnMenuItemClickListener true
}
else -> false
}
}
}
}
override fun getItemCount(): Int = displayList.size
// Sorting/Filtering methods
}
The fragment displaying the adapter:
class HomeFragment : Fragment() {
private lateinit var context: MainActivity
private lateinit var verticalRecyclerView: RecyclerView
private lateinit var buttonAddShow: Button
private lateinit var showsAdapter: ShowAdapter
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.fragment_home, container, false)
context = getContext() as MainActivity
buttonAddShow = view.findViewById(R.id.home_button_add_show)
buttonAddShow.setOnClickListener{ // Starts activity to add a show
startActivity(Intent(context, AddShowActivity::class.java))
}
verticalRecyclerView = view.findViewById(R.id.home_recycler_view)
showsAdapter = ShowAdapter(context, R.layout.item_show, null)
verticalRecyclerView.adapter = showsAdapter
return view
}
}
The MainActivity:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
loadFragment(HomeFragment())
}
private fun loadFragment(fragment: Fragment){
val repo = ShowRepository()
if(showsListener != null) databaseRef.removeEventListener(showsListener!!)
repo.updateData{
val transaction = supportFragmentManager.beginTransaction()
transaction.replace(R.id.fragment_container, fragment)
transaction.addToBackStack(null)
if(supportFragmentManager.isStateSaved)transaction.commitAllowingStateLoss()
else transaction.commit()
}
}
}
The repository:
class ShowRepository {
object Singleton{
val databaseRef = FirebaseDatabase.getInstance().getReference("shows")
val showList = arrayListOf<ShowModel>()
var showsListener: ValueEventListener? = null
}
fun updateData(callback: () -> Unit){
showsListener = databaseRef.addValueEventListener(object : ValueEventListener {
override fun onDataChange(snapshot: DataSnapshot) {
showList.clear()
for(ds in snapshot.children){
val show = ds.getValue(ShowModel::class.java)
if(show != null) showList.add(show)
}
callback()
}
override fun onCancelled(p0: DatabaseError) { }
})
}
fun insertShow(show: ShowModel){
databaseRef.child(show.id).setValue(show)
}
fun deleteShow(show: ShowModel){
databaseRef.child(show.id).removeValue()
}
}
From what I understand of LiveData and ViewModel, what I should do is creating a ShowViewModel containing a MutableLiveData<List<ShowModel>> containing the shows, and then observe it in my HomeFragment and update the adapter depending on the changes happening. However, everytime I start something to implement it, I encounter a situation where I'm lost and don't know what I should do, which leads me back to square one once again. I've been trying this for more than a week without progressing even a little bit, and that's why I'm here, hoping for some insight.
Sorry for the silly question and the absurd amount of informations, and hoping someone will be able to help me understand what I do wrong/should do.
(this ended up longer than I meant it to be - hope it's not too much! There's a lot to learn, but you don't have to make it super complicated at first)
Broadly, working backwards, it should go like this:
Adapter
displays whatever the Fragment tells it to (some kind of setData function that updates its internal list and refreshes)
passes events to the Fragment (deleteItem(item), showDetails(item) etc.) - don't have the Adapter doing things like starting Activites, that's not its responsibility
Fragment
grabs a reference to any ViewModels (only certain components like Fragments and Activities can actually "own" them)
observes any LiveData (or collects Flows if you're doing it that way) on the VM, and updates stuff in the UI in response
e.g. model.shows.observe(viewLifecycleOwner) { shows -> adapter.setData(shows) }
handles UI events and calls methods on the VM in response, e.g. click listeners, events from the Adapter
ViewModel
acts as a go-between for the UI (the Fragment) and the data layer (the repository)
exposes methods for handling events like deleting items, interacts with the data layer as required (e.g. calling the appropriate delete function)
exposes data state for the UI to observe, so it can react to changes/updates (e.g. a LiveData containing the current list of shows that the data layer has provided)
That's the basic setup - the VM exposes data which the UI layer observes and reacts to, by displaying it. The UI layer also produces events (usually down to user interaction) which are passed to the VM. You can read more about this general approach in this guide about app architecture - it's worth reading because not only is it recommended as a way to build apps, a lot of the components you use in modern Android are designed with this kind of approach in mind (like the reactive model of wiring stuff up).
You could handle the Adapter events like this:
// in your Adapter
var itemDeletedListener: ((Item) -> Unit)? = null
// when the delete event happens for an item
itemDeletedListener?.invoke(item)
// in your Fragment
adapter.itemDeletedListener = { viewModel.deleteItem(it) }
which is easier than implementing an interface, and lets you wire up your Adapter similar to doing setOnClickListener on a button. Notice we're passing the actual Item object here instead of a list index - generally this is easier to work with, you don't need to maintain multiple copies of a list just so you can look up an index given to you by something else. Passing a unique ID can make sense though, especially if you're working with a database! But usually the object itself is more useful and consistent
The data layer is the tricky bit - the ViewModel needs to communicate with that to get the current state. Say you delete an item - you then need to get the current, updated list of shows. You have three approaches:
Call the delete function, immediately after fetch the current data, and set it on the appropriate LiveData
This can work, but it's not very reactive - you're doing one action, then immediately doing another because you know your data is stale. It would be better if the new data just arrived automatically and you could react to that by pushing it out. The other issue is that calling the delete function might not have an immediate effect - if you fetch the current data, nothing might have changed yet. It's better if the data layer is responsible for announcing updates.
This is the simplest approach though, and probably a good start! You could run this task in a coroutine (viewModelScope.launch { // delete and fetch and update LiveData }) so any slowness doesn't block the current thread.
Have the data layer's functions return the current, updated data that results
Similar to above, you're just sort of pushing the fetching into the data layer. This requires all those functions to be written to return the current state, which could take a while! And depending on what data you want, this might be impossible - if you have an active query on some data, how does the function know what specific data to return?
Make the ViewModel observe the data it wants, so when the data layer updates, you get the results automatically
This is the recommended reactive approach - again it's that two-way idea. The VM calling a function on the data layer is completely separate from the VM receiving new data. One thing just happens as a natural consequence of the other, they don't need to be tied together. You just need to wire them up right!
How do you actually do that though? If you're working with something like Room, that's already baked in. Queries can return async data providers like LiveData or Flows - your VM just needs to observe those and expose the results, or just expose them directly. That way, when a table is updated, any queries (like the current shows) push a new value, and the observers receive it and do whatever they need to do, like telling the Adapter to display the data. It all Just Works once it's wired up.
Since you have your own repo, you need to expose your own data sources. You could have a currentShows LiveData or (probably preferably) the flow equivalent, StateFlow. When the repo initialises, and when any data is changed, it updates that currentShows data. Anything observing that (e.g. the VM, the Fragment through a LiveData/Flow that the VM exposes) will automatically get the new values. So broadly:
// Repo
// this setup is exactly the same as your typical LiveData, except you need an initial value
private val _currentShows = MutableStateFlow<List<Show>>(emptyList()) // or whatever default
val currentShows: StateFlow<List<Show>> = _currentShows
fun deleteItem(item: Item) {
// do the deletion
// get the updated show list
_currentShows.value = updatedShowList
}
// ViewModel
// one way of doing things - you have a lot of options! This literally just exposes
// the state from the data layer, and turns it into a LiveData (if you want that)
val currentShows = repo.currentShows.asLiveData()
// Fragment
// wire things up so you handle new data as it arrives
viewModel.currentShows.observe(viewLifecycleOwner) { shows -> adapter.setData(shows) }
That's basically it. I've skimmed over a lot because honestly, there's a lot to learn with this - especially about Flows and coroutines if you're not already familiar with those. But hopefully that gives you an overview of the general idea, and don't be afraid to take shortcuts (like just updating your data in the ViewModel by setting its LiveData values) while you're learning and getting the hang of it. Definitely give that app architecture guide a read, and also the guides for ViewModels and LiveData. It'll start to click when you get the general idea!

How does ViewModel cache LiveData?

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.

Should livedata be always used in ViewModel?

It seems like recommended pattern for fields in viewmodel is:
val selected = MutableLiveData<Item>()
fun select(item: Item) {
selected.value = item
}
(btw, is it correct that the selected field isn't private?)
But what if I don't need to subscribe to the changes in the ViewModel's field. I just need passively pull that value in another fragment.
My project details:
one activity and a bunch of simple fragments replacing each other with the navigation component
ViewModel does the business logic and carries some values from one fragment to another
there is one ViewModel for the activity and the fragments, don't see the point to have more than one ViewModel, as it's the same business flow
I'd prefer to store a value in one fragment and access it in the next one which replaces the current one instead of pass it into a bundle and retrieve again and again manually in each fragment
ViewModel:
private var amount = 0
fun setAmount(value: Int) { amount = value}
fun getAmount() = amount
Fragment1:
bnd.button10.setOnClickListener { viewModel.setAmount(10) }
Fragment2:
if(viewModel.getAmount() < 20) { bnd.textView.text = "less than 20" }
Is this would be a valid approach? Or there is a better one? Or should I just use LiveData or Flow?
Maybe I should use SavedStateHandle? Is it injectable in ViewModel?
To answer your question,
No, It is not mandatory to use LiveData always inside ViewModel, it is just an observable pattern to inform the caller about updates in data.
If you have something which won't be changed frequently and can be accessed by its instance. You can completely ignore wrapping it inside LiveData.
And anyways ViewModel instance will be preserved and so are values inside it.
And regarding private field, MutableLiveData should never be exposed outside the class, as the data flow is always from VM -> View which is beauty of MVVM pattern
private val selected = MutableLiveData<Item>()
val selectedLiveData : LiveData<Item>
get() = selected
fun select(item: Item) {
selected.value = item
}

View models for RecyclerView items

My activity has a Google's ViewModel that fetches some model items. These items are then transformed into adapter items of a RecyclerView. There are also many types of adapter items supported by one RecyclerView.
I would like to have separate view model object for each of these model objects so that I can have more complex logic encapsulated only within that "small" view model.
Currently when I have some asynchronous logic (that needs to be stopped in onCleared()) that is related only to some adapter item I have to somehow route callbacks through main view model so that everything is properly unregistered.
I was considering using ViewModelProvider::get(key, modelClass) but my items are changing over time and I can't find a nice way to "clear" old items.
How are you handling these cases in your projects?
Edit: To add more information about my concern, maybe in different words: I want my "small" ViewModel to live as long as the model item which it represents. It means that:
I must receive onCleared() callback in the same scenarios in which parent of these items receive
I must receive onCleared() callback when item is no longer
Edit: Please try to compare it to a ViewPager with Fragments as items. Every individual model item is represented as a Fragment with its ViewModel. I would like achieve something similar but for RecyclerView.
androidx.lifecycle.ViewModel's are not meant to be used on RecyclerView items by default
Why?
ViewModel is AAC (Android Architecture Component) whose sole purpose is to survive configuration changes of Android Activity/Fragment lifecycle, so that data can be persisted via ViewModel for such case.
This achieved by caching VM instance in storage tied to hosting activity.
That's why it shouldn't be used on RecyclerView (ViewHolder) Items directly as the Item View itself would be part of Activity/Fragment and it (RecyclerView/ViewHolder) doesn't contain any specific API to provide ViewModelStoreOwner (From which ViewModels are basically derived for given Activity/Fragment instance).
Simplistic syntax to get ViewModel is:
ViewModelProvider(this).get(ViewModel::class.java)
& here this would be referred to Activity/Fragment context.
So even if you end up using ViewModel in RecyclerView Items, It would give you same instance due to context might be of Activity/Fragment is the same across the RecyclerView which doesn't make sense to me. So ViewModel is useless for RecyclerView or It doesn't contribute to this case much.
TL;DR
Solution?
You can directly pass in LiveData object that you need to observe from your Activity/Fragment's ViewModel in your RecyclerView.Adapter class. You'll need to provide LifecycleOwner as well for you adapter to start observing that given live data.
So your Adapter class would look something like below:
class RecyclerViewAdapter(private val liveDataToObserve: LiveData<T>, private val lifecycleOwner: LifecycleOwner) : RecyclerView.Adapter<ViewHolder>() {
init {
liveDataToObserve.observe(lifecycleOwner) { t ->
// Notify data set or something...
}
}
}
If this is not the case & you want to have it on ViewHolder class then you can pass your LiveData object during onCreateViewHolder method to your ViewHolder instance along with lifecycleOwner.
Bonus point!
If you're using data-binding on RecyclerView items then you can easily obtain lifecyclerOwner object from your binding class. All you need to do is set it during onCreateViewHolder() something like below:
class RecyclerViewAdapter(private val liveDataToObserve: LiveData<T>, private val lifecycleOwner: LifecycleOwner) : RecyclerView.Adapter<ViewHolder>() {
override fun onCreateViewHolder: ViewHolder {
// Some piece of code for binding
binding.lifecycleOwner = this#RecyclerViewAdapter.lifecycleOwner
// Another piece of code and return viewholder
}
}
class ViewHolder(private val someLiveData: LiveData<T>, binding: ViewDataBinding): RecyclerView.ViewHolder(binding.root) {
init {
someLiveData.observe(requireNotNull(binding.lifecycleOwner)) { t->
// set your UI by live data changes here
}
}
}
So yes, you can use wrapper class for your ViewHolder instances to provide you LiveData out of the box but I would discourage it if wrapper class is extending ViewModel class.
As soon as concern about mimicking onCleared() method of ViewModel, you can make a method on your wrapper class that gets called when ViewHolder gets recycled or detaches from window via method onViewRecycled() or onViewDetachedFromWindow() whatever fits best in your case.
Edit for comment of #Mariusz: Concern about using Activity/Fragment as LifecycleOwner is correct. But there would be slightly misunderstanding reading this as POC.
As soon as one is using lifecycleOwner to observe LiveData in given RecyclerViewHolder item, it is okay to do so because LiveData is lifecycle aware component and it handles subscription to lifecycle internally thus safe to use. Even if you can explicitly remove observation if wanted to, using onViewRecycled() or onViewDetachedFromWindow() method.
About async operation inside ViewHolder:
If you're using coroutines then you can use lifecycleScope from lifecycleOwner to call your operation and then provide data back to particular observing LiveData without explicitly handling clear out case (LifecycleScope would take care of it for you).
If not using Coroutines then you can still make your asyc call and provide data back to observing LiveData & not to worry about clearing your async operation during onViewRecycled() or onViewDetachedFromWindow() callbacks. Important thing here is LiveData which respects lifecycle of given LifecycleOwner, not the ongoing async operation.
Don't know if google has nice support for nested ViewModel's, looks like not.
Thankfully, we don't need to stick to androidx.lifecycle.ViewModel to apply MVVM approach where we need. And there is a small example I decided to write:
Fragment, nothing changes:
#Override public void onCreate(#Nullable Bundle savedInstanceState) {
final ItemListAdapter adapter = new ItemListAdapter();
binding.getRoot().setAdapter(adapter);
viewModel = new ViewModelProvider(this).get(ItemListViewModel.class);
viewModel.getItems().observe(getViewLifecycleOwner(), adapter::submitList);
}
ItemListAdapter, in addition to populate view, it also becomes responsible for notifying item's observers - should they continue to listen, or not. In my example adapter was ListAdapter which extends RecyclerView.Adapter, so it receives list of items. This is unintentionally, just edited some code I already have. It's probably much better to use different base implementation, but it's acceptable for demonstration purposes:
#Override public Holder onCreateViewHolder(ViewGroup parent, int viewType) {
return new Holder(parent);
}
#Override public void onBindViewHolder(Holder holder, int position) {
holder.lifecycle.setCurrentState(Lifecycle.State.RESUMED);
holder.bind(getItem(position));
}
#Override public void onViewRecycled(Holder holder) {
holder.lifecycle.setCurrentState(Lifecycle.State.DESTROYED);
}
// Idk, but these both may be used to pause/resume, while bind/recycle for start/stop.
#Override public void onViewAttachedToWindow(Holder holder) { }
#Override public void onViewDetachedFromWindow(Holder holder) { }
Holder. It implements LifecycleOwner, which allows to unsubscribe automatically, just copied from androidx.activity.ComponentActivity sources so all should be okay :D :
static class Holder extends RecyclerView.Holder implements LifecycleOwner {
/*pkg*/ LifecycleRegistry lifecycle = new LifecycleRegistry(this);
/*pkg*/ Holder(ViewGroup parent) { /* creating holder using parent's context */ }
/*pkg*/ void bind(ItemViewModel viewModel) {
viewModel.getItem().observe(this, binding.text1::setText);
}
#Override public Lifecycle getLifecycle() { return lifecycle; }
}
List view-model, "classique" androidx-ish ViewModel, but very rough, also provide nested view models. Please, pay attention, in this sample all view-models start to operate immediately, in constructor, until parent view-model is commanded to clear! Don't Try This at Home!
public class ItemListViewModel extends ViewModel {
private final MutableLiveData<List<ItemViewModel>> items = new MutableLiveData<>();
public ItemListViewModel() {
final List<String> list = Items.getInstance().getItems();
// create "nested" view-models which start background job immediately
final List<ItemViewModel> itemsViewModels = list.stream()
.map(ItemViewModel::new)
.collect(Collectors.toList());
items.setValue(itemsViewModels);
}
public LiveData<List<ItemViewModel>> getItems() { return items; }
#Override protected void onCleared() {
// need to clean nested view-models, otherwise...
items.getValue().stream().forEach(ItemViewModel::cancel);
}
}
Item's view-model, using a bit of rxJava to simulate some background work and updates. Intentionally I do not implement it as androidx....ViewModel, just to highlight that view-model is not what google names ViewModel but what behaves as view-model. In actual program it most likely will extend, though:
// Wow, we can implement ViewModel without androidx.lifecycle.ViewModel, that's cool!
public class ItemViewModel {
private final MutableLiveData<String> item = new MutableLiveData<>();
private final AtomicReference<Disposable> work = new AtomicReference<>();
public ItemViewModel(String topicInitial) {
item.setValue(topicInitial);
// start updating ViewModel right now :D
DisposableHelper.set(work, Observable
.interval((long) (Math.random() * 5 + 1), TimeUnit.SECONDS)
.map(i -> topicInitial + " " + (int) (Math.random() * 100) )
.subscribe(item::postValue));
}
public LiveData<String> getItem() { return item; }
public void cancel() {
DisposableHelper.dispose(work);
}
}
Few notes, in this sample:
"Parent" ViewModel lives in activity scope, so all its data (nested view models) as well.
In this example all nested vm start to operate immediately. Which is not what we want. We want to modify constructors, onBind, onRecycle and related methods accordingly.
Please, test it on memory leaks.
Although that is true that Android uses ViewModels in Android Architecture Components it does not mean that they are just part of AAC. In fact, ViewModels are one of the components of the MVVM Architecture Pattern, which is not Android only related. So ViewModel's actual purpose is not to preserve data across Android's lifecycle changes. However, because of exposing its data without having a View's reference makes it ideal for the Android specific case in which the View can be recreated without affecting to the component that holds its state (the ViewModel). Nonetheless, it has other benefits such as facilitating the Separation of Concerns among others.
It is also important to mention that your case can not be 100% compared to the ViewPager-Fragments case, as the main difference is that the ViewHolders will be recycled between items. Even if ViewPager's Fragments are destroyed and recreated, they will still represent the same Fragment with that same data. That is why they can safely bind the data provided by their already existing ViewModel. However, in the ViewHolder case, when it is recreated, it can be representing a totally new item, so the data its supposed ViewModel could be providing may be incorrect, referencing the old item.
That being said you could easily make the ViewHolder become a ViewModelStoreOwner:
class MyViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), ViewModelStoreOwner {
private var viewModelStore: ViewModelStore = ViewModelStore()
override fun getViewModelStore(): ViewModelStore = viewModelStore
}
This can still be useful if the data provided by the ViewModel is the same independently of the ViewHolder's item (shared state between all items). However, if that is not the case, then you would need to invalidate the ViewModelStore by calling viewModelStore.clear() and create a new ViewModel instance probably in ViewHolder's onViewRecycled. You will loose the advantage of keeping the state no matter the view's lifecycle, but can sometimes still be useful as to follow Separation of Concerns.
Finally, regarding to the option of using a LiveData instance to control the state, no matter if it is provided by a ViewHolder's shared or specific ViewModel or it is passed through the Adapter, you will need a LifecycleOwner to observe it. A better approach to using the current Fragment or Activity lifecycle is to just use the specific ViewHolder's actual lifecycle, as they are actually created and destroyed, by making them implement the LifecycleOwner interface. I created a small library which does exactly that.
I followed this wonderfull answer HERE by aeracode with a one exception. Instead of ViewModel I've used Rx BehaviourSubject that work perfectly for me.
In case of coroutines You can use alternatively StateFlow.
clas MyFragment: Fragment(){
private val listSubject = BehaviorSubject.create<List<Items>>()
...
private fun observeData() {
viewModel.listLiveData.observe(viewLifecycleOwner) { list ->
listSubject.onNext(list)
}
}
}
RecyclerView
class MyAdapter(
private val listObservable: BehaviorSubject<List<Items>>
) : RecyclerView.Adapter<MyViewHolder>() {
[...]
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
holder.bindToData(getItem(position))
}
override fun onViewRecycled(holder: MyViewHolder) {
holder.onViewRecycled()
}
...
class MyViewHolder(val binding: LayoutBinding) :
RecyclerView.ViewHolder(binding.root) {
private var disposable: Disposable? = null
fun bindToData(item: Item) = with(binding) {
titleTv.text = item.title
disposable = listObservable.subscribe(::setItemList) <- Here You listen
}
fun onViewRecycled() {
disposable?.dispose()
}
}

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
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 */
}
}

Categories

Resources