A UDF Implementation
That UDF explanation may make more sense once we work through an example.
The DiceLight sample module in the Sampler and SamplerJ projects implement a “diceware” app. It allows the user to randomly generate a passphrase from a word list. Later, we will look at a more complex Diceware sample that allows the user to supply an alternative word list. For now, we will settle for having a word list packaged with the app.
The UI
When you launch the app, you are immediately greeted by a random passphrase:
The refresh toolbar button will generate a fresh passphrase, in case you do not like the original one. The “WORDS” item will display a list of word counts:
Switching to a different word count will give you a fresh passphrase with that number of words, in case you want one that is shorter or longer.
The words come from the EFF’s short wordlist, where we randomly choose a few out of the 1,296 in the list. That word list is packaged in the assets/ directory, so it is part of the Android APK that is our compiled app.
The View-State
In order to render this UI, we need a randomly-generated passphrase. However, there are two other scenarios to consider:
- What happens while we are loading the words? Initially, they are not in memory, and until they are, we cannot randomly generate a passphrase from them. That disk I/O will be quick, but not instantaneous. The speed of network I/O is a lot worse, though in this case we do not have any of that. So, in theory, we should show something, such as a
ProgressBar, while we are loading the words. - What happens if we have some sort of problem loading the words? That should not happen here, as we know that our words are available in our APK. However, in general, disk I/O and network I/O can trigger exceptions. If we have such an exception, we really ought to tell the user about it.
So, now we have three pieces of data to track:
- Whether or not we are loading
- The current passphrase, if we have it
- The current error, if we have one
Our MainViewState encapsulates those three pieces of data… but how it does so varies by language.
Java
In Java, MainViewState is a simple class:
package com.commonsware.jetpack.diceware;
class MainViewState {
final boolean isLoading;
final String content;
final int wordCount;
final Throwable error;
MainViewState(boolean isLoading, String content, int wordCount, Throwable error) {
this.isLoading = isLoading;
this.content = content;
this.wordCount = wordCount;
this.error = error;
}
}
We will use null to indicate that we do not have our passphrase (content) yet or do not have a current error.
Kotlin
That Java representation works, and it is simple, but it is not great.
Partly, the problem is that those three pieces of data are mutually exclusive to an extent:
- If we have our passphrase, we did not crash, so we will not have an error
- If we have an error, we will not have a passphrase
- If we are loading, we will not have either of those things
Plus, using null as a “we don’t have the data yet” value is annoying in Kotlin. You keep having to use safe calls (?.) and the like to deal with null.
In Kotlin, we take a substantially different approach, using a sealed class:
package com.commonsware.jetpack.diceware
sealed class MainViewState {
object Loading : MainViewState()
data class Content(val passphrase: String, val wordCount: Int) : MainViewState()
data class Error(val throwable: Throwable) : MainViewState()
}
Now, any instance of MainViewState has just the data that is appropriate for it (e.g., the passphrase and word count for Content). And, for a state that has no data — such as Loading — it is an object, rather than a class.
This loading/content/error pattern is increasingly common in Android, as a way of representing the three major states.
The Repository
So, we need to create those states and get them to the activity, so the activity can do something with that information to display in the UI. A lot of that work is going to be handled by our repository.
The PassphraseRepository is responsible for loading the words and generating a random subset for us on demand. The core logic is the same between the Java and Kotlin implementations, but the API is different, owing to different ways in handling background work and singletons.
Java
Our PassphraseRepository is set up as a classic Java singleton, using a static volatile field and a synchronized, lazy-initializing get() method:
package com.commonsware.jetpack.diceware;
import android.content.Context;
import android.content.res.AssetManager;
import com.google.common.util.concurrent.ListenableFuture;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicReference;
import androidx.concurrent.futures.CallbackToFutureAdapter;
class PassphraseRepository {
private static final String ASSET_FILENAME = "eff_short_wordlist_2_0.txt";
private static volatile PassphraseRepository INSTANCE;
synchronized static PassphraseRepository get(Context context) {
if (INSTANCE == null) {
INSTANCE = new PassphraseRepository(context.getApplicationContext());
}
return INSTANCE;
}
private final AssetManager assets;
private final AtomicReference<List<String>> wordsCache =
new AtomicReference<>();
private final SecureRandom random = new SecureRandom();
private final Executor threadPool = Executors.newSingleThreadExecutor();
private PassphraseRepository(Context context) {
assets = context.getAssets();
}
ListenableFuture<List<String>> generate(int wordCount) {
return CallbackToFutureAdapter.getFuture(completer -> {
threadPool.execute(() -> {
try {
List<String> words = wordsCache.get();
if (words == null) {
InputStream in = assets.open(PassphraseRepository.ASSET_FILENAME);
words = readWords(in);
in.close();
synchronized (wordsCache) {
wordsCache.set(words);
}
}
completer.set(rollDemBones(words, wordCount));
}
catch (Throwable t) {
completer.setException(t);
}
});
return "generate words";
});
}
private List<String> rollDemBones(List<String> words, int wordCount) {
List<String> result = new ArrayList<>();
int size = words.size();
for (int i = 0; i < wordCount; i++) {
result.add(words.get(random.nextInt(size)));
}
return result;
}
private List<String> readWords(InputStream in) throws IOException {
InputStreamReader isr = new InputStreamReader(in);
BufferedReader reader = new BufferedReader(isr);
String line;
List<String> result = new ArrayList<>();
while ((line = reader.readLine()) != null) {
String[] pieces = line.split("\s");
if (pieces.length == 2) {
result.add(pieces[1]);
}
}
return result;
}
}
get() takes a Context as a parameter. We need a Context to be able to read in our word list stored in assets. However, we do not want to hold onto an arbitary Context in this singleton — if the Context is an Activity, we will wind up with a memory leak. So, we hold onto the Application singleton edition of Context instead.
The singleton has a single visible method: generate(). Given a word count, generate() generates a list of randomly-chosen words. However, this may involve disk I/O to read in the assets, if this is the first time we are trying to generate a passphrase in this process. As a result, we use a background thread for that work, in the form of an Executor created from Executors.newSingleThreadExecutor(). The execute() method that we call takes a Runnable (here implemented as a Java 8 lambda expression) and does that work on that background thread.
However, we need to be able to get the list of words to the caller when that background task completes. To that end, generate() returns a ListenableFuture. A Future is an object that represents some outstanding work; ListenableFuture is one that lets one register a listener to find out when that work is done. CallbackToFutureAdapter is a Jetpack utility class that helps you create a ListenableFuture. Specifically, in the Resolver that you pass to getFuture() (here implemented as a Java 8 lambda expression), you are passed a “completer” where you can:
- Provide the data to be returned by the background work (
set()), or - Provide a
Throwableif something went wrong (setException())
The lambda expression needs to return a String, but this is only used for logging purposes.
ListenableFuture and CallbacktoFutureAdapter are obtained via the androidx.concurrent:concurrent-futures library:
}
The repository holds onto the read-in words in a cache, wrapped by an AtomicReference to ensure that we handle possible parallel access to the cache across multiple threads. generate() will:
- See if we have cached the words
- If not, read in the words and save them in the cache
- Randomly choose the requested number of words, passing that
Listto the caller via theListenableFuture
The word list contains a “die roll” value and a word for each line, separated by tabs, such as:
1154 again
1155 agency
1156 aggressor
1161 aghast
1162 agitate
1163 agnostic
The readWords() method needs to split each line along the whitespace and take the second part.
Kotlin
The Kotlin implementation does the same work, with three key differences:
- Kotlin offers an
objectkeyword for creating singletons, so we use that rather than manage our own singleton - Since such singletons cannot have a constructor, we pass the
Contextintogenerate(), whereas Java supplied it to the repository constructor - Rather than use
ListenableFuture,generate()is asuspendfunction, doing the work on a background thread, and we can still return our result list or throw an exception
package com.commonsware.jetpack.diceware
import android.content.Context
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.BufferedReader
import java.io.InputStream
import java.io.InputStreamReader
import java.security.SecureRandom
import java.util.concurrent.atomic.AtomicReference
private const val ASSET_FILENAME = "eff_short_wordlist_2_0.txt"
object PassphraseRepository {
private val wordsCache = AtomicReference<List<String>>()
private val random = SecureRandom()
suspend fun generate(context: Context, count: Int): List<String> {
val words: List<String>? = wordsCache.get()
return words?.let { rollDemBones(it, count) }
?: loadAndGenerate(context, count)
}
private suspend fun loadAndGenerate(
context: Context,
count: Int
): List<String> =
withContext(Dispatchers.IO) {
val inputStream = context.assets.open(ASSET_FILENAME)
inputStream.use {
val words = it.readLines()
.map { line -> line.split("\t") }
.filter { pieces -> pieces.size == 2 }
.map { pieces -> pieces[1] }
wordsCache.set(words)
rollDemBones(words, count)
}
}
private fun rollDemBones(words: List<String>, wordCount: Int) =
List(wordCount) { words[random.nextInt(words.size)] }
private fun InputStream.readLines(): List<String> {
val result = mutableListOf<String>()
BufferedReader(InputStreamReader(this)).forEachLine { result.add(it); }
return result
}
}
As a result, our repository is a bit simpler (no singleton code, plus lots of Kotlin functions to make it easier to read in and process the words). And, since we are handling the background thread in the repository, consumers of this repository can be a bit simpler as well.
The Motor
In Android, we have a problem with our UIs: they keep getting destroyed, courtesy of configuration changes. We saw how we can use a ViewModel for retaining objects across configuration changes, and we saw how we can have a ViewModel expose LiveData objects to the UI layer to deliver data updates.
A motor is simply a ViewModel exposing LiveData.
The reason for the “motor” name (e.g., MainMotor) instead of a “viewmodel” name (e.g., ViewModel) comes from other GUI architectures. In MVVM (Model-View-Viewmodel) and MVP (Model-View-Presenter), what gets referred to as the “viewmodel” fills the sort of role that we have here as the view-state: it is the data to be displayed in the UI. However, in Android’s Architecture Components, just because we use a ViewModel (for configuration changes) does not mean we want that object to serve as a viewmodel (representing the data to be displayed). So, this book uses “motor” to identify a ViewModel that exposes a LiveData of view-state objects.
And, as with our repository, the motor is a bit different between Java and Kotlin, principally due to the way threading is handled.
Java
In Java, our repository gives us a ListenableFuture. Since that API involves I/O, we need to get its work onto a background thread. So, MainMotor adapts the ListenableFuture to LiveData:
package com.commonsware.jetpack.diceware;
import android.app.Application;
import android.text.TextUtils;
import com.google.common.util.concurrent.ListenableFuture;
import java.util.List;
import androidx.annotation.NonNull;
import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
public class MainMotor extends AndroidViewModel {
private static final int DEFAULT_WORD_COUNT = 6;
private final MutableLiveData<MainViewState> viewStates =
new MutableLiveData<>();
private final PassphraseRepository repo;
public MainMotor(@NonNull Application application) {
super(application);
repo = PassphraseRepository.get(application);
generatePassphrase(DEFAULT_WORD_COUNT);
}
LiveData<MainViewState> getViewStates() {
return viewStates;
}
void generatePassphrase() {
final MainViewState current = viewStates.getValue();
if (current == null) {
generatePassphrase(DEFAULT_WORD_COUNT);
}
else {
generatePassphrase(current.wordCount);
}
}
void generatePassphrase(int wordCount) {
viewStates.setValue(new MainViewState(true, null, wordCount, null));
ListenableFuture<List<String>> future = repo.generate(wordCount);
future.addListener(() -> {
try {
viewStates.postValue(new MainViewState(false,
TextUtils.join(" ", future.get()), wordCount, null));
}
catch (Exception e) {
viewStates.postValue(new MainViewState(false, null, wordCount, e));
}
}, Runnable::run);
}
}
MainMotor has a MutableLiveData for our MainViewState, exposing it to the UI layer via a getViewStates() method returning a LiveData. MainMotor also has reference to the PassphraseRepository singleton. And, since we need to supply a Context to the repository, MainMotor extends AndroidViewModel, so we have a getApplication() method to retrieve the Application singleton Context.
The generatePassphrase(int) method first emits a view-state indicating that we are loading. Then, it gets the ListenableFuture from the repository via a call to generate(). Next, it adds a listener to the ListenableFuture, to be notified when the work is complete. This takes a Runnable to be invoked at that time, plus an Executor implementation to control what thread is used to execute the Runnable. In our case, courtesy of Java 8 method references, we can replace that Executor with a Runnable::run method reference, to say “execute the Runnable on whatever thread you are on, please”.
Then, in the Runnable (lambda expression), we:
- Use Android’s
TextUtils.join()method to convert the list of words into a single space-delimited passphrase - Emit a view-state with that result, or emit a view-state with the exception if we ran into a problem
We use postValue() on MutableLiveData, to be sure that no matter what thread the Runnable executes, it is safe for us to update the MutableLiveData.
We also have a generatePassphrase() method that takes no parameters. This will use the wordCount from the previous view-state. If there was no previous view-state, it uses an overall default value.
Note that MainMotor immediately generates a passphrase, using the default word count, once it is created. This way, the activity does not need to tell the motor to do anything — the activity just observes the LiveData and reacts when the initial passphrase is emitted.
Kotlin
In Kotlin, since our repository exposes a suspend function, we use viewModelScope to launch our coroutine, receiving the results on the main application thread:
package com.commonsware.jetpack.diceware
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
private const val DEFAULT_WORD_COUNT = 6
class MainMotor(application: Application) : AndroidViewModel(application) {
private val _results = MutableLiveData<MainViewState>()
val results: LiveData<MainViewState> = _results
init {
generatePassphrase(DEFAULT_WORD_COUNT)
}
fun generatePassphrase() {
generatePassphrase(
(results.value as? MainViewState.Content)?.wordCount ?: DEFAULT_WORD_COUNT
)
}
fun generatePassphrase(wordCount: Int) {
_results.value = MainViewState.Loading
viewModelScope.launch(Dispatchers.Main) {
_results.value = try {
val randomWords = PassphraseRepository.generate(
getApplication(),
wordCount
)
MainViewState.Content(randomWords.joinToString(" "), wordCount)
} catch (t: Throwable) {
MainViewState.Error(t)
}
}
}
}
We also:
- Use the
PassphraseRepositorysingleton supplied by theobjectdeclaration - Use Kotlin’s own
joinToString()instead of Android’sTextUtils.join()to convert the list of words into the passphrase - Use our
sealedclass implementations, so we are emitting some sub-type ofMainViewState
Otherwise, this works the same as its Java equivalent, including the two varieties of generatePassphrase() (one with an explicit word count, one without) and generating a passphrase when the MainMotor is created.
The Activity
Our layout is based on a CardView: a widget that is a simple rounded rectangle with a bit of a drop shadow. Inside of there, we have a TextView and a ProgressBar:
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="8dp">
<androidx.cardview.widget.CardView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:padding="8dp">
<TextView
android:id="@+id/passphrase"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:freezesText="true"
android:textSize="20sp"
android:typeface="monospace" />
<ProgressBar
android:id="@+id/progress"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</androidx.cardview.widget.CardView>
</FrameLayout>
Managing that layout is a simple activity. We could have used a fragment here, but there is only one screen. In general, the remaining sample apps will use fragments and the Navigation component when there is more than one screen, but just a single Activity otherwise.
In onCreate(), we get our MainMotor, start observing the LiveData, apply the MainViewState to our widgets:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ActivityMainBinding binding =
ActivityMainBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
motor = new ViewModelProvider(this).get(MainMotor.class);
motor.getViewStates().observe(this, viewState -> {
binding.progress.setVisibility(
viewState.isLoading ? View.VISIBLE : View.GONE);
if (viewState.content != null) {
binding.passphrase.setText(viewState.content);
}
else if (viewState.error != null) {
binding.passphrase.setText(viewState.error.getLocalizedMessage());
Log.e("Diceware", "Exception generating passphrase",
viewState.error);
}
else {
binding.passphrase.setText("");
}
});
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
motor.results.observe(this) { viewState ->
when (viewState) {
MainViewState.Loading -> {
binding.progress.visibility = View.VISIBLE
binding.passphrase.text = ""
}
is MainViewState.Content -> {
binding.progress.visibility = View.GONE
binding.passphrase.text = viewState.passphrase
}
is MainViewState.Error -> {
binding.progress.visibility = View.GONE
binding.passphrase.text = viewState.throwable.localizedMessage
Log.e(
"Diceware",
"Exception generating passphrase",
viewState.throwable
)
}
}
}
}
We also have a menu resource for the word count options and the refresh button:
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/word_count"
app:showAsAction="ifRoom"
android:title="@string/menu_words">
<menu>
<group android:checkableBehavior="single">
<item
android:id="@+id/word_count_4"
android:title="4" />
<item
android:id="@+id/word_count_5"
android:title="5" />
<item
android:id="@+id/word_count_6"
android:checked="true"
android:title="6" />
<item
android:id="@+id/word_count_7"
android:title="7" />
<item
android:id="@+id/word_count_8"
android:title="8" />
<item
android:id="@+id/word_count_9"
android:title="9" />
<item
android:id="@+id/word_count_10"
android:title="10" />
</group>
</menu>
</item>
<item
android:id="@+id/refresh"
android:icon="@drawable/ic_cached_white_24dp"
app:showAsAction="ifRoom"
android:title="@string/menu_refresh" />
</menu>
The word_count menu item is a bit unusual, following the Android recipe for creating such a selection menu:
- The
<item>has a<menu>child… - …which in turn holds a
<group>withandroid:checkableBehavior="single"… - … which wraps a set of
<item>elements, one for each checkable menu item… - … with
android:checked="true"on theword_count_6item, to pre-check that one
We then have code in onCreateOptionsMenu() and onOptionsItemSelected() on MainActivity to set up and handle that menu:
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.actions, menu);
return super.onCreateOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.refresh:
motor.generatePassphrase();
return true;
case R.id.word_count_4:
case R.id.word_count_5:
case R.id.word_count_6:
case R.id.word_count_7:
case R.id.word_count_8:
case R.id.word_count_9:
case R.id.word_count_10:
item.setChecked(!item.isChecked());
motor.generatePassphrase(Integer.parseInt(item.getTitle().toString()));
return true;
}
return super.onOptionsItemSelected(item);
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.actions, menu)
return super.onCreateOptionsMenu(menu)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.refresh -> {
motor.generatePassphrase()
return true
}
R.id.word_count_4, R.id.word_count_5, R.id.word_count_6, R.id.word_count_7,
R.id.word_count_8, R.id.word_count_9, R.id.word_count_10 -> {
item.isChecked = !item.isChecked
motor.generatePassphrase(Integer.parseInt(item.title.toString()))
return true
}
}
return super.onOptionsItemSelected(item)
}
The refresh item is simple: we just call generatePassphrase() again, causing the motor to get a fresh set of words from the repository and emitting another MainViewState to update our UI.
For the word count, we have more work to do. First, we need to toggle the isChecked state of the MenuItem, because Android (inexplicably) does not handle that for us when the user clicks a checkable menu item. Then, we cheat a bit and parse the actual text of the menu item as an Integer — this only works because this app is English-only, so we know that the menu item captions can be parsed by Integer.parseInt(). We then call generatePassphrase() to generate a passphrase with the new number of words.
Revisiting the Unidirectional Data Flow
Our activity takes user input (activity launch, refresh, word-count change) and converts those into function calls on the motor:
The motor then converts those calls into operations to be performed on our repository, in an asynchronous fashion:
The repository does the work and asynchronously delivers the result to the motor:
The motor takes those results and crafts a revised view-state that the activity can use to render the UI:
Obviously, this is a very simple example. There are plenty of MVI frameworks, such as Spotify’s Mobius, that provide a scaffold around this sort of flow that you can plug into. However, in many cases, that level of sophistication (and corresponding complexity) is not needed, and a simple motor-based flow like the one shown here can suffice.
Prev Table of Contents Next
This book is licensed under the Creative Commons Attribution-ShareAlike 4.0 International license.