Implementing LiveData
With that as background, let’s see LiveData
in action. The Sensor/LiveList
sample project implements LiveData
for sensor readings coming from a SensorManager
. We can use this to track the accelerometer, ambient light, and so on.
However, the technique shown here can be used for lots of different system-level data sources, such as:
- Other system services (e.g.,
LocationManager
,ClipboardManager
) - System broadcasts, for cases where you want to dynamically register for the broadcast via
registerReceiver()
- Local broadcasts, using
LocalBroadcastManager
- Content changes in providers, via a
ContentObserver
Dependencies
To use Lifecycle
and LifecycleOwner
, you needed two dependencies: the lifecycle runtime
library and its compiler
annotation processor.
LiveData
has its own dependency: android.arch.lifecycle:livedata
:
dependencies {
implementation 'com.android.support:recyclerview-v7:28.0.0'
implementation 'com.android.support:support-fragment:28.0.0'
implementation 'android.arch.lifecycle:livedata:1.1.1'
}
Of note:
- You do not need the
android.arch.lifecycle:runtime
dependency, as theandroid.arch.lifecycle:livedata
dependency will pull that in for you - You do not need the lifecycle annotation processor to use
LiveData
State Transitions
We have a SensorLiveData
class that extends the LiveData
base class, offering to support a custom Event
static
nested class:
package com.commonsware.android.livedata;
import android.arch.lifecycle.LiveData;
import android.content.Context;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import java.util.Date;
class SensorLiveData extends LiveData<SensorLiveData.Event> {
final private SensorManager sensorManager;
private final Sensor sensor;
private final int delay;
SensorLiveData(Context ctxt, int sensorType, int delay) {
sensorManager=
(SensorManager)ctxt.getApplicationContext()
.getSystemService(Context.SENSOR_SERVICE);
this.sensor=sensorManager.getDefaultSensor(sensorType);
this.delay=delay;
if (this.sensor==null) {
throw new IllegalStateException("Cannot obtain the requested sensor");
}
}
@Override
protected void onActive() {
super.onActive();
sensorManager.registerListener(listener, sensor, delay);
}
@Override
protected void onInactive() {
sensorManager.unregisterListener(listener);
super.onInactive();
}
final private SensorEventListener listener=new SensorEventListener() {
@Override
public void onSensorChanged(SensorEvent event) {
setValue(new Event(event));
}
@Override
public void onAccuracyChanged(Sensor sensor, int accuracy) {
// unused
}
};
static class Event {
final Date date=new Date();
final float[] values;
Event(SensorEvent event) {
values=new float[event.values.length];
System.arraycopy(event.values, 0, values, 0, event.values.length);
}
}
}
In the constructor, we hold onto configuration details, such as the particular sensor to monitor and how frequently we should ask for updates. We also obtain an instance of the SensorManager
system service and try to find the actual requested Sensor
, throwing a runtime exception if there is no matching sensor on this device.
However, we do not register for sensor events in the constructor. Until we have 1+ active observers, we do not need those events, and monitoring sensor events drains the battery. So, we postpone registering for events until onActive()
, unregistering in the corresponding onInactive()
callback.
Updating the Observers
The SensorEventListener
that we use, in its onSensorChanged()
method, creates a new instance of our Event
, grabbing data from the SensorEvent
. We use our own Event
class for two reasons:
-
SensorEvent
objects get recycled, and so it is not safe to hold onto one of those after the end ofonSensorChanged()
, so we copy the sensor resultsfloat
values into our own object - While a
SensorEvent
has a timestamp, it is a pain to use, and this is a casual book sample, so we just track our ownDate
for simplicity
That Event
is passed to setValue()
on the LiveData
, which in turn will pass the result to observers. Note that setValue()
needs to be called on the main application thread — we will see how to handle events originating on background threads later in this chapter.
Retaining the LiveData
So, we have a LiveData
for sensor readings. We can have an activity that displays those readings, by having it create a SensorLiveData
instance and registering to observe those events. But now we run into a problem… what do we do with the SensorLiveData
object after that?
One possibility is that we just hold onto it in a field, mostly to ensure that nothing gets garbage-collected that would interrupt the sensor readings. If we undergo a configuration change, we just create a new SensorLiveData
objects and a fresh observer. While this is not completely ridiculous for this particular scenario, it is bad for cases where setting up the LiveData
is expensive.
The most likely solution would be to hold it in a viewmodel — we will see that in an upcoming chapter.
In this sample app, we take a third approach, using onRetainCustomNonConfigurationInstance()
inside the activity that is going to use the sensor readings. Since the UI is going to be a RecyclerView
of readings, we also need to hold onto past readings, so we do not lose them when we undergo the configuration change.
So, we have a State
static
nested class that holds onto the SensorLiveData
and outstanding readings:
private static class State {
final ArrayList<SensorLiveData.Event> events=new ArrayList<>();
SensorLiveData sensorLiveData;
}
In onCreate()
, we set up that State
if we do not already have one, storing it in a state
field. This includes setting up the SensorLiveData
, in this case for the ambient light sensor:
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
RecyclerView rv=findViewById(R.id.transcript);
state=(State)getLastCustomNonConfigurationInstance();
if (state==null) {
state=new State();
state.sensorLiveData=
new SensorLiveData(this, Sensor.TYPE_LIGHT,
SensorManager.SENSOR_DELAY_UI);
}
adapter=new EventLogAdapter();
rv.setAdapter(adapter);
state.sensorLiveData.observe(this, event -> adapter.add(event));
}
We also register our Observer
, which will be called with onChanged()
with a new Event
as sensor readings come in. Our EventLogAdapter
knows how to add()
that to the list of historical readings and update the RecyclerView
.
However, the LiveData
will automatically deliver the last-received reading to our observer when we attach a fresh observer after a configuration change. That could result in onChanged()
being given the same Event
object as before, one that we already put into the ArrayList
. So, the EventLogAdapter
add()
method checks that first, before actually adding it:
void add(SensorLiveData.Event what) {
if (!state.events.contains(what)) {
state.events.add(what);
notifyItemInserted(getItemCount()-1);
}
}
And we override onRetainNonConfigurationInstance()
to return the State
instance, so onCreate()
can retrieve it after a configuration change:
@Override
public Object onRetainCustomNonConfigurationInstance() {
return(state);
}
Prev Table of Contents Next
This book is licensed under the Creative Commons Attribution-ShareAlike 4.0 International license.