If you're displaying a list of data, you likely want to bind the Chat
objects to a RecyclerView
.
This means implementing a custom RecyclerView.Adapter
and coordinating updates with the
ChildEventListener
.
Fear not, FirebaseUI does all of this for you automatically!
Choosing an adapter
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.
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.
Using the FirebaseRecyclerAdapter
The FirebaseRecyclerAdapter
binds a Query
to a RecyclerView
. When data is added, removed,
or changed these updates are automatically applied to your UI in real time.
First, configure the adapter by building FirebaseRecyclerOptions
. In this case we will continue
with our chat example:
FirebaseRecyclerOptions<Chat> options =
new FirebaseRecyclerOptions.Builder<Chat>()
.setQuery(query, Chat.class)
.build();
If you need to customize how your model class is parsed, you can use a custom SnapshotParser
:
...setQuery(..., new SnapshotParser<Chat>() {
@NonNull
@Override
public Chat parseSnapshot(@NonNull DataSnapshot snapshot) {
return ...;
}
});
Next create the FirebaseRecyclerAdapter
object. You should already have a ViewHolder
subclass
for displaying each item. In this case we will use a custom ChatHolder
class:
FirebaseRecyclerAdapter adapter = new FirebaseRecyclerAdapter<Chat, ChatHolder>(options) {
@Override
public ChatHolder onCreateViewHolder(ViewGroup parent, int viewType) {
// Create a new instance of the ViewHolder, in this case we are using a custom
// layout called R.layout.message for each item
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.message, parent, false);
return new ChatHolder(view);
}
@Override
protected void onBindViewHolder(ChatHolder holder, int position, Chat model) {
// Bind the Chat object to the ChatHolder
// ...
}
};
Finally attach the adapter to your RecyclerView
with the RecyclerView#setAdapter()
method.
Don't forget to also set a LayoutManager
!
FirebaseRecyclerAdapter lifecycle
Start/stop listening
The FirebaseRecyclerAdapter
uses an event listener to monitor changes to the Firebase query.
To begin listening for data, call the startListening()
method. You may want to call this in your
onStart()
method. Make sure you have finished any authentication necessary to read the data
before calling startListening()
or your query will fail.
@Override
protected void onStart() {
super.onStart();
adapter.startListening();
}
Similarly, the stopListening()
call removes the event listener and all data in the adapter.
Call this method when the containing Activity or Fragment stops:
@Override
protected void onStop() {
super.onStop();
adapter.stopListening();
}
Automatic listening
If you don't want to manually start/stop listening you can use
Android Architecture Components to automatically manage the lifecycle of the
FirebaseRecyclerAdapter
. Pass a LifecycleOwner
to
FirebaseRecyclerOptions.Builder#setLifecycleOwner(...)
and FirebaseUI will automatically
start and stop listening in onStart()
and onStop()
.
Data and error events
When using the FirebaseRecyclerAdapter
you may want to perform some action every time data
changes or when there is an error. To do this, override the onDataChanged()
and onError()
methods of the adapter:
FirebaseRecyclerAdapter adapter = new FirebaseRecyclerAdapter<Chat, ChatHolder>(options) {
// ...
@Override
public void onDataChanged() {
// Called each time there is a new data snapshot. You may want to use this method
// to hide a loading spinner or check for the "no documents" state and update your UI.
// ...
}
@Override
public void onError(DatabaseError e) {
// Called when there is an error getting data. You may want to update
// your UI to display an error message to the user.
// ...
}
};
Using the FirebaseRecyclerPagingAdapter
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 FirebaseRecyclerAdapter
.
The FirebaseRecyclerPagingAdapter
is built on top of the Android Paging 3 Library.
Before using the adapter in your application, you must add a dependency on that library:
implementation 'androidx.paging:paging-runtime:3.x.x'
First, configure the adapter by building DatabasePagingOptions
. Since the paging adapter
is not appropriate for a chat application (it would not detect new messages), we will consider
an adapter that loads a generic Item
:
// The "base query" is a query with no startAt/endAt/limit clauses that the adapter can use
// to form smaller queries for each page.
Query baseQuery = mDatabase.getReference().child("items");
// 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.
DatabasePagingOptions<Item> options = new DatabasePagingOptions.Builder<Item>()
.setLifecycleOwner(this)
.setQuery(baseQuery, config, Item.class)
.build();
If you need to customize how your model class is parsed, you can use a custom SnapshotParser
:
...setQuery(..., new SnapshotParser<Item>() {
@NonNull
@Override
public Item parseSnapshot(@NonNull DocumentSnapshot snapshot) {
return ...;
}
});
Next, create the FirebaseRecyclerPagingAdapter
object. You should already have a ViewHolder
subclass
for displaying each item. In this case we will use a custom ItemViewHolder
class:
FirebaseRecyclerPagingAdapter<Item, ItemViewHolder> adapter =
new FirebaseRecyclerPagingAdapter<Item, ItemViewHolder>(options) {
@NonNull
@Override
public ItemViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
// Create the ItemViewHolder
// ...
}
@Override
protected void onBindViewHolder(@NonNull ItemViewHolder holder,
int position,
@NonNull Item model) {
// Bind the item to the view holder
// ...
}
};
Finally attach the adapter to your RecyclerView
with the RecyclerView#setAdapter()
method.
Don't forget to also set a LayoutManager
!
FirebaseRecyclerPagingAdapter
lifecycle
Start/stop listening
The FirebaseRecyclerPagingAdapter
listens for scrolling events and loads additional pages from the
database only when needed.
To begin populating data, call the startListening()
method. You may want to call this
in your onStart()
method. Make sure you have finished any authentication necessary to read the
data before calling startListening()
or your query will fail.
@Override
protected void onStart() {
super.onStart();
adapter.startListening();
}
Similarly, the stopListening()
call freezes the data in the RecyclerView
and prevents any future
loading of data pages.
Call this method when the containing Activity or Fragment stops:
@Override
protected void onStop() {
super.onStop();
adapter.stopListening();
}
Automatic listening
If you don't want to manually start/stop listening you can use
Android Architecture Components to automatically manage the lifecycle of the
FirebaseRecyclerPagingAdapter
. Pass a LifecycleOwner
to
DatabasePagingOptions.Builder#setLifecycleOwner(...)
and FirebaseUI will automatically
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:
In Java
Use the addLoadStateListener
method from the adapter:
adapter.addLoadStateListener(new Function1<CombinedLoadStates, Unit>() {
@Override
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
// ...
return null;
}
if (refresh instanceof LoadState.NotLoading) {
// The previous load (either initial or additional) completed
// ...
return null;
}
}
return null;
}
});
In Kotlin
Use the loadStateFlow
exposed by the adapter, in a Coroutine Scope:
// 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
// ...
}
}
}
}
}