diff --git a/app/src/main/java/com/firebase/uidemo/database/realtime/FirebaseDbPagingActivity.java b/app/src/main/java/com/firebase/uidemo/database/realtime/FirebaseDbPagingActivity.java index f741a47f3..7c6aab3da 100644 --- a/app/src/main/java/com/firebase/uidemo/database/realtime/FirebaseDbPagingActivity.java +++ b/app/src/main/java/com/firebase/uidemo/database/realtime/FirebaseDbPagingActivity.java @@ -12,10 +12,8 @@ import com.firebase.ui.database.paging.DatabasePagingOptions; import com.firebase.ui.database.paging.FirebaseRecyclerPagingAdapter; -import com.firebase.ui.database.paging.LoadingState; import com.firebase.uidemo.R; import com.firebase.uidemo.databinding.ActivityDatabasePagingBinding; -import com.google.firebase.database.DatabaseError; import com.google.firebase.database.FirebaseDatabase; import com.google.firebase.database.Query; @@ -24,7 +22,8 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; -import androidx.paging.PagedList; +import androidx.paging.LoadState; +import androidx.paging.PagingConfig; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; @@ -53,11 +52,7 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { private void setUpAdapter() { //Initialize Paging Configurations - PagedList.Config config = new PagedList.Config.Builder() - .setEnablePlaceholders(false) - .setPrefetchDistance(5) - .setPageSize(30) - .build(); + PagingConfig config = new PagingConfig(30, 5, false); //Initialize Firebase Paging Options DatabasePagingOptions options = new DatabasePagingOptions.Builder() @@ -83,34 +78,47 @@ protected void onBindViewHolder(@NonNull ItemViewHolder holder, @NonNull Item model) { holder.bind(model); } - - @Override - protected void onLoadingStateChanged(@NonNull LoadingState state) { - switch (state) { - case LOADING_INITIAL: - case LOADING_MORE: - mBinding.swipeRefreshLayout.setRefreshing(true); - break; - case LOADED: - mBinding.swipeRefreshLayout.setRefreshing(false); - break; - case FINISHED: - mBinding.swipeRefreshLayout.setRefreshing(false); - showToast(getString(R.string.paging_finished_message)); - break; - case ERROR: - showToast(getString(R.string.unknown_error)); - break; - } - } - - @Override - protected void onError(DatabaseError databaseError) { - mBinding.swipeRefreshLayout.setRefreshing(false); - Log.e(TAG, databaseError.getDetails(), databaseError.toException()); - } }; + mAdapter.addLoadStateListener(states -> { + LoadState refresh = states.getRefresh(); + LoadState append = states.getAppend(); + + if (refresh instanceof LoadState.Error) { + LoadState.Error loadStateError = (LoadState.Error) refresh; + mBinding.swipeRefreshLayout.setRefreshing(false); + Log.e(TAG, loadStateError.getError().getLocalizedMessage()); + } + if (append instanceof LoadState.Error) { + LoadState.Error loadStateError = (LoadState.Error) append; + mBinding.swipeRefreshLayout.setRefreshing(false); + Log.e(TAG, loadStateError.getError().getLocalizedMessage()); + } + + if (append instanceof LoadState.Loading) { + mBinding.swipeRefreshLayout.setRefreshing(true); + } + + if (append instanceof LoadState.NotLoading) { + LoadState.NotLoading notLoading = (LoadState.NotLoading) append; + if (notLoading.getEndOfPaginationReached()) { + // This indicates that the user has scrolled + // until the end of the data set. + mBinding.swipeRefreshLayout.setRefreshing(false); + showToast("Reached end of data set."); + return null; + } + + if (refresh instanceof LoadState.NotLoading) { + // This indicates the most recent load + // has finished. + mBinding.swipeRefreshLayout.setRefreshing(false); + return null; + } + } + return null; + }); + mBinding.pagingRecycler.setLayoutManager(new LinearLayoutManager(this)); mBinding.pagingRecycler.setAdapter(mAdapter); diff --git a/database/README.md b/database/README.md index 6571bf709..f1d27f2c6 100644 --- a/database/README.md +++ b/database/README.md @@ -121,7 +121,7 @@ FirebaseUI offers two types of RecyclerView adapters for the Realtime Database: * `FirebaseRecyclerAdapter` — binds a `Query` to a `RecyclerView` and responds to all real-time events included items being added, removed, moved, or changed. Best used with small result sets since all results are loaded at once. - * `FirebasePagingRecyclerAdapter` — binds a `Query` to a `RecyclerView` by loading data in pages. Best + * `FirebaseRecyclerPagingAdapter` — binds a `Query` to a `RecyclerView` by loading data in pages. Best used with large, static data sets. Real-time events are not respected by this adapter, so it will not detect new/removed items or changes to items already loaded. @@ -245,13 +245,13 @@ FirebaseRecyclerAdapter adapter = new FirebaseRecyclerAdapter( The `FirebaseRecyclerPagingAdapter` binds a `Query` to a `RecyclerView` by loading documents in pages. This results in a time and memory efficient binding, however it gives up the real-time events -afforded by the `FirestoreRecyclerAdapter`. +afforded by the `FirebaseRecyclerAdapter`. -The `FirebaseRecyclerPagingAdapter` is built on top of the [Android Paging Support Library][paging-support]. -Before using the adapter in your application, you must add a dependency on the support library: +The `FirebaseRecyclerPagingAdapter` is built on top of the [Android Paging 3 Library][paging-support]. +Before using the adapter in your application, you must add a dependency on that library: ```groovy -implementation 'androidx.paging:paging-runtime:2.x.x' +implementation 'androidx.paging:paging-runtime:3.x.x' ``` First, configure the adapter by building `DatabasePagingOptions`. Since the paging adapter @@ -263,13 +263,10 @@ an adapter that loads a generic `Item`: // to form smaller queries for each page. Query baseQuery = mDatabase.getReference().child("items"); -// This configuration comes from the Paging Support Library -// https://developer.android.com/reference/androidx/paging/PagedList.Config -PagedList.Config config = new PagedList.Config.Builder() - .setEnablePlaceholders(false) - .setPrefetchDistance(10) - .setPageSize(20) - .build(); +// This configuration comes from the Paging 3 Library +// https://developer.android.com/reference/kotlin/androidx/paging/PagingConfig +PagingConfig config = new PagingConfig(/* page size */ 20, /* prefetchDistance */ 10, + /* enablePlaceHolders */ false); // The options for the adapter combine the paging configuration with query information // and application-specific options for lifecycle, etc. @@ -360,34 +357,98 @@ start and stop listening in `onStart()` and `onStop()`. #### Paging events When using the `FirebaseRecyclerPagingAdapter`, you may want to perform some action every time data -changes or when there is an error. To do this, override the `onLoadingStateChanged()` -method of the adapter: +changes or when there is an error. To do this: -```java -FirebaseRecyclerPagingAdapter adapter = - new FirebaseRecyclerPagingAdapter(options) { +##### In Java - // ... +Use the `addLoadStateListener` method from the adapter: +```java +adapter.addLoadStateListener(new Function1() { @Override - protected void onLoadingStateChanged(@NonNull LoadingState state) { - switch (state) { - case LOADING_INITIAL: - // The initial load has begun - // ... - case LOADING_MORE: - // The adapter has started to load an additional page + public Unit invoke(CombinedLoadStates states) { + LoadState refresh = states.getRefresh(); + LoadState append = states.getAppend(); + + if (refresh instanceof LoadState.Error || append instanceof LoadState.Error) { + // The previous load (either initial or additional) failed. Call + // the retry() method in order to retry the load operation. + // ... + } + + if (refresh instanceof LoadState.Loading) { + // The initial Load has begun + // ... + } + + if (append instanceof LoadState.Loading) { + // The adapter has started to load an additional page + // ... + } + + if (append instanceof LoadState.NotLoading) { + LoadState.NotLoading notLoading = (LoadState.NotLoading) append; + if (notLoading.getEndOfPaginationReached()) { + // The adapter has finished loading all of the data set // ... - case LOADED: + return null; + } + + if (refresh instanceof LoadState.NotLoading) { // The previous load (either initial or additional) completed // ... - case ERROR: - // The previous load (either initial or additional) failed. Call - // the retry() method in order to retry the load operation. - // ... + return null; + } } + return null; } - }; + }); +``` + +#### In Kotlin + +Use the `loadStateFlow` exposed by the adapter, in a Coroutine Scope: + +```kotlin +// Activities can use lifecycleScope directly, but Fragments should instead use +// viewLifecycleOwner.lifecycleScope. +lifecycleScope.launch { + pagingAdapter.loadStateFlow.collectLatest { loadStates -> + when (loadStates.refresh) { + is LoadState.Error -> { + // The initial load failed. Call the retry() method + // in order to retry the load operation. + // ... + } + is LoadState.Loading -> { + // The initial Load has begun + // ... + } + } + + when (loadStates.append) { + is LoadState.Error -> { + // The additional load failed. Call the retry() method + // in order to retry the load operation. + // ... + } + is LoadState.Loading -> { + // The adapter has started to load an additional page + // ... + } + is LoadState.NotLoading -> { + if (loadStates.append.endOfPaginationReached) { + // The adapter has finished loading all of the data set + // ... + } + if (loadStates.refresh is LoadState.NotLoading) { + // The previous load (either initial or additional) completed + // ... + } + } + } + } +} ``` @@ -458,4 +519,4 @@ The order in which you receive your data depends on the order from `keyRef`, not [indexed-data]: https://firebase.google.com/docs/database/android/structure-data#best_practices_for_data_structure [recyclerview]: https://developer.android.com/reference/androidx/recyclerview/widget/RecyclerView [arch-components]: https://developer.android.com/topic/libraries/architecture/index.html -[paging-support]: https://developer.android.com/topic/libraries/architecture/paging +[paging-support]: https://developer.android.com/topic/libraries/architecture/paging/v3-overview diff --git a/database/build.gradle.kts b/database/build.gradle.kts index 9036bdb43..bc7e9aac8 100644 --- a/database/build.gradle.kts +++ b/database/build.gradle.kts @@ -43,6 +43,11 @@ android { consumerProguardFiles("proguard-rules.pro") } } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } } dependencies { @@ -54,6 +59,7 @@ dependencies { api(Config.Libs.Androidx.recyclerView) compileOnly(Config.Libs.Androidx.paging) + api(Config.Libs.Androidx.pagingRxJava) annotationProcessor(Config.Libs.Androidx.lifecycleCompiler) androidTestImplementation(Config.Libs.Test.junit) diff --git a/database/src/main/java/com/firebase/ui/database/paging/DatabasePagingOptions.java b/database/src/main/java/com/firebase/ui/database/paging/DatabasePagingOptions.java index 2561dc409..012de7bcb 100644 --- a/database/src/main/java/com/firebase/ui/database/paging/DatabasePagingOptions.java +++ b/database/src/main/java/com/firebase/ui/database/paging/DatabasePagingOptions.java @@ -10,8 +10,10 @@ import androidx.annotation.Nullable; import androidx.lifecycle.LifecycleOwner; import androidx.lifecycle.LiveData; -import androidx.paging.LivePagedListBuilder; -import androidx.paging.PagedList; +import androidx.paging.Pager; +import androidx.paging.PagingConfig; +import androidx.paging.PagingData; +import androidx.paging.PagingLiveData; import androidx.recyclerview.widget.DiffUtil; /** @@ -22,11 +24,11 @@ public final class DatabasePagingOptions { private final SnapshotParser mParser; - private final LiveData> mData; + private final LiveData> mData; private final DiffUtil.ItemCallback mDiffCallback; private final LifecycleOwner mOwner; - private DatabasePagingOptions(@NonNull LiveData> data, + private DatabasePagingOptions(@NonNull LiveData> data, @NonNull SnapshotParser parser, @NonNull DiffUtil.ItemCallback diffCallback, @Nullable LifecycleOwner owner) { @@ -37,7 +39,7 @@ private DatabasePagingOptions(@NonNull LiveData> data, } @NonNull - public LiveData> getData() { + public LiveData> getData() { return mData; } @@ -61,7 +63,7 @@ public LifecycleOwner getOwner() { */ public static final class Builder { - private LiveData> mData; + private LiveData> mData; private SnapshotParser mParser; private LifecycleOwner mOwner; private DiffUtil.ItemCallback mDiffCallback; @@ -70,11 +72,11 @@ public static final class Builder { * Sets the query using a {@link ClassSnapshotParser} based * on the given class. * - * See {@link #setQuery(Query, PagedList.Config, SnapshotParser)}. + * See {@link #setQuery(Query, PagingConfig, SnapshotParser)}. */ @NonNull public Builder setQuery(@NonNull Query query, - @NonNull PagedList.Config config, + @NonNull PagingConfig config, @NonNull Class modelClass) { return setQuery(query, config, new ClassSnapshotParser<>(modelClass)); } @@ -90,10 +92,12 @@ public Builder setQuery(@NonNull Query query, */ @NonNull public Builder setQuery(@NonNull Query query, - @NonNull PagedList.Config config, + @NonNull PagingConfig config, @NotNull SnapshotParser parser) { - FirebaseDataSource.Factory factory = new FirebaseDataSource.Factory(query); - mData = new LivePagedListBuilder<>(factory, config).build(); + final Pager pager = new Pager<>(config, + () -> new DatabasePagingSource(query)); + mData = PagingLiveData.cachedIn(PagingLiveData.getLiveData(pager), + mOwner.getLifecycle()); mParser = parser; return this; @@ -135,7 +139,7 @@ public DatabasePagingOptions build() { } if (mDiffCallback == null) { - mDiffCallback = new DefaultSnapshotDiffCallback(mParser); + mDiffCallback = new DefaultSnapshotDiffCallback<>(mParser); } return new DatabasePagingOptions<>(mData, mParser, mDiffCallback, mOwner); diff --git a/database/src/main/java/com/firebase/ui/database/paging/DatabasePagingSource.java b/database/src/main/java/com/firebase/ui/database/paging/DatabasePagingSource.java new file mode 100644 index 000000000..442b6e388 --- /dev/null +++ b/database/src/main/java/com/firebase/ui/database/paging/DatabasePagingSource.java @@ -0,0 +1,122 @@ +package com.firebase.ui.database.paging; + +import android.annotation.SuppressLint; + +import com.google.android.gms.tasks.Task; +import com.google.android.gms.tasks.Tasks; +import com.google.firebase.database.DataSnapshot; +import com.google.firebase.database.DatabaseError; +import com.google.firebase.database.Query; + +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.ExecutionException; + +import androidx.annotation.NonNull; +import androidx.paging.PagingState; +import androidx.paging.rxjava3.RxPagingSource; +import io.reactivex.rxjava3.core.Single; +import io.reactivex.rxjava3.schedulers.Schedulers; + +public class DatabasePagingSource extends RxPagingSource { + private final Query mQuery; + + private static final String STATUS_DATABASE_NOT_FOUND = "DATA_NOT_FOUND"; + private static final String MESSAGE_DATABASE_NOT_FOUND = "Data not found at given child path!"; + private static final String DETAILS_DATABASE_NOT_FOUND = "No data was returned for the given query: "; + + public DatabasePagingSource(Query query) { + this.mQuery = query; + } + + /** + * DatabaseError.fromStatus() is not meant to be public. + */ + @SuppressLint("RestrictedApi") + @NonNull + @Override + public Single> loadSingle(@NonNull LoadParams params) { + Task task; + if (params.getKey() == null) { + task = mQuery.limitToFirst(params.getLoadSize()).get(); + } else { + task = mQuery.startAt(null, params.getKey()).limitToFirst(params.getLoadSize() + 1).get(); + } + + return Single.fromCallable(() -> { + try { + Tasks.await(task); + DataSnapshot dataSnapshot = task.getResult(); + if (dataSnapshot.exists()) { + + //Make List of DataSnapshot + List data = new ArrayList<>(); + String lastKey = null; + + if (params.getKey() == null) { + for (DataSnapshot snapshot : dataSnapshot.getChildren()) { + data.add(snapshot); + } + } else { + Iterator iterator = dataSnapshot.getChildren().iterator(); + + //Skip First Item + if (iterator.hasNext()) { + iterator.next(); + } + + while (iterator.hasNext()) { + DataSnapshot snapshot = iterator.next(); + data.add(snapshot); + } + } + + //Detect End of Data + if (!data.isEmpty()) { + //Get Last Key + lastKey = getLastPageKey(data); + } + return toLoadResult(data, lastKey); + } else { + String details = DETAILS_DATABASE_NOT_FOUND + mQuery.toString(); + throw DatabaseError.fromStatus( + STATUS_DATABASE_NOT_FOUND, + MESSAGE_DATABASE_NOT_FOUND, + details).toException(); + } + } catch (ExecutionException e) { + throw new Exception(e.getCause()); + } + }).subscribeOn(Schedulers.io()).onErrorReturn(LoadResult.Error::new); + } + + private LoadResult toLoadResult( + @NonNull List snapshots, + String nextPage + ) { + return new LoadResult.Page<>( + snapshots, + null, // Only paging forward. + nextPage, + LoadResult.Page.COUNT_UNDEFINED, + LoadResult.Page.COUNT_UNDEFINED); + } + + @Nullable + private String getLastPageKey(@NonNull List data) { + if (data.isEmpty()) { + return null; + } else { + return data.get(data.size() - 1).getKey(); + } + } + + @Nullable + @Override + public String getRefreshKey(@NonNull PagingState state) { + return null; + } +} diff --git a/database/src/main/java/com/firebase/ui/database/paging/FirebaseDataSource.java b/database/src/main/java/com/firebase/ui/database/paging/FirebaseDataSource.java deleted file mode 100644 index 8d8a325b6..000000000 --- a/database/src/main/java/com/firebase/ui/database/paging/FirebaseDataSource.java +++ /dev/null @@ -1,225 +0,0 @@ -package com.firebase.ui.database.paging; - -import android.annotation.SuppressLint; -import android.util.Log; - -import com.google.android.gms.tasks.OnFailureListener; -import com.google.android.gms.tasks.OnSuccessListener; -import com.google.firebase.database.DataSnapshot; -import com.google.firebase.database.DatabaseError; -import com.google.firebase.database.Query; - -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.RestrictTo; -import androidx.lifecycle.LiveData; -import androidx.lifecycle.MutableLiveData; -import androidx.paging.DataSource; -import androidx.paging.PageKeyedDataSource; - -/** - * Data source to power a {@link FirebaseRecyclerPagingAdapter}. - * - * Note: although loadInitial, loadBefore, and loadAfter are not called on the main thread by the - * paging library, we treat them as if they were so that we can facilitate retry without - * managing our own thread pool or requiring the user to pass us an executor. - */ -@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) -public class FirebaseDataSource extends PageKeyedDataSource { - private static final String TAG = "FirebaseDataSource"; - - private Query mQuery; - - private final MutableLiveData mLoadingState = new MutableLiveData<>(); - private final MutableLiveData mError = new MutableLiveData<>(); - - private static final String STATUS_DATABASE_NOT_FOUND = "DATA_NOT_FOUND"; - private static final String MESSAGE_DATABASE_NOT_FOUND = "Data not found at given child path!"; - private static final String DETAILS_DATABASE_NOT_FOUND = "No data was returned for the given query: "; - - private Runnable mRetryRunnable; - - public static class Factory extends DataSource.Factory { - - private final Query mQuery; - - public Factory(@NonNull Query query) { - mQuery = query; - } - - @Override - @NonNull - public DataSource create() { - return new FirebaseDataSource(mQuery); - } - } - - FirebaseDataSource(Query mQuery){ - this.mQuery = mQuery; - } - - @Override - public void loadInitial(@NonNull final LoadInitialParams params, - @NonNull final LoadInitialCallback callback) { - - // Set initial loading state - mLoadingState.postValue(LoadingState.LOADING_INITIAL); - - Query mInitQuery = mQuery.limitToFirst(params.requestedLoadSize); - mInitQuery.get().addOnSuccessListener(dataSnapshot -> { - if (dataSnapshot.exists()) { - - //Make List of DataSnapshot - List data = new ArrayList<>(); - - for (DataSnapshot snapshot : dataSnapshot.getChildren()){ - data.add(snapshot); - } - - //Get Last Key - String lastKey = getLastPageKey(data); - - //Update State - mLoadingState.postValue(LoadingState.LOADED); - mRetryRunnable = null; - - callback.onResult(data, lastKey, lastKey); - - } else { - mRetryRunnable = getRetryLoadInitial(params, callback); - setDatabaseNotFoundError(); - } - }).addOnFailureListener(e -> { - mRetryRunnable = getRetryLoadInitial(params, callback); - setError(e); - }); - } - - @Override - public void loadBefore(@NonNull LoadParams params, @NonNull LoadCallback callback) { - // Ignored for now, since we only ever append to the initial load. - } - - @Override - public void loadAfter(@NonNull final LoadParams params, - @NonNull final LoadCallback callback) { - - // Set loading state - mLoadingState.postValue(LoadingState.LOADING_MORE); - - //Load params.requestedLoadSize+1 because, first data item is getting ignored. - Query mNewQuery = mQuery.startAt(null, params.key).limitToFirst(params.requestedLoadSize + 1); - mNewQuery.get().addOnSuccessListener(dataSnapshot -> { - if (dataSnapshot.exists()) { - - //Make List of DataSnapshot - List data = new ArrayList<>(); - String lastKey = null; - - Iterator iterator = dataSnapshot.getChildren().iterator(); - - //Skip First Item - if (iterator.hasNext()) { - iterator.next(); - } - - while (iterator.hasNext()) { - DataSnapshot snapshot = iterator.next(); - data.add(snapshot); - } - - //Update State - mLoadingState.postValue(LoadingState.LOADED); - mRetryRunnable = null; - - //Detect End of Data - if (data.isEmpty()) - mLoadingState.postValue(LoadingState.FINISHED); - else { - //Get Last Key - lastKey = getLastPageKey(data); - } - - callback.onResult(data, lastKey); - - } else { - mRetryRunnable = getRetryLoadAfter(params, callback); - setDatabaseNotFoundError(); - } - }).addOnFailureListener(e -> { - mRetryRunnable = getRetryLoadAfter(params, callback); - setError(e); - }); - } - - @NonNull - private Runnable getRetryLoadAfter(@NonNull final LoadParams params, - @NonNull final LoadCallback callback) { - return () -> loadAfter(params, callback); - } - - @NonNull - private Runnable getRetryLoadInitial(@NonNull final LoadInitialParams params, - @NonNull final LoadInitialCallback callback) { - return () -> loadInitial(params, callback); - } - - public void retry() { - LoadingState currentState = mLoadingState.getValue(); - if (currentState != LoadingState.ERROR) { - Log.w(TAG, "retry() not valid when in state: " + currentState); - return; - } - - if (mRetryRunnable == null) { - Log.w(TAG, "retry() called with no eligible retry runnable."); - return; - } - - mRetryRunnable.run(); - } - - - @Nullable - private String getLastPageKey(@NonNull List data) { - if (data.isEmpty()) { - return null; - } else { - return data.get(data.size() - 1).getKey(); - } - } - - /** - * DatabaseError.fromStatus() is not meant to be public. - */ - @SuppressLint("RestrictedApi") - private void setDatabaseNotFoundError(){ - String details = DETAILS_DATABASE_NOT_FOUND + mQuery.toString(); - mError.postValue(DatabaseError.fromStatus( - STATUS_DATABASE_NOT_FOUND, - MESSAGE_DATABASE_NOT_FOUND, - details)); - - mLoadingState.postValue(LoadingState.ERROR); - } - - private void setError(Exception e){ - mError.postValue(DatabaseError.fromException(e)); - mLoadingState.postValue(LoadingState.ERROR); - } - - @NonNull - public LiveData getLoadingState() { - return mLoadingState; - } - - @NonNull - public LiveData getLastError(){ - return mError; - } - -} diff --git a/database/src/main/java/com/firebase/ui/database/paging/FirebaseRecyclerPagingAdapter.java b/database/src/main/java/com/firebase/ui/database/paging/FirebaseRecyclerPagingAdapter.java index 11ddeacd7..7b20bad48 100644 --- a/database/src/main/java/com/firebase/ui/database/paging/FirebaseRecyclerPagingAdapter.java +++ b/database/src/main/java/com/firebase/ui/database/paging/FirebaseRecyclerPagingAdapter.java @@ -1,23 +1,18 @@ package com.firebase.ui.database.paging; -import android.util.Log; - import com.firebase.ui.database.SnapshotParser; import com.google.firebase.database.DataSnapshot; -import com.google.firebase.database.DatabaseError; import com.google.firebase.database.DatabaseReference; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.arch.core.util.Function; import androidx.lifecycle.Lifecycle; import androidx.lifecycle.LifecycleObserver; import androidx.lifecycle.LiveData; import androidx.lifecycle.Observer; import androidx.lifecycle.OnLifecycleEvent; -import androidx.lifecycle.Transformations; -import androidx.paging.PagedList; -import androidx.paging.PagedListAdapter; +import androidx.paging.PagingData; +import androidx.paging.PagingDataAdapter; import androidx.recyclerview.widget.RecyclerView; /** @@ -26,47 +21,24 @@ * Configured with {@link DatabasePagingOptions}. */ public abstract class FirebaseRecyclerPagingAdapter - extends PagedListAdapter + extends PagingDataAdapter implements LifecycleObserver { - private final String TAG = "FirebasePagingAdapter"; - private DatabasePagingOptions mOptions; private SnapshotParser mParser; - private LiveData> mPagedList; - private LiveData mLoadingState; - private LiveData mDatabaseError; - private LiveData mDataSource; - - - /* - LiveData created via Transformation do not have a value until an Observer is attached. - We attach this empty observer so that our getValue() calls return non-null later. - */ - private final Observer mDataSourceObserver = source -> { - - }; - - //State Observer - private final Observer mStateObserver = state -> { - if (state == null) { - return; - } - - onLoadingStateChanged(state); - }; + private LiveData> mPagingData; //Data Observer - private final Observer> mDataObserver = snapshots -> { - if (snapshots == null) { - return; + private final Observer> mDataObserver = new Observer>() { + @Override + public void onChanged(@Nullable PagingData snapshots) { + if (snapshots == null) { + return; + } + submitData(mOptions.getOwner().getLifecycle(), snapshots); } - submitList(snapshots); }; - //DatabaseError Observer - private final Observer mErrorObserver = databaseError -> onError(databaseError); - /** * Construct a new FirestorePagingAdapter from the given {@link DatabasePagingOptions}. */ @@ -82,25 +54,7 @@ public FirebaseRecyclerPagingAdapter(@NonNull DatabasePagingOptions options){ * Initializes Snapshots and LiveData */ public void init() { - mPagedList = mOptions.getData(); - - //Init Data Source - mDataSource = Transformations.map(mPagedList, - input -> (FirebaseDataSource) input.getDataSource()); - - //Init Loading State - mLoadingState = Transformations.switchMap(mPagedList, - input -> { - FirebaseDataSource dataSource = (FirebaseDataSource) input.getDataSource(); - return dataSource.getLoadingState(); - }); - - //Init Database Error - mDatabaseError = Transformations.switchMap(mPagedList, - input -> { - FirebaseDataSource dataSource = (FirebaseDataSource) input.getDataSource(); - return dataSource.getLastError(); - }); + mPagingData = mOptions.getData(); mParser = mOptions.getParser(); @@ -109,32 +63,6 @@ public void init() { } } - /** - * If {@link #onLoadingStateChanged(LoadingState)} indicates error state, call this method - * to attempt to retry the most recent failure. - */ - public void retry(){ - FirebaseDataSource mFirebaseDataSource = mDataSource.getValue(); - if (mFirebaseDataSource == null) { - Log.w(TAG, "Called retry() when FirebaseDataSource is null!"); - return; - } - - mFirebaseDataSource.retry(); - } - - /** - * To attempt to refresh the list. It will reload the list from beginning. - */ - public void refresh(){ - FirebaseDataSource mFirebaseDataSource = mDataSource.getValue(); - if (mFirebaseDataSource == null) { - Log.w(TAG, "Called refresh() when FirebaseDataSource is null!"); - return; - } - mFirebaseDataSource.invalidate(); - } - /** * Re-initialize the Adapter with a new set of options. Can be used to change the query without * re-constructing the entire adapter. @@ -143,7 +71,7 @@ public void updateOptions(@NonNull DatabasePagingOptions options) { mOptions = options; // Tear down old options - boolean hasObservers = mPagedList.hasObservers(); + boolean hasObservers = mPagingData.hasObservers(); if (mOptions.getOwner() != null) { mOptions.getOwner().getLifecycle().removeObserver(this); } @@ -162,10 +90,7 @@ public void updateOptions(@NonNull DatabasePagingOptions options) { */ @OnLifecycleEvent(Lifecycle.Event.ON_START) public void startListening() { - mPagedList.observeForever(mDataObserver); - mLoadingState.observeForever(mStateObserver); - mDatabaseError.observeForever(mErrorObserver); - mDataSource.observeForever(mDataSourceObserver); + mPagingData.observeForever(mDataObserver); } /** @@ -174,10 +99,7 @@ public void startListening() { */ @OnLifecycleEvent(Lifecycle.Event.ON_STOP) public void stopListening() { - mPagedList.removeObserver(mDataObserver); - mLoadingState.removeObserver(mStateObserver); - mDatabaseError.removeObserver(mErrorObserver); - mDataSource.removeObserver(mDataSourceObserver); + mPagingData.removeObserver(mDataObserver); } @Override @@ -192,22 +114,6 @@ public void onBindViewHolder(@NonNull VH viewHolder, int position) { */ protected abstract void onBindViewHolder(@NonNull VH viewHolder, int position, @NonNull T model); - /** - * Called whenever the loading state of the adapter changes. - * - * When the state is {@link LoadingState#ERROR} the adapter will stop loading any data - */ - protected abstract void onLoadingStateChanged(@NonNull LoadingState state); - - /** - * Called whenever the {@link DatabaseError} is caught. - * - * When {@link DatabaseError} is caught the adapter will stop loading any data - */ - protected void onError(@NonNull DatabaseError databaseError) { - Log.w(TAG, "onError", databaseError.toException()); - } - @NonNull public DatabaseReference getRef(int position){ return getItem(position).getRef();