December 22, 2017
Hey, welcome to part 3 in this series about using lifecycle-aware Android Architecture Components with Firebase Realtime Database. In
part 1
, we started with a simple Activity that uses database listeners to keep its UI fresh as data changes in the database. We converted that to use LiveData and ViewModel to remove the boilerplate of dealing with the listeners during the Activity lifecycle. Then, in
part 2
, we completely refactored away all mention of Realtime Database from the Activity, and implemented a performance enhancement. This optimization uses
MediatorLiveData
and some threading, for the case where data manipulation might be too expensive operation to perform on the main thread.
There’s one more optimization that can be applied in the code. It could have a large impact on performance, depending on how much data your database listeners are receiving. It has to do with how our
FirebaseQueryLiveData
implementation deals with the database listener during its
onActive()
and
onInactive()
methods. Here it is again:
public
class
FirebaseQueryLiveData
extends
LiveData
<
DataSnapshot
>
{
private
static
final
String
LOG_TAG
=
"FirebaseQueryLiveData"
;
private
final
Query
query
;
private
final
MyValueEventListener
listener
=
new
MyValueEventListener
(
)
;
public
FirebaseQueryLiveData
(
Query
query
)
{
this
.
query
=
query
;
}
public
FirebaseQueryLiveData
(
DatabaseReference
ref
)
{
this
.
query
=
ref
;
}
@Override
protected
void
onActive
(
)
{
query
.
addValueEventListener
(
listener
)
;
}
@Override
protected
void
onInactive
(
)
{
query
.
removeEventListener
(
listener
)
;
}
private
class
MyValueEventListener
implements
ValueEventListener
{
@Override
public
void
onDataChange
(
DataSnapshot
dataSnapshot
)
{
setValue
(
dataSnapshot
)
;
}
@Override
public
void
onCancelled
(
DatabaseError
databaseError
)
{
Log
.
e
(
LOG_TAG
,
"Can't listen to query "
+
query
,
databaseError
.
toException
(
)
)
;
}
}
}
The key detail to note here is that a database listener is added during
onActive()
and removed during
onInactive()
. The Activity that makes use of
FirebaseQueryLiveData
executes this code during its
onCreate()
:
HotStockViewModel
viewModel
=
ViewModelProviders
.
of
(
this
)
.
get
(
HotStockViewModel
.
class
)
;
LiveData
<
DataSnapshot
>
liveData
=
viewModel
.
getDataSnapshotLiveData
(
)
;
liveData
.
observe
(
this
,
new
Observer
<
DataSnapshot
>
(
)
{
@Override
public
void
onChanged
(
@Nullable
DataSnapshot
dataSnapshot
)
{
if
(
dataSnapshot
!=
null
)
{
}
}
}
)
;
The observer here follows the lifecycle of the
Activity
.
LiveData
considers an observer to be in an active state if its lifecycle is in the
STARTED
or
RESUMED
state. The observer transitions to an inactive state if its lifecycle is in the
DESTROYED
state. The
onActive()
method is called when the
LiveData
object has at least one active observer, and the
onInactive()
method is called when the
LiveData
object doesn’t have any active observers. So, what happens here when the Activity is launched, then goes through a
configuration change
(such as a device reorientation)? The sequence of events (when there is a single UI controller observing a
FirebaseQueryLiveData
) is like this:
- Activity is started.
- LiveData is observed and becomes active, invoking its
onActive()
method.
- Database listener is added.
- Data is received; UI is updated.
- Device is reoriented, Activity is destroyed
- LiveData is unobserved and becomes inactive, invoking its
onInactive()
method.
- Database listener is removed.
- New Activity is started to take the place of the original.
- LiveData is observed and becomes active again, invoking its
onActive()
method.
- Database listener is added.
- Data is received; UI is updated.
I’ve bolded the steps that deal with the database listener. You can see here the Activity configuration change caused the listener to be removed and added again. These steps spell out the cost of a second round trip to and from the Realtime Database server to pull down all the data for the second query, even if the results didn’t change. I definitely don’t want that to happen, because
LiveData
already retains the latest snapshot of data! This extra query is wasteful, both of the end user’s data plan, and and counts against the quota or the bill of your Firebase project.
How do we prevent this unnecessary query?
There’s no easy way to change the way that the
LiveData
object becomes active or inactive. But we can make some guesses about how quickly that state could change when the Activity is going through a configuration change. Let’s make the assumption that a configuration change will take no more than two seconds (it’s normally much faster). With that, one strategy could add a delay before
FirebaseQueryLiveData
removes the database listener after the call to
onInactive()
. Here’s an implementation of that, with a few changes and additions to
FirebaseQueryLiveData
:
private
boolean
listenerRemovePending
=
false
;
private
final
Handler
handler
=
new
Handler
(
)
;
private
final
Runnable
removeListener
=
new
Runnable
(
)
{
@Override
public
void
run
(
)
{
query
.
removeEventListener
(
listener
)
;
listenerRemovePending
=
false
;
}
}
;
@Override
protected
void
onActive
(
)
{
if
(
listenerRemovePending
)
{
handler
.
removeCallbacks
(
removeListener
)
;
}
else
{
query
.
addValueEventListener
(
listener
)
;
}
listenerRemovePending
=
false
;
}
@Override
protected
void
onInactive
(
)
{
handler
.
postDelayed
(
removeListener
,
2000
)
;
listenerRemovePending
=
true
;
}
Here, I’m using a
Handler
to schedule the removal of the database listener (by posting a
Runnable
callback that performs the removal) on a two second delay after the
LiveData
becomes inactive. If it becomes active again before those two seconds have elapsed, we simply eliminate that scheduled work from the
Handler
, and allow the listener to keep listening. This is great for both our users and our wallets!
Are you using lifecycle-aware Android Architecture components along with Firebase in your app? How’s it going? Join the discussion of all things Firebase on our Google group
firebase-talk
.