How to cache PagingData in a way that it does not cause its data to reflow

2.4k views Asked by At

I have a simple setup of 2 fragments: ConversationFragment and DetailsFragment

I am using Room with Paging 3 library and to populate the ConversationFragment I am using a PagingLiveData implementation together with a AndroidViewModel belonging to the ConversationFragment.

I am not using the Navigation Components here, just a common fragment navigation as per Android documentation.

From that fragment I can open the DetailsFragment and then return back to the fragment again. Everything is working well, until I open said fragment and return, then the observer that was tied in the ConversationFragment is lost since that fragment is being destroyed when opening the DetailsFragment.

So far this is not a big issue, I can restart the observer again and it does work when I do that. However, when I attach the observer again the entire list reflows, this causes the items in the RecyclerView to go wild, the position the list was on is lost and the scrollbar changes sizes which confirms pages are being loaded/reloaded.

I could withstand the weird behavior to a degree, but to have the position lost on top of that is not acceptable.

I looked into caching the results in the view model, but the examples I could find in the available documentation are basic and do not show how the same could be achieved using a LiveData<PagingData<...> object.

Currently this is what I have:

ConversationFragment

    @Override
    public void onViewCreated(
        @NonNull View view,
        @Nullable Bundle savedInstanceState
    ) {

        if (viewModel == null) {
            viewModel = new ViewModelProvider(this).get(ConversationViewModel.class);
        }

        if (savedInstanceState == null) {
            // adapter is initialized in onCreateView
            viewModel
.getList(getViewLifecycleOwner())
.observe(getViewLifecycleOwner(), pagingData -> adapter.submitData(lifecycleOwner.getLifecycle(), pagingData));
        }

        super.onViewCreated(view, savedInstanceState);

    }

ConversationViewModel

public class ConversationViewModel extends AndroidViewModel {

    final PagingConfig pagingConfig = new PagingConfig(10, 10, false, 20);
    private final Repository repository;
    private final MutableLiveData<PagingData<ItemView>> messageList;

    public ConversationFragmentVM(@NonNull Application application) {
        super(application);
        messageList = new MutableLiveData<>();
        repository = new Repository(application);
    }

    public LiveData<PagingData<ItemView>> getList(@NonNull LifecycleOwner lifecycleOwner) {

        // at first I tried only setting the value if it was null
        // but since the observer is lost on destroy and the value
        // is not it would never be able to "restart" the observer
        // again
//        if (messageList.getValue() == null) {
            PagingLiveData.cachedIn(
                PagingLiveData.getLiveData(new Pager<>(pagingConfig, () -> repository.getMessageList())),
                lifecycleOwner.getLifecycle()
            ).observe(lifecycleOwner, messageList::setValue);
//        }

        return messageList;

    }

}

As it is, even if I return the result of the PagingLiveData.cachedIn the behavior is the same when I return to the fragment; the items show an erratic behavior in the recyclerview list and the position it was on is totally lost.

This is what I was trying to achieve to see if it fixed my issue: enter image description here

This is a code lab available here: https://developer.android.com/codelabs/android-training-livedata-viewmodel#8

As you can see the mAllWords are cached and they are only initialized when the view model is constructed for the first time, any subsequent changes are simply updates and would only require new observers to be attached when the fragment is destroyed and created again while still in the back stack.

This is what I was trying to do, but it does not work the way I thought it did, at least it is not as straight forward as I thought.

How can this be achieved?

1

There are 1 answers

4
Scott Cooper On BEST ANSWER

There's quite a lot to unpack here but my best guess would be your getList method in ConversationViewModel. You're on the right track with using ViewModels and LiveData to persist data across navigation but here you're recreating the LiveData every time this method is called, meaning when you resume ConversationFragment and onViewCreated is called, it creates a new Pager which fetches new data.

The solution would be to create the pager when ConversationViewModel is first created and then accessing the LiveData object itself, rather than the method. You can see this in the Codelab example, they assign the LiveData in the constructor and simply return the already created LiveData in the getAllWords() method.

I'm using this as an example, change ConversationViewModel to something like this and change it to use your config and repository.

private final LiveData<PagingData<ItemView>> messageList;

public ConversationFragmentVM(@NonNull Application application) {
    super(application);
    repository = new Repository(application);

    // CoroutineScope helper provided by the lifecycle-viewmodel-ktx artifact.
    CoroutineScope viewModelScope = ViewModelKt.getViewModelScope(viewModel);
    Pager<Integer, User> pager = Pager<>(
      new PagingConfig(/* pageSize = */ 20),
      () -> ExamplePagingSource(backend, query));

    messageList = PagingLiveData.cachedIn(PagingLiveData.getLiveData(pager), viewModelScope);
}

public LiveData<PagingData<ItemView>> getList(){
    return messageList;
}

Then in your fragment, you simply observe getList() like usual, except this time it's returning a prexisting version.

viewModel.getList().observe(getViewLifecycleOwner(), pagingData -> 
    adapter.submitData(lifecycleOwner.getLifecycle(), pagingData));
}

I haven't been able to test that this compiles or works so let me know if it doesn't and I'll update this answer.