Skip to content

Commit da7beaf

Browse files
authored
refactor(database): migrate to paging 3 (#1983)
1 parent da1aa80 commit da7beaf

File tree

7 files changed

+294
-412
lines changed

7 files changed

+294
-412
lines changed

app/src/main/java/com/firebase/uidemo/database/realtime/FirebaseDbPagingActivity.java

Lines changed: 42 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,8 @@
1212

1313
import com.firebase.ui.database.paging.DatabasePagingOptions;
1414
import com.firebase.ui.database.paging.FirebaseRecyclerPagingAdapter;
15-
import com.firebase.ui.database.paging.LoadingState;
1615
import com.firebase.uidemo.R;
1716
import com.firebase.uidemo.databinding.ActivityDatabasePagingBinding;
18-
import com.google.firebase.database.DatabaseError;
1917
import com.google.firebase.database.FirebaseDatabase;
2018
import com.google.firebase.database.Query;
2119

@@ -24,7 +22,8 @@
2422
import androidx.annotation.NonNull;
2523
import androidx.annotation.Nullable;
2624
import androidx.appcompat.app.AppCompatActivity;
27-
import androidx.paging.PagedList;
25+
import androidx.paging.LoadState;
26+
import androidx.paging.PagingConfig;
2827
import androidx.recyclerview.widget.LinearLayoutManager;
2928
import androidx.recyclerview.widget.RecyclerView;
3029
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
@@ -53,11 +52,7 @@ protected void onCreate(@Nullable Bundle savedInstanceState) {
5352
private void setUpAdapter() {
5453

5554
//Initialize Paging Configurations
56-
PagedList.Config config = new PagedList.Config.Builder()
57-
.setEnablePlaceholders(false)
58-
.setPrefetchDistance(5)
59-
.setPageSize(30)
60-
.build();
55+
PagingConfig config = new PagingConfig(30, 5, false);
6156

6257
//Initialize Firebase Paging Options
6358
DatabasePagingOptions<Item> options = new DatabasePagingOptions.Builder<Item>()
@@ -83,34 +78,47 @@ protected void onBindViewHolder(@NonNull ItemViewHolder holder,
8378
@NonNull Item model) {
8479
holder.bind(model);
8580
}
86-
87-
@Override
88-
protected void onLoadingStateChanged(@NonNull LoadingState state) {
89-
switch (state) {
90-
case LOADING_INITIAL:
91-
case LOADING_MORE:
92-
mBinding.swipeRefreshLayout.setRefreshing(true);
93-
break;
94-
case LOADED:
95-
mBinding.swipeRefreshLayout.setRefreshing(false);
96-
break;
97-
case FINISHED:
98-
mBinding.swipeRefreshLayout.setRefreshing(false);
99-
showToast(getString(R.string.paging_finished_message));
100-
break;
101-
case ERROR:
102-
showToast(getString(R.string.unknown_error));
103-
break;
104-
}
105-
}
106-
107-
@Override
108-
protected void onError(DatabaseError databaseError) {
109-
mBinding.swipeRefreshLayout.setRefreshing(false);
110-
Log.e(TAG, databaseError.getDetails(), databaseError.toException());
111-
}
11281
};
11382

83+
mAdapter.addLoadStateListener(states -> {
84+
LoadState refresh = states.getRefresh();
85+
LoadState append = states.getAppend();
86+
87+
if (refresh instanceof LoadState.Error) {
88+
LoadState.Error loadStateError = (LoadState.Error) refresh;
89+
mBinding.swipeRefreshLayout.setRefreshing(false);
90+
Log.e(TAG, loadStateError.getError().getLocalizedMessage());
91+
}
92+
if (append instanceof LoadState.Error) {
93+
LoadState.Error loadStateError = (LoadState.Error) append;
94+
mBinding.swipeRefreshLayout.setRefreshing(false);
95+
Log.e(TAG, loadStateError.getError().getLocalizedMessage());
96+
}
97+
98+
if (append instanceof LoadState.Loading) {
99+
mBinding.swipeRefreshLayout.setRefreshing(true);
100+
}
101+
102+
if (append instanceof LoadState.NotLoading) {
103+
LoadState.NotLoading notLoading = (LoadState.NotLoading) append;
104+
if (notLoading.getEndOfPaginationReached()) {
105+
// This indicates that the user has scrolled
106+
// until the end of the data set.
107+
mBinding.swipeRefreshLayout.setRefreshing(false);
108+
showToast("Reached end of data set.");
109+
return null;
110+
}
111+
112+
if (refresh instanceof LoadState.NotLoading) {
113+
// This indicates the most recent load
114+
// has finished.
115+
mBinding.swipeRefreshLayout.setRefreshing(false);
116+
return null;
117+
}
118+
}
119+
return null;
120+
});
121+
114122
mBinding.pagingRecycler.setLayoutManager(new LinearLayoutManager(this));
115123
mBinding.pagingRecycler.setAdapter(mAdapter);
116124

database/README.md

Lines changed: 93 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ FirebaseUI offers two types of RecyclerView adapters for the Realtime Database:
121121
* `FirebaseRecyclerAdapter` — binds a `Query` to a `RecyclerView` and responds to all real-time
122122
events included items being added, removed, moved, or changed. Best used with small result sets
123123
since all results are loaded at once.
124-
* `FirebasePagingRecyclerAdapter` — binds a `Query` to a `RecyclerView` by loading data in pages. Best
124+
* `FirebaseRecyclerPagingAdapter` — binds a `Query` to a `RecyclerView` by loading data in pages. Best
125125
used with large, static data sets. Real-time events are not respected by this adapter, so it
126126
will not detect new/removed items or changes to items already loaded.
127127

@@ -245,13 +245,13 @@ FirebaseRecyclerAdapter adapter = new FirebaseRecyclerAdapter<Chat, ChatHolder>(
245245

246246
The `FirebaseRecyclerPagingAdapter` binds a `Query` to a `RecyclerView` by loading documents in pages.
247247
This results in a time and memory efficient binding, however it gives up the real-time events
248-
afforded by the `FirestoreRecyclerAdapter`.
248+
afforded by the `FirebaseRecyclerAdapter`.
249249

250-
The `FirebaseRecyclerPagingAdapter` is built on top of the [Android Paging Support Library][paging-support].
251-
Before using the adapter in your application, you must add a dependency on the support library:
250+
The `FirebaseRecyclerPagingAdapter` is built on top of the [Android Paging 3 Library][paging-support].
251+
Before using the adapter in your application, you must add a dependency on that library:
252252

253253
```groovy
254-
implementation 'androidx.paging:paging-runtime:2.x.x'
254+
implementation 'androidx.paging:paging-runtime:3.x.x'
255255
```
256256

257257
First, configure the adapter by building `DatabasePagingOptions`. Since the paging adapter
@@ -263,13 +263,10 @@ an adapter that loads a generic `Item`:
263263
// to form smaller queries for each page.
264264
Query baseQuery = mDatabase.getReference().child("items");
265265

266-
// This configuration comes from the Paging Support Library
267-
// https://developer.android.com/reference/androidx/paging/PagedList.Config
268-
PagedList.Config config = new PagedList.Config.Builder()
269-
.setEnablePlaceholders(false)
270-
.setPrefetchDistance(10)
271-
.setPageSize(20)
272-
.build();
266+
// This configuration comes from the Paging 3 Library
267+
// https://developer.android.com/reference/kotlin/androidx/paging/PagingConfig
268+
PagingConfig config = new PagingConfig(/* page size */ 20, /* prefetchDistance */ 10,
269+
/* enablePlaceHolders */ false);
273270

274271
// The options for the adapter combine the paging configuration with query information
275272
// and application-specific options for lifecycle, etc.
@@ -360,34 +357,98 @@ start and stop listening in `onStart()` and `onStop()`.
360357
#### Paging events
361358

362359
When using the `FirebaseRecyclerPagingAdapter`, you may want to perform some action every time data
363-
changes or when there is an error. To do this, override the `onLoadingStateChanged()`
364-
method of the adapter:
360+
changes or when there is an error. To do this:
365361

366-
```java
367-
FirebaseRecyclerPagingAdapter<Item, ItemViewHolder> adapter =
368-
new FirebaseRecyclerPagingAdapter<Item, ItemViewHolder>(options) {
362+
##### In Java
369363

370-
// ...
364+
Use the `addLoadStateListener` method from the adapter:
371365

366+
```java
367+
adapter.addLoadStateListener(new Function1<CombinedLoadStates, Unit>() {
372368
@Override
373-
protected void onLoadingStateChanged(@NonNull LoadingState state) {
374-
switch (state) {
375-
case LOADING_INITIAL:
376-
// The initial load has begun
377-
// ...
378-
case LOADING_MORE:
379-
// The adapter has started to load an additional page
369+
public Unit invoke(CombinedLoadStates states) {
370+
LoadState refresh = states.getRefresh();
371+
LoadState append = states.getAppend();
372+
373+
if (refresh instanceof LoadState.Error || append instanceof LoadState.Error) {
374+
// The previous load (either initial or additional) failed. Call
375+
// the retry() method in order to retry the load operation.
376+
// ...
377+
}
378+
379+
if (refresh instanceof LoadState.Loading) {
380+
// The initial Load has begun
381+
// ...
382+
}
383+
384+
if (append instanceof LoadState.Loading) {
385+
// The adapter has started to load an additional page
386+
// ...
387+
}
388+
389+
if (append instanceof LoadState.NotLoading) {
390+
LoadState.NotLoading notLoading = (LoadState.NotLoading) append;
391+
if (notLoading.getEndOfPaginationReached()) {
392+
// The adapter has finished loading all of the data set
380393
// ...
381-
case LOADED:
394+
return null;
395+
}
396+
397+
if (refresh instanceof LoadState.NotLoading) {
382398
// The previous load (either initial or additional) completed
383399
// ...
384-
case ERROR:
385-
// The previous load (either initial or additional) failed. Call
386-
// the retry() method in order to retry the load operation.
387-
// ...
400+
return null;
401+
}
388402
}
403+
return null;
389404
}
390-
};
405+
});
406+
```
407+
408+
#### In Kotlin
409+
410+
Use the `loadStateFlow` exposed by the adapter, in a Coroutine Scope:
411+
412+
```kotlin
413+
// Activities can use lifecycleScope directly, but Fragments should instead use
414+
// viewLifecycleOwner.lifecycleScope.
415+
lifecycleScope.launch {
416+
pagingAdapter.loadStateFlow.collectLatest { loadStates ->
417+
when (loadStates.refresh) {
418+
is LoadState.Error -> {
419+
// The initial load failed. Call the retry() method
420+
// in order to retry the load operation.
421+
// ...
422+
}
423+
is LoadState.Loading -> {
424+
// The initial Load has begun
425+
// ...
426+
}
427+
}
428+
429+
when (loadStates.append) {
430+
is LoadState.Error -> {
431+
// The additional load failed. Call the retry() method
432+
// in order to retry the load operation.
433+
// ...
434+
}
435+
is LoadState.Loading -> {
436+
// The adapter has started to load an additional page
437+
// ...
438+
}
439+
is LoadState.NotLoading -> {
440+
if (loadStates.append.endOfPaginationReached) {
441+
// The adapter has finished loading all of the data set
442+
// ...
443+
}
444+
if (loadStates.refresh is LoadState.NotLoading) {
445+
// The previous load (either initial or additional) completed
446+
// ...
447+
}
448+
}
449+
}
450+
}
451+
}
391452
```
392453

393454

@@ -458,4 +519,4 @@ The order in which you receive your data depends on the order from `keyRef`, not
458519
[indexed-data]: https://firebase.google.com/docs/database/android/structure-data#best_practices_for_data_structure
459520
[recyclerview]: https://developer.android.com/reference/androidx/recyclerview/widget/RecyclerView
460521
[arch-components]: https://developer.android.com/topic/libraries/architecture/index.html
461-
[paging-support]: https://developer.android.com/topic/libraries/architecture/paging
522+
[paging-support]: https://developer.android.com/topic/libraries/architecture/paging/v3-overview

database/build.gradle.kts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,11 @@ android {
4343
consumerProguardFiles("proguard-rules.pro")
4444
}
4545
}
46+
47+
compileOptions {
48+
sourceCompatibility = JavaVersion.VERSION_1_8
49+
targetCompatibility = JavaVersion.VERSION_1_8
50+
}
4651
}
4752

4853
dependencies {
@@ -54,6 +59,7 @@ dependencies {
5459
api(Config.Libs.Androidx.recyclerView)
5560

5661
compileOnly(Config.Libs.Androidx.paging)
62+
api(Config.Libs.Androidx.pagingRxJava)
5763
annotationProcessor(Config.Libs.Androidx.lifecycleCompiler)
5864

5965
androidTestImplementation(Config.Libs.Test.junit)

database/src/main/java/com/firebase/ui/database/paging/DatabasePagingOptions.java

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,10 @@
1010
import androidx.annotation.Nullable;
1111
import androidx.lifecycle.LifecycleOwner;
1212
import androidx.lifecycle.LiveData;
13-
import androidx.paging.LivePagedListBuilder;
14-
import androidx.paging.PagedList;
13+
import androidx.paging.Pager;
14+
import androidx.paging.PagingConfig;
15+
import androidx.paging.PagingData;
16+
import androidx.paging.PagingLiveData;
1517
import androidx.recyclerview.widget.DiffUtil;
1618

1719
/**
@@ -22,11 +24,11 @@
2224
public final class DatabasePagingOptions<T> {
2325

2426
private final SnapshotParser<T> mParser;
25-
private final LiveData<PagedList<DataSnapshot>> mData;
27+
private final LiveData<PagingData<DataSnapshot>> mData;
2628
private final DiffUtil.ItemCallback<DataSnapshot> mDiffCallback;
2729
private final LifecycleOwner mOwner;
2830

29-
private DatabasePagingOptions(@NonNull LiveData<PagedList<DataSnapshot>> data,
31+
private DatabasePagingOptions(@NonNull LiveData<PagingData<DataSnapshot>> data,
3032
@NonNull SnapshotParser<T> parser,
3133
@NonNull DiffUtil.ItemCallback<DataSnapshot> diffCallback,
3234
@Nullable LifecycleOwner owner) {
@@ -37,7 +39,7 @@ private DatabasePagingOptions(@NonNull LiveData<PagedList<DataSnapshot>> data,
3739
}
3840

3941
@NonNull
40-
public LiveData<PagedList<DataSnapshot>> getData() {
42+
public LiveData<PagingData<DataSnapshot>> getData() {
4143
return mData;
4244
}
4345

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

64-
private LiveData<PagedList<DataSnapshot>> mData;
66+
private LiveData<PagingData<DataSnapshot>> mData;
6567
private SnapshotParser<T> mParser;
6668
private LifecycleOwner mOwner;
6769
private DiffUtil.ItemCallback<DataSnapshot> mDiffCallback;
@@ -70,11 +72,11 @@ public static final class Builder<T> {
7072
* Sets the query using a {@link ClassSnapshotParser} based
7173
* on the given class.
7274
*
73-
* See {@link #setQuery(Query, PagedList.Config, SnapshotParser)}.
75+
* See {@link #setQuery(Query, PagingConfig, SnapshotParser)}.
7476
*/
7577
@NonNull
7678
public Builder<T> setQuery(@NonNull Query query,
77-
@NonNull PagedList.Config config,
79+
@NonNull PagingConfig config,
7880
@NonNull Class<T> modelClass) {
7981
return setQuery(query, config, new ClassSnapshotParser<>(modelClass));
8082
}
@@ -90,10 +92,12 @@ public Builder<T> setQuery(@NonNull Query query,
9092
*/
9193
@NonNull
9294
public Builder<T> setQuery(@NonNull Query query,
93-
@NonNull PagedList.Config config,
95+
@NonNull PagingConfig config,
9496
@NotNull SnapshotParser<T> parser) {
95-
FirebaseDataSource.Factory factory = new FirebaseDataSource.Factory(query);
96-
mData = new LivePagedListBuilder<>(factory, config).build();
97+
final Pager<String, DataSnapshot> pager = new Pager<>(config,
98+
() -> new DatabasePagingSource(query));
99+
mData = PagingLiveData.cachedIn(PagingLiveData.getLiveData(pager),
100+
mOwner.getLifecycle());
97101

98102
mParser = parser;
99103
return this;
@@ -135,7 +139,7 @@ public DatabasePagingOptions<T> build() {
135139
}
136140

137141
if (mDiffCallback == null) {
138-
mDiffCallback = new DefaultSnapshotDiffCallback<T>(mParser);
142+
mDiffCallback = new DefaultSnapshotDiffCallback<>(mParser);
139143
}
140144

141145
return new DatabasePagingOptions<>(mData, mParser, mDiffCallback, mOwner);

0 commit comments

Comments
 (0)