Modifying PagedList in Android Paging Architecture library - android

I'm currently looking into incorporating the Paging Architecture library (version 2.1.0-beta01 at the time of writing) into my app. One components is a list which allows the user to delete individual items from it. This list is network-only and caching localy with Room does not make sense.
PagedList is immutable and does not support modification. I have read that having a copy of the list which is than modified and returned as the new one is the way to go. The documentation states the same:
If you have more granular update signals, such as a network API signaling an update to a single item in the list, it's recommended to load data from the network into memory. Then present that data to the PagedList via a DataSource that wraps an in-memory snapshot. Each time the in-memory copy changes, invalidate the previous DataSource, and a new one wrapping the new state of the snapshot can be created.
I currently have the basic recommended implementation to show a simple list. My DataSource looks like this:
class MyDataSource<SomeItem> : PageKeyedDataSource<Int, SomeItem>() {
override fun loadInitial(params: LoadInitialParams<Int>, callback: LoadInitialCallback<Int, SomeItem>) {
// Simple load from API and notification of `callback`.
}
override fun loadAfter(params: LoadParams<Int>, callback: LoadCallback<Int, SomeItem>) {
// Simple load from API and notification of `callback`.
}
override fun loadBefore(params: LoadParams<Int>, callback: LoadCallback<Int, SomeItem>) {
// Simple load from API and notification of `callback`.
}
}
How would a concrete implementation of an in-memory cache (without Room and without invalidating the entire dataset) as referenced in the documentation look like?

If you want to modify your list without going all the way down to the data layer, you will need to override submitList in your adapter, and then set a callback on your PagedList object. Whenever the PagedList changes, you can then copy those changes to your local dataset. This is not recommended but it's a pretty minimal hack to get working.
Here's an example:
class MyListAdapter : PagedListAdapter<MyDataItem, MyViewHolder>(MyDiffCallback()) {
/**
* This data set is a bit of a hack -- we are copying everything the PagedList loads into our
* own list. That way we can modify it. The docs say you should go all the way down to the
* data source, modify it there, and then bubble back up, but I don't think that will actually
* work for us when the changes are coming from the UI itself.
*/
private val dataSet = arrayListOf<MyDataItem>()
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
//Forces the next page to load when we reach the bottom of the list
getItem(position)
dataSet.getOrNull(position)?.let {
holder.populateFrom(it)
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
val view = parent.inflate(R.layout.my_view_holder)
return MyViewHolder(view)
}
class MyDiffCallback : DiffUtil.ItemCallback<MyDataItem>() {
override fun areItemsTheSame(oldItem: MyDataItem, newItem: MyDataItem) =
oldItem.id == newItem.id
override fun areContentsTheSame(oldItem: MyDataItem, newItem: MyDataItem) =
oldItem == newItem
}
override fun submitList(pagedList: PagedList<MyDataItem>?) {
pagedList?.addWeakCallback(listOf(), object : PagedList.Callback() {
override fun onChanged(position: Int, count: Int) {
dataSet.clear()
dataSet.addAll(pagedList)
}
override fun onInserted(position: Int, count: Int) {
dataSet.clear()
dataSet.addAll(pagedList)
}
override fun onRemoved(position: Int, count: Int) {
dataSet.clear()
dataSet.addAll(pagedList)
}
})
super.submitList(pagedList)
}
}

