avatarIan Lake

Free AI web copilot to create summaries, insights and extended knowledge, download it at here

7637

Abstract

tle">JsonLiveData</span>(<span class="hljs-variable"><span class="hljs-class">application</span></span>); }</span></pre></div><div id="0025"><pre> <span class="hljs-keyword">public</span> <span class="hljs-title class_">LiveData</span><<span class="hljs-title class_">List</span><<span class="hljs-title class_">String</span>>> <span class="hljs-title function_">getData</span>(<span class="hljs-params"></span>) { <span class="hljs-keyword">return</span> data; } }</pre></div><div id="656e"><pre>public <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">JsonLiveData</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">LiveData<List<String>></span> </span>{ <span class="hljs-keyword">private</span> <span class="hljs-keyword">final</span> <span class="hljs-type">Context</span> context;</pre></div><div id="504c"><pre> public JsonLiveData(Context context) { this.context <span class="hljs-operator">=</span> context<span class="hljs-comment">;</span> loadData()<span class="hljs-comment">;</span> }</pre></div><div id="a07d"><pre> <span class="hljs-keyword">private</span> void loadData() { <span class="hljs-keyword">new</span> <span class="hljs-type">AsyncTask</span><<span class="hljs-keyword">Void</span>,<span class="hljs-keyword">Void</span>,List<<span class="hljs-keyword">String</span>>>() { <span class="hljs-meta">@Override</span> protected List<<span class="hljs-keyword">String</span>> doInBackground(<span class="hljs-keyword">Void</span>… voids) { File jsonFile = <span class="hljs-keyword">new</span> <span class="hljs-type">File</span>(getApplication().getFilesDir(), <span class="hljs-string">"downloaded.json"</span>); List<<span class="hljs-keyword">String</span>> data = <span class="hljs-keyword">new</span> <span class="hljs-type">ArrayList</span><>(); <span class="hljs-comment">// Parse the JSON using the library of your choice</span> <span class="hljs-keyword">return</span> data; }</pre></div><div id="4949"><pre> <span class="hljs-meta">@Override</span> <span class="hljs-keyword">protected</span> <span class="hljs-built_in">void</span> <span class="hljs-title function_">onPostExecute</span>(<span class="hljs-params">List<<span class="hljs-built_in">String</span>> data</span>) { <span class="hljs-title function_">setValue</span>(data); } }.<span class="hljs-title function_">execute</span>(); } }</pre></div><p id="890f">So our <code>ViewModel</code> gets considerably simpler, as you’d expect. Our <code>LiveData</code> now completely encapsulates the loading process, loading the data only once.</p><h1 id="bdf4">Data changes in a LiveData world</h1><p id="803e">Just like how <a href="https://readmedium.com/making-loading-data-on-android-lifecycle-aware-897e12760832#1e8c">Loaders can react to changes elsewhere</a>, this same functionality is key when working with <code>LiveData</code> — as the name implies, the data is expected to change! We can easily rework our class to continue to load data while there’s an observer:</p><div id="96dd"><pre>public <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">JsonLiveData</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">LiveData<List<String>></span> </span>{ <span class="hljs-keyword">private</span> <span class="hljs-keyword">final</span> <span class="hljs-type">Context</span> context; <span class="hljs-keyword">private</span> <span class="hljs-keyword">final</span> <span class="hljs-type">FileObserver</span> fileObserver;</pre></div><div id="2abc"><pre><span class="hljs-keyword">public</span> <span class="hljs-title function_">JsonLiveData</span><span class="hljs-params">(Context context)</span> { <span class="hljs-built_in">this</span>.context = context; <span class="hljs-type">String</span> <span class="hljs-variable">path</span> <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">File</span>(context.getFilesDir(), <span class="hljs-string">"downloaded.json"</span>).getPath(); fileObserver = <span class="hljs-keyword">new</span> <span class="hljs-title class_">FileObserver</span>(path) { <span class="hljs-meta">@Override</span> <span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title function_">onEvent</span><span class="hljs-params">(<span class="hljs-type">int</span> event, String path)</span> { <span class="hljs-comment">// The file has changed, so let’s reload the data</span> loadData(); } }; loadData(); }</pre></div><div id="f04c"><pre> <span class="hljs-meta">@Override</span> <span class="hljs-keyword">protected</span> <span class="hljs-built_in">void</span> <span class="hljs-title function_">onActive</span>(<span class="hljs-params"></span>) { fileObserver.<span class="hljs-title function_">startWatching</span>(); }</pre></div><div id="eebf"><pre> <span class="hljs-meta">@Override</span> <span class="hljs-keyword">protected</span> <span class="hljs-built_in">void</span> <span class="hljs-title function_">onInactive</span>(<span class="hljs-params"></span>) { fileObserver.<span class="hljs-title function_">stopWatching</span>(); }</pre></div><div id="b64b"><pre> <span class="hljs-keyword">private</span> void loadData() { <span class="hljs-keyword">new</span> <span class="hljs-type">AsyncTask</span><<span class="hljs-keyword">Void</span>,<span class="hljs-keyword">Void</span>,List<<span class="hljs-keyword">String</span>>>() { <span class="hljs-meta">@Override</span> protected List<<span class="hljs-keyword">String</span>> doInBackground(<span class="hljs-keyword">Void</span>… voids) { File jsonFile = <span class="hljs-keyword">new</span> <span class="hljs-type">File</span>(getApplication().getFilesDir(), <span class="hljs-string">"downloaded.json"</span>); List<<span class="hljs-keyword">String</span>> data = <span class="hljs-keyword">new</span> <span class="hljs-type">ArrayList</span><>(); <span class="hljs-comment">// Parse the JSON using the library of your choice</span> <span class="hljs-keyword">return</span> data; }</pre></div><div id="c178"><pre> <span class="hljs-meta">@Override</span> <span class="hljs-keyword">protected</span> <span class="hljs-built_in">void</span> <span class="hljs-title function_">onPostExecute</span>(<span class="hljs-params">List<<span class="hljs-built_in">String</span>> data</span>) { <span class="hljs-title function_">setValue</span>(data); } }.<span class="hljs-title function_">execute</span>(); } }</pre></div><p id="6a1d">Now that we’re interested in listening to changes, we can take advantage of LiveData’s <code>onActive()</code> and <code>onInactive()</code> callbacks to only listen when there’s an active observer on our data — as long as someone is observing, they can be guaranteed to get the latest data.</p><h1 id="d9b7">Observing data</h1><p id="ca68">In the <code>Loader</code> world, getting your data to your UI would involve a <code>LoaderManager</code>, calling <code>initLoader()</code> in the right place, and building a <code>LoaderCallbacks</code>. The world is a bit more straightforward in the Architecture Components world.</p><p id="61c4">There’s two things we need to do:</p><o

Options

l><li>Get a reference to our ViewModel</li><li>Start observing our LiveData</li></ol><p id="49c2">But explaining that is almost as long as the code itself:</p><div id="e65b"><pre>public <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">MyActivity</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">AppCompatActivity</span> </span>{ public void onCreate(<span class="hljs-type">Bundle</span> savedInstanceState) { <span class="hljs-keyword">super</span>.onCreate(savedInstanceState); <span class="hljs-type">JsonViewModel</span> model = <span class="hljs-type">ViewModelProviders</span>.of(<span class="hljs-keyword">this</span>).get(<span class="hljs-type">JsonViewModel</span>.<span class="hljs-keyword">class</span>); model.getData().observe(<span class="hljs-keyword">this</span>, data -> { <span class="hljs-comment">// update UI</span> }); } }</pre></div><p id="96b2">You’ll note there’s no need to clean things up after the fact: <code>ViewModels</code> automatically live only as long as they are needed and <code>LiveData</code> automatically only passes you data when calling <code>Activity</code>/<code>Fragment</code>/<a href="https://developer.android.com/topic/libraries/architecture/lifecycle.html#lco"><code>LifecycleOw</code>ner</a> is started or resumed.</p><h1 id="d13e">Load all the things</h1><p id="82b9">Now, if you’re still in the vehemently-against-<code>AsyncTask</code> camp, I’m totally okay with that: <code>LiveData</code> is a lot more flexible than being tied to only that construct.</p><p id="3f24">For example, <a href="https://developer.android.com/topic/libraries/architecture/room.html">Room</a> lets you have <a href="https://developer.android.com/topic/libraries/architecture/room.html#daos-query-observable">observerable queries</a> — database queries that return <code>LiveData</code> so that database changes automatically propagate up through your ViewModel to your UI. Kind of like a <code>CursorLoader</code> without touching Cursors or Loaders.</p><p id="238a">We can also rewrite the <a href="https://readmedium.com/making-loading-data-on-android-lifecycle-aware-897e12760832#85b1"><code>FusedLocation</code>Api example</a> with a <code>LiveData</code> class:</p><div id="30e6"><pre>public <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">LocationLiveData</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">LiveData<Location></span> <span class="hljs-title">implements</span></span> <span class="hljs-type">GoogleApiClient</span>.<span class="hljs-type">ConnectionCallbacks</span>, <span class="hljs-type">GoogleApiClient</span>.<span class="hljs-type">OnConnectionFailedListener</span>, <span class="hljs-type">LocationListener</span> { <span class="hljs-keyword">private</span> <span class="hljs-type">GoogleApiClient</span> googleApiClient;</pre></div><div id="94d5"><pre> <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-title">LocationLiveData</span><span class="hljs-params">(Context context)</span> </span>{ googleApiClient = <span class="hljs-keyword">new</span> GoogleApiClient.<span class="hljs-built_in">Builder</span>(context, <span class="hljs-keyword">this</span>, <span class="hljs-keyword">this</span>)  .<span class="hljs-built_in">addApi</span>(LocationServices.API)  .<span class="hljs-built_in">build</span>(); }</pre></div><div id="e3e0"><pre> <span class="hljs-meta">@Override</span> <span class="hljs-keyword">protected</span> <span class="hljs-built_in">void</span> <span class="hljs-title function_">onActive</span>(<span class="hljs-params"></span>) { <span class="hljs-comment">// Wait for the GoogleApiClient to be connected</span> googleApiClient.<span class="hljs-title function_">connect</span>(); }</pre></div><div id="e36b"><pre> <span class="hljs-meta">@Override</span> <span class="hljs-keyword">protected</span> <span class="hljs-built_in">void</span> <span class="hljs-title function_">onInactive</span>(<span class="hljs-params"></span>) { <span class="hljs-keyword">if</span> (googleApiClient.<span class="hljs-title function_">isConnected</span>()) { <span class="hljs-title class_">LocationServices</span>.<span class="hljs-property">FusedLocationApi</span>.<span class="hljs-title function_">removeLocationUpdates</span>( googleApiClient, <span class="hljs-variable language_">this</span>); } googleApiClient.<span class="hljs-title function_">disconnect</span>(); }</pre></div><div id="866a"><pre> <span class="hljs-meta">@Override</span> <span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title function_">onConnected</span><span class="hljs-params">(Bundle connectionHint)</span> { <span class="hljs-comment">// Try to immediately find a location</span> <span class="hljs-type">Location</span> <span class="hljs-variable">lastLocation</span> <span class="hljs-operator">=</span> LocationServices.FusedLocationApi .getLastLocation(googleApiClient); <span class="hljs-keyword">if</span> (lastLocation != <span class="hljs-literal">null</span>) { setValue(lastLocation); }</pre></div><div id="b72d"><pre> <span class="hljs-comment">// Request updates if there’s someone observing</span> <span class="hljs-keyword">if</span> (hasActiveObservers()) { LocationServices.FusedLocationApi.requestLocationUpdates( googleApiClient, <span class="hljs-keyword">new</span> <span class="hljs-type">LocationRequest</span>(), <span class="hljs-built_in">this</span>); } }</pre></div><div id="dfe0"><pre> <span class="hljs-meta">@Override</span> <span class="hljs-keyword">public</span> <span class="hljs-built_in">void</span> <span class="hljs-title function_">onLocationChanged</span>(<span class="hljs-params">Location location</span>) { <span class="hljs-comment">// Deliver the location changes</span> <span class="hljs-title function_">setValue</span>(location); }</pre></div><div id="5315"><pre> <span class="hljs-meta">@Override</span> <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">onConnectionSuspended</span><span class="hljs-params">(<span class="hljs-keyword">int</span> cause)</span> </span>{ <span class="hljs-comment">// Cry softly, hope it comes back on its own</span> }</pre></div><div id="b73a"><pre> <span class="hljs-variable">@Override</span> public void <span class="hljs-built_in">onConnectionFailed</span>( <span class="hljs-variable">@NonNull</span> ConnectionResult connectionResult) { <span class="hljs-comment">// Consider exposing this state as described here:</span> <span class="hljs-comment">// https://d.android.com/topic/libraries/architecture/guide.html#addendum</span> } }</pre></div><h1 id="1d8a">Just scratching the surface of the Architecture Components</h1><p id="6a47">There’s a lot more to the Android Architecture Components, so make sure to check out <a href="https://developer.android.com/topic/libraries/architecture/index.html">all of the documentation</a>.</p><p id="01e0">I’d personally strongly recommend reading through the entire <a href="https://developer.android.com/topic/libraries/architecture/guide.html">Guide to App Architecture</a> to give you an idea on how all of these components come together to form a solid architecture for your entire app.</p></article></body>

Lifecycle Aware Data Loading with Architecture Components

In my previous blog post, I talked about how you can use Loaders to load data in a way that automatically handles configuration changes.

With the introduction of Architecture Components, there’s an alternative that provides a modern, flexible, and testable solution to this use case.

Separation of concerns

Two of the largest benefits of Loaders were:

  • They encapsulate the process of data loading
  • They survive configuration changes, preventing unnecessarily reloading data

With Architecture Components, these two benefits are now handled by two separate classes:

  • LiveData provides a lifecycle aware base class for encapsulating loading data
  • ViewModels are automatically retained across configuration changes

One significant advantage of this separation is that you can reuse the same LiveData in multiple ViewModels, compose multiple LiveData sources together through a MediatorLiveData, or use them in a Service, avoiding the effort of trying to munge a Loader into a scenario where you don’t have a LoaderManager.

While Loaders espoused a separation between your UI and data loading (one of the first steps to a testable app!), this model expands on that advantage — your ViewModel can be completely tested by mocking out your data sources and the LiveData can be tested in complete isolation. A clean, testable architecture was a large focus in the Guide to App Architecture.

Keep it simple

That all sounds good in theory. An illustrative example recreating our AsyncTaskLoader might help make the ideas a bit more concrete:

public class JsonViewModel extends AndroidViewModel {
  // You probably have something more complicated
  // than just a String. Roll with me
  private final MutableLiveData<List<String>> data =
      new MutableLiveData<List<String>>();
  public JsonViewModel(Application application) {
    super(application);
    loadData();
  }
  public LiveData<List<String>> getData() {
    return data;
  }
  private void loadData() {
    new AsyncTask<Void,Void,List<String>>() {
      @Override
      protected List<String> doInBackground(Void... voids) {
        File jsonFile = new File(getApplication().getFilesDir(),
            "downloaded.json");
        List<String> data = new ArrayList<>();
        // Parse the JSON using the library of your choice
        return data;
      }
      @Override
      protected void onPostExecute(List<String> data) {
        this.data.setValue(data);
      }
    }.execute();
  }
}

Wait, an AsyncTask? How is this safe? There’s two safety features in play here:

  1. The AndroidViewModel (a subclass of ViewModel) only has a reference to the application Context, so we’re very importantly not referencing the Context of an Activity, etc. that could present a leak — there’s even a Lint check to avoid these kind of issues.
  2. LiveData only delivers the results if there’s something observing it

But we haven’t quite captured the essence of the Architecture Components: our ViewModel is directly building and managing our LiveData.

public class JsonViewModel extends AndroidViewModel {
  private final JsonLiveData data;
  public JsonViewModel(Application application) {
    super(application);
    data = new JsonLiveData(application);
  }
  public LiveData<List<String>> getData() {
    return data;
  }
}
public class JsonLiveData extends LiveData<List<String>> {
  private final Context context;
  public JsonLiveData(Context context) {
    this.context = context;
    loadData();
  }
  private void loadData() {
    new AsyncTask<Void,Void,List<String>>() {
      @Override
      protected List<String> doInBackground(Void… voids) {
        File jsonFile = new File(getApplication().getFilesDir(),
            "downloaded.json");
        List<String> data = new ArrayList<>();
        // Parse the JSON using the library of your choice
        return data;
      }
      @Override
      protected void onPostExecute(List<String> data) {
        setValue(data);
      }
    }.execute();
  }
}

So our ViewModel gets considerably simpler, as you’d expect. Our LiveData now completely encapsulates the loading process, loading the data only once.

Data changes in a LiveData world

Just like how Loaders can react to changes elsewhere, this same functionality is key when working with LiveData — as the name implies, the data is expected to change! We can easily rework our class to continue to load data while there’s an observer:

public class JsonLiveData extends LiveData<List<String>> {
  private final Context context;
  private final FileObserver fileObserver;
public JsonLiveData(Context context) {
    this.context = context;
    String path = new File(context.getFilesDir(),
        "downloaded.json").getPath();
    fileObserver = new FileObserver(path) {
      @Override
      public void onEvent(int event, String path) {
        // The file has changed, so let’s reload the data
        loadData();
      }
    };
    loadData();
  }
  @Override
  protected void onActive() {
    fileObserver.startWatching();
  }
  @Override
  protected void onInactive() {
    fileObserver.stopWatching();
  }
  private void loadData() {
    new AsyncTask<Void,Void,List<String>>() {
      @Override
      protected List<String> doInBackground(Void… voids) {
        File jsonFile = new File(getApplication().getFilesDir(),
            "downloaded.json");
        List<String> data = new ArrayList<>();
        // Parse the JSON using the library of your choice
        return data;
      }
      @Override
      protected void onPostExecute(List<String> data) {
        setValue(data);
      }
    }.execute();
  }
}

Now that we’re interested in listening to changes, we can take advantage of LiveData’s onActive() and onInactive() callbacks to only listen when there’s an active observer on our data — as long as someone is observing, they can be guaranteed to get the latest data.

Observing data

In the Loader world, getting your data to your UI would involve a LoaderManager, calling initLoader() in the right place, and building a LoaderCallbacks. The world is a bit more straightforward in the Architecture Components world.

There’s two things we need to do:

  1. Get a reference to our ViewModel
  2. Start observing our LiveData

But explaining that is almost as long as the code itself:

public class MyActivity extends AppCompatActivity {
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    JsonViewModel model =
        ViewModelProviders.of(this).get(JsonViewModel.class);
    model.getData().observe(this, data -> {
      // update UI
    });
  }
}

You’ll note there’s no need to clean things up after the fact: ViewModels automatically live only as long as they are needed and LiveData automatically only passes you data when calling Activity/Fragment/LifecycleOwner is started or resumed.

Load all the things

Now, if you’re still in the vehemently-against-AsyncTask camp, I’m totally okay with that: LiveData is a lot more flexible than being tied to only that construct.

For example, Room lets you have observerable queries — database queries that return LiveData so that database changes automatically propagate up through your ViewModel to your UI. Kind of like a CursorLoader without touching Cursors or Loaders.

We can also rewrite the FusedLocationApi example with a LiveData class:

public class LocationLiveData extends LiveData<Location> implements
    GoogleApiClient.ConnectionCallbacks,
    GoogleApiClient.OnConnectionFailedListener,
    LocationListener {
  private GoogleApiClient googleApiClient;
  public LocationLiveData(Context context) {
    googleApiClient =
      new GoogleApiClient.Builder(context, this, this)
      .addApi(LocationServices.API)
      .build();
  }
  @Override
  protected void onActive() {
    // Wait for the GoogleApiClient to be connected
    googleApiClient.connect();
  }
  @Override
  protected void onInactive() {
    if (googleApiClient.isConnected()) {
      LocationServices.FusedLocationApi.removeLocationUpdates(
          googleApiClient, this);
    }
    googleApiClient.disconnect();
  }
  @Override
  public void onConnected(Bundle connectionHint) {
    // Try to immediately find a location
    Location lastLocation = LocationServices.FusedLocationApi
        .getLastLocation(googleApiClient);
    if (lastLocation != null) {
      setValue(lastLocation);
    }
    // Request updates if there’s someone observing
    if (hasActiveObservers()) {
      LocationServices.FusedLocationApi.requestLocationUpdates(
          googleApiClient, new LocationRequest(), this);
    }
  }
  @Override
  public void onLocationChanged(Location location) {
    // Deliver the location changes
    setValue(location);
  }
  @Override
  public void onConnectionSuspended(int cause) {
    // Cry softly, hope it comes back on its own
  }
  @Override
  public void onConnectionFailed(
      @NonNull ConnectionResult connectionResult) {
    // Consider exposing this state as described here:
    // https://d.android.com/topic/libraries/architecture/guide.html#addendum
  }
}

Just scratching the surface of the Architecture Components

There’s a lot more to the Android Architecture Components, so make sure to check out all of the documentation.

I’d personally strongly recommend reading through the entire Guide to App Architecture to give you an idea on how all of these components come together to form a solid architecture for your entire app.

Android App Development
AndroidDev
Architecture Components
Recommended from ReadMedium