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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With