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:

DiceLight, As Initially Launched
DiceLight, As Initially Launched

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:

DiceLight, Showing Word Counts
DiceLight, Showing 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:

So, now we have three pieces of data to track:

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:

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:

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:

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:

  1. Kotlin offers an object keyword for creating singletons, so we use that rather than manage our own singleton
  2. Since such singletons cannot have a constructor, we pass the Context into generate(), whereas Java supplied it to the repository constructor
  3. Rather than use ListenableFuture, generate() is a suspend function, 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:

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:

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:

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:

UI Events Trigger Calls on Motor
UI Events Trigger Calls on Motor

The motor then converts those calls into operations to be performed on our repository, in an asynchronous fashion:

Motor Manipulates the Repository
Motor Manipulates the Repository

The repository does the work and asynchronously delivers the result to the motor:

Repository Responds to Motor
Repository Responds to Motor

The motor takes those results and crafts a revised view-state that the activity can use to render the UI:

Motor Emits Up-to-Date View-State
Motor Emits Up-to-Date View-State

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.