Using PagedList without a RecyclerView - android

All the examples I have seen using the PagedList use it in conjunction with a RecyclerView. I have the scenario where I would like to use the Paging library but without any need for the UI. For example, I initially have a Recyclerview that does retrieve a list of data through the use of the PagedList that is bound to a PagedListAdapter. The user can then remove items on the list they don't want and save the list.
The problem is, that the list can be large and the user may not necessarily scroll through the entire list. When the user goes to save their choices, I need to run the query again a second time that generated the RecyclerView items, only this time, the items are handled either by my ViewModel or some other business object layer. The user could hit the back button and leave the screen where they initiated the saving. They could do this while the saving is not even finished. So the saving of this data must not be bound to the UI because once the activity or fragment is destroyed, the paging data will no longer be received.
All the examples I've seen use LiveData with an observer. Because the saving can take a long time depending on the number of items to be saved, retrieving all the records must be done in batches. According to the PagedList documentation, the load around method is supposed to load the next batch of records. I wasn't able to get this to work. So I have two problems. How do I avoid using LiveData to obtain the batched records and how do I get load around to load the next batch. Here is the code in my ViewModel:
fun getConnectionsFromDB(forSearchResults: Boolean): LiveData<PagedList<Connection>> {
return LivePagedListBuilder(connections.getConnectionsFromDB(forSearchResults), App.context.repository.LIST_PAGE_SIZE).build()
}
fun storeGroupConnections(groupId: String, connections: PagedList<Connection>) {
val groupConnections = mutableListOf<GroupConnection>()
connections.forEach { connection ->
if (connection != null)
groupConnections.add(GroupConnection(groupId = groupId, connectionId = connection.rowid))
}
val disposable = Observable.fromCallable { groups.storeGroupConnections(groupConnections) }
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ },
{ ex ->
App.context.displayErrorMessage(R.string.problem_storing_connections)
},
{
connections.loadAround(groupConnections.size - 1)
}
)
disposables.add(disposable)
}
And in my fragment:
fun storeGroupConnections(groupId: String) {
connectionsViewModel.getConnectionsFromDB(args.forSearchResults).observe(this, Observer { connections ->
groupsViewModel.storeGroupConnections(groupId, connections)
})
}

Related

Mapping a Flow list to include child Flows for each item

I have a list of items that emits from a Flow on a regular cadence of once every minute. Once this Flow emits its list of items, each of those items has a list of images associated with it where each individual image can be in different states e.g. Placeholder, Loaded Image. This is where the second Flow comes in.
So once the parent Flow emits the items, I need to go and retrieve those images for each item. I want to kick off another Flow to get those images. These images first emit with their Placeholder states and then once they have successfully extracted the actual images, they can then emit the Loaded Image.
Here is the code I have so far. I'm struggling to deal with how to make the child image flow actually trigger the parent flow to emit another value. As I first want it to set the state with all of the items with placeholders for their images and then one by one update the state once the images extract and get emitted by the child Flow:
flowOfItems.map { items -> // This is a Flow
items.map { item ->
val images = repository.getImages(item) // e.g. 3 images
.map { extractImage(it) } // This is a list of Flows
Item(
item = item, // (Item)
images = images // (List<Image>)
)
}
}
.flowOn(Dispatchers.IO)
.collect {
withContext(Dispatchers.Main) {
_viewState.value = ScreenViewState(items = it)
}
}
Thanks in advance! I'd really appreciate any help as I'm very new to using Flows and this is blocking me heavily.

Kotlin Flow Offline Caching