You are correct in that a DataSource is meant to hold immutable data.
I believe this is because Room and Paging Library is trying to have more opinionated design decisions and advocate for immutable data.
This is why in the official docs, they have a section for updating or mutating your dataset should invalidate the datasource when such a change occurs.
Updating Paged Data: If you have more granular update signals, such as a network API signaling an update to a single item in the list, it's recommended to load data from network into memory. Then present that data to the PagedList via a DataSource that wraps an in-memory snapshot. Each time the in-memory copy changes, invalidate the previous DataSource, and a new one wrapping the new state of the snapshot can be created.
Source: https://developer.android.com/reference/android/arch/paging/DataSource
With that in mind, I believe it's possible to solve the problem you described using a couple of steps.
This may not be the cleanest way, as it involves 2 steps.
You can get a reference the the snapshot that the PagedList is holding, which is a type MutableList. Then, you can just remove or update the item inside that snapshot, without invalidating the data source.
Then step two would be to calling something like notifyItemRemoved(index) or notifyItemChanged(index).
Since you can't force the DataSource to notify the observers of the change, you'll have to do that manually.
pagedList.snapshot().remove(index) // Removes item from the pagedList
adapter.notifyItemRemoved(index) // Triggers recyclerview to redraw/rebind to account for the deleted item.
There maybe a better solution found in your DataSource.Factory.
According to the official docs, your DataSource.Factory should be the one to emit a new PagedList once the data is updated.
Updating Paged Data: To page in data from a source that does provide updates, you can create a DataSource.Factory, where each DataSource created is invalidated when an update to the data set occurs that makes the current snapshot invalid. For example, when paging a query from the Database, and the table being queried inserts or removes items. You can also use a DataSource.Factory to provide multiple versions of network-paged lists. If reloading all content (e.g. in response to an action like swipe-to-refresh) is required to get a new version of data, you can connect an explicit refresh signal to call invalidate() on the current DataSource.
Source: https://developer.android.com/reference/android/arch/paging/DataSource
I haven't found a good solution for this second approach however.

Related

Coroutines inside Recycler Adapter

for purposes of Room db. I want to run some coroutines inside Recycler View.
Necessary suspend functions are handled as a class parameters:
class RecyclerAdapter (private val exist : suspend (lastName : String) -> Boolean)
And then, when needed I'm using following construction:
GlobalScope.launch(Dispatchers.IO) {
if (exist(dataSet[position].lastName))
[...]
I'm not sure if using the Global Scope is the best practice. I considered using lifecycleScope but in Adapter lifecycleOwner is not available, handling it as a parameter is not a good practice.
What would you guys suggest?
I think it goes against the single responsibility pattern, as the purpose of an adapter is mainly to take care of how the data is laid out.
I would move this information to the list of items, and do the call from the viewmodel, with:
viewModelScope.launch{}
than update a LiveData/StateFlow, observe it from the view, and submit the list to the adapter accordingly
I suggest to use:
CoroutineScope(Dispatchers.IO).launch {}
You can get LifecycleCoroutineScope in onBindViewHolder.
override fun onBindViewHolder(holder: EventViewHolder, position: Int) {
val coroutineScope =
holder.itemView.findViewTreeLifecycleOwner()?.lifecycleScope ?: CoroutineScope(Dispatchers.IO)
}
But as Róbert Nagy said, I don't think it's a good idea to deal directly with business logic inside RecyclerView.

Paging3 without RecyclerView

I know that the Paging3 library was designed to work together with RecyclerView, however I have a use case where the paged results are also presented on a map. If you look inside the PagingDataAdapter class, you will notice that it is backed by AsyncPagingDiffer. So for now, I'm trying to make it work using the AsyncPagingDiffer class, which in turn receives a ListUpdateCallback, so that UI is notified when data updates occur. Thus, as soon as ListUpdateCallback dispatches any update, I should be able to retrieve the data just by calling AsyncPagingDiffer.snapshot().
This snippet illustrates well what I'm trying to do:
class MapAdapter : ListUpdateCallback {
private val differ = AsyncPagingDataDiffer(MapDiff(), this)
suspend fun submitData(pagingData: PagingData<Foo>) {
differ.submitData(pagingData)
}
override fun onInserted(position: Int, count: Int) {
val data = differ.snapshot()
// Update UI
}
// Other callbacks...
}
but the snapshot is always empty or out of date when trying to recover it this way. In other words, the snapshot is actually available only after the callback has already been notified, which to me is unwanted behavior.
I can confirm that this approach works with Paging 2 (or whatever it is called), but I wish there was some way to use it with Paging 3 as well, as I am reluctant to downgrade other features that are underway with Paging 3.

Android MVVM/ViewModel for RecyclerView with infinite scrolling (load more) - How to handle data on configuration change

So I have a RecyclerView with infinite scrolling. I first do a network call to my API and get a first page of 20 items.
In my ViewModel (code below), I have an observable that triggers the network call in my repository using the page number.
So, when the user scrolls to the bottom, the page number is incremented, and it triggers another network request.
Here's the code in my ViewModel:
private val scheduleObservable = Transformations.switchMap(scheduleParams) { params: Map<String, Any> ->
ScheduleRepository.schedule(params["organizationId"] as String, params["page"] as Int)
}
// This is the method I call in my Fragment to fetch another page
fun fetchSchedule(organizationId: String, page: Int) {
val params = mapOf(
"organizationId" to organizationId,
"page" to page
)
scheduleParams.value = params
}
fun scheduleObservable() : LiveData<Resource<Items>> {
return scheduleObservable
}
In my fragment, I observe scheduleObservable, and when it emits data, I append them to my RecyclerView's adapter:
viewModel.scheduleObservable().observe(this, Observer {
it?.data?.let {
if (!isAppending) {
adapter.replaceData(it)
} else {
adapter.addData(it)
}
}
})
The problem with my current implementation is that, on configuration change, I rebind my observer, and the observable emits the last fetched data. In my case, it will emit the last fetched page only.
When coming back from a configuration change, I would want to have the full list of items fetched to this point so I can repopulate the adapter with these.
I'm wondering what's the best way to solve this. Should I have two observables? Should I create a list variable in my ViewModel to store all the items fetched and use that list for my adapter?
I checked android-architecture-components on GitHub, but it's usually overkill compared for my needs (no database, no paging library, etc) and I get lost since I am still trying to wrap my head around architecture components.

ListAdapter not updating item in RecyclerView

I'm using the new support library ListAdapter. Here's my code for the adapter
class ArtistsAdapter : ListAdapter<Artist, ArtistsAdapter.ViewHolder>(ArtistsDiff()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(parent.inflate(R.layout.item_artist))
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(getItem(position))
}
class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
fun bind(artist: Artist) {
itemView.artistDetails.text = artist.artistAlbums
.plus(" Albums")
.plus(" \u2022 ")
.plus(artist.artistTracks)
.plus(" Tracks")
itemView.artistName.text = artist.artistCover
itemView.artistCoverImage.loadURL(artist.artistCover)
}
}
}
I'm updating the adapter with
musicViewModel.getAllArtists().observe(this, Observer {
it?.let {
artistAdapter.submitList(it)
}
})
My diff class
class ArtistsDiff : DiffUtil.ItemCallback<Artist>() {
override fun areItemsTheSame(oldItem: Artist?, newItem: Artist?): Boolean {
return oldItem?.artistId == newItem?.artistId
}
override fun areContentsTheSame(oldItem: Artist?, newItem: Artist?): Boolean {
return oldItem == newItem
}
}
What's happening is when submitList is called the first time the adapter renders all the items, but when submitList is called again with updated object properties it does not re-render the view which has changed.
It re-renders the view as I scroll the list, which in turn calls bindView()
Also, I've noticed that calling adapter.notifyDatasSetChanged() after submit list renders the view with updated values, but I don't want to call notifyDataSetChanged() because the list adapter has diff utils built-in
Can anyone help me here?
Edit: I understand why this happens that wasn't my point. My point is that it at least needs to give a warning or call the notifyDataSetChanged() function. Because apparently I am calling the submitList(...) function for a reason. I am pretty sure people are trying to figure out what went wrong for hours until they figure out the submitList() ignores silently the call.
This is because of Googles weird logic. So if you pass the same list to the adapter it does not even call the DiffUtil.
public void submitList(final List<T> newList) {
if (newList == mList) {
// nothing to do
return;
}
....
}
I really don't understand the whole point of this ListAdapter if it can't handle changes on the same list. If you want to change the items on the list you pass to the ListAdapter and see the changes then either you need to create a deep copy of the list or you need to use regular RecyclerView with your own DiffUtill class.
The library assumes you are using Room or any other ORM which offers a new async list every time it gets updated, so just calling submitList on it will work, and for sloppy developers, it prevents doing the calculations twice if the same list is called.
The accepted answer is correct, it offers the explanation but not the solution.
What you can do in case you're not using any such libraries is:
submitList(null);
submitList(myList);
Another solution would be to override submitList (which doesn't cause that quick blink) as such:
#Override
public void submitList(final List<Author> list) {
super.submitList(list != null ? new ArrayList<>(list) : null);
}
Or with Kotlin code:
override fun submitList(list: List<CatItem>?) {
super.submitList(list?.let { ArrayList(it) })
}
Questionable logic but works perfectly.
My preferred method is the second one because it doesn't cause each row to get an onBind call.
with Kotlin just you need to convert your list to new MutableList like this or another type of list according to your usage
.observe(this, Observer {
adapter.submitList(it?.toMutableList())
})
I had a similar problem but the incorrect rendering was caused by a combination of setHasFixedSize(true) and android:layout_height="wrap_content". For the first time, the adapter was supplied with an empty list so the height never got updated and was 0. Anyway, this resolved my issue. Someone else might have the same problem and will think it is problem with the adapter.
If you encounter some issues when using
recycler_view.setHasFixedSize(true)
you should definitly check this comment:
https://github.com/thoughtbot/expandable-recycler-view/issues/53#issuecomment-362991531
It solved the issue on my side.
(Here is a screenshot of the comment as requested)
According to the official docs :
Whenever you call submitList it submits a new list to be diffed and displayed. This is why whenever you call submitList on the previous (already submitted list), it does not calculate the Diff and does not notify the adapter for change in the dataset.
Wasted so much time to figure out the problem in same case.
But in my situation the problem was that i forgot to specify a layoutManager for my recyclerView: vRecyclerView.layoutManager = LinearLayoutManager(requireContext())
I hope no one will repeat my mistake...
Today I also stumbled upon this "problem".
With the help of insa_c's answer and RJFares's solution I made myself a Kotlin extension function:
/**
* Update the [RecyclerView]'s [ListAdapter] with the provided list of items.
*
* Originally, [ListAdapter] will not update the view if the provided list is the same as
* currently loaded one. This is by design as otherwise the provided DiffUtil.ItemCallback<T>
* could never work - the [ListAdapter] must have the previous list if items to compare new
* ones to using provided diff callback.
* However, it's very convenient to call [ListAdapter.submitList] with the same list and expect
* the view to be updated. This extension function handles this case by making a copy of the
* list if the provided list is the same instance as currently loaded one.
*
* For more info see 'RJFares' and 'insa_c' answers on
* https://stackoverflow.com/questions/49726385/listadapter-not-updating-item-in-reyclerview
*/
fun <T, VH : RecyclerView.ViewHolder> ListAdapter<T, VH>.updateList(list: List<T>?) {
// ListAdapter<>.submitList() contains (stripped):
// if (newList == mList) {
// // nothing to do
// return;
// }
this.submitList(if (list == this.currentList) list.toList() else list)
}
which can then be used anywhere, e.g.:
viewModel.foundDevices.observe(this, Observer {
binding.recyclerViewDevices.adapter.updateList(it)
})
and it only (and always) copies the list if it is the same as currently loaded one.
In my case I forgot to set the LayoutManager for the RecyclerView. The effect of that is the same as described above.
I got some strange behavior. I'm using MutableList in LiveDate.
In kotlin, the following codes don't work:
mViewModel.products.observe(viewLifecycleOwner, {
mAdapter.submitList(it)
})
But, when I change it to it.toList(), it works
mViewModel.products.observe(viewLifecycleOwner, {
mAdapter.submitList(it.toList())
})
Although, "it" was the same list.
For me, this issue appeared if I was using RecyclerView inside of ScrollView with nestedScrollingEnabled="false" and RV height set to wrap_content.
The adapter updated properly and the bind function was called, but the items were not shown - the RecyclerView was stuck at its' original size.
Changing ScrollView to NestedScrollView fixed the issue.
I had a similar problem. The issue was in the Diff functions, which didn't adequately compare the items. Anyone with this issue, make sure your Diff functions (and by extension your data object classes) contain proper comparison definitions - i.e. comparing all fields which might be updated in the new item. For example in the original post
override fun areContentsTheSame(oldItem: Artist?, newItem: Artist?): Boolean {
return oldItem == newItem
}
This function (potentially) does not do what it says on the label: it does not compare the contents of the two items - unless you have overridden the equals() function in the Artist class. In my case, I had not, and the definition of areContentsTheSame only checked one of the necessary fields, due to my oversight when implementing it. This is structural equality vs. referential equality, you can find more about it here
The reason your ListAdapter .submitlist is not called is because the object
you updated still holds the same adress in memory.
When you update an object with lets say .setText it changes the value in the original object.
So that when you check if object.id == object2.id it will return as the same
because the both have a reference to the same location in memory.
The solution is to create a new object with the updated data and insert that in your list. Then submitList will be called and it will work correctly
It solve my problem. I think the best way is not to override submitList but add a new function to add new list.
fun updateList(list: MutableList<ScaleDispBlock>?) {
list?.let {
val newList = ArrayList<ScaleDispBlock>(list)
submitList(newList)
}
}
I also ran into similar issue, my usecase was i had a clickHandler and item will be selected/not selected (toggle on click).
I tried most of the approach from the above answers, only thing that worked is
adapter.submitList(null)
adapter.submitList(modifiedList)
but problem with this is everytime i click on any clickHandler the whole list is being redrawn again which is very ineffecient.
What i did ?
I made a live data that will store last clicked item and observing that live data, we can tell adapter that live data has been updated like below
viewModel.lastClicked.observe(viewLifeCycleOwner, {
adapter.notifyItemChanged(it)
}
Had a VERY similar issue, to this one, and decided to open a new thread and even create a GitHub project to mess around with. Most solutions didn't quite work for me, not even the toMutableList() way. In my case, the problem was solved by using immutable classes and submitting immutable Lists to the Adapter.
For anyone who's scenario is same as mine, I leave my solution, which I don't know why it's working, here.
The solution which worked for me was from #Mina Samir, which is submitting the list as a mutable list.
My Issue scenario :
-Loading a friend list inside a fragment.
ActivityMain attaches the FragmentFriendList(Observes to the livedata of friend db items) and on the same time, requests a http request to the server to get all of my friend list.
Update or insert the items from the http server.
Every change ignites the onChanged callback of the livedata. But, when it's my first time launching the application, which means that there was nothing on my table, the submitList succeeds without any error of any kind, but nothing appears on the screen.
However, when it's my second time launching the application, data are being loaded to the screen.
The solution is, as metioned above, submitting the list as a mutableList.
As has already been mentioned, you cannot submit a List with the same reference because the ListAdapter will see the lists are in the same location and will therefore not be able to use the DiffUtil.
The simplest solution would be to make a shallow copy of the list.
submitList(ArrayList(list))
Be wary converting the List to a MutableList, as that can create conditions for Exceptions and hard to find bugs.
this will work ....
what happen Is when you get the current list you are pointing to the same list at same location
I needed to modify my DiffUtils
override fun areContentsTheSame(oldItem: Vehicle, newItem: Vehicle): Boolean {
To actually return whether the contents are new, not just compare the id of the model.
Using #RJFares first answer updates the list successfully, but doesn't maintain the scroll state. The entire RecyclerView starts from 0th position. As a workaround, this is what I did:
fun updateDataList(newList:List<String>){ //new list from DB or Network
val tempList = dataList.toMutableList() // dataList is the old list
tempList.addAll(newList)
listAdapter.submitList(tempList) // Recyclerview Adapter Instance
dataList = tempList
}
This way, I'm able to maintain the scroll state of RecyclerView along with modified data.
Optimal Soltion:
for Kotlin
var list :ArrayList<BaseModel> = ArrayList(adapter.currentList)
list.add(Item("Content"))
adapter.submitList(list) {
Log.e("ListAdaptor","List Updated Successfully")
}
We should not maintain another base list as adapter.currentList will return a list in which diff is already calculated.
We have to provide a new instance every time a list updated because of DiffUtil
As per android documentation
DiffUtil is a utility class that calculates the difference between two lists and outputs a list of update operations that converts the first list into the second one.
One list is already maintained by AsyncListDiffer which runs the diffutil on the background thread and another one has to be passed using adaptor.submitList()
The way that worked for me is to override the submitList() and create a copy of the incoming list and each item inside it too:
override fun submitList(list: List<Item>?) {
val listCopy =
mutableListOf<Item>().apply {
list?.map {
add(Item(it.id, it.name, it.imageUrl))
}
}
super.submitList(listCopy)
}
I encounter a very similar issue.
After the data list changed, I submit it again, the recycler view doesn't show as I wanted. It shows duplicated items.
I haven't found the root cause, but I find a workaround, that is to set the adapter to recycler view again. I guess this makes recycler viewer forget the memory before and render again correctly.
userNftListFiltered = SOME_NEW_VALUE
binding.nftSendSearchList.adapter = searchNftAdapter //set adapter again
searchNftAdapter.submitList(userNftListFiltered)
Once you have modify the array list, you have to let adapter know that which position that should be change
this code below is working in my case wish it may help
private fun addItem() {
val index = myArrayList.size
val position = myArrayList.size+1
myArrayList.add(
index, MyArrayClass("1", "Item Name")
)
myAdapter.notifyItemInserted(position) // in case of insert
// in case of remove item
// val index = myArrayList.size-1
// myAdapter.notifyItemRemoved(index)
}
just call adapter.notifyDataSetChanged() after differ.submitList
In my case i was using same object(from adadptar) to update Room database.
Create new object to update database and it'll fix the issue.
Example: I was doing this ->
val playlist = adapter.getItem(position)
playlist.name = "new name"
updatePlaylistObjectInRoomDatabase(playlist)
above code will change object in adapter before room database. So no change will be detected by DiffUtil callback.
Now doing this ->
val playlist = adapter.getItem(position)
val newPlaylist = Playlist()
newPlaylist.id = playlist.id
newPlaylist.name = "new name"
updatePlaylistObjectInRoomDatabase(newPlaylist)
Above code will not change anything in adapter list and will only change data in room database. so submitList will have different values DiffUtil callback can detect.
Enjoy the little things :)
This is something naturally expecte to be available on the official API, but as it isn't, this can be a way to deal with it:
fun <T, VH : RecyclerView.ViewHolder> ListAdapter<T, VH>.clearItems() {
submitList(null)
submitList(emptyList())
}
The adapter can not understand that you have some updates, I don't know why!?
I am adding some entities to the list ad I m expected to collect them at the consumption point. But, nothing happens.
As a solution that worked for me you can use the script below:
artistAdapter.submitList(it.toMutableList())
Because the problem lays inside the ListAdapter, I would like to solve it inside the ListAdapter.
Thanks to Kotlin extension, we can write it like:
class MyItemAdapter() :
ListAdapter<Item, RecyclerView.ViewHolder>(ItemDiffCallback) {
// ...
override fun submitList(list: List<Item>?) {
super.submitList(list?.toList())
}
}
It does look like a tricky hack. So I'd like to make a comment too:
super.submitList(list?.toList()) // to make submitList work, new value MUST be a new list. https://stackoverflow.com/a/50031492/9735961
And yes, thank you, RecyclerView developers.

Android repository-pattern & RxJava: Use Flowable or Single?

Recently, I´ve read about how important it is to have a Single-Source-Of-Truth (SSOT) when designing an app´s backend (repository, not server-side-backend). https://developer.android.com/topic/libraries/architecture/guide.html
By developing a news-feed app (using the awesome https://newsapi.org/) I am trying to learn more about app architecture.
However, I am unsure of how to design the repository interface for my app.
Btw.: I am using MVVM for my presentation layer. The View subscribes to the ViewModel´s LiveData. The ViewModel subscribes to RxJava streams.
So I came up with 2 approaches:
Approach 1:
interface NewsFeedRepository {
fun loadFeed(): Flowable<List<Article>>
fun refreshFeed(): Completable
fun loadMore(): Completable
}
interface SearchArticleRepository {
fun searchArticles(sources: List<NewsSource>? = null, query: String? = null): Flowable<List<Article>>
fun moreArticles(): Completable
}
interface BookmarkRepository {
fun getBookmarkedArticles(): Flowable<List<Article>>
fun bookmarkArticle(id: String): Completable
}
This approach is primarily using Flowables which emit data if the corresponding data in the underlying SSOT (database) changes (e.g old data gets replaced with fresh data from API, more data was loaded from API, ...). However, I am unsure if using a Flowable for SearchArticleRepository#searchArticles(...) makes sense. As it is like some request/response thing, where maybe a Single might me be more intuitive.
Approach 2:
interface NewsFeedRepository {
fun loadFeed(): Single<List<Article>>
fun refreshFeed(): Single<List<Article>>
fun loadMore(): Single<List<Article>>
}
interface SearchArticleRepository {
fun searchArticles(sources: List<NewsSource>? = null, query: String? = null): Single<List<Article>>
fun moreArticles(): Single<List<Article>>​
}
interface BookmarkRepository {
fun getBookmarkedArticles(): Single<List<Article>>
fun bookmarkArticle(id: String): Single<Article> // Returns the article that was modified. Articles are immutable. ​
}
This approach is using Singles instead of Flowables. This seems very intuitive but if the data in the SSOT changes, no changes will be emitted. Instead, a call to the repository has to be made again. Another aspect to take into account is that the ViewModel may have to manage its own state.
Let´s take the FeedViewModel for example (pseudo-code).
class FeedViewModel : ViewModel() {
// Variables, Boilerplate, ...
val newsFeed: LiveData<List<Article>>
private val articles = mutableListOf<Article>()
fun loadNewsFeed() {
// ...
repository.loadFeed()
//...
// On success, clear the feed and append the loaded articles.
.subscribe({articles.clear(); articles.addAll(it)})
// ...
}
fun loadMore() {
// ...
repository.loadMore()
//...
// On success, append the newly loaded articles to the feed.
.subscribe({articles.addAll(it)})
// ...
}
}
So this might not be crucial for a smaller app like mine, but it definitely can get a problem for a larger app (see state management: http://hannesdorfmann.com/android/arch-components-purist).
Finally, I wanted to know which approach to take and why. Are there any best-practices? I know many of you have already done some larger software-projects/apps and it would be really awesome if some of you could share some knowledge with me and others.
Thanks a lot!
I'd rather go for the first approach using Observables instead of Flowables in your case:
interface NewsFeedRepository {
fun loadFeed(): Observable<List<Article>>
fun refreshFeed(): Completable
fun loadMore(): Completable
}
interface SearchArticleRepository {
fun searchArticles(sources: List<NewsSource>? = null, query: String? = null): Observable<List<Article>>
fun moreArticles(): Completable
}
interface BookmarkRepository {
fun getBookmarkedArticles(): Observable<List<Article>>
fun bookmarkArticle(id: String): Completable
}
I don't see any reason you should necessarily use Flowable for this purpose since you'll never have any OOME related issues checking your repository changes. In other words, for your use case IMHO backpressure is not necessary at all.
Check this official guide which gives us an advice of when to a Flowable over an Observable.
On the other hand, and not related to the question itself, I have serious doubts of what's the purpose of loadMore or moreArticles methods since they return a Completable. Without knowing the context, it may seem you could either refactor the method name by a better name or change the return type if they do what they seem to do by the name.
I believe the first approach is better, Your repo will update the data whenever the data is changed and your view model will be notified automatically and that's cool, while in your second approach you have to call the repo again and that's not really reactive programming.
Also, assume that the data can be changed by something rather than load more event from the view, like when new data added to the server, or some other part of the app changes the data, Now in the first approach again you get the data automatically while for the second your not even know about the changed data and you don't know when to call the method again.

Categories

Resources