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:

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:

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:

  1. SensorEvent objects get recycled, and so it is not safe to hold onto one of those after the end of onSensorChanged(), so we copy the sensor results float values into our own object
  2. While a SensorEvent has a timestamp, it is a pain to use, and this is a casual book sample, so we just track our own Date 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.