I am new with kotlin flow and I am working about this document. Kotlin Flows. In this code every five seconds datasource fetch data from api and emits it.
This is my example datasource class.
I am getting data and emitting it.
class RemoteDataSourceImpl #Inject constructor(
private val api:CryptoApi
): RemoteDataSource {
override suspend fun cryptoList(): Flow<List<CryptoCoinDto>> {
return flow {
while (true){
val data = api.getCoinList()
emit(data)
delay(5000L)
}
}
}
}
This is my example repository.
I am mapping data and saving it room database. I want to get data from room database and emit it because of single source of truth principle but I still have to return dataSource because if I open new flow{} I can't reach datasource's data. Of course I can fix the problem by using List instead of Flow<List> inside of RemoteDataSource class. But I want to understand this example. How can I apply here single source of truth.
class CoinRepositoryImpl #Inject constructor(
private val dataSource:RemoteDataSource,
private val dao: CryptoDao
):CoinRepository {
override fun getDataList(): Flow<List<CryptoCoin>> {
dataSource.cryptoList().map { dtoList ->
val entityList = dtoList.map { dto ->
dto.toCryptoEntity()
}
dao.insertAll(entityList)
}
return dataSource.cryptoList().map {
it.map { it.toCryptoCoin() }
}
}
This is actually more complicated than it seems. Flows were designed to support back-pressure which means that they usually only produce items on demand, when being consumed. They are passive, instead of pushing items, items are pulled from the flow.
(Disclaimer: this is all true for cold flows, not for hot flows. But cryptoList() is a cold flow.)
It was designed this way to greatly simplify cases when the consumer is slower than producer or nobody is consuming items at all. Then producer just stops producing and everything is fine.
In your case there are two consumers, so this is again more complicated. You need to decide what should happen if one consumer is slower than the other. For example, what should happen if nobody collects data from getDataList()? There are multiple options, each requires a little different approach:
Stop consuming the source flow and therefore stop updating the database.
Update the database all the time and queue items if nobody is collecting from getDataList(). What if there are more and more items in the queue?
Update the database all the time and discard items if nobody is collecting from getDataList().
Ad.1.
It can be done by using onEach():
return dataSource.cryptoList().onEach {
// update db
}.map {
it.map { it.toCryptoCoin() }
}
In this solution updating the database is a "side effect" of consuming the getDataList() flow.
Ad.2. and Ad.3.
In this case we can't passively wait until someone asks us for an item. We need to actively consume items from the source flow and push them to the downstream flow. So we need a hot flow: SharedFlow. Also, because we remain the active side in this case, we have to launch a coroutine that will do this in the background. So we need a CoroutineScope.
Solution depends on your specific needs: do you need a queue or not, what should happen if queue exceeded the size limit, etc., but it will be similar to:
return dataSource.cryptoList().onEach {
// update db
}.map {
it.map { it.toCryptoCoin() }
}.shareIn(scope, SharingStarted.Eagerly)
You can also read about buffer() and MutableSharedFlow - they could be useful to you.

How to listen to a series of modifications in a ViewModel without skipping an event?

I have a ViewModel that has a list of items that a RecyclerView.Adapter uses.
When the user clicks on one of those items, one or more of the following things can happen:
the item can be modified;
another item(s) can be deleted.
I was using LiveData as a way to signal that an item of the list was being modified (more performatic than telling that the entire list was modified). But I was forgetting that LiveData can skip values.
Example:
// on background thread
mutableModificationEvent.postValue(ModificationEvent(...)) // will be skipped
mutableModificationEvent.postValue(ModificationEvent(...))
What is the most suitable way to do this job?
I know that LiveData#setValue() exists. But since #postValue can skip values and be wrongly added to the code at some point, I'm discarding LiveData as an option.
You could probably use one of the reactive streams such as in RxJava or Kotlin Coroutines. Here is an example of kotlin flow (taken from here):
val latestNews: Flow<List<ArticleHeadline>> = flow {
while(true) {
val latestNews = newsApi.fetchLatestNews()
emit(latestNews) // Emits the result of the request to the flow
delay(refreshIntervalMs) // Suspends the coroutine for some time
}
}
Here is an example of using flow with transforming it to LiveData (link):
val user: LiveData<User> = liveData {
val data = database.loadUser() // loadUser is a suspend function.
emit(data)
}
I have not tried this but it looks like the flow above should emit items one by one and no items should be skipped.

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.

Categories

Resources