Using Paging Library 3 with LiveData from Java

I want to use Paging Library 3 in conjunction with LiveData from Java. The documentation explains how to use Guava Futures, RxJava Singles and Kotlin Coroutines but not how to use it with LiveData from Java. I can probably The various PagingSource classes provide load, loadSingle and loadFuture.

The load example in Kotlin loads data using retrofit with a coroutine and can thus return a LoadResult object. But with LiveData, I need to make an asynchronous call from retrofit and set the value on LiveData object. There is no separate load utility method for LiveData like there is for RxJava and Guava. So, How can I achieve this using LiveData from Java ?

package org.metabrainz.mobile.data.repository;

import androidx.annotation.NonNull;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.paging.PagingSource;

import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.metabrainz.mobile.data.sources.Constants;
import org.metabrainz.mobile.data.sources.api.MusicBrainzServiceGenerator;
import org.metabrainz.mobile.data.sources.api.SearchService;
import org.metabrainz.mobile.data.sources.api.entities.mbentity.MBEntity;
import org.metabrainz.mobile.data.sources.api.entities.mbentity.MBEntityType;
import org.metabrainz.mobile.presentation.features.adapters.ResultItem;
import org.metabrainz.mobile.presentation.features.adapters.ResultItemUtils;

import java.util.ArrayList;
import java.util.List;

import kotlin.coroutines.Continuation;
import okhttp3.ResponseBody;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;

public class SearchPagingSource extends PagingSource<Integer, ResultItem> {

    @NonNull
    private final static SearchService service = MusicBrainzServiceGenerator
            .createService(SearchService.class, true);
    @NonNull
    private MBEntityType entity;
    @NonNull
    private String searchTerm;

    public SearchPagingSource(@NonNull MBEntityType entity, @NonNull String searchTerm) {
        this.entity = entity;
        this.searchTerm = searchTerm;
    }

    @NotNull
    @Override
    public LiveData<LoadResult<Integer, ResultItem>> load(@NotNull LoadParams<Integer> loadParams,
                         @NotNull Continuation<? super LoadResult<Integer, ResultItem>> continuation) {

        Integer pageSize = loadParams.getLoadSize();
        Integer offset = loadParams.getKey() == null ? 0 : loadParams.getKey();

        MutableLiveData<LoadResult<Integer, ResultItem>> resultsLiveData = new MutableLiveData<>();
        service.searchEntity(entity.name, searchTerm, pageSize.toString(),
                String.valueOf(offset * pageSize))
                .enqueue(new Callback<ResponseBody>() {
                    @Override
                    public void onResponse(@NonNull Call<ResponseBody> call,
                                           @NonNull Response<ResponseBody> response) {
                        try {
                            List<ResultItem> data = ResultItemUtils
                                    .getJSONResponseAsResultItemList(response.body().string(), entity);

                            LoadResult.Page<Integer, ResultItem> loadResult
                                    = new LoadResult.Page<>(data, Math.max(0, offset - pageSize),
                                    offset + pageSize, LoadResult.Page.COUNT_UNDEFINED,
                                    LoadResult.Page.COUNT_UNDEFINED);
                            resultsLiveData.setValue(loadResult);
                        } catch (Exception e) {
                            e.printStackTrace();
                            LoadResult.Error<Integer, ResultItem> error = new LoadResult.Error<>(e);
                            resultsLiveData.setValue(error);
                        }
                    }

                    @Override
                    public void onFailure(@NonNull Call<ResponseBody> call, @NonNull Throwable t) {

                    }
                });
        return resultsLiveData;
    }


}

This however crashes at runtime

