Let’s Roll the Dice

In The Busy Coder’s Guide to Android Development, one of the sample apps is a “diceware” app, to help you generate a passphrase made up of a series of randomly-selected words, such as correct horse battery staple. However, that sample puts most of the work inside a single fragment, which is messy. So, let’s rebuild that app, hiding all of the data-loading details in a repository, with a view-model to mediate communications between the fragment and the repository. The results can be found in the Diceware/Repository sample project.

The Repository

In our case, the words come from two locations: a “baked in” word list in assets/ and a word list of the user’s choosing, obtained via ACTION_OPEN_DOCUMENT. However, the data structure for each is the same: a list of words, one per line. Hence, we do not have a sophisticated data model, only a list of strings. So our repository does not need to worry about normalizing disparate model objects, though we might if we obtained words from some Web service. And, our repository does not need to worry about data modification, as the word lists are treated as read-only.

However, we still need a nice reactive API. The code for getting the words from a user-chosen document is a bit different from the code for getting the words from an asset. Moreover, if we want to cache the words, we need to handle the case where we have not yet loaded the words and the case where the words are cached.

API

In the end, what our UI needs is a set of randomly-selected words, with the UI providing the number of words and the source of those words.

To that end, Repository has a single instance method that is exposed to the rest of the app: getWords(). It takes the Uri representing the data source and the number of words to return. The words themselves will be a List of String objects. We wrap that in an RxJava Single, as we do not know how long it will take to come up with those words at compile time, since the word list from the data source may not be cached yet. However, we do know that this is a one-shot event, and so Single makes more sense than does a generic Observable.

The Repository is a singleton, so we will have a static method named get() to retrieve that singleton, given a Context to use for lazy initialization.

Implementation

getWords() breaks the problem down into two pieces: getting the full word list and then choosing a random subset of those words:

  Single<List<String>> getWords(Uri source, final int count) {
    return(getWordsFromSource(source)
      .map(strings -> (randomSubset(strings, count))));
  }

The map() operator delegates the “choose a random subset” work to a randomSubset() method, which uses a SecureRandom instance to choose the words:

  private List<String> randomSubset(List<String> words, int count) {
    List<String> result=new ArrayList<>();
    int size=words.size();

    for (int i=0;i<count;i++) {
      result.add(words.get(random.nextInt(size)));
    }

    return(result);
  }

getWordsFromSource() needs to look to see if we have a cached copy of the word list for the requested Uri. If not, we need to arrange to load and cache those words; otherwise, we can just use the cache. Our cache is ConcurrentHashMap mapping the Uri to the word list:

  private final ConcurrentHashMap<Uri, List<String>> cache=new ConcurrentHashMap<>();

getWordsFromSource() checks the cache and creates an Single chain based on whether or not the words are cached:

  synchronized private Single<List<String>> getWordsFromSource(Uri source) {
    List<String> words=cache.get(source);
    final Single<List<String>> result;

    if (words==null) {
      result=Single.just(source)
        .subscribeOn(Schedulers.io())
        .map(uri -> (open(uri)))
        .map(in -> (readWords(in)))
        .doOnSuccess(strings -> cache.put(source, strings));
    }
    else {
      result=Single.just(words);
    }

    return(result);
  }

If the words are cached, our job is simple: just return that word list, wrapped in an Single, for getWords() to use to come up with the random subset.

If the words are not yet cached, we:

  private InputStream open(Uri uri) throws IOException {
    String scheme=uri.getScheme();
    String path=uri.getPath();

    if ("file".equals(scheme) && path.startsWith("/android_asset")) {
      return(ctxt.getAssets().open(ASSET_FILENAME));
    }

    ContentResolver cr=ctxt.getContentResolver();

    cr.takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION);

    return(cr.openInputStream(uri));
  }
  private static 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);
  }

The ViewModel

The Repository API is fairly clean, isolating the caching and data loading and stuff behind a reactive response. However, there is one hiccup: each call to getWords() results in a new Single. This is somewhat of a headache for the UI, as we would need a fresh subscription — via a fresh LiveData — whenever the user asks for a new set of words, or changes the word count, or opens a new word list. That is on top of having to manage the subscriptions across lifecycle events.

What would be nice is if the UI could have a single Observable, on which all the words would come in, regardless of the trigger (including getting a set of words on first launch). We would still have to deal with lifecycle events, but we have LiveData for that.

So, this app has a ViewModel implementation — named PassphraseViewModel — that offers a LiveData of the incoming words that the UI can use, in addition to tracking our current word source and word count across configuration changes.

Repository Integration

The PassphraseViewModel constructor takes, among other things, a Context as a parameter, to use to retrieve the Repository singleton, held in a field named repo. We also have source and count fields to hold how many words we should retrieve and where we should retrieve them from, initialized to some starter values:

  private final Repository repo;
  private Uri source=Uri.parse("file:///android_asset/eff_short_wordlist_2_0.txt");
  private int count=6;
  private Disposable sub=Disposables.empty();

Getting words to the UI is handled by a words() method, that returns a LiveData for random word list subsets. That LiveData is in the form of a liveWords field:

  LiveData<List<String>> words() {
    return(liveWords);
  }

As noted, though, we will get multiple Single instances from the Repository, one for each getWords() call that we need. If we want the UI to use this stable LiveData instance, we need a way to feed different Single results to it over time.

The approach that PassphraseViewModel takes is to have liveWords be a MutableLiveData:

  private final MutableLiveData<List<String>> liveWords=new MutableLiveData<>();

The PassphraseViewModel has a refresh() method. Partly, this is used literally for a “refresh” operation, to load a fresh batch of words given the current count and source values. In fact, everything else that needs to trigger loading words routes through refresh(). refresh() calls the getWords() method that we have on Repository and forwards the events to the liveWords by using a Java 8 method reference to tie the subscribe() of the Single to postValue() of the liveWords:

  void refresh() {
    sub.dispose();
    sub=repo.getWords(source, count)
      .observeOn(Schedulers.io())
      .subscribe(liveWords::postValue);
  }

The net effect is that every time refresh() is called, the liveWords eventually will deliver a new random subset of the current word list.

Saving State

We need to get the source and the count from the UI, for use in refresh(). And, along the way, we can hold onto that information across configuration changes, since this is a ViewModel. Plus, we can also have the PassphraseViewModel store this information in the saved instance state Bundle, so the view-model is the single “source of truth” for the current source and count.

To that end, the constructor on PassphraseViewModel takes a saved instance state Bundle as input and — if the Bundle is not null — populates the source and count from its contents:

  PassphraseViewModel(Context ctxt, Bundle state) {
    repo=Repository.get(ctxt);

    if (state!=null) {
      source=state.getParcelable(STATE_SOURCE);
      count=state.getInt(STATE_COUNT, 6);
    }

    refresh();
  }

The constructor also calls refresh(), to queue up the first random set of words, so we can populate the UI as quickly as possible.

PassphraseViewModel then has its own onSaveInstanceState(), where it fills in the state Bundle using the same keys that its constructor uses to read the values out:

  void onSaveInstanceState(Bundle state) {
    state.putParcelable(STATE_SOURCE, source);
    state.putInt(STATE_COUNT, count);
  }

(hat tip to Danny Preussler for the idea of centralizing both view-model and saved instance state logic in the ViewModel)

The Factory

However, by default, the Architecture Components’ ViewModel system has no way to create an instance of PassphraseViewModel. After all, it has no idea what this Bundle is.

To help with that, PassphraseViewModel has a Factory nested class that implements ViewModelProvider.Factory. This provides the “glue” for tying the Architecture Components to PassphraseViewModel, by creating an instance of PassphraseViewModel as needed:

  static class Factory implements ViewModelProvider.Factory {
    private final Bundle state;
    private final Context ctxt;

    Factory(Context ctxt, Bundle state) {
      this.ctxt=ctxt.getApplicationContext();
      this.state=state;
    }

    @NonNull
    @Override
    public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
      return((T)new PassphraseViewModel(ctxt, state));
    }
  }

When we create an instance of the Factory, we need to provide a Context (such as the Activity hosting our UI) and the incoming saved instance state Bundle, for the Factory to pass along to the newly-created instance.

The Fragment

The launcher (and only) activity — MainActivity — simply sets up a PassphraseFragment:

package com.commonsware.android.diceware;

import android.os.Bundle;
import android.support.v4.app.FragmentActivity;

public class MainActivity extends FragmentActivity {
  @Override
  protected void onCreate(Bundle state) {
    super.onCreate(state);

    if (getSupportFragmentManager().findFragmentById(android.R.id.content) == null) {
      getSupportFragmentManager().beginTransaction()
        .add(android.R.id.content,
          new PassphraseFragment()).commit();
    }
  }
}

All of the real UI/UX work resides in the fragment.

The UI

The UI for PassphraseFragment consists of a TextView for the words, wrapped in a CardView to make it a bit more aesthetically interesting:

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  android:padding="8dp">

  <android.support.v7.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:textSize="20sp"
      android:typeface="monospace" />
  </android.support.v7.widget.CardView>

</FrameLayout>

The core of our UI setup is in onViewCreated():

  @Override
  public void onViewCreated(View view, Bundle state) {
    super.onViewCreated(view, state);

    passphrase=view.findViewById(R.id.passphrase);
    viewModel=ViewModelProviders
      .of(this, new PassphraseViewModel.Factory(getActivity(), state))
      .get(PassphraseViewModel.class);
    updateMenu();
    viewModel.words().observe(this,
      words -> passphrase.setText(TextUtils.join(" ", words)));
  }

Here, we:

For a newly-created PassphraseViewModel, the constructor’s call to refresh() will give us some words to show automatically. On a configuration change, our LiveData will hand back our last set of words automatically. Plus, the LiveData handles the rest of the lifecycle work for us.

The Menu

The fragment also has a menu, with a drop-down for the word count, a “refresh” item to get a fresh random subset of words, and an “open” item to choose a word list document:

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
  <item
    android:id="@+id/word_count"
    android: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"
    android:showAsAction="ifRoom"
    android:title="@string/menu_refresh" />
  <item
    android:id="@+id/open"
    android:enabled="false"
    android:showAsAction="never"
    android:title="@string/open" />

</menu>

The “refresh” item ties directly to the refresh() method on the view-model, while the word count items update their checked state and route to a setCount() method on the view-model:

  @Override
  public boolean onOptionsItemSelected(MenuItem item) {
    switch(item.getItemId()) {
      case R.id.open:
        open();
        return(true);

      case R.id.refresh:
        viewModel.refresh();
        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());
        viewModel.setCount(Integer.parseInt(item.getTitle().toString()));

        return(true);
    }

    return(super.onOptionsItemSelected(item));
  }

The “open” item routes to an open() method, which brings up an ACTION_OPEN_DOCUMENT activity for the user to choose a word list:

  private void open() {
    Intent i=
      new Intent()
        .setType("text/plain")
        .setAction(Intent.ACTION_OPEN_DOCUMENT)
        .addCategory(Intent.CATEGORY_OPENABLE);

    startActivityForResult(i, REQUEST_OPEN);
  }

If we get a document, that is passed over to a setSource() method on the view-model:

  @Override
  public void onActivityResult(int requestCode, int resultCode,
                               Intent resultData) {
    if (resultCode==Activity.RESULT_OK) {
      viewModel.setSource(resultData.getData());
    }
  }

The setCount() and setSource() methods on PassphraseViewModel not only update their respective fields, but they also call refresh(), to deliver a fresh set of words based on the new count or new source of words:

  void setSource(Uri source) {
    this.source=source;
    refresh();
  }

  void setCount(int count) {
    this.count=count;
    refresh();
  }

As a result, when the user chooses any of those action bar items, if there is an actual state change (e.g., a new count), we get a new roster of words.

We also need to set the checked state of the word count items based on the count, either from the default value or from our state (view-model or saved instance state). Since we do not know whether the menu or the view-model will be set up first, we call a central updateMenu() method from a couple of places to check the right action bar item:

  private void updateMenu() {
    if (menu!=null && viewModel!=null) {
      MenuItem checkable=menu.findItem(WORD_COUNT_MENU_IDS[viewModel.getCount()-4]);

      if (checkable!=null) {
        checkable.setChecked(true);
      }
    }
  }

And, our fragment’s onSaveInstanceState() forwards that Bundle to the PassphraseViewModel for saving the state:

  @Override
  public void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);
    viewModel.onSaveInstanceState(outState);
  }

The Event Flow

When our fragment is created, we create our view-model. It, in turn, asks the repository to give us our initial random subset of words, triggering some background file I/O to read in the initial word list. When that is done, the random words wind their way to the fragment, which pops them into the UI.

When the user requests different words — a different count, a different source, or just words that maybe they might like better — the fragment updates the view-model, which in turn asks the repository for words for the now-current word count and word source. Eventually, another random subset of words make its way to fragment, which displays them using the same code as before.

On a configuration change, our newly-recreated fragment winds up connecting to the same view-model as before, courtesy of the Architecture Components’ ViewModel system. Our LiveData gives us back the words we were showing in the previous fragment, so we can show them again.

So, each layer has its role in the event flow:

And, of course, the user gets a reasonably-secure passphrase to use for some app or site that needs it:

Diceware Demo, Showing Random Words
Diceware Demo, Showing Random Words

Prev Table of Contents Next

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