Map LiveData<PagedList<X>> to LiveData<PagedList<Y>> - android

I need to map domain objects to UI objects and display using a live paged list.
I have tried to map LiveData<PagedList<X>> to LiveData<PagedList<Y>>, and map PositionalDataSource<X> to PositionalDataSource<Y>, but due to package private and private restrictions these both appear to be impossible without placing my code in android.arch.paging package and using reflection, or using a modified version of the paging lib.
Does anyone know of a way to do this without resorting to such undesirable methods?
(Note that this would not be an issue if the paging library API used interfaces instead of abstract base classes - that would allow wrapping any paged list/data source and add appropriate mappings.)

DataSource and DataSource.Factory have mapBy() and mapPageBy(), which could help you in this case. Just be careful cause these two will restrict the size of the "Y"-result-list.
If the size of the result differs from the original list's size then DataSource will throw an Exception.

For me following worked :
val dataSourceFactory =
cache.getDataSourceFactory(params)
.map {
convertXToY(it)
}

Paging 3 library PagingData mapping (RxPagingSource+RxJava2)
val pagingData: PagingData<X> = //TODO
pagingData.map { pagingData ->
pagingData.mapAsync { x ->
Single.just(Y(x))
}
}

Related

How to make RecyclerView with Composable ViewHolder render faster?

As per https://developer.android.com/jetpack/compose/interop/compose-in-existing-ui#compose-recyclerview, the composable ViewHolder that can be used in RecyclerView is as below
import androidx.compose.ui.platform.ViewCompositionStrategy
class MyComposeViewHolder(
val composeView: ComposeView
) : RecyclerView.ViewHolder(composeView) {
init {
composeView.setViewCompositionStrategy(
ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed
)
}
fun bind(input: String) {
composeView.setContent {
MdcTheme {
Text(input)
}
}
}
}
It looks like, every time it is bind the composable setContent (i.e. redraw again).
I measure the speed using Profile GPU Rendering, it clearly show that the Hybrid of ReyclerView+ComposeViewHolder is slower than pure RecyclerView or LazyColumn.
You can get the design here
How can we speed up the Hybrid RecyclerView+ComposeViewHolder?
The latest version of RecyclerView (1.3.0-alpha02) and Compose (1.2.0-beta02) have improved support and performance for Compose when used in RecyclerView thanks to the PoolingContainer library it uses. No need to dispose nor set the ViewCompositionStrategy. Note that the content in https://developer.android.com/jetpack/compose/interop/compose-in-existing-ui#compose-recyclerview is only needed if you are using stable versions of the libraries.
Please run release build of the application with R8 enabled. This increases performance significantly.
Try using a diff view composition strategy, one that is tied to the lifecycle of the fragment/activity. That way your composable isn't based on attach/detatch of the view which happens a lot

Can I modify the data set of a PagingDataAdapter using peek()?

I am looking for a way to update specific items in my PagingDataAdapter from the Paging 3 library. The recommended way at the moment seems to be to invalidate the PagingSource but this causes the adapter to fetch the whole data set again, which is not efficient and also shows my loading spinner.
However, I noticed that I can access and modify items in the adapter using the peek() method and it seems to work quite well. Am I missing anything here? Will this fall apart in certain scenarios? I know that it's good practice to keep data classes immutable but this approach makes my life a lot easier.
Here is an example of my usage and it seems to work quite well:
viewModel.chatMessageUpdateEvents.collect { messageEvent ->
when (messageEvent) {
is FirestoreChatMessageListener.ChatMessageUpdateEvent.MessageUpdate -> {
val update = messageEvent.chatMessage
val historyAdapterItems = chatMessagesHistoryAdapter.snapshot().items
val updatedMessage =
historyAdapterItems.find { chatMessage ->
chatMessage.documentId == messageEvent.chatMessage.documentId
}
if (updatedMessage != null) {
val messagePosition = historyAdapterItems.indexOf(updatedMessage)
chatMessagesHistoryAdapter.peek(messagePosition)?.unsent = update.unsent
chatMessagesHistoryAdapter.peek(messagePosition)?.imageUrl = update.imageUrl
chatMessagesHistoryAdapter.notifyItemChanged(messagePosition)
}
}
}
}
I replied in a separate comment but wanted to post here for visibility.
This is really not recommended and a completely unsupported usage of paging.
One of the primary ways of restoring state if Pager().flow() hasn't been cleaned up (say if ViewModel hasn't been cleared yet) is via the .cachedIn(scope) method, which will cache out-of-date data in your case. This is also the only way to multicast (make the loaded data in PagingData re-usable) for usage in Flow operations like .combine() that allow you to mix transformations with external signals.
You'll also need to handle races between in-flight loads, what happens if you get a messageEvent the same time an append finishes? Who wins in this case and is it possible between taking the .snapshot() a new page is inserted so your notify position is no longer correct?
In general it's much simpler to have a single source of truth and this is the recommended path, so the advice has always been to invalidate on every backing dataset update.
There is an open FR in Paging's issue tracker to support Flow<Item> or Flow<Page> style data to allow granular updates, but it's certainly a farther future thing: https://issuetracker.google.com/160232968

How can we mutate items loaded using Paging 3 android?

I am loading posts from network and for this i'm using Paging 3, but now problem is that my list items contains Like/Dislike button, suppose Like is clicked then how can i update data for that item without reloading whole dataset?
i have read this android-pagedlist-updates, but it seems that this is for older Paging 2 or 1, So what is perfect way to to achieve this in Paging 3
In Paging3 you still need to rely on PagingSource.invalidate to submit updates, this isn't so much about immutability, but more about having a single source of truth.
In general, the correct way to do this is to update the backing dataset and call invalidate, which will trigger a REFRESH + DiffUtil that shouldn't cause any UI changes, but guarantees that if that page is dropped and refetched, the loaded pages will still be up-to-date. The easiest way to do this is to use a PagingSource implementation that already has self-invalidation built-in, like the one provided by Room, and just update the corresponding row onClick of the like / dislike button.
There is an open bug tracking the work to support highly frequent, granular updates to the list with a Flow<>, which you can follow here if this is your use case: https://issuetracker.google.com/160232968
I overcome this challenge with below mechanism. Maintain the internal Hashmap to hold key and object, keep this internal hashmap inside your pagedlist adapter. As the list scrolls , you will add remote like/dislike into internal hashmap as initial status by using its something unique key, since the key is unique, you will not going to duplicate and then you refer this internal hashmap for your update UI.
onClick listener of like and dislike will update this internal hashmap. again internal hashmap is reference for UI update.
Solution is simple - collecting helpful data on another internal hashmap for later manipulation.
I found a work-around with which u can achieve this, giving some of the background of the code-base that I am working with:
I have a PagingDataAdapter (Pagination-3.0 adapter) as a recycler view adapter.
I have a viewmodel for fragment
I have a repository which returns flow of PaginationData
And exposing this flow as liveData to fragment
Code for repository:
override fun getPetsList(): Flow<PagingData<Pets>> {
return Pager(
config = PagingConfig(
pageSize = 15,
enablePlaceholders = false,
prefetchDistance = 4
),
pagingSourceFactory = {
PetDataSource(petService = petService)
}
).flow
}
Code for viewmodel:
// create a hashmap that stores the (key, value) pair for the object that have changed like (id:3, pet: fav=true .... )
viewModelScope.launch {
petRepository.getPetsList()
.cachedIn(viewModelScope)
.collect {
_petItems.value = it
}
}
Now the code for fragment where mapping and all the magic happens
viewModel.petItems.observe(viewLifecycleOwner) { pagingData ->
val updatedItemsHashMap = viewModel.updatedPetsMap
val updatedPagingData = pagingData.map { pet ->
if (updatedItemsHashMap.containsKey(pet.id))
updatedItemsHashMap.getValue(pet.id)
else
pet
}
viewLifecycleOwner.lifecycleScope.launch {
petAdapter.submitData(updatedPagingData)
}
}
So that is how you can achieve this, the crux is to do mapping of pagingData which is emitted from repository.
Things which won't work:
_petItems.value = PagingData.from(yourList)
This won't work because as per docs this is used for static list, and you would loose the pagination power that comes with pagination 3.0. So mapping pagingData seems the only way.

Paging library : How to transform a PagedList to another PagedList with different size?

Currently, we have a data structure stored in Room SQLite, named Todo
Our current workflow is as follow
Dao is returning DataSource.Factory<Integer, Todo>
Use LivePagedListBuilder to turn DataSource.Factory<Integer, Todo> into LiveData<PagedList<Todo>>
Observe LiveData<PagedList<Todo>>, and use submitList to pass PagedList<Todo> to PagedListAdapter<Todo, TodoAdapter.ViewHolder>
So far so good for such simple use case.
However, right now, we have a more complex UI requirement. We need to transform Todo to TransformedTodo, before submitList to PagedListAdapter.
Here's our transform function.
List<TransformedTodo> transform(Todo todo)
Note, it is possible to transform 1 Todo, to 1 or many TransformedTodo.
My initial planned workflow is
Dao is returning DataSource.Factory<Integer, Todo>
Use DataSource.Factory.mapByPage, to transform DataSource.Factory<Integer, Todo> to DataSource.Factory<Integer, TransformedTodo>
Use LivePagedListBuilder to turn DataSource.Factory<Integer, TransformedTodo> into LiveData<PagedList<TransformedTodo>>
Observe LiveData<PagedList<TransformedTodo>>, and use submitList to pass PagedList<TransformedTodo> to PagedListAdapter<TransformedTodo, TodoAdapter.ViewHolder>
The tricky part is step 2.
If the transform function, is returning output List<TransformedTodo> where its size is different from input List<Todo>, exception will be thrown.
todosDataSourceFactory.mapByPage(new Function<List<Todo>, List<TransformedTodo>>() {
#Override
public List<TransformedTodo> apply(List<Todo> input) {
// Exception will be thrown if we are returning a List where its size is different from input.
}
});
The thrown exception is something which looks like
java.lang.IllegalStateException: Invalid Function
com.yocto.wetodo.repository.TodoRepository$1#17b6f1b changed return
size. This is not supported.
Here's why
// androidx.paging.DataSource
static <A, B> List<B> convert(Function<List<A>, List<B>> function, List<A> source) {
List<B> dest = function.apply(source);
if (dest.size() != source.size()) {
throw new IllegalStateException("Invalid Function " + function
+ " changed return size. This is not supported.");
}
return dest;
}
Seem like a limitation in paging library. Is there a way, to transform a PagedList to another PagedList with different size?
Reference links :
https://issuetracker.google.com/issues/142890117
How to use AAC paging library with list size different than list size returned by Room database
TL;DR
I have created a library to allow Paging Library's DataSources page mutation.
The problem here is that Paging Library mapByPage is considered to return the same number of items as the input ones. To be able to return different number of items than the input, a custom mutating function would be required.
Another important thing to note before proceeding, specially when working with Room, is that the DataSource.Factory<Key, Value> will in fact end up generating a PositionalDataSource. Unlike ItemKeyedDataSource and PageKeyedDataSource, it requires that the resulting page items respect the defined pageSize. So, even if we create a mapByPage-like function to allow the mutation of its items, accepting different returned list size, it would still cause problems when working with a PositionalDataSource as it does not allow this list size modification.
A solution that could solve the problem would be to wrap the PositionalDataSource inside another of the two resting DataSources types, which allow to work with non-fixed page size, and make it behave as if it would actually be a PositionalDataSource.
Once we have a non-fixed page size compatible "PositionalDataSource", we can then add the mapByPage-like mutating function.
I have actually created a simple MutableDataSource library which does exactly that. It wraps the PositionalDataSource inside an equally behaving PageKeyedDataSource. Then, a mutateByPage function can be applied, allowing to change the resulting items, even changing the list size or item type. Additionally, I have also added support for mutating the ItemKeyedDataSource and PageKeyedDataSource.
Please, keep in mind this library could still contain bugs, so feel free to report any issue or to contribute.

Android paging library with clean architecture

I was trying the paging library from Android Architecture Component but I have doubts integrating it in a clean architecture based project.
Generally I have 3 modules:
Main Module (App)
Data Module (Android module with network and db dependencies)
Domain Module (Pure Kotlin Module)
In order to introduce pagination, I had to consider PagedList<T> class as a domain class. (IMO is not a terrible idea since in the end is a list and the data source is abstracted)
So in the domain layer I can have a repostiory like:
interface ItemRepository {
fun getItems():PagedList<Item>
}
And then in the data module create the implementation like this:
class ItemRepositoryImpl: ItemRepositoy(private val factory:ItemDataSourceFavtory) {
fun getItems():PagedList<Item> {
val pageConfigurations = PagedList.Config.Builder()
.setPageSize(10)
.setInitialLoadSizeHint(15)
.setPrefetchDistance(5)
.setEnablePlaceholders(false)
.build()
return RxPagedListBuilder(locationDataSourceFactory, pageConfigurations)
.setInitialLoadKey(1)
.buildObservable()
}
So far so good. My doubt is when we need to transform the domains model for the presentation layer (let's say my item needs to be aware if was checked to show a checked Icon) normally I would map my domain model into a presentation one.
Im aware DataSourceFactory has map and mapByPage methods, but the factory resides in the data layer. My Viewmodel consumes data from the model layer which in this cases would be a PagedList, and as far as I know, paged list doesn´t support mapping.
So what would be the appropiate thing to do in this situation?
You cannot map PagedList into presentation model, since PagedListAdapter need the PagedList to load next/previous pages.
The PagedList have two main function, firstly it's a data structure to hold a List of items that are paged (Part present and part absent), using snapshot() you can easily get the present items, map domain model into a presentation one and pass it to the ListAdapter to show list of items.
Secondly it has to alert DataSource when more pages are needed. PagedListAdapter receive a PagedList, and upon binding items it depend on loadAround() method of PagedList to detect when new pages are needed.
DISCLAIMER: This is just my opinion and open to discussion
but PagingLibrary is not a clean solution by default, the easiest way is to map domain model inside DataStore upon fetching them (either network or db) and pass the PagedList with the presentation models to the presentation layer. Mapping PagedList itself is not logical (though possible) as it is tightly coupled with PagedListAdapter in View.

Categories

Resources