Model refactor

This splits up the POJOs from the app data types to make it easier to
persist in both Firebase and Syncbase.

Change-Id: I4609e1ee0fa9772952e9aa12cf362a02e6098fee
diff --git a/app/build.gradle b/app/build.gradle
index 0b7fa60..ed30d8b 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -46,5 +46,4 @@
             'com.android.support:recyclerview-v7:23.1.1'
     )
     firebaseCompile 'com.firebase:firebase-client-android:2.5.2'
-    syncbaseCompile 'com.fasterxml.jackson.core:jackson-annotations:2.7.3'
 }
diff --git a/app/src/firebase/java/io/v/todos/persistence/firebase/ChildEventListenerAdapter.java b/app/src/firebase/java/io/v/todos/persistence/firebase/ChildEventListenerAdapter.java
index 282eecd..4f22548 100644
--- a/app/src/firebase/java/io/v/todos/persistence/firebase/ChildEventListenerAdapter.java
+++ b/app/src/firebase/java/io/v/todos/persistence/firebase/ChildEventListenerAdapter.java
@@ -8,37 +8,20 @@
 import com.firebase.client.DataSnapshot;
 import com.firebase.client.FirebaseError;
 
-import io.v.todos.model.KeyedData;
-import io.v.todos.persistence.ListEventListener;
-
-public class ChildEventListenerAdapter<T extends KeyedData> implements ChildEventListener {
-    private final Class<T> mType;
-    private final ListEventListener<T> mDelegate;
-
-    public ChildEventListenerAdapter(Class<T> type, ListEventListener<T> delegate) {
-        mType = type;
-        mDelegate = delegate;
-    }
-
-    private T prepareKeyedData(DataSnapshot dataSnapshot) {
-        T value = dataSnapshot.getValue(mType);
-        value.setKey(dataSnapshot.getKey());
-        return value;
-    }
-
+/**
+ * Basic Java adapter pattern for a {@link ChildEventListener}.
+ */
+public abstract class ChildEventListenerAdapter implements ChildEventListener {
     @Override
     public void onChildAdded(DataSnapshot dataSnapshot, String prevKey) {
-        mDelegate.onItemAdd(prepareKeyedData(dataSnapshot));
     }
 
     @Override
     public void onChildChanged(DataSnapshot dataSnapshot, String prevKey) {
-        mDelegate.onItemUpdate(prepareKeyedData(dataSnapshot));
     }
 
     @Override
     public void onChildRemoved(DataSnapshot dataSnapshot) {
-        mDelegate.onItemDelete(dataSnapshot.getKey());
     }
 
     @Override
diff --git a/app/src/firebase/java/io/v/todos/persistence/firebase/FirebaseMain.java b/app/src/firebase/java/io/v/todos/persistence/firebase/FirebaseMain.java
index 9066a0b..2df421e 100644
--- a/app/src/firebase/java/io/v/todos/persistence/firebase/FirebaseMain.java
+++ b/app/src/firebase/java/io/v/todos/persistence/firebase/FirebaseMain.java
@@ -14,10 +14,14 @@
 import com.firebase.client.Transaction;
 
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.Map;
+import java.util.Set;
 
 import io.v.todos.model.ListMetadata;
+import io.v.todos.model.ListSpec;
 import io.v.todos.model.Task;
+import io.v.todos.model.TaskSpec;
 import io.v.todos.persistence.ListEventListener;
 import io.v.todos.persistence.MainPersistence;
 
@@ -40,28 +44,25 @@
         // This handler will forward events to the passed in listener after ensuring that all the
         // data in the ListMetadata is set and can automatically update.
         mTodoListsListener = mTodoLists.addChildEventListener(
-                new ChildEventListenerAdapter<>(ListMetadata.class, new ListEventListener<ListMetadata>() {
+                new ChildEventListenerAdapter() {
                     @Override
-                    public void onItemAdd(ListMetadata item) {
-                        // Hook up listeners for the # completed and # tasks. Then forward the item.
-                        startWatchTodoListTasks(item);
-                        mListener.onItemAdd(item);
+                    public void onChildAdded(DataSnapshot dataSnapshot, String prevKey) {
+                        mListener.onItemAdd(startWatchTodoListTasks(
+                                dataSnapshot.getKey(), dataSnapshot.getValue(ListSpec.class)));
                     }
 
                     @Override
-                    public void onItemUpdate(ListMetadata item) {
-                        // Retrieve # completed and # tasks. Then forward the item.
-                        setTaskCompletion(item);
-                        mListener.onItemUpdate(item);
+                    public void onChildChanged(DataSnapshot dataSnapshot, String prevKey) {
+                        mListener.onItemUpdate(updateListSpec(
+                                dataSnapshot.getKey(), dataSnapshot.getValue(ListSpec.class)));
                     }
 
                     @Override
-                    public void onItemDelete(String key) {
-                        // Remove listeners for the # completed and # tasks. Then forward the item.
-                        stopWatchTodoListTasks(key);
-                        mListener.onItemDelete(key);
+                    public void onChildRemoved(DataSnapshot dataSnapshot) {
+                        stopWatchTodoListTasks(dataSnapshot.getKey());
+                        mListener.onItemDelete(dataSnapshot.getKey());
                     }
-                }));
+                });
 
         mListener = listener;
         mTodoListTaskListeners = new HashMap<>();
@@ -69,8 +70,8 @@
     }
 
     @Override
-    public void addTodoList(ListMetadata listMetadata) {
-        mTodoLists.push().setValue(listMetadata);
+    public void addTodoList(ListSpec listSpec) {
+        mTodoLists.push().setValue(listSpec);
     }
 
     @Override
@@ -85,44 +86,46 @@
     @Override
     public void completeAllTasks(final ListMetadata listMetadata) {
         // Update all child tasks for this key to have done = true.
-        Firebase tasksRef = getFirebase().child(FirebaseTodoList.TASKS).child(listMetadata.getKey());
+        Firebase tasksRef = getFirebase().child(FirebaseTodoList.TASKS).child(listMetadata.key);
         tasksRef.runTransaction(new Transaction.Handler() {
             @Override
             public Transaction.Result doTransaction(MutableData mutableData) {
                 // Note: This is very easy to make conflicts with. It may be better to avoid doing
                 // this in a batch or to split up the Task into components.
-                for (Task t : mTodoListTrackers.get(listMetadata.getKey()).mTasks.values()) {
-                    Task tCopy = t.copy();
-                    tCopy.setDone(true);
-                    mutableData.child(t.getKey()).setValue(tCopy);
+                for (MutableData taskData : mutableData.getChildren()) {
+                    TaskSpec spec = taskData.getValue(TaskSpec.class);
+                    spec.setDone(true);
+                    taskData.setValue(spec);
                 }
                 return Transaction.success(mutableData);
             }
 
             @Override
-            public void onComplete(FirebaseError firebaseError, boolean b, DataSnapshot dataSnapshot) {
+            public void onComplete(FirebaseError firebaseError, boolean b,
+                                   DataSnapshot dataSnapshot) {
             }
         });
 
         // Further, update this todo list to set its last updated time.
-        mTodoLists.child(listMetadata.getKey()).setValue(new ListMetadata(listMetadata.getName()));
+        ListSpec spec = listMetadata.toSpec();
+        spec.setUpdatedAt(System.currentTimeMillis());
+        mTodoLists.child(listMetadata.key).setValue(spec);
     }
 
-    private void setTaskCompletion(ListMetadata listMetadata) {
-        TodoListTasksListener tracker = mTodoListTrackers.get(listMetadata.getKey());
-        tracker.swapTodoList(listMetadata);
+    private ListMetadata updateListSpec(String key, ListSpec updatedSpec) {
+        TodoListTasksListener tracker = mTodoListTrackers.get(key);
+        tracker.listSpec = updatedSpec;
+        return tracker.getListMetadata();
     }
 
-    private void startWatchTodoListTasks(final ListMetadata listMetadata) {
-        final String todoListKey = listMetadata.getKey();
-
-        Firebase taskRef = getFirebase().child(FirebaseTodoList.TASKS).child(todoListKey);
-        TodoListTasksListener tasksListener = new TodoListTasksListener(listMetadata);
+    private ListMetadata startWatchTodoListTasks(String key, final ListSpec listSpec) {
+        Firebase taskRef = getFirebase().child(FirebaseTodoList.TASKS).child(key);
+        TodoListTasksListener tasksListener = new TodoListTasksListener(key, listSpec);
         ChildEventListener l = taskRef.addChildEventListener(
-                new ChildEventListenerAdapter<>(Task.class, tasksListener)
-        );
-        mTodoListTrackers.put(todoListKey, tasksListener);
-        mTodoListTaskListeners.put(todoListKey, l);
+                new TaskChildEventListener(tasksListener));
+        mTodoListTrackers.put(key, tasksListener);
+        mTodoListTaskListeners.put(key, l);
+        return tasksListener.getListMetadata();
     }
 
     private void stopWatchTodoListTasks(String key) {
@@ -140,30 +143,28 @@
     }
 
     private class TodoListTasksListener implements ListEventListener<Task> {
-        ListMetadata mListMetadata; // The list whose numCompleted and numTasks fields will be updated.
-        final Map<String, Task> mTasks;
-        boolean disabled = false;
+        final String listKey;
+        ListSpec listSpec;
+        final Set<String> completedTaskKeys;
+        int numTasks;
+        boolean disabled;
 
-        TodoListTasksListener(ListMetadata listMetadata) {
-            mListMetadata = listMetadata;
-            mTasks = new HashMap<>();
+        TodoListTasksListener(String listKey, ListSpec listSpec) {
+            this.listKey = listKey;
+            this.listSpec = listSpec;
+
+            completedTaskKeys = new HashSet<>();
         }
 
         // Prevent this listener from propagating any more updates.
         // Note: It looks like Firebase will continue firing listeners if they have more data, so
         // call this if you absolutely don't need any more events to fire.
-        public void disable() {
+        void disable() {
             disabled = true;
         }
 
-        public void swapTodoList(ListMetadata otherList) {
-            if (disabled) {
-                return;
-            }
-            assert mListMetadata.getKey() == otherList.getKey();
-            otherList.numCompleted = mListMetadata.numCompleted;
-            otherList.numTasks = mListMetadata.numTasks;
-            mListMetadata = otherList;
+        ListMetadata getListMetadata() {
+            return new ListMetadata(listKey, listSpec, completedTaskKeys.size(), numTasks);
         }
 
         @Override
@@ -171,13 +172,12 @@
             if (disabled) {
                 return;
             }
-            mListMetadata.numTasks++;
-            if (item.getDone()) {
-                mListMetadata.numCompleted++;
+            numTasks++;
+            if (item.done) {
+                completedTaskKeys.add(item.key);
             }
-            mTasks.put(item.getKey(), item);
 
-            mListener.onItemUpdate(mListMetadata);
+            mListener.onItemUpdate(getListMetadata());
         }
 
         @Override
@@ -185,16 +185,14 @@
             if (disabled) {
                 return;
             }
-            Task oldItem = mTasks.get(item.getKey());
-            mTasks.put(item.getKey(), item);
 
-            if (oldItem.getDone() != item.getDone()) {
-                if (item.getDone()) {
-                    mListMetadata.numCompleted++;
-                } else {
-                    mListMetadata.numCompleted--;
-                }
-                mListener.onItemUpdate(mListMetadata);
+            // Short-circuiting performs the appropriate Set update (add if done, remove if not).
+            boolean changedDone =
+                    item.done && completedTaskKeys.add(item.key) ||
+                    !item.done && completedTaskKeys.remove(item.key);
+
+            if (changedDone) {
+                mListener.onItemUpdate(getListMetadata());
             }
         }
 
@@ -203,13 +201,10 @@
             if (disabled) {
                 return;
             }
-            mListMetadata.numTasks--;
-            Task t = mTasks.remove(key);
-            if (t.getDone()) {
-                mListMetadata.numCompleted--;
-            }
+            numTasks--;
+            completedTaskKeys.remove(key);
 
-            mListener.onItemUpdate(mListMetadata);
+            mListener.onItemUpdate(getListMetadata());
         }
     }
 }
diff --git a/app/src/firebase/java/io/v/todos/persistence/firebase/FirebaseTodoList.java b/app/src/firebase/java/io/v/todos/persistence/firebase/FirebaseTodoList.java
index 402cd62..393bb44 100644
--- a/app/src/firebase/java/io/v/todos/persistence/firebase/FirebaseTodoList.java
+++ b/app/src/firebase/java/io/v/todos/persistence/firebase/FirebaseTodoList.java
@@ -12,8 +12,9 @@
 import com.firebase.client.FirebaseError;
 import com.firebase.client.ValueEventListener;
 
-import io.v.todos.model.ListMetadata;
+import io.v.todos.model.ListSpec;
 import io.v.todos.model.Task;
+import io.v.todos.model.TaskSpec;
 import io.v.todos.persistence.TodoListListener;
 import io.v.todos.persistence.TodoListPersistence;
 
@@ -24,7 +25,7 @@
     private final ValueEventListener mTodoListListener;
     private final ChildEventListener mTasksListener;
 
-    private ListMetadata mList;
+    private ListSpec mListSpec;
 
     public FirebaseTodoList(Context context, String todoListKey, final TodoListListener listener) {
         super(context);
@@ -35,28 +36,26 @@
         mTodoListListener = mTodoList.addValueEventListener(new ValueEventListener() {
             @Override
             public void onDataChange(DataSnapshot dataSnapshot) {
-                ListMetadata listMetadata = dataSnapshot.getValue(ListMetadata.class);
-                if (listMetadata == null) {
+                ListSpec listSpec = dataSnapshot.getValue(ListSpec.class);
+                if (listSpec == null) {
                     listener.onDelete();
                 } else {
-                    mList = listMetadata;
-                    listener.onUpdate(listMetadata);
+                    mListSpec = listSpec;
+                    listener.onUpdate(listSpec);
                 }
             }
 
             @Override
             public void onCancelled(FirebaseError firebaseError) {
-
             }
         });
 
-        mTasksListener = mTasks.addChildEventListener(
-                new ChildEventListenerAdapter<>(Task.class, listener));
+        mTasksListener = mTasks.addChildEventListener(new TaskChildEventListener(listener));
     }
 
     @Override
-    public void updateTodoList(ListMetadata listMetadata) {
-        mTodoList.setValue(listMetadata);
+    public void updateTodoList(ListSpec listSpec) {
+        mTodoList.setValue(listSpec);
     }
 
     @Override
@@ -64,22 +63,27 @@
         mTodoList.removeValue();
     }
 
+    private void updateListTimestamp() {
+        mListSpec.setUpdatedAt(System.currentTimeMillis());
+        mTodoList.setValue(mListSpec);
+    }
+
     @Override
-    public void addTask(Task task) {
+    public void addTask(TaskSpec task) {
         mTasks.push().setValue(task);
-        mTodoList.setValue(new ListMetadata(mList.getName()));
+        updateListTimestamp();
     }
 
     @Override
     public void updateTask(Task task) {
-        mTasks.child(task.getKey()).setValue(task);
-        mTodoList.setValue(new ListMetadata(mList.getName()));
+        mTasks.child(task.key).setValue(task.toSpec());
+        updateListTimestamp();
     }
 
     @Override
     public void deleteTask(String key) {
         mTasks.child(key).removeValue();
-        mTodoList.setValue(new ListMetadata(mList.getName()));
+        updateListTimestamp();
     }
 
     @Override
diff --git a/app/src/firebase/java/io/v/todos/persistence/firebase/TaskChildEventListener.java b/app/src/firebase/java/io/v/todos/persistence/firebase/TaskChildEventListener.java
new file mode 100644
index 0000000..0c0023b
--- /dev/null
+++ b/app/src/firebase/java/io/v/todos/persistence/firebase/TaskChildEventListener.java
@@ -0,0 +1,38 @@
+// Copyright 2016 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package io.v.todos.persistence.firebase;
+
+import com.firebase.client.DataSnapshot;
+
+import io.v.todos.model.Task;
+import io.v.todos.model.TaskSpec;
+import io.v.todos.persistence.ListEventListener;
+
+public class TaskChildEventListener extends ChildEventListenerAdapter {
+    private final ListEventListener<Task> mDelegate;
+
+    public TaskChildEventListener(ListEventListener<Task> delegate) {
+        mDelegate = delegate;
+    }
+
+    protected Task extractValue(DataSnapshot dataSnapshot) {
+        return new Task(dataSnapshot.getKey(), dataSnapshot.getValue(TaskSpec.class));
+    }
+
+    @Override
+    public void onChildAdded(DataSnapshot dataSnapshot, String prevKey) {
+        mDelegate.onItemAdd(extractValue(dataSnapshot));
+    }
+
+    @Override
+    public void onChildChanged(DataSnapshot dataSnapshot, String prevKey) {
+        mDelegate.onItemUpdate(extractValue(dataSnapshot));
+    }
+
+    @Override
+    public void onChildRemoved(DataSnapshot dataSnapshot) {
+        mDelegate.onItemDelete(dataSnapshot.getKey());
+    }
+}
diff --git a/app/src/main/java/io/v/todos/MainActivity.java b/app/src/main/java/io/v/todos/MainActivity.java
index 93e84b3..0b8e1c0 100644
--- a/app/src/main/java/io/v/todos/MainActivity.java
+++ b/app/src/main/java/io/v/todos/MainActivity.java
@@ -20,6 +20,7 @@
 
 import io.v.todos.model.DataList;
 import io.v.todos.model.ListMetadata;
+import io.v.todos.model.ListSpec;
 import io.v.todos.persistence.ListEventListener;
 import io.v.todos.persistence.MainPersistence;
 import io.v.todos.persistence.PersistenceFactory;
@@ -112,7 +113,7 @@
 
             @Override
             public void onItemUpdate(ListMetadata item) {
-                int start = snackoosList.findIndexByKey(item.getKey());
+                int start = snackoosList.findIndexByKey(item.key);
                 int end = snackoosList.updateInOrder(item);
 
                 adapter.notifyItemMoved(start, end);
@@ -143,7 +144,7 @@
                 .setView(todoItem)
                 .setPositiveButton("Add", new DialogInterface.OnClickListener() {
                     public void onClick(DialogInterface dialog, int whichButton) {
-                        mPersistence.addTodoList(new ListMetadata(todoItem.getText().toString()));
+                        mPersistence.addTodoList(new ListSpec(todoItem.getText().toString()));
                     }
                 })
                 .setNegativeButton("Cancel", new DialogInterface.OnClickListener() {
diff --git a/app/src/main/java/io/v/todos/TaskRecyclerAdapter.java b/app/src/main/java/io/v/todos/TaskRecyclerAdapter.java
index ffd1fa6..73533dc 100644
--- a/app/src/main/java/io/v/todos/TaskRecyclerAdapter.java
+++ b/app/src/main/java/io/v/todos/TaskRecyclerAdapter.java
@@ -49,7 +49,7 @@
 
     private int nonDoneSize() {
         for (int i = 0; i < backup.size(); i++) {
-            if (backup.get(i).getDone()) {
+            if (backup.get(i).done) {
                 return i;
             }
         }
diff --git a/app/src/main/java/io/v/todos/TaskViewHolder.java b/app/src/main/java/io/v/todos/TaskViewHolder.java
index 7354ac6..2aed003 100644
--- a/app/src/main/java/io/v/todos/TaskViewHolder.java
+++ b/app/src/main/java/io/v/todos/TaskViewHolder.java
@@ -22,24 +22,24 @@
 
     public void bindTask(Task task, View.OnClickListener listener) {
         final ImageView doneMark = (ImageView) itemView.findViewById(R.id.task_done);
-        doneMark.setVisibility(task.getDone() ? View.VISIBLE : View.GONE);
+        doneMark.setVisibility(task.done ? View.VISIBLE : View.GONE);
 
         final TextView name=(TextView) itemView.findViewById(R.id.task_text);
-        name.setText(task.getText());
+        name.setText(task.text);
 
         final TextView created=(TextView) itemView.findViewById(R.id.task_time);
         created.setText(computeCreated(task));
 
-        getCardView().setCardBackgroundColor(task.getDone() ? 0xFFCCCCCC : 0xFFFFFFFF);
+        getCardView().setCardBackgroundColor(task.done ? 0xFFCCCCCC : 0xFFFFFFFF);
 
-        itemView.setTag(task.getKey());
+        itemView.setTag(task.key);
         itemView.setOnClickListener(listener);
 
-        itemView.setVisibility(!showDone && task.getDone() ? View.GONE : View.VISIBLE);
+        itemView.setVisibility(!showDone && task.done ? View.GONE : View.VISIBLE);
     }
 
     private String computeCreated(Task task) {
-        return UIUtil.computeTimeAgo("Created", task.getAddedAt());
+        return UIUtil.computeTimeAgo("Created", task.addedAt);
     }
 
     public void setShowDone(boolean showDone) {
diff --git a/app/src/main/java/io/v/todos/TodoListActivity.java b/app/src/main/java/io/v/todos/TodoListActivity.java
index 4d6a5a2..8bd6d9a 100644
--- a/app/src/main/java/io/v/todos/TodoListActivity.java
+++ b/app/src/main/java/io/v/todos/TodoListActivity.java
@@ -19,8 +19,9 @@
 import android.widget.Toolbar;
 
 import io.v.todos.model.DataList;
-import io.v.todos.model.ListMetadata;
+import io.v.todos.model.ListSpec;
 import io.v.todos.model.Task;
+import io.v.todos.model.TaskSpec;
 import io.v.todos.persistence.PersistenceFactory;
 import io.v.todos.persistence.TodoListListener;
 import io.v.todos.persistence.TodoListPersistence;
@@ -40,7 +41,7 @@
 public class TodoListActivity extends Activity {
     private TodoListPersistence mPersistence;
 
-    private ListMetadata snackoo;
+    private ListSpec snackoo;
     private DataList<Task> snackoosList = new DataList<Task>();
     private boolean showDone = false; // TODO(alexfandrianto): Load from shared preferences...
 
@@ -91,7 +92,7 @@
         mPersistence = PersistenceFactory.getTodoListPersistence(this, snackooKey,
                 new TodoListListener() {
             @Override
-            public void onUpdate(ListMetadata value) {
+            public void onUpdate(ListSpec value) {
                 snackoo = value;
                 getActionBar().setTitle(snackoo.getName());
             }
@@ -110,7 +111,7 @@
 
             @Override
             public void onItemUpdate(Task item) {
-                int start = snackoosList.findIndexByKey(item.getKey());
+                int start = snackoosList.findIndexByKey(item.key);
                 int end = snackoosList.updateInOrder(item);
                 adapter.notifyItemMoved(start, end);
                 adapter.notifyItemChanged(end);
@@ -133,18 +134,14 @@
     }
 
     public void addTodoItem(String todo) {
-        mPersistence.addTask(new Task(todo));
+        mPersistence.addTask(new TaskSpec(todo));
     }
 
     public void updateTodoItem(String fbKey, String todo) {
-        Task task = snackoosList.findByKey(fbKey).copy();
-        task.setText(todo);
-        mPersistence.updateTask(task);
+        mPersistence.updateTask(snackoosList.findByKey(fbKey).withText(todo));
     }
     public void markAsDone(String fbKey) {
-        Task task = snackoosList.findByKey(fbKey).copy();
-        task.setDone(!task.getDone());
-        mPersistence.updateTask(task);
+        mPersistence.updateTask(snackoosList.findByKey(fbKey).withToggleDone());
     }
 
     public void deleteTodoItem(String fbKey) {
@@ -172,7 +169,7 @@
 
     private void initiateTaskEdit(final String fbKey) {
         final EditText todoItem = new EditText(this);
-        todoItem.setText(snackoosList.findByKey(fbKey).getText());
+        todoItem.setText(snackoosList.findByKey(fbKey).text);
 
         AlertDialog dialog = new AlertDialog.Builder(this)
                 .setTitle("Editing Task")
@@ -221,8 +218,8 @@
     }
 
 
-    public void updateTodoList(String todo) {
-        mPersistence.updateTodoList(new ListMetadata(todo));
+    public void updateTodoList(String name) {
+        mPersistence.updateTodoList(new ListSpec(name));
     }
 
     public void deleteTodoList() {
diff --git a/app/src/main/java/io/v/todos/TodoListViewHolder.java b/app/src/main/java/io/v/todos/TodoListViewHolder.java
index b170523..5ddfd8c 100644
--- a/app/src/main/java/io/v/todos/TodoListViewHolder.java
+++ b/app/src/main/java/io/v/todos/TodoListViewHolder.java
@@ -19,7 +19,7 @@
 
     public void bindTodoList(ListMetadata listMetadata, View.OnClickListener listener) {
         final TextView name=(TextView) itemView.findViewById(R.id.todo_list_name);
-        name.setText(listMetadata.getName());
+        name.setText(listMetadata.name);
 
         final TextView completedStatus=(TextView) itemView.findViewById(R.id.todo_list_completed);
         completedStatus.setText(computeCompleted(listMetadata));
@@ -27,18 +27,18 @@
         final TextView timeAgo=(TextView) itemView.findViewById(R.id.todo_list_time);
         timeAgo.setText(computeTimeAgo(listMetadata));
 
-        getCardView().setCardBackgroundColor(listMetadata.getDone() ? 0xFFCCCCCC : 0xFFFFFFFF);
+        getCardView().setCardBackgroundColor(listMetadata.isDone() ? 0xFFCCCCCC : 0xFFFFFFFF);
 
-        itemView.setTag(listMetadata.getKey());
+        itemView.setTag(listMetadata.key);
         itemView.setOnClickListener(listener);
     }
 
     private String computeTimeAgo(ListMetadata listMetadata) {
-        return UIUtil.computeTimeAgo("Last Updated", listMetadata.getUpdatedAt());
+        return UIUtil.computeTimeAgo("Last Updated", listMetadata.updatedAt);
     }
 
     private String computeCompleted(ListMetadata listMetadata) {
-        if (listMetadata.getDone()) {
+        if (listMetadata.isDone()) {
             return "Done!";
         } else if (listMetadata.numTasks == 0) {
             return "Needs Tasks";
diff --git a/app/src/main/java/io/v/todos/model/DataList.java b/app/src/main/java/io/v/todos/model/DataList.java
index 47fbda3..90d4a30 100644
--- a/app/src/main/java/io/v/todos/model/DataList.java
+++ b/app/src/main/java/io/v/todos/model/DataList.java
@@ -28,7 +28,7 @@
     // We have to replace the old item while keeping sort order.
     // It is easiest to remove and then insertInOrder.
     public int updateInOrder(T item) {
-        removeByKey(item.getKey());
+        removeByKey(item.key);
         return insertInOrder(item);
     }
 
@@ -43,7 +43,7 @@
     public int findIndexByKey(String key) {
         for (int i = 0; i < size(); i++) {
             T oldItem = get(i);
-            if (oldItem.getKey().equals(key)) {
+            if (oldItem.key.equals(key)) {
                 return i;
             }
         }
diff --git a/app/src/main/java/io/v/todos/model/KeyedData.java b/app/src/main/java/io/v/todos/model/KeyedData.java
index e0b2252..2fede3c 100644
--- a/app/src/main/java/io/v/todos/model/KeyedData.java
+++ b/app/src/main/java/io/v/todos/model/KeyedData.java
@@ -5,12 +5,15 @@
 package io.v.todos.model;
 
 /**
- * KeyedData represents data that has a key and is comparable.
- * Most subclasses will use this key as part of their comparison function.
+ * Represents data that has a key and is comparable. Most subclasses will use this key as part of
+ * their comparison function.
  *
  * @author alexfandrianto
  */
-public interface KeyedData<T> extends Comparable<T> {
-    String getKey();
-    void setKey(String key);
+public abstract class KeyedData<T> implements Comparable<T> {
+    public final String key;
+
+    protected KeyedData(String key) {
+        this.key = key;
+    }
 }
diff --git a/app/src/main/java/io/v/todos/model/ListMetadata.java b/app/src/main/java/io/v/todos/model/ListMetadata.java
index d455f62..46b89ec 100644
--- a/app/src/main/java/io/v/todos/model/ListMetadata.java
+++ b/app/src/main/java/io/v/todos/model/ListMetadata.java
@@ -4,60 +4,69 @@
 
 package io.v.todos.model;
 
-import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import android.support.annotation.NonNull;
 
 /**
- * ListMetadata is a Firebase-compatible class that tracks information regarding a particular todo list.
- *
- * @author alexfandrianto
+ * Tracks information regarding a particular todo list.
  */
-@JsonIgnoreProperties({ "numCompleted", "numTasks", "done", "key" })
-public class ListMetadata implements KeyedData<ListMetadata> {
-    private String name;
-    private long updatedAt;
+public class ListMetadata extends KeyedData<ListMetadata> {
+    public final String name;
+    public final long updatedAt;
 
-    // Not serialized.
-    public int numCompleted = 0;
-    public int numTasks = 0;
-    private String key = null; // Usually assigned for comparison/viewing.
-    //public List<String> sharedWith ??
+    public final int numCompleted;
+    public final int numTasks;
 
-    // The default constructor is used by Firebase.
-    public ListMetadata() {}
-
-    // Use this constructor when creating a new Task for the first time.
-    public ListMetadata(String name) {
+    public ListMetadata(String key, String name, long updatedAt, int numCompleted, int numTasks) {
+        super(key);
         this.name = name;
-        this.updatedAt = System.currentTimeMillis();
+        this.updatedAt = updatedAt;
+        this.numCompleted = numCompleted;
+        this.numTasks = numTasks;
     }
 
-    public String getName() {
-        return name;
-    }
-    public long getUpdatedAt() {
-        return updatedAt;
+    public ListMetadata(String key, ListSpec spec, int numCompleted, int numTasks) {
+        this(key, spec.getName(), spec.getUpdatedAt(), numCompleted, numTasks);
     }
 
-    public boolean getDone() {
+    public boolean isDone() {
         return numTasks > 0 && numCompleted == numTasks;
     }
-    public boolean canCompleteAll() { return numCompleted < numTasks; }
-    public void setKey(String key) {
-        this.key = key;
-    }
-    public String getKey() {
-        return key;
+
+    public boolean canCompleteAll() {
+        return numCompleted < numTasks;
     }
 
     @Override
-    public int compareTo(ListMetadata other) {
-        if (key == null && other.key != null) {
-            return 1;
-        } else if (key != null && other.key == null) {
-            return -1;
-        } else if (key == null && other.key == null) {
+    public boolean equals(Object o) {
+        return this == o ||
+                o instanceof ListMetadata &&
+                ((ListMetadata) o).canEqual(this) &&
+                compareTo((ListMetadata)o) == 0;
+    }
+
+    protected boolean canEqual(Object other) {
+        return other instanceof ListMetadata;
+    }
+
+    @Override
+    public int hashCode() {
+        return key.hashCode();
+    }
+
+    @Override
+    public int compareTo(@NonNull ListMetadata other) {
+        if (this == other) {
             return 0;
+        } else if (!other.canEqual(this)) {
+            throw new ClassCastException("Cannot compare " + getClass() + " to " +
+                    other.getClass());
+        } else {
+            return key.compareTo(other.key);
         }
-        return key.compareTo(other.key);
+        // TODO(rosswang): factor out ordering.
+    }
+
+    public ListSpec toSpec() {
+        return new ListSpec(name, updatedAt);
     }
 }
diff --git a/app/src/main/java/io/v/todos/model/ListSpec.java b/app/src/main/java/io/v/todos/model/ListSpec.java
new file mode 100644
index 0000000..06302d8
--- /dev/null
+++ b/app/src/main/java/io/v/todos/model/ListSpec.java
@@ -0,0 +1,43 @@
+// Copyright 2016 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package io.v.todos.model;
+
+/**
+ * POJO of persisted information regarding a particular todo list.
+ */
+public class ListSpec {
+    private String mName;
+    private long mUpdatedAt;
+
+    public ListSpec() {}
+
+    public ListSpec(String name, long updatedAt) {
+        mName = name;
+        mUpdatedAt = updatedAt;
+    }
+
+    /**
+     * Convenience constructor that initializes {@code updatedAt} to now.
+     */
+    public ListSpec(String name) {
+        this(name, System.currentTimeMillis());
+    }
+
+    public String getName() {
+        return mName;
+    }
+
+    public void setName(String value) {
+        mName = value;
+    }
+
+    public long getUpdatedAt() {
+        return mUpdatedAt;
+    }
+
+    public void setUpdatedAt(long value) {
+        mUpdatedAt = value;
+    }
+}
diff --git a/app/src/main/java/io/v/todos/model/Task.java b/app/src/main/java/io/v/todos/model/Task.java
index d8af531..e415048 100644
--- a/app/src/main/java/io/v/todos/model/Task.java
+++ b/app/src/main/java/io/v/todos/model/Task.java
@@ -4,78 +4,81 @@
 
 package io.v.todos.model;
 
-import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import android.support.annotation.NonNull;
+
+import java.util.Objects;
 
 /**
  * Task is a Firebase-compatible class that tracks information regarding a particular task.
  *
  * @author alexfandrianto
  */
-@JsonIgnoreProperties({ "key" })
-public class Task implements KeyedData<Task> {
-    private String text;
-    private long addedAt;
-    private boolean done;
-
-    // Unserialized properties.
-    private String key; // Usually assigned for comparison/viewing.
-
-    // The default constructor is used by Firebase.
-    public Task() {}
+public class Task extends KeyedData<Task> {
+    public final String text;
+    public final long addedAt;
+    public final boolean done;
 
     // Use this constructor when creating a new Task for the first time.
-    public Task(String text) {
+    public Task(String key, String text, long addedAt, boolean done) {
+        super(key);
         this.text = text;
-        this.addedAt = System.currentTimeMillis();
-        this.done = false;
+        this.addedAt = addedAt;
+        this.done = done;
     }
 
-    public Task copy() {
-        Task t = new Task();
-        t.text = text;
-        t.addedAt = addedAt;
-        t.done = done;
-        t.key = key;
-        return t;
+    public Task(String key, TaskSpec spec) {
+        this(key, spec.getText(), spec.getAddedAt(), spec.getDone());
     }
 
-    public String getText() {
-        return text;
-    }
-    public long getAddedAt() {
-        return addedAt;
-    }
-    public boolean getDone() {
-        return done;
-    }
-    public void setKey(String key) {
-        this.key = key;
-    }
-    public String getKey() {
-        return key;
+    public Task withText(String value) {
+        return Objects.equals(text, value) ? this : new Task(key, value, addedAt, done);
     }
 
-    public void setText(String newText) {
-        text = newText;
-    }
-    public void setDone(boolean newDone) {
-        done = newDone;
+    public Task withToggleDone() {
+        return new Task(key, text, addedAt, !done);
     }
 
     @Override
-    public int compareTo(Task other) {
-        if (done && !other.done) {
+    public boolean equals(Object o) {
+        return this == o ||
+                o instanceof Task &&
+                ((Task) o).canEqual(this) &&
+                compareTo((Task)o) == 0;
+    }
+
+    protected boolean canEqual(Object other) {
+        return other instanceof Task;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(addedAt, done);
+    }
+
+    @Override
+    public int compareTo(@NonNull Task other) {
+        // TODO(rosswang): factor out ordering.
+        if (other == this) {
+            return 0;
+        } else if (!other.canEqual(this)) {
+            throw new ClassCastException("Cannot compare " + getClass() + " to " +
+                    other.getClass());
+        } else if (done && !other.done) {
             return 1;
         } else if (!done && other.done) {
             return -1;
-        }
-        if (key == null && other.key != null) {
+        } else if (key == null && other.key != null) {
             return 1;
         } else if (key != null && other.key == null) {
             return -1;
-        } else if (key == null && other.key == null) {
+        } else if (key == null) {
             return 0;
+        } else {
+            return key.compareTo(other.key);
         }
-        return key.compareTo(other.key);
+    }
+
+    public TaskSpec toSpec() {
+        return new TaskSpec(text, addedAt, done);
     }
 }
diff --git a/app/src/main/java/io/v/todos/model/TaskSpec.java b/app/src/main/java/io/v/todos/model/TaskSpec.java
new file mode 100644
index 0000000..f3f243d
--- /dev/null
+++ b/app/src/main/java/io/v/todos/model/TaskSpec.java
@@ -0,0 +1,55 @@
+// Copyright 2016 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package io.v.todos.model;
+
+/**
+ * POJO of persisted information regarding a particular task.
+ *
+ * @author alexfandrianto
+ */
+public class TaskSpec {
+    private String mText;
+    private long mAddedAt;
+    private boolean mDone;
+
+    public TaskSpec() {}
+
+    public TaskSpec(String text, long addedAt, boolean done) {
+        mText = text;
+        mAddedAt = addedAt;
+        mDone = done;
+    }
+
+    /**
+     * Convenience constructor that creates an undone task with an {@code addedAt} timestamp of now.
+     */
+    public TaskSpec(String text) {
+        this(text, System.currentTimeMillis(), false);
+    }
+
+    public String getText() {
+        return mText;
+    }
+
+    public void setText(String value) {
+        mText = value;
+    }
+
+    public long getAddedAt() {
+        return mAddedAt;
+    }
+
+    public void setAddedAt(long value) {
+        mAddedAt = value;
+    }
+
+    public boolean getDone() {
+        return mDone;
+    }
+
+    public void setDone(boolean value) {
+        mDone = value;
+    }
+}
diff --git a/app/src/main/java/io/v/todos/persistence/MainPersistence.java b/app/src/main/java/io/v/todos/persistence/MainPersistence.java
index 7516f3d..0d3a5a3 100644
--- a/app/src/main/java/io/v/todos/persistence/MainPersistence.java
+++ b/app/src/main/java/io/v/todos/persistence/MainPersistence.java
@@ -5,9 +5,10 @@
 package io.v.todos.persistence;
 
 import io.v.todos.model.ListMetadata;
+import io.v.todos.model.ListSpec;
 
 public interface MainPersistence extends Persistence {
-    void addTodoList(ListMetadata listMetadata);
+    void addTodoList(ListSpec listSpec);
     void deleteTodoList(String key);
     void completeAllTasks(ListMetadata listMetadata);
 }
diff --git a/app/src/main/java/io/v/todos/persistence/TodoListListener.java b/app/src/main/java/io/v/todos/persistence/TodoListListener.java
index de06278..7ae1ebe 100644
--- a/app/src/main/java/io/v/todos/persistence/TodoListListener.java
+++ b/app/src/main/java/io/v/todos/persistence/TodoListListener.java
@@ -4,10 +4,10 @@
 
 package io.v.todos.persistence;
 
+import io.v.todos.model.ListSpec;
 import io.v.todos.model.Task;
-import io.v.todos.model.ListMetadata;
 
 public interface TodoListListener extends ListEventListener<Task> {
-    void onUpdate(ListMetadata value);
+    void onUpdate(ListSpec value);
     void onDelete();
 }
diff --git a/app/src/main/java/io/v/todos/persistence/TodoListPersistence.java b/app/src/main/java/io/v/todos/persistence/TodoListPersistence.java
index 2dcb24c..5dfb1ff 100644
--- a/app/src/main/java/io/v/todos/persistence/TodoListPersistence.java
+++ b/app/src/main/java/io/v/todos/persistence/TodoListPersistence.java
@@ -4,13 +4,14 @@
 
 package io.v.todos.persistence;
 
-import io.v.todos.model.ListMetadata;
+import io.v.todos.model.ListSpec;
 import io.v.todos.model.Task;
+import io.v.todos.model.TaskSpec;
 
 public interface TodoListPersistence extends Persistence {
-    void updateTodoList(ListMetadata listMetadata);
+    void updateTodoList(ListSpec listSpec);
     void deleteTodoList();
-    void addTask(Task task);
+    void addTask(TaskSpec task);
     void updateTask(Task task);
     void deleteTask(String key);
 }