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:runtimedependency, as theandroid.arch.lifecycle:livedatadependency 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:
-
SensorEventobjects get recycled, and so it is not safe to hold onto one of those after the end ofonSensorChanged(), so we copy the sensor resultsfloatvalues into our own object - While a
SensorEventhas a timestamp, it is a pain to use, and this is a casual book sample, so we just track our ownDatefor 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.