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)
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.