Paging 3: How to load list at item position or at item with specific id
Asked Answered
G

1

6

I have a list of messages. Each message has a unique GUID.

My setup is working for normal usage: user clicks on conversation, list opens with all the messages belonging to that conversation, ordered by most recent first.

ConversationFragment


    @Override
    public void onViewCreated(
        @NonNull View view,
        @Nullable Bundle savedInstanceState
    ) {
        LifecycleOwner lifecycleOwner = getViewLifecycleOwner();
        viewModel = new ViewModelProvider(this).get(ConversationViewModel.class);
        viewModel
            .getMessageList(lifecycleOwner, conversationId) // conversationId is a global variable
            .observe(lifecycleOwner, messagePagingData -> adapter.submitData(
                lifecycleOwner.getLifecycle(),
                messagePagingData 
            ));

        super.onViewCreated(view, savedInstanceState);

    }

ConversationViewModel


    final PagingConfig pagingConfig = new PagingConfig(10, 10, false, 20);
    private final ConversationRepository conversationRepository;

    public ConversationViewModel(@NonNull Application application) {
        super(application);
        conversationRepository = new ConversationRepository(application);
    }

    public LiveData<PagingData<ItemMessage>> getMessageList(
        @NonNull LifecycleOwner lifecycleOwner,
        @NonNull String conversationId
    ) {
        return PagingLiveData.cachedIn(
            PagingLiveData.getLiveData(new Pager<>(pagingConfig, () -> conversationRepository.getMessageList(conversationId))),
            lifecycleOwner.getLifecycle()
        );
    }

ConversationRepository

    private final MessageDao messageDao;

    public ConversationRepository(@NonNull Context context) {
        AppDatabase database = AppDatabase.getDatabase(context);
        messageDao = database.messageDao();
    }

    public PagingSource<Integer, ItemMessage> getMessageList(@NonNull String conversationId) {
        return messageDao.getMessageList(conversationId);
    }

MessageDao

    @Query(
        "SELECT * FROM Message " +
        "WHERE Message.conversationId = :conversationId " +
        "ORDER BY Message.time DESC"
    )
    public abstract PagingSource<Integer, ItemMessage> getMessageList(String conversationId);

Now my goal is to be able to open the conversation already scrolled at a specific message.

I also do not want to load the entire conversation and then scroll to the message, some conversations can be very long and I do not want to put the user on an auto scroll that can take ages to reach the specific message.

Ideally the way I envision this being done correct is to pass the message id to be in view, load a chunk of X messages surrounding before and after that message id and then after it is already presented to the user in the RecyclerView it will load more if the user goes up or down.

This is not meant to use network requests, the entire conversation is available in the database already so it will only use the information that is already in the database.

I've tried understanding the examples that use ItemKeyedDataSource or PageKeyedDataSource, but I cannot go anywhere because every single time those examples are in Kotlin only and require Retrofit to work, which I do not use. As it is these examples are completely useless for anyone like me that is in Java and not using Retrofit.

How can this be achieved?

Please provide an answer in Java, not just Kotlin only (kotlin is OK as long as it's in java as well) and please do not suggest new libraries.

Gott answered 12/11, 2020 at 21:3 Comment(5)
The way to configure LoadParams.key for REFRESH is to change initialKey in Pager or to implement PagingSource.getRefreshKey (for subsequent calls). Unfortunately since Room's PagingSource is positionally keyed, this might be a bit hard to achieve (might be easier to flatMap to a dynamic query). I would probably just recommend implementing your own item keyed PagingSource directly in this case and then you can simply pass the item you're loading around directly to initialKey in PagerRoger
@Roger thanks for the pointers, but I'm completely lost. Could you point me to where I can learn how to do your second suggestion? Implementing my own item keyed PagingSource.Gott
@Roger the closest thing I found to your suggestion was this developer.android.com/reference/kotlin/androidx/paging/… but it just shows how to do it using Retrofit. I am not using Retrofit, in fact I just want to load the data from the Room DB itself and that documentation makes no sense for that. I am at a total loss with this whole Paging 3 documentation.Gott
@Roger I am not able to reach any working solution with the available information I found. I cannot understand how to instruct the getRefreshKey, use a different initial key in pager nor how to implement my own item keyed PagingSource. Can you provide any useful examples?Gott
Forgot to say how far I got in my rationale: get the position of the item in the resulting list in the DB, calculate in which page that item sits and use that page number as initialKey. I also tried to understand what you meant by flatMap to a dynamic query, but got no luck there either.Gott
G
3

As far as I could find the official documentation does not provide any sort of clue on how to solve this one for a Paging + Room integration. In fact, it doesn't provide any solution whatsoever to scroll to an item in a PagingDataAdapter, period.

The only thing that worked for me so far was to run two queries every single time I wish to accomplish this: one to find the item position in the result query list and the other to actually load said list with the initialKey set in the Pager constructor with the value of the item position we queried previously.

And if you're feeling a bit confused, this does not end here, because even the explanation for what is initialKey and how to use it is just not documented. No, seriously: What does the initialKey parameter do in the Pager constructor

So there's two guessing games here: one to find a proper way to lookup the item index from a result list and another to set it up properly in the final query.

I hope the Paging 3 documentation gets improved soon to cover these very basic issues.

In the end this is an example of how I managed to get this problem kind of working for me, even though I have no idea if this is the proper way to do it because, again, their documentation is absolutely lacking in this department.

  1. Create two identical queries for the list results you desire
  2. One of those queries only returns a full list of the results based on a key you'll use to uniquely identify an item. In my case it is messageId.
  3. Load the query in 2 and individually iterate the results list using a for... loop until you find the item you want to know its position in the list. That position is given by the iterator you use in your loop block.
  4. Pass the item position from 3 as initialKey parameter into your Pager builder of the final query
  5. The first chunk of data you'll receive now will contain the item you want
  6. If you want you can now scroll to that item in your RecyclerView, but you'll have to query it from the current list of items loaded in the adapter. See about using the .snapshot() in the PagingAdapter

That's it, now I can finally load an item at a certain position using Paging 3 + Room, with absolutely no idea of whether this is the proper way to do it thanks to the completely absent documentation for this.

Gott answered 27/11, 2020 at 14:33 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.