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()
}
}
Related
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.
}
}
}
I have an app structured in MVVM. I have different fragments within the same activity. Each fragment has its own ViewModel and all data are retrieved from a REST API.
In FragmentA, there is a RecyclerView that lists X class instances. I want to set OnClickListener on the RecyclerView and I want to pass related X object to FragmentB when an item clicked in the RecyclerView. How can I achieve this?
How I imagine it is the following.
The Fragment passes a listener object to the adapter, which in turn passes it to the ViewHolders
Here is a quick sketch of how it should look like
class Fragment {
val listener = object: CustomAdapter.CustomViewHolderListener() {
override fun onCustomItemClicked(x: Object) {}
}
fun onViewCreated() {
val adapter = CustomAdapter(listener)
}
}
---------------
class CustomAdapter(private val listener: CustomViewHolderListener) {
val listOfXObject = emptyList() // this is where you save your x objects
interface CustomViewHolderListener{
fun onCustomItemClicked(x : Object)
}
override fun onBindViewHolder(holder: CustomViewHolder, position: Int) {
holder.itemView.setOnClickListener {
listener.onCustomItemClicked(listOfXObject[position])
}
}
}
Here are some articles that might help you get the general gist of the things.
They don't answer your question directly though
Hope it is helpful
link 1 link 2
if you're using data binding you need to pass your view(which is Fragment in your case) into the layout via adapter class and you need to import your view in layout file to be able to call view's method
android:onClick="#{() -> view.onXXXClick(item)}"
pass your current model class which is item into this new method and then create onXXXClick method in your view and do whatever you wish.
if you will be doing view related operations such as navigation from one fragment to another or starting a service you should create above function in your view, if you're doing network or db related operations it should be in your ViewModel
you can check out my GitHub repository to understand better.
I have a collection of parent objects each having a collection of child objects. Call these ParentModels and ChildModels.
On screen I want to display a RecyclerView of rendered ParentModels, each containing inter alia a RecyclerView of rendered ChildModels.
Wishing to avoid having a god LiveData that redraws everything just because one property of one ChildModel changes, I intend to separate these.
I can't figure out how to structure this with Recyclerview Adapters and Holders plus whatever Fragments and ViewModels I need. Right now I have
class MyFragment: Fragment() {
private lateinit val mViewModel: FragmentViewModel
// ...
fun onViewCreated(/*...*/) {
val parentAdapter = ParentAdapter()
view.findViewById<RecyclerView>(/*...*/).apply {
adapter = parentAdapter
//...
}
viewModel.getParents().observe(this, Observer {
parentAdapter.setParents(it)
}
}
}
class FragmentViewModel #Inject constructor(repository: RoomRepo): ViewModel() {
mParents: LiveData<List<ParentModel>> = repository.getParents()
fun getParents() = mParents
//...
}
class ParentAdapter: RecyclerView.Adapter<ParentHolder>() {
private lateinit var mParents: List<ParentModel>
fun setParents(list: List<ParentModel>) {
mParents = list
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, /*...*/) {
return ParentHolder(LayoutInflater.from(parent.context).inflate(R.layout.parent, parent, false))
}
override fun onBindViewHolder(holder: ParentHolder, position: Int) {
holder.bind(/*UNKNOWN*/)
}
// ...
inner class ParentHolder(private val mView: View): RecyclerView.ViewHolder(mView) {
fun bind(/*UNKNOWN*/) {
// WHAT TO DO HERE???
}
}
}
Plus my R.layout.parent (I've omitted other irrelevant stuff like a View that just draws a horizontal line, but that's why I have my RecyclerView nested inside a LinearLayout):
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
android:orientation="vertical"
android:layout_height="wrap_content"
android:layout_width="match_parent"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
I have written a ChildAdapter, ChildHolder, and a few other things unthinkingly because I thought this would be trivial to implement, but at this point something's gunked up my brain and I'm likely not seeing the obvious thing.
I've got the first RecyclerView loading correctly based on underlying data. But this parent recyclerview also needs to:
fetch children based on a single parent.id
create a child recyclerview for a single parent recyclerview item that displays the children
Room returns a LiveData> from function repository.getChildrenByParentId(id: Long). That's the data I'm working from.
But where do I fetch this, how do I hook it into the relevant child recyclerview that belongs to the parent recyclerview?
I don't want to have a God fragment that does
viewModel.getParents().observe(...) { parentAdapter.update(it) } and also have to do some kind of viewModel.getChildren().observe(...) { parentAdapter.updateChildren(it) }
because that destroys separation of concerns. Seems to me each item in the parent recyclerview should have a viewmodel that fetches the children that would belong to it, then creates a recyclerview and uses a ChildAdapter to display these children, but I can't seem to figure out where to plug in the ChildFragment and ChildViewModel (with repository.getChildrenByParentId in it) to get this all working.
All examples I find online don't seem to help as they use contrived examples with no LiveData and a God fragment/activity that puts everything inside a single adapter.
I would literally have 1 adapter that can render everything, using the DiffUtil (or its async version) class to ensure I don't (and I quote) "redraw everything just because one property of one ChildModel changes".
I would move this complex responsibility of constructing (and providing) the data, to your repository (or, if you prefer to have it closer, to your ViewModel acting as a coordinator between 1 or more (I don't know how your model looks, so I am only imagining) repositories providing data.
This would allow you to offer to the ui a much more curated immutable list of ParentsAndChildren together and your RecyclerView/Adapter's responsibility is suddenly much simpler, display this, and bind the correct view for each row. Your UI is suddenly faster, spends much less time doing things on the main thread and you can even unit test the logic to create this list, completely independent of your Activity/Fragment.
I imagine ParentsAndChildren to be something like:
class ParentChildren(parent: Parent?, children: Children?)
Your bind could then inflate one view when parent is not null and children is. When children is not null, you know it's a children (you could include the parent as well, depends on how you construct this data). Problem solved here, your adapter would look like
class YourAdapter : ListAdapter<ParentChildren, RecyclerView.ViewHolder>(DiffUtilCallback()) {
...
You'd need to implement your DiffUtilCallback():
internal class DiffUtilCallback : DiffUtil.ItemCallback<ParentChildren>() {
and its two methods (areContentsTheSame, areItemsTheSame).
And your adapter's two methods:
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val inflater = LayoutInflater.from(parent.context)
return when (viewType) {
viewTypeParent -> YourParentViewHolder(inflater.inflate(R.layout.your_layout_for_parent), parent, false))
viewTypeChildren -> YourChildrenViewHolder(inflater.inflate(R.layout.your_layout_for_children), parent, false))
else -> throw IllegalArgumentException("You must supply a valid type for this adapter")
}
}
I would have an abstract base to simplify the adapter even further:
internal abstract class BaseViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
abstract fun bind(data: ParentChildren)
}
This allows you to have your
// I'm writing pseudo code here... keep it in mind
internal class ParentViewHolder(itemView: View) : BaseViewHolder(itemView) {
private val name: TextView = itemView.findViewById(R.id.item_text)
override fun bind(data: ParentChildren) {
name.text = parentChildren.parent?.name
}
}
internal class ChildrenViewHolder(itemView: View) : BaseViewHolder(itemView) {
private val name: TextView = itemView.findViewById(R.id.item_text)
override fun bind(data: ParentChildren) {
name.text = parentChildren.children?.name
}
}
You get the idea.
Now... ListAdapter<> has a method called submitList(T) where T is the Type of the adapter ParentChildren in the above pseudo-example.
This is as far as I go, and now you have to provide this Activity or Fragment hosting this adapter, the list via either LiveData or whatever is that you prefer for the architecture you have.
It can be a repository passing it to a MutableLiveData inside the viewModel and the ViewModel exposing a LiveData<List<ParentChildren> or similar to the UI.
The sky is the limit.
This shifts the complexity of putting this data together, closer to where the data is, and where the power of SQL/Room can leverage how you combine and process this, regardless of what the UI needs or wants to do with it.
This is my suggestion, but based upon the very limited knowledge I have about your project.
Good luck! :)
My structure is like this, in the Fragment I call the ViewModel which instantiate the Adapter
For each item of my RecyclerView, I have a button which I listen
binding.addItem.setOnClickListener(onAddItemClicked(product))
private fun onAddToQuoteClicked(product: Product): View.OnClickListener {
return View.OnClickListener {
// TODO add item
}
}
I don't know how to send this item data to the fragment, is there a way to do that?
Should I use interface between Adapter and ViewModel and between ViewModel and Fragment ?
Or Can I use RxEvent & RxBus ?
First for navigation between ViewModel and Fragment -
you can use RxJava PublishSubject make it public and subscribe to it in your Fragment which can use public methods of ViewModel.
or you can use an interface, better in my opinion as using interface you are using simpler logic, much less errors in that case, and also it is more clean code as you can name methods in interface as you want - easier to read.
Second for communication between Adapter and Fragment - you can use same strategy as described above. In that case I recommend to use RxJava, as usually you would just need to handle one or few click listeners (so it is for example for observable which emits some data object).
If you need some code examples text me in comments.
UPDATE - it is better practice to create your Adapter instance in View, as I assume you are using MVVM design pattern (where all UI related code must be located in View component).
Example Fragment - Adapter communication Rx - PublishSubject
In your Adapter
private val publisher = PublishSubject.create<Product>()
fun getObservableProductItem(): Observable<Product> = publisher.hide()
binding.addItem.setOnClickListener(onAddItemClicked(product))
private fun onAddToQuoteClicked(product: Product): View.OnClickListener {
return View.OnClickListener {
publisherRideItem.onNext(items[adapterPosition])
}
}
override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) {
publisherRideItem.onComplete()
}
And in your Fragment
compositeDisposable.add(adapter.getObservableProductItem().subscribe {
//TODO do whatever you want with it
})
override fun onDestroy() {
super.onDestroy()
compositeDisposable.clear()
}
RxJava PublishSubject is the best option, I would prefer it and it is very easy.
I tried with recyclerView - Fragment - ViewModel. Some will have listadapter -> recyclerVire -> Fragment -> ViewModel. I have explained that below.
List Adapter
specify your PublishSubject here
class myapadter : ListAdapter
{
object RxBus {
val itemClickStream: PublishSubject<View> = PublishSubject.create()
}
}
ViewHolder
in your viewHolder trigger that PublishSubject.
class myViewholder : RecyclerView.ViewHolder(itemView)
{
itemView.myButton.setOnClickListener {
v -> RxBus.itemClickStream.onNext(v) }
}
Fragment
subscribe to PublishSubject here in the fragment, this will automatically get notified.
myapadter.RxBus.itemClickStream.subscribe(){ v ->
if (v.id == R.id.myButton) {
viewModel.callyourfunction()
}
You can directly add item through ViewModel reference and subscribe to RxJava EventBus or LiveData change in your Fragment or Activity
I have a complex screen in my project which I'm breaking in more than one fragment. I'm trying to follow the MVVM architecture for these classes, so which fragment has its own ViewModel and Contract class.
The issue is that all the ViewModels needs the same object instance (let's call it Book) to do Room transactions.
Does it have a correct way to share data (or LiveData) between ViewModels? I know the concept of Shared ViewModel but I don't know if I can apply it to this case. I also thought about using MediatorLiveData but didn't get a good approach on it too.
I'm thinking about having a class (let's call BookObservableProvider) with a LiveData<Book> (or Rx Subject<Book>) where each ViewModel injects the same instance and load/update always the same value.
Is it a good approach?
In my personal opinion your approach is not bad for this situation, but if want to try something else, I can advise you RxBus method. Here is a great article about it. With this approach you can simply publish data in activity, which holds fragments, and then listen to this particular event in all your fragments.
Something like :
//Activity
RxBus.publish(RxEvent.EventOnBookProvide(bookObject)
and
//Fragment
RxBus.listen(RxEvent.EventOnBookProvide::class.java).subscribe {
useObject(it)
}
And don't forget to use Disposable and .dispose() it in onDestroy() if using Activity and onDestroyView() if using fragment.
You should share those data between fragments/activities (maybe using Intents for activities) , and than handle those data by the other ViewModel
The answer is as usual, it depends.
If the reason behind your question is Room access, then it is recmended to have a DataRepository class that handles all Database access and you just pass that repository singleton to each AndroidViewModel.
mRepository = ((MainApp) application).getRepository();
In MainApp:
public DataRepository getRepository() {
return DataRepository.getInstance(getDatabase(), mAppExecutors);
}
And the Repository:
public class DataRepository {
private static DataRepository sInstance;
private MediatorLiveData<String> mObservableString;
private DataRepository(final AppDatabase database, final AppExecutors executors) {
mObservableString.addSource(database.myDao().loadString(),
mString -> {
if (database.getDatabaseCreated().getValue() != null) {
mObservableString.postValue(mString);
}
});
}
public static DataRepository getInstance(final AppDatabase database, final AppExecutors executors) {
if (sInstance == null) {
synchronized (DataRepository.class) {
if (sInstance == null) {
sInstance = new DataRepository(database, executors);
}
}
}
return sInstance;
}
// and then your access methods
public LiveData<String> getString() {
return mObservableString;
}
In the repository it is recommended to have a MediatorLivedata if you want to change the reference (source). Otherwise a normal LiveData does the job.
Regarding ViewModels:
In theory each fragment gets it's own Viewmodel. And if you get it by using requireActivity() as reference, you can get each ViewModel everywhere and have it therefore shared.
As an example:
viewModelA = new ViewModelProvider(requireActivity()).get(ViewModelA.class);
viewModelB = new ViewModelProvider(requireActivity()).get(ViewModelB.class);
This you could call in every Fragment and get the same ViewModel instances. If the DataRepository setup seems overkill to you, make one ViewModel with Room access and load it from every Fragment.
I am facing the same issue. But if you don't have different view models for the various fragments or the design does not necessitate using different view models for the various fragments you can just share one fragment between the entire activity( all the other fragment) and they will all share the same data instances.
follow this link for more https://developer.android.com/guide/fragments/communicate
what you need to do is make sure all the fragments initiate the view model(main view model) with the same context.
public class FilterFragment extends Fragment {
private ListViewModel viewModel;
#Override
public void onViewCreated(#NonNull View view, Bundle savedInstanceState) {
viewModel = new ViewModelProvider(requireActivity()).get(ListViewModel.class);
viewModel.getFilters().observe(getViewLifecycleOwner(), set -> {
// Update the selected filters UI
});
}
}
take note of this
viewModel = new ViewModelProvider(requireActivity()).get(ListViewModel.class);
requireActivity() makes sure all fragments call the context of the host activity.
you can't share data with activities this way though since the view model instance is destroyed when the activity is destroyed