Swapping SharedPrefs for SQLite with Room


I love my to-do list. It’s the first app I’ve brought to reasonably-complete status in my seven-ish years as an Android developer (except at work — if anyone asks, I am effective at work) and I use it every day. I would like it to sync between devices though and maybe having a web client.

The app is simple — it’s called Chunks (or 48), and it lets you add tasks for “Today”, “Tomorrow” and “Later”. At midnight, anything that’s marked as complete is deleted, and anything under “Tomorrow” moves to “Today”.


Since it’s so simple, I’ve been using SharedPreferences to store all the data as a single JSON string.

In order to get a decent multi-device sync though, it’s no good storing a blob of data. I’ll need to persist each item separately, each with its own created/updated timestamp.

This is great time to learn Room, a persistence library which is part of Google’s Architecture Components suite.

What better way to learn something than trying it? Well, for me, a better way is trying it, failing, being hand-held through it by an expert, then trying again on my own. This is me sharing the last bit.

Luckily, past-me was nice, and declared a nice interface for persistence:

public interface ChunksRepository {

Chunks getChunks();

void persist(Chunks chunks);
}

Chunks includes the three lists of items, so everything we need to display the UI.

The current implementation looks something like this:

class SharedPrefsChunksRepository implements ChunksRepository {
    private static final String KEY_ITEMS = ...
    ...
    @Override
public Chunks getChunks() {
if (sharedPreferences.contains(KEY_ITEMS)) {
String json = prefs.getString(KEY_ITEMS, "");
return chunksFromJson(json);
} else {
return Chunks.empty(...);
}
}

private Chunks chunksFromJson(String json) {
...
}

@Override
public void persist(Chunks chunks) {
String json = jsonFrom(chunks);
prefs.edit()
.putString(ALL_ENTRIES, json)
.apply();
}
    private String jsonFrom(Chunks chunks) {
...
}
}

We need a Room version of this. Let’s start with the components we’ll need, then we’ll create the implementation of the ChunksRepository.

class ChunksRoom {

private static final String TABLE_NAME = "chunks";
private static final String COLUMN_NAME_PRIMARY_KEY = "primary_key";
private static final String COLUMN_NAME_JSON = "json";
private static final int PRIMARY_KEY = 1;

@Database(entities = {ChunksEntity.class}, version = 1)
static abstract class ChunksDatabase extends RoomDatabase {

abstract ChunksDao chunksDao();
}

@Dao
public interface ChunksDao {

@Nullable
@Query
("SELECT * FROM " + TABLE_NAME + " WHERE " + COLUMN_NAME_PRIMARY_KEY + " = " + PRIMARY_KEY)
ChunksEntity readChunks();

@Insert(onConflict = OnConflictStrategy.REPLACE)
void insertChunks(ChunksEntity chunksRecord);
}

@Entity(tableName = TABLE_NAME)
static class ChunksEntity {

@PrimaryKey
@ColumnInfo
(name = COLUMN_NAME_PRIMARY_KEY)
private final int primaryKey;

@ColumnInfo(name = COLUMN_NAME_JSON)
private final String json;

@Ignore // Room will use the other ctor when unmarshalling
ChunksEntity(String json) {
this(PRIMARY_KEY, json);
}

ChunksEntity(int primaryKey, String json) {
this.primaryKey = primaryKey;
this.json = json;
}

int primaryKey() {
return primaryKey;
}

String json() {
return json;
}
}
}

Entity

The ChunksEntity represents a row in our chunks table (see the value in the class annotation). Since I’m looking to overwrite the same value (everything is stored inside the JSON string), I hardcode the primary key.

From the outside, I’ll want to use the single-parameter constructor, passing only the JSON string, since from the outside, I don’t know or care about the primary key.

We can use the @Ignore annotation on this constructor so Room will disregard it. The only other requirement is that we provide getters for all the fields, which is a shame since I don’t care about the primary key.

Dao

The ChunksDao class is the “data-access object”. This is what we’ll use to read and write Chunks to the database.

@Nullable
@Query
("SELECT * FROM chunks WHERE primary_key = 1")
ChunksEntity readChunks();

So when we call ChunkDao.readChunks(), Room will execute that SQLite statement. It’ll either find a row in the chunks table or the table will be empty. In the first case, it will use the ChunksEntity(int, String) constructor to make us an ChunksEntity object. In the second case, it’ll return null.

Database

@Database(entities = {ChunksEntity.class}, version = 1)
static abstract class ChunksDatabase extends RoomDatabase {

abstract ChunksDao chunksDao();
}

Here we just need to define a method that returns the data access object type we need. You’ll need to specify the entities in your database at the top, and also the version of the database, which will be used for migrations.

All together

Now we can create a Room implementation of ChunksRepository:


class RoomChunksRepository implements ChunksRepository {

static final String DATABASE_NAME = "any-name-for-your-db-file";

static RoomChunksRepository create(Context context, ...) {
ChunksDatabase database = Room.databaseBuilder(
context,
ChunksDatabase.class,
DATABASE_NAME
)
.allowMainThreadQueries() // :ok_hand:
.build();
return new RoomChunksRepository(database.chunksDao(), ...);
}

private final ChunksRoom.Dao dataAccessObject;
    ...

@Override
public Chunks getChunks() {
ChunksRoom.Entity entity = dataAccessObject.readChunks();
if (entity == null) {
return Chunks.empty(...);
} else {
String json = entity.json();
return chunksFromJson(json);
}
}
    private Chunks chunksFromJson(String json) {
...
}
    @Override
public void persist(Chunks chunks) {
String json = jsonFrom(chunks);

ChunksRoom.Entity entity = new ChunksRoom.Entity(json);
dataAccessObject.insertChunks(entity);
}
    private String jsonFrom(Chunks chunks) {
...
}
}

The RoomChunksRepository only needs the ChunksDao to read and write data.

I only have the one data-access object, but in more substantial apps, it’s likely you’ll have more; be mindful about scope and consider obtaining and passing the relevant data-access object directly to your repository (as opposed to creating the database in the same class as I do above).

And it works! You can check out Chunks on GitHub repo and you can download APKs from the releases tab.


Well, kind of. I still have all my data in SharedPreferences and I need to migrate that to SQLite first. I’ll do it tomorrow. Maybe.