I have an activity which contains fragment and this fragment have Recyclerview. For RecyclerView Adapter, I am using ViewModel with live data. When I add a record to room database recyclerView is updated but when i update or delete a record from recylerView, live data is not updated due to which recyclerVeiw adapter is not updated. To observer live data I used this code in the fragment
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
ViewModelProviders.of(this).get(OrderViewModel::class.jav
orderViewModel.getBrokerClientOrder(clientUid).observe(viewLifecycleOwner, androidx.lifecycle.Observer {
orderAdapter!!.refreshOrderAdapter(it)
})}
In Adapter to get an object of ViewModel
orderViewModel = ViewModelProviders.of(context as AppCompatActivity).get(OrderViewModel::class.java)
While in activity i used the above mechenism every thing work fine.
In your code you are observing model changes and passing it to recyclerView, but you need to observe recyclerView changes and pass it to your model.
Related
I develop an Android-App which has a RecyclerView, each item displayed in the RecyclerView represents a video saved locally. Now, when a button is clicked in one of the items in the RecyclerView, the file should be uploaded to a server. Now my question: How can I implement a solution to do this using MVVM?
I also created a little image which describes my problem:
Note: A short, conceptual answeser is sufficient
your adapter takes in a callback method to invoke once the button is tapped, the fragment then does this upload by using the VM, once that is done, success or failure you alert your UI to make whatever changes are needed. there's no interaction between your viewmodel and your adapter directly
class MyAdapter constructor(
private val callback: (item: YourType) -> Unit,
) : RecyclerView.Adapter.... {
then, you would call this in onBind or wherever you're setting up your button for the adapter, by calling:
myButton.setOnClickListener {
callback.invoke(yourItem)
}
usually this is in the format of:
override fun onBindViewHolder(holder: SomeViewHolder, position: Int) {
val yourItem: YourType = items[position]
....
yourButton.setOnClickListener {
callback.invoke(yourItem)
}
}
when you create your adapter, you now do:
myAdapter = MyAdapter () { callback ->
//here, callback will be of type `YourType`
//here, you can do whatever service call you want with your viewmodel, because this is in your fragment
}
In summary, the logic for what has to happen when an item is tapped is now shifted on to the class making use of the adapter, usually the fragment or the activity. This makes it so that you can easily reuse your adapter, because it doesn't contain any actual business logic inside of it - your adapter basically informs your fragment that:
"something was tapped, here's the corresponding item".
Your adapter notifies your fragment that an item was tapped, your fragment can then decide what to do, in this case it can shift this along to the Viewmodel so that it can then perform any network operation you need and inform the UI of the result of that operation, usually this is done by observing on to live data
In my app i use the Room library to handle user data, all the functionality has been implemented like in the "Android Basics in Kotlin" Tutorial Unit 5 on developer.android.com.
In one fragment i need to fetch a single item out of the database - for that i implemented the function in the fragment's viewmodel:
fun retrievePlaceItem(id: Int): LiveData<PlaceItem> {
return itemDao.getPlaceItem(id).asLiveData()
}
the ItemDao is passed into the ViewModel Factory from the Room Database instance, which itself is instantiated in the custom Application class.
this is the query used in the ItemDao interface:
#Query("SELECT * FROM placeItem WHERE id = :id")
fun getPlaceItem(id: Int): Flow<PlaceItem>
Data in the ItemDao is returned as a Flow, and turned into LiveData in the fetching function.
The Fragment itself observes the return of the function with a passed id, and when the observer triggers, the value is stored in a lateinit var of the corresponding datatype.
lateinit var placeItem: PlaceItem
...
override fun onViewCreated(...) {
super.onViewCreated(view, savedInstanceState)
...
val id = navigationArgs.itemId
sharedViewModel.retrievePlaceItem(id)
.observe(this.viewLifecycleOwner) { selectedItem ->
placeItem = selectedItem
}
...
}
this works flawlessly, the item is retrieved, the observer gets triggered, and the lateinit var placeItem is initialized for further use.
in another fragment, that follows later on, i use a different viewmodel with the exact same function - i try to retrieve the value in the exact same way, observing the function return within the onViewCreated method of the fragment. the code is exactly the same, and i tried comparing it to the things taught in the tutorial - no deviations whatsoever. when i now go to use the value, i get an error
kotlin.UninitializedPropertyAccessException: lateinit property placeItem has not been initialized
after inspecting my code using logs, i understood the following:
the viewmodel function to retrieve the item is called
the correct item id is used
the code inside the observer curly brackets is not executed
i tried using the same viewmodel in both fragments, anything until there was no more conceivable difference between these two pieces of code. yet the first one works, the second one doesn't. something in my code creates a difference between the two instances of me using the database to fetch an item.
I avoided the problem by moving the other functions, that will handle the lateinit var, into the observer brackets. i'm unsure as to why that was not needed in my other fragment, but this works just fine!
I'm showing my recycler view like this:
parrafoLeyViewModel.allParrafosLey.observe(this, Observer {
recyclerview_lectura_ley.layoutManager = LinearLayoutManager(this)
adapterListar = AdapterListarLey(this, it, this)
recyclerview_lectura_ley.adapter = adapterListar
})
So when I insert data into the RoomDatabase my recyclerview restarts and goes back to the start
GlobalScope.launch {
db.parrafoLeyDao().updateComentarioLey(
ComentarioLey(
idParrfo,
spTipoComentario.selectedItem.toString(),
etInserteComentario.text.toString()
)
)
}
What I want is that when I insert the data it remains in the same position. I am using ViewModel
You are not correctly passing the data to Recycler View
Please make sure your Data Source is wrapped inside a Live Data
And then pass this live data to your Recycler View Adapter.
This way Recycler View will only re-draw the changed item and not the entire list. Because Recycler View works well with Live Data.
Any method of Recycler View which will cause the list to re-draw should be excluded.
for e.g. notifyDataSetChanged() method.
Instead call submitList()
And lastly please go through below documentation:
Recycler View Sample
RecyclerView.State
I have two fragments. One fragment (InterfaceFragment) displays a spinner, which displays a list of strings (groupNames). I have this linked up to MutableLiveData<List> in my ViewModel. This sets the value of the MutableLiveData from a Room database. This all works and displays great.
Simplifying slightly, this main fragment can navigate to another fragment (EditGroupFragment) which can add or remove these groupNames from the database. Now of course I would like that when I navigate back it would automatically update the spinner's contents, however it doesn't. I am using the same viewmodel in both fragments, however I believe they are using two different instances of the viewmodel, so when the viewmodel in EditGroupFragment changes the value, this calls the onChanged() listener of the observers since I am re-installing them, however the value returned is outdated.
When the InterfaceFragment's onCreateView() is called, it re-initialises the viewmodel, and the spinner items are updated with the current correct items.
A simple solution is manually re-initialising the desired values in the viewmodel in the InterfaceFragment's onResume() method, however I feel this isn't the 'correct' solution, there is clearly something going wrong.
I believe I would need a way that changing the MutableLiveData value in EditGroupFragment causes the observers I set up in InterfaceFragment to call onChanged() and update the UI.
Different instances of the viewmodel:
/*
I/EditDictGroupFrag: dictViewModel = com.example.mydictionaryapp.dictionary.DictionaryInterfaceViewModel#8326975
DictInterface: dictViewModel = com.example.mydictionaryapp.dictionary.DictionaryInterfaceViewModel#80e8488
//I set the value in EditGroupFragment
I/onGetAllDictGroups: allDictGroups.value set to : [Test Group]
//I check what the value is in my InterfaceFragment
DictInterface : dictViewModel.allDictGroups.value = [Test Group 1, Test Group 2]
*/
Okay time for some code.
This is how I'm getting the viewmodel in each Fragment. (I don't wanna post all the other irrelevant code)
class DictionaryInterfaceFragment: Fragment() {
private lateinit var dictViewModel: DictionaryInterfaceViewModel
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
dictViewModel = ViewModelProvider(this).get(DictionaryInterfaceViewModel::class.java)
dictViewModel.dictGroupNames.observe(viewLifecycleOwner, Observer {
allDictGroups -> // set spinner items
}
}
}
So while writing out the question I stumbled upon a solution. By passing the activity into the ViewModelProvider both fragments can gain access to the same viewmodel, rather than the provider supplying different instances to each fragment.
eg.
dictViewModel = ViewModelProvider(requireActivity()).get(DictionaryInterfaceViewModel::class.java)
Please feel free to contribute anything else to the discussion, and I hope this helps someone.
Question : Can I implement android app with MVVM without using Databinding.
Problem I am trying to solve is pretty simple:
read a list of items from backend API and show them in a Recylerview.
How I am implementing:
In the View - I have Activity and RecyclerViewAdapter
Model : ApiResponse and data models
network - retrofit API service, RxJava2
for ViewModel part - I have a ViewModel class(that doesn't derive from anything) that basically calls Retrofit Service and gets data using RxJava calls.
ViewModel has calls such as :
void getItems();
void addItemData();
void removeItem();
which call service with RXJava2 as
ObServable<ApiResponse> getItems();
ObServable<ApiResponse> addItemData();
ObServable<ApiResponse> removeItem();
View instantiates ViewModel object.
ViewModel gets the instance of Adapter object during creation.
In the View, clicking a button calls a ClickHandler in the Activity which calls a ViewModel#getItems() method. Since ViewModel has link to Adapter, the viewModel updates the items in the adapter so that RecyclerView is automatically updated.
I am not sure if this is right approach for MVVM.
Databinding seems a bit like spaghetti to me.
Again, can we implement MVVM in android without DataBinding ?
Is the approach OK?
Yes! You can. But i think your approach can be better.
Remember that the view model must not have a reference to your view.
ViewModel expose observables, and in your view, you should observe those observables and react over changes.
You can have something like this:
Note: This example is with Kotlin and LiveData because, why not? But you can take this and use it with Java & Rx
ItemsViewModel : ViewModel() {
private val items = MutableLiveData<List<Items>>()
fun getAllItems() : LiveData<List<Items>> {
return items
}
//..
}
ItemsActivity : Activity() {
private var itemsAdapter: ItemsAdapter? = null
private var viewModel: ItemsViewModel? = null
override fun onCreate(savedInstance: Bundle) {
// ...
// Create your Adapter
itemsAdapter = ItemsAdapter()
recyclerView.adapter = itemsAdapter
// Create and observe your view model
viewModel = ViewModelProviders.of(this).get(ItemsViewModel::class.java)
viewModel.getAllItems().observe(this, Observer {
it?.let {
adapter?.datasource = it
}
}
In this case, the view observes view model, and notify the adapter. Then in your adapter, you do the bind as usual, without databinding.
Definitely possible, it's totally up to you how you interpret the "binding" part of MVVM. In our team, we use MVVM with RxJava instead of Android Data Binding.
Your ViewModel has an interface with outputs and inputs like this:
interface TasksViewModel {
// inputs
Observer<Task> taskAddedTrigger();
Observer<Task> taskClickedTrigger();
Observer<Task> taskCompletedTrigger();
// outputs
Observable<Boolean> isLoading();
Observable<List<Task>> tasks();
}
Your ViewModel then just uses RxJava to map inputs to outputs in a very functional style.
You Fragment supplies Inputs to the ViewModel whenever User input is received. It subscribes to Outputs and updates the user interface accordingly when the ViewModel's Output changes.
Here is a blog post which covers the topic in detail (Disclaimer: I wrote it)
The distinguishing characteristic of MVVM is that the ViewModel is not directly coupled to a View (indeed, you could bind your ViewModel to different layouts). This also has implications on the ease of unit testing. By having a reference to the Adapter, it is technically more like MVC. You don't have to use databinding, but for true MVVM, I think you would need another Observer Pattern mechanism for the View to be notified of changes so that it could pull the data it needs.
Your saying Since ViewModel has a link to Adapter and that is the problem because ViewModel should not have reference to view and In your adapter, you have views so by doing this your not following MVVM at all!!
You can still use MVVM without data binding but you need some way to notify the view about data changes, It can be LiveData (preferred way), Java Observable, Rx or even a custom implementation. The view will get notified about the changes and updates itself, in your case, view will update the adapter.
see my answer here for an example Are actions allowed in MVVM? Android
I think you should use data binding to notify the data changed from network or database, your viewmodel should expose methods for requiring or updating data, when the data arrived you can do some operation on your data, and post them to your container(activity or fragment), in there you can update your RecyclerView and its adapter