Skip to content

Commit

Permalink
refactor(database): migrate to paging 3 (#1983)
Browse files Browse the repository at this point in the history
  • Loading branch information
thatfiredev authored Jul 30, 2021
1 parent da1aa80 commit da7beaf
Show file tree
Hide file tree
Showing 7 changed files with 294 additions and 412 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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;
Expand Down Expand Up @@ -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<Item> options = new DatabasePagingOptions.Builder<Item>()
Expand All @@ -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);

Expand Down
125 changes: 93 additions & 32 deletions database/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -245,13 +245,13 @@ FirebaseRecyclerAdapter adapter = new FirebaseRecyclerAdapter<Chat, ChatHolder>(

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
Expand All @@ -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.
Expand Down Expand Up @@ -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<Item, ItemViewHolder> adapter =
new FirebaseRecyclerPagingAdapter<Item, ItemViewHolder>(options) {
##### In Java

// ...
Use the `addLoadStateListener` method from the adapter:

```java
adapter.addLoadStateListener(new Function1<CombinedLoadStates, Unit>() {
@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
// ...
}
}
}
}
}
```


Expand Down Expand Up @@ -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
6 changes: 6 additions & 0 deletions database/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,11 @@ android {
consumerProguardFiles("proguard-rules.pro")
}
}

compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
}

dependencies {
Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand All @@ -22,11 +24,11 @@
public final class DatabasePagingOptions<T> {

private final SnapshotParser<T> mParser;
private final LiveData<PagedList<DataSnapshot>> mData;
private final LiveData<PagingData<DataSnapshot>> mData;
private final DiffUtil.ItemCallback<DataSnapshot> mDiffCallback;
private final LifecycleOwner mOwner;

private DatabasePagingOptions(@NonNull LiveData<PagedList<DataSnapshot>> data,
private DatabasePagingOptions(@NonNull LiveData<PagingData<DataSnapshot>> data,
@NonNull SnapshotParser<T> parser,
@NonNull DiffUtil.ItemCallback<DataSnapshot> diffCallback,
@Nullable LifecycleOwner owner) {
Expand All @@ -37,7 +39,7 @@ private DatabasePagingOptions(@NonNull LiveData<PagedList<DataSnapshot>> data,
}

@NonNull
public LiveData<PagedList<DataSnapshot>> getData() {
public LiveData<PagingData<DataSnapshot>> getData() {
return mData;
}

Expand All @@ -61,7 +63,7 @@ public LifecycleOwner getOwner() {
*/
public static final class Builder<T> {

private LiveData<PagedList<DataSnapshot>> mData;
private LiveData<PagingData<DataSnapshot>> mData;
private SnapshotParser<T> mParser;
private LifecycleOwner mOwner;
private DiffUtil.ItemCallback<DataSnapshot> mDiffCallback;
Expand All @@ -70,11 +72,11 @@ public static final class Builder<T> {
* 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<T> setQuery(@NonNull Query query,
@NonNull PagedList.Config config,
@NonNull PagingConfig config,
@NonNull Class<T> modelClass) {
return setQuery(query, config, new ClassSnapshotParser<>(modelClass));
}
Expand All @@ -90,10 +92,12 @@ public Builder<T> setQuery(@NonNull Query query,
*/
@NonNull
public Builder<T> setQuery(@NonNull Query query,
@NonNull PagedList.Config config,
@NonNull PagingConfig config,
@NotNull SnapshotParser<T> parser) {
FirebaseDataSource.Factory factory = new FirebaseDataSource.Factory(query);
mData = new LivePagedListBuilder<>(factory, config).build();
final Pager<String, DataSnapshot> pager = new Pager<>(config,
() -> new DatabasePagingSource(query));
mData = PagingLiveData.cachedIn(PagingLiveData.getLiveData(pager),
mOwner.getLifecycle());

mParser = parser;
return this;
Expand Down Expand Up @@ -135,7 +139,7 @@ public DatabasePagingOptions<T> build() {
}

if (mDiffCallback == null) {
mDiffCallback = new DefaultSnapshotDiffCallback<T>(mParser);
mDiffCallback = new DefaultSnapshotDiffCallback<>(mParser);
}

return new DatabasePagingOptions<>(mData, mParser, mDiffCallback, mOwner);
Expand Down
Loading

0 comments on commit da7beaf

Please sign in to comment.