Colors… Live!

The InLivingColor sample module in the Sampler and SamplerJ projects implement the same basic UI as we saw in the TwoActivities sample earlier in the book.

Part of the changes are to switch from two activities to two fragments and use the Navigation component. That largely mirrors what we saw previously, so we will not focus on those changes here.

The change of importance for these samples are how we get our list of randomly-generated colors. In the earlier list-of-colors examples, we just generated colors directly in the ColorViewModel. This time, we are going to pretend that it takes “real work” to get these colors, such as having to call out to some random-color-generating Web service. To that end, we have a ColorLiveData subclass of LiveData that handles the threading aspects and generates the colors. ColorViewModel now exposes a LiveData of colors to observers, and a ColorListFragment pours those colors into the RecyclerView when they are ready.

ColorLiveData

Our ColorLiveData is responsible for creating a random set of colors, using a background thread. We only want to do this once we have one active observer — otherwise, the colors are pointless. So, our LiveData subclass overrides onActive() and handles the work there:

package com.commonsware.jetpack.samplerj.livedata;

import android.os.SystemClock;
import java.util.ArrayList;
import java.util.Random;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import androidx.lifecycle.LiveData;

class ColorLiveData extends LiveData<ArrayList<Integer>> {
  private final Random random = new Random();
  private final Executor executor = Executors.newSingleThreadExecutor();

  @Override
  public void onActive() {
    super.onActive();

    if (getValue() == null) {
      executor.execute(() -> {
        SystemClock.sleep(2000);  // use only for book samples!
        postValue(buildItems());
      });
    }
  }

  private ArrayList<Integer> buildItems() {
    ArrayList<Integer> result = new ArrayList<>(25);

    for (int i = 0; i < 25; i++) {
      result.add(random.nextInt());
    }

    return result;
  }
}
package com.commonsware.jetpack.sampler.livedata

import android.os.SystemClock
import androidx.lifecycle.LiveData
import java.util.*
import java.util.concurrent.Executors

class ColorLiveData : LiveData<List<Int>>() {
  private val random = Random()
  private val executor = Executors.newSingleThreadExecutor()

  override fun onActive() {
    super.onActive()

    if (value == null) {
      executor.execute {
        SystemClock.sleep(2000)  // use only for book samples!
        postValue(List(25) { random.nextInt() })
      }
    }
  }
}

We use a Java Executor for performing our background work. For what we are doing now, a simple single-thread Executor is sufficient, but Executor gives us the flexibility to swap in a thread pool if we decided that we needed lots of random colors.

In onActive(), we see if we already have our colors. We do this by calling getValue(), which will return the value held by the LiveData or null if we have not yet generated any colors. If we do not already have colors, we use the Executor to run some code on a background thread to generate the colors.

We use SystemClock.sleep() to simulate two seconds worth of I/O to get these random colors, as our fictitious random-color-generating Web service is overloaded and our pretend Internet connection is poor. If you have used Thread.sleep() in Java before, SystemClock.sleep() works much the same way, blocking the current thread for the stated number of milliseconds. The biggest difference: Thread.sleep() throws an InterruptedException, which is unnecessary here and the resulting try/catch block just clutters up the example.

To update the LiveData with the new colors, we call postValue(). This updates the LiveData and — back on the main application thread — lets each of the registered observers know about the new data.

This particular LiveData only ever calls postValue() once. That is all this sample needs. However, there is nothing stopping you from having a LiveData that delivers a stream of updates, such as a stream of sensor readings. There, you might call postValue() hundreds or thousands of times, with the LiveData informing its observers each time.

ColorViewModel Changes

Our ColorViewModel now just holds onto a ColorLiveData instance:

package com.commonsware.jetpack.samplerj.livedata;

import java.util.ArrayList;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.ViewModel;

public class ColorViewModel extends ViewModel {
  final LiveData<ArrayList<Integer>> numbers = new ColorLiveData();
}
package com.commonsware.jetpack.sampler.livedata

import androidx.lifecycle.ViewModel

class ColorViewModel : ViewModel() {
  val numbers = ColorLiveData()
}

When the ColorViewModel is created, we create that ColorLiveData instance, and the viewmodel holds onto that instance for the lifetime of the viewmodel. That way, even after the work is completed and we have our list of colors, we still hold onto those colors across configuration changes. However, to keep this example simple, we are skipping the saved-state logic to try to handle cases where the process is terminated shortly after the user moves the app to the background.

Since we do not start our background work until onActive() is called on the ColorLiveData, just creating the ColorLiveData is cheap. And, if for some reason we never observe the ColorLiveData, we do not go through the “expense” of generating this list of random colors.

Observing the Colors

In the earlier list-of-colors example, we would call submitList() on our ColorAdapter for our colors with the numbers that we get from the ColorViewModel. Now, we observe the numbers property, as that is a LiveData.

In Java, we use the observe() method on LiveData itself:

package com.commonsware.jetpack.samplerj.livedata;

import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import com.commonsware.jetpack.samplerj.livedata.databinding.FragmentListBinding;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.fragment.NavHostFragment;
import androidx.recyclerview.widget.DividerItemDecoration;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;

public class ColorListFragment extends Fragment {
  private FragmentListBinding binding;

  @Override
  public View onCreateView(@NonNull LayoutInflater inflater,
                           ViewGroup container,
                           Bundle savedInstanceState) {
    binding = FragmentListBinding.inflate(inflater, container, false);

    return binding.getRoot();
  }

  @Override
  public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
    ColorViewModel vm = new ViewModelProvider(this).get(ColorViewModel.class);

    ColorAdapter colorAdapter = new ColorAdapter(getLayoutInflater(),
      this::navTo);

    binding.items.setLayoutManager(new LinearLayoutManager(requireContext()));
    binding.items.addItemDecoration(new DividerItemDecoration(requireContext(),
      DividerItemDecoration.VERTICAL));
    binding.items.setAdapter(colorAdapter);

    vm.numbers.observe(getViewLifecycleOwner(), colorAdapter::submitList);
  }

  @Override
  public void onDestroyView() {
    super.onDestroyView();

    binding = null;
  }

  private void navTo(int color) {
    NavHostFragment.findNavController(this)
      .navigate(ColorListFragmentDirections.showColor(color));
  }
}

observe() on a LiveData takes two parameters:

  1. A LifecycleOwner, usually supplied by the activity or fragment that is using the LiveData, so the LiveData knows when it is active or inactive
  2. An Observer implementation

An Observer has a single method, observe(), that returns void and takes an instance of whatever type the LiveData holds. So, in our case, observe() will be passed an ArrayList<Int>. Here, in the Java scenario, we use a Java 8 method reference to hand that ArrayList over to the ColorAdapter, whereas in Kotlin, we just use an ordinary lambda expression:

package com.commonsware.jetpack.sampler.livedata

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import com.commonsware.jetpack.sampler.livedata.databinding.FragmentListBinding
import com.commonsware.jetpack.sampler.util.ViewBindingFragment

class ColorListFragment :
  ViewBindingFragment<FragmentListBinding>(FragmentListBinding::inflate) {
  private val vm: ColorViewModel by viewModels()

  override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    val colorAdapter = ColorAdapter(layoutInflater) { color ->
      navTo(color)
    }

    useBinding { binding ->
      binding.items.apply {
        layoutManager = LinearLayoutManager(activity)

        addItemDecoration(
          DividerItemDecoration(
            activity,
            DividerItemDecoration.VERTICAL
          )
        )

        adapter = colorAdapter
      }
    }

    vm.numbers.observe(viewLifecycleOwner) { colorAdapter.submitList(it) }
  }

  private fun navTo(color: Int) {
    findNavController().navigate(ColorListFragmentDirections.showColor(color))
  }
}

We will look more at the LifecycleOwner parameter, and what viewLifecycleOwner is, shortly.

The Results

When the fragment is first created, it creates the ColorViewModel, which creates a ColorLiveData. When the fragment then calls observe() on the LiveData, and the fragment is later shown on the screen, the ColorLiveData will be called with onActive(), as the fragment will now be started. At that point, the ColorLiveData will generate the colors (after a two-second delay). When ColorLiveData calls postValue(), the Observer registered by the fragment will be called, and the ColorAdapter will get its colors.

If the user rotates the screen, our original ColorListFragment will be destroyed and recreated. When the fresh ColorListFragment gets its ColorViewModel, it will be the already-existing ColorLiveData instance. When the fragment calls observe(), its Observer will be called immediately if the ColorLiveData already has its colors — that is handled automatically by LiveData. Our fragment’s code does not care whether it is the first or second instance of the ColorListFragment — it gets the colors the same way and consumes them the same way.


Prev Table of Contents Next

This book is licensed under the Creative Commons Attribution-ShareAlike 4.0 International license.