I have a question that is more related to proper design and architecture of MVVM than to coding itself. In my project I have a situation that on ViewModel is suplying data, that are later used in RecyclerView.Adapter to create a proper view.
However I wonder if this would be also correct (from proper 'way of doing things' POV) if some of the data would be supplied in form of id's to be further fetched from Room or external server? For instance during onBindViewHolder use some LiveData with observe() to update certain fields on succesfull load.
Doing data fetch in the views is a no-go. It defeats the very purpose of MVVM and in particular the Android Jetpack efforts. One of the big reason is the data needs to survive configurations. Putting "data fetching" mechanism in the view defeats that as the view can be destroyed and recreated anytime when need be.
So I would suggest you make sure all calls to the network or any other source of data for that matter revolve around the ViewModel and never a view. Let the VM feed the data to the View through observer.
Exception to what I have just said is such use case as loading images with Picasso or Glide where by you feed them URL and they load image. But that's a different thing as they are designed to handle that.
UPDATE with followup Questions
it's ok to put observe() still inside Adapter, during the binding process?
No! Data sent to the adapter must be complete in the purpose it is supposed to serve. For example, if you have to do list app and your Top-Level Activity displays all Todos, then you must feed adapter will complete data (Title, Created time, et al).
The data that are not going to be displayed (like descriptions or sub-to-do-lists) and aren't necessary to identify specific to do should not be fetched (unless you want to store them for sole purpose of avoiding a second network call and pass them to the next activity as serialized data).
When user clicks specific To-Do, then launch new activity with its own ViewModel that will fetch details for that activity (assuming you passed some ID with intent).
If the first, then I understand that observe() should not only update data, but also populate it later to Observer and call notifyDataSetChanged(), right?
Observe is a way to post data to the view when either it have changed or the old view was destroyed and so the new view is being given the same old data that survived the "view death". So it is in the same method where updating data of the Adapter should be done and hence call to notifyDataSetChanged() or similar methods.
I hope that makes it clear.
I think it's best to keep the ViewModel separate from the Adapter. From what I'm gathering you want to basically have each list item load it's own data from Room by having each list item observe on a particular ID for that item. This sounds rather inefficient as you're basically having each item execute a query on the database (or network call) to retrieve just one item for all items that are going to be displayed, think about how it will scale if you were displaying a 100 items. I think it's best that you just execute one query to get the list of data and set the data to the list items in the adapter, keep it simple. Note that onBindViewHolder() doesn't just get called once but multiple times when you're scrolling the screen, so it could get quite ugly if you're trying to lazily load every list item.
Related
In my app I get some data from server in json. Then I fetch fields from json and fill my RecyclerView list with custom adapter. This list is filled with data about jobs. When I click on a job of my recyclerView I move from hosting fragment to dialogfragment for viewing it. This dialogFragment contains homesUp button after pressing which I return to hosting activity. At this dialogFragment I have button which send request to server about this job for adding it to another DB at server.
But when I press this button and send request to the server then after returning to hosting activity I see that data of selected isn't changed. I have several ways of solving this problem:
Add to my Singleton class variable of ArrayList which will be filled by data of shown job and then I will check whether my Arraylist from Singleton contains this id
another method - notifyItemChanged()
All these methods don't work because my list is filled by old data and I only dismiss dialogFragment and return to fragment which contains the list with old data.
Only one way - send request to server for filling the list again.
So, I need your help or useful advice :)
You need to broadcast changed data from dialog fragment to previous screen. For doing this can use LocalBroadcastManager or rxjava or even live data.
Or create a pagedlistadapter from paging library released in android architecture complement and load data directly from query.
Reference: https://developer.android.com/topic/libraries/architecture/paging
Let me just say up front that this is more of a "structural" question, and I'm not asking anybody to write code; I'm just trying to figure out how I should be structuring my application.
I'm using Android's DrawerLayout/NavigationView for my app. This means that MainActivity is the host for all my fragments.
I currently have three fragments (in reality it's many more, but they are more or less exactly like these three fragments, just for different sets of data).
ListFragment
DetailFragment
EditFragment (used for both adding and editing)
On my ListFragment I have (surprise!) a list of items. This is a LiveData collection on SharedViewModel (which is tied to MainActivity's lifecycle). When an item is tapped I pass the event through to MainActivity by means of an interface listener.
MainActivity then loads up the DetailFragment. In the same call, I load an instance of SharedViewModel (again tied to MainActivity). I set SharedViewModel.selectedItem to be the tapped item. Then, in DetailFragment's onCreate function, I get the selected item via ViewModelProviders.of(activity).get(SharedViewModel::class.java).selectedItem.
On the DetailFragment, there's an edit button. This goes through more or less the same routine described above, but loading up the EditFragment instead. When the edited/added item is saved, I add/replace the item in SharedViewModel's collection through MainActivity's interface listener.
Obviously this is not optimal for several reasons. For one, it means that I've got at least five large sets of data hanging around for MainActivity's lifecycle (the entire lifecycle of the app, essentially). Also, MainActivity grows way out of hand as I have to add more and more functions to handle events.
What I want to do is have, for example, my list of items on a ListFragmentViewModel which is tied to ListFragment's lifecycle. My selected item on a DetailFragmentViewModel, my editing item on an EditFragmentViewModel, etc.
My problem is that I'm not sure how to properly pass the data around in this case. For example, let's say I add a new item in EditFragment. How do I get that into ListFragmentViewModel's collection of items? ListFragment is in the back-stack, so its viewmodel hangs around and doesn't reload the data when it's navigated back to, since it still has the collection from before. This makes sense and is probably how it should be (after all, who wants to wait for all the data to load when they go to DetailFragment and back to ListFragment?), but it means that I don't get my new item in the collection.
That's just one example, but I'm running into quite a few issues like it (e.g. passing the selected item to DetailFragmentViewModel.)
I'm not quite sure what direction I should even be going here. Can someone more experienced help me out?
let's say I add a new item in EditFragment. How do I get that into ListFragmentViewModel's collection of items?
EditFragment tells your item repository, "yo! here's a new item!". The item repository arranges to update your backing store, plus emits an event to interested parties notifying about the data change (e.g., emits an event on an RxJava PublishSubject). The ListFragmentViewModel listens for those events and reacts accordingly.
ListFragment is in the back-stack, so its viewmodel hangs around and doesn't reload the data when it's navigated back to, since it still has the collection from before
It should be finding out about the data change from your item repository, and doing whatever makes sense to reflect that data change. That could be simply taking data from the data-change event and updating its in-memory content. That could be re-requesting information from the backing store. In principle, it could be something else.
Updating ListViews with a CursorLoader was a simple way of displaying datas from a DB into UI. Model modifications where propagated to the UI with no extra work, and maybe not so efficiently.
RecyclerView.adapter, gives access to more granularity, allowing for instance to specify the adapter that a particular item was removed.
But, what is the best place to call those preferred methods (notifyItem*), replacing notifyDataSetChanged?
Obviously, the adapter must not observe the contentProvider, otherwise it won't know the nature of the model modification (just as before).
Different patterns could be used, like adding a bus to publish modifications from the provider, creating a singleton model which would hold a reference to the adapter, maybe using presenters (introduced in L), or creating an activity-bound service.
Here is a common use case: a sync process inserts an entry in DB (or a gcm notification is received, which also inserts an entry in DB), then i want UI (if launched) to be updated via a call to notifyItemInserted. Where to place this call?
Thanks.
I have a List of items that I want in a ListView, and I can make it work with setting a custom adapter every time the List grows, but the program flow is kind of weird and I have problems with persistence. (If I switch tabs, the UI gets rebuilt with an empty ListView.)
Now, in my day job I'm a C# developer, so when I look at this problem I see a WPF ListView bound to an ObservableCollection. Does Android/Java have something like that, a "fire and forget" connection between a UI element and a data structure?
You don't need to replace the adapter every time you change the data. The adapter "adapts" between data and view. There is no need to change the adapter as long as the way it adapts does not change.
Activity / Fragment lifecycle is not necessarily the lifecycle of your data collection. You can for example make a singleton data collection somewhere and use an adapter to display that collection all the time. Call .notifyDataSetChanged() on the adapter if you changed the data.
A persistent data collection in Android is probably best backed by a database. Take a look at LoaderManager & ContentProvider to provide and load data then displayed via CursorAdapter.
There is no automatic way of keeping a bunch of data available outside of your Activity / Fragment / .. lifecycle and it can get quite complicated but that's basically what you have to do if you want to keep data for longer than a given lifecycle. Singletons, Activity#onSaveInstanceState(), Activity#getLastNonConfigurationInstance(), Fragment#setRetainInstance(), ... are useful utilities to keep data in memory, databases are good for persistent data.
You have to do a little bit work yourself but it's possible. Use a ContentProvider as your DataSource. How the data is stored is up to you. I would prefer a SQLite-DB. A content provider has the possibility to add ContentObservers. (See this related question.)
You can write a CourserAdapter to fetch the Data from your content provider. And your ContentObserver should call notifyDataSetChanged() on your adapter. This closes the circle and your UI refreshes itself.
In Addition to zapls answer:
You can also write an adapter which contains a BroadcastReceiver. When your DataSource changes you can send a LocalBroadcast. The broadcast handler just calls notifyDataSetChanged() of your adapter. I think this would work around most of the lifecycle problems because only active elements will get the broadcast.
The google documentation has an example for such a solution.
I have listview that is populated through CursorLoader. CursorLoader is created by LoaderManager.LoaderCallback's createLoader method. I have no problems with this. The problem is in that I want to start another task when listview population is completed and fill listviews with additional data.
My current solution is to start a another loader inside onLoadFinished method.
Is this right solution or it can be done in more elegant and efficient way? Could you give some advice, because I don't have much experience in android development.
Thanks in advance.
Loading from onLoadFinished will working without a problem. I did something similar in a pet project I had. I loaded data from my own ContentProvider and from there loaded contacts data from the phone's Contact ContentProvider. Each entry in my db could reference multiple contacts so I had to load my item before I knew which contact info to load. I chained the init/restart LoaderManager call to when I first received my item data in onLoadFinished. It works just fine and I used the contact data as a list afterwards. Granted I didn't use this approach to load data into a view inside an existing list view item. I used the data inside its own list in a detail view for my item. It should still work with what you want to do, but it can get messy appending data to the views and whatnot, especially since the view "lifecycle" is outside your control.
A better approach, IMO, would be to code a custom CursorAdapter that would use an AsyncTask or AsyncQueryHandler to fetch the extra data as the views are being created. Make sure to cache the data for subsequent use as the list scrolls. This second approach has the benefit of being independent of the external/secondary loader. It encapsulates all the logic required to display the data you need which includes loading the missing parts. It keeps the view data and display logic cohesive, safely tucked away inside a reusable module.