The Repositories

Eventually, our Controller will need to update backing stores, so that our to-do items and filter modes are persistent from run to run of our app. This sample app uses the repository pattern, with two repositories: ToDoRepository for to-do items and FilterModeRepository for filter modes.

The objective of each repository is simple:

Each repository is a singleton, so all logic routes through central points for each type of data.

ToDoRepository

The ToDoRepository wraps around a ToDoDatabase and ToDoEntity objects, where those have the appropriate Room annotations to set up a SQLite database.

ToDoEntity has fields that map to the same pieces of data that ToDoModel holds. In principle, we could have dispensed with the separation and passed around ToDoEntity objects where we are currently passing around ToDoModel objects. However, it is possible that some future edition of this sample might have multiple backing stores (e.g., local database and a server), in which case keeping some separation between the in-memory model and the persistent representations is worthwhile. In this case, converting between the two is relatively simple, and ToDoEntity has a constructor and toModel() methods that do just that:

package com.commonsware.android.todo.impl;

import android.arch.persistence.room.Dao;
import android.arch.persistence.room.Delete;
import android.arch.persistence.room.Entity;
import android.arch.persistence.room.Index;
import android.arch.persistence.room.Insert;
import android.arch.persistence.room.PrimaryKey;
import android.arch.persistence.room.Query;
import android.arch.persistence.room.Update;
import android.support.annotation.NonNull;
import java.util.Calendar;
import java.util.List;
import io.reactivex.Single;

@Entity(tableName="todos", indices=@Index(value="id"))
public class ToDoEntity {
  @PrimaryKey
  @NonNull final String id;
  @NonNull final String description;
  final String notes;
  final boolean isCompleted;
  @NonNull final Calendar createdOn;

  public static ToDoEntity fromModel(ToDoModel model) {
    return(new ToDoEntity(model.id(), model.description(), model.isCompleted(),
      model.notes(), model.createdOn()));
  }

  ToDoEntity(@NonNull String id, @NonNull String description, boolean isCompleted,
             String notes, @NonNull Calendar createdOn) {
    this.id=id;
    this.description=description;
    this.isCompleted=isCompleted;
    this.notes=notes;
    this.createdOn=createdOn;
  }

  public ToDoModel toModel() {
    return(ToDoModel.builder()
      .id(id)
      .description(description)
      .isCompleted(isCompleted)
      .notes(notes)
      .createdOn(createdOn)
      .build());
  }

  @Dao
  public interface Store {
    @Query("SELECT * FROM todos ORDER BY description ASC")
    Single<List<ToDoEntity>> all();

    @Insert
    void insert(ToDoEntity... entities);

    @Update
    void update(ToDoEntity... entities);

    @Delete
    void delete(ToDoEntity... entities);

    @Delete
    void delete(List<ToDoEntity> entities);

    @Query("DELETE FROM todos")
    void deleteAll();
  }
}

ToDoRepository then maps between the models that it uses as its API and the entities that it uses with the ToDoDatabase:

package com.commonsware.android.todo.impl;

import android.content.Context;
import java.util.ArrayList;
import java.util.List;
import io.reactivex.Single;
import io.reactivex.annotations.NonNull;
import io.reactivex.functions.Function;

public class ToDoRepository {
  private static volatile ToDoRepository INSTANCE=null;
  private final ToDoDatabase db;

  public synchronized static ToDoRepository get(Context ctxt) {
    if (INSTANCE==null) {
      INSTANCE=new ToDoRepository(ctxt.getApplicationContext());
    }

    return(INSTANCE);
  }

  private ToDoRepository(Context ctxt) {
    db=ToDoDatabase.get(ctxt);
  }

  public Single<List<ToDoModel>> all() {
    return(db.todoStore().all().map(entities -> {
      ArrayList<ToDoModel> result=new ArrayList<>(entities.size());

      for (ToDoEntity entity : entities) {
        result.add(entity.toModel());
      }

      return(result);
    }));
  }

  public void add(ToDoModel model) {
    db.todoStore().insert(ToDoEntity.fromModel(model));
  }

  public void replace(ToDoModel model) {
    db.todoStore().update(ToDoEntity.fromModel(model));
  }

  public void delete(List<ToDoModel> models) {
    List<ToDoEntity> entities=new ArrayList<>();

    for (ToDoModel model : models) {
      entities.add(ToDoEntity.fromModel(model));
    }

    db.todoStore().delete(entities);
  }
}

Its all() method is a Single, used for the initial data load, which gets all the entities, then uses a map() operator to convert those entities into models.

FilterModeRepository

The filter mode is saved in SharedPreferences. The FilterMode itself is an enum that knows how to map between stable int values and the corresponding enum values:

package com.commonsware.android.todo.impl;

public enum FilterMode {
  ALL(0),
  COMPLETED(1),
  OUTSTANDING(2);

  private final int value;

  static FilterMode forValue(int value) {
    FilterMode result=ALL;

    if (value==1) {
      result=COMPLETED;
    }
    else if (value==2) {
      result=OUTSTANDING;
    }

    return(result);
  }

  FilterMode(int value) {
    this.value=value;
  }

  int getValue() {
    return(value);
  }
}

FilterModeRepository hides the storage details, accepting in FilterMode objects and offering a load() method to obtain the current FilterMode via a Single:

package com.commonsware.android.todo.impl;

import android.annotation.SuppressLint;
import android.content.Context;
import android.content.SharedPreferences;
import io.reactivex.Single;

class FilterModeRepository {
  private static final String PREF_MODE="filterMode";
  private static volatile FilterModeRepository INSTANCE=null;
  private SharedPreferences prefs=null;

  synchronized static FilterModeRepository get() {
    if (INSTANCE==null) {
      INSTANCE=new FilterModeRepository();
    }

    return(INSTANCE);
  }

  Single<FilterMode> load(Context ctxt) {
    final Context app=ctxt.getApplicationContext();

    return(Single.create(e -> {
      synchronized(this) {
        if (prefs==null) {
          prefs=app.getSharedPreferences(getClass().getCanonicalName(),
            Context.MODE_PRIVATE);
        }
      }

      e.onSuccess(FilterMode.forValue(prefs.getInt(PREF_MODE, 0)));
    }));
  }

  @SuppressLint("ApplySharedPref")
  void save(FilterMode mode) {
    prefs.edit().putInt(PREF_MODE, mode.getValue()).commit();
  }
}

Prev Table of Contents Next

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