org.metabrainz.android E/AndroidRuntime: FATAL EXCEPTION: main
    Process: org.metabrainz.android, PID: 2222
    java.lang.ClassCastException: androidx.lifecycle.MutableLiveData cannot be cast to androidx.paging.PagingSource$LoadResult
        at androidx.paging.PageFetcherSnapshot.doInitialLoad(PageFetcherSnapshot.kt:302)
        at androidx.paging.PageFetcherSnapshot$pageEventFlow$1.invokeSuspend(PageFetcherSnapshot.kt:149)
        at androidx.paging.PageFetcherSnapshot$pageEventFlow$1.invoke(Unknown Source:10)
        at androidx.paging.CancelableChannelFlowKt$cancelableChannelFlow$1.invokeSuspend(CancelableChannelFlow.kt:35)
        at androidx.paging.CancelableChannelFlowKt$cancelableChannelFlow$1.invoke(Unknown Source:10)
        at kotlinx.coroutines.flow.ChannelFlowBuilder.collectTo$suspendImpl(Builders.kt:327)
        at kotlinx.coroutines.flow.ChannelFlowBuilder.collectTo(Unknown Source:0)
        at kotlinx.coroutines.flow.internal.ChannelFlow$collectToFun$1.invokeSuspend(ChannelFlow.kt:33)
        at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
        at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:56)
        at kotlinx.coroutines.EventLoop.processUnconfinedEvent(EventLoop.common.kt:69)
        at kotlinx.coroutines.DispatchedContinuationKt.resumeCancellableWith(DispatchedContinuation.kt:321)
        at kotlinx.coroutines.intrinsics.CancellableKt.startCoroutineCancellable(Cancellable.kt:26)
        at kotlinx.coroutines.CoroutineStart.invoke(CoroutineStart.kt:109)
        at kotlinx.coroutines.AbstractCoroutine.start(AbstractCoroutine.kt:158)
        at kotlinx.coroutines.BuildersKt__Builders_commonKt.launch(Builders.common.kt:54)
        at kotlinx.coroutines.BuildersKt.launch(Unknown Source:1)
        at kotlinx.coroutines.BuildersKt__Builders_commonKt.launch$default(Builders.common.kt:47)
        at kotlinx.coroutines.BuildersKt.launch$default(Unknown Source:1)
        at androidx.lifecycle.BlockRunner.maybeRun(CoroutineLiveData.kt:174)
        at androidx.lifecycle.CoroutineLiveData.onActive(CoroutineLiveData.kt:240)
        at androidx.lifecycle.LiveData$ObserverWrapper.activeStateChanged(LiveData.java:437)
        at androidx.lifecycle.LiveData$LifecycleBoundObserver.onStateChanged(LiveData.java:395)
        at androidx.lifecycle.LifecycleRegistry$ObserverWithState.dispatchEvent(LifecycleRegistry.java:361)
        at androidx.lifecycle.LifecycleRegistry.forwardPass(LifecycleRegistry.java:300)
        at androidx.lifecycle.LifecycleRegistry.sync(LifecycleRegistry.java:339)
        at androidx.lifecycle.LifecycleRegistry.moveToState(LifecycleRegistry.java:145)
        at androidx.lifecycle.LifecycleRegistry.handleLifecycleEvent(LifecycleRegistry.java:131)
        at androidx.lifecycle.ReportFragment.dispatch(ReportFragment.java:68)
        at androidx.lifecycle.ReportFragment.dispatch(ReportFragment.java:144)
        at androidx.lifecycle.ReportFragment.onStart(ReportFragment.java:109)
        at android.app.Fragment.performStart(Fragment.java:2637)
        at android.app.FragmentManagerImpl.moveToState(FragmentManager.java:1312)
        at android.app.FragmentManagerImpl.moveFragmentToExpectedState(FragmentManager.java:1549)
        at android.app.FragmentManagerImpl.moveToState(FragmentManager.java:1611)
        at android.app.FragmentManagerImpl.dispatchMoveToState(FragmentManager.java:3039)
        at android.app.FragmentManagerImpl.dispatchStart(FragmentManager.java:2996)
        at android.app.FragmentController.dispatchStart(FragmentController.java:189)
        at android.app.Activity.performStart(Activity.java:7007)
        at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2867)
        at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2986)
        at android.app.ActivityThread.-wrap11(Unknown Source:0)
        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1671)
        at android.os.Handler.dispatchMessage(Handler.java:108)
        at android.os.Looper.loop(Looper.java:206)
        at android.app.ActivityThread.main(ActivityThread.java:6784)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.Zygote$MethodAndArgsCaller.run(Zygote.java:240)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:852)

Answer

I had opened a feature request for shipping a LiveData Paging Source with the Paging Library. I received the following reply from the Google devs.

The Java Guava samples on d.android.com are for Guava + LiveData, in the coroutine equivalent we use: Guava’s ListenableFuture as an async primitive that returns a single result (equivalent to a Coroutine or RxJava Single), and LiveData for multiple results / stream of results (equivalent of Kotlin Flow, or RxJava Observable).

I was recommended to write something on the lines of the following code snippet I if I wanted to use LiveData.

abstract class SearchPagingSource extends RxPagingSource<Integer, ResultItem>() {

    @NotNull
    public abstract LiveData<LoadResult<Integer, ResultItem>> loadLiveData(params: LoadParams<Key>);

    @NotNull
    @Override
    public Single<LoadResult<Integer, ResultItem>> loadSingle(@NotNull LoadParams<Integer> loadParams) {
        return loadLiveData(params).toRxJavaSingle(); // You must implement this bit!
    }
}

The LiveData used in the above snippet should be a SingleLiveEvent

PS: The Google devs are open to reconsider their position on shipping this in the library itself if more developers request it. The relevant Google Issue tracker ticket is this.