Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(database): migrate to paging 3 #1983

Merged
merged 6 commits into from
Jul 30, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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`.
thatfiredev marked this conversation as resolved.
Show resolved Hide resolved
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