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:
- A
LifecycleOwner
, usually supplied by the activity or fragment that is using theLiveData
, so theLiveData
knows when it is active or inactive - 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.