TBR: TODOs: TodoLists track numCompleted. Can mark all as done.
These changes introduce MainListener, which allows the MainActivity
to update the UI when the tasks of a TodoList have changed.
FirebaseMain also supports tracking the child tasks of the TodoLists.
This also applies when a TodoList is updated.
TodoLists can also mark all as done, though it's an open question
what should happen when all of them are already marked as done.
Bugfix: 50-50's the UI for the message and timestamp correctly.
Bugfix: notifyDataSetChanged a little more often.
Change-Id: I587cb72ff2ea946177e6643310a5402c196fa557
diff --git a/projects/todos/app/src/firebase/java/io/v/todos/persistence/firebase/ChildEventListenerAdapter.java b/projects/todos/app/src/firebase/java/io/v/todos/persistence/firebase/ChildEventListenerAdapter.java
index 6c3508f..0d85b88 100644
--- a/projects/todos/app/src/firebase/java/io/v/todos/persistence/firebase/ChildEventListenerAdapter.java
+++ b/projects/todos/app/src/firebase/java/io/v/todos/persistence/firebase/ChildEventListenerAdapter.java
@@ -20,18 +20,20 @@
mDelegate = delegate;
}
- @Override
- public void onChildAdded(DataSnapshot dataSnapshot, String prevKey) {
+ private T prepareKeyedData(DataSnapshot dataSnapshot) {
T value = dataSnapshot.getValue(mType);
value.setKey(dataSnapshot.getKey());
- mDelegate.onItemAdd(value);
+ return value;
+ }
+
+ @Override
+ public void onChildAdded(DataSnapshot dataSnapshot, String prevKey) {
+ mDelegate.onItemAdd(prepareKeyedData(dataSnapshot));
}
@Override
public void onChildChanged(DataSnapshot dataSnapshot, String prevKey) {
- T value = dataSnapshot.getValue(mType);
- value.setKey(dataSnapshot.getKey());
- mDelegate.onItemUpdate(value);
+ mDelegate.onItemUpdate(prepareKeyedData(dataSnapshot));
}
@Override
diff --git a/projects/todos/app/src/firebase/java/io/v/todos/persistence/firebase/FirebaseMain.java b/projects/todos/app/src/firebase/java/io/v/todos/persistence/firebase/FirebaseMain.java
index 67f774e..2605728 100644
--- a/projects/todos/app/src/firebase/java/io/v/todos/persistence/firebase/FirebaseMain.java
+++ b/projects/todos/app/src/firebase/java/io/v/todos/persistence/firebase/FirebaseMain.java
@@ -7,8 +7,16 @@
import android.content.Context;
import com.firebase.client.ChildEventListener;
+import com.firebase.client.DataSnapshot;
import com.firebase.client.Firebase;
+import com.firebase.client.FirebaseError;
+import com.firebase.client.MutableData;
+import com.firebase.client.Transaction;
+import java.util.HashMap;
+import java.util.Map;
+
+import io.v.todos.Task;
import io.v.todos.TodoList;
import io.v.todos.persistence.ListEventListener;
import io.v.todos.persistence.MainPersistence;
@@ -19,13 +27,45 @@
private final Firebase mTodoLists;
private final ChildEventListener mTodoListsListener;
+ private final ListEventListener<TodoList> mListener;
+
+ private final Map<String, ChildEventListener> mTodoListTaskListeners;
+ private final Map<String, TodoListTasksListener> mTodoListTrackers;
+
public FirebaseMain(Context context, final ListEventListener<TodoList> listener) {
super(context);
mTodoLists = getFirebase().child(TODO_LISTS);
+ // This handler will forward events to the passed in listener after ensuring that all the
+ // data in the TodoList is set and can automatically update.
mTodoListsListener = mTodoLists.addChildEventListener(
- new ChildEventListenerAdapter<>(TodoList.class, listener));
+ new ChildEventListenerAdapter<>(TodoList.class, new ListEventListener<TodoList>() {
+ @Override
+ public void onItemAdd(TodoList item) {
+ // Hook up listeners for the # completed and # tasks. Then forward the item.
+ startWatchTodoListTasks(item);
+ mListener.onItemAdd(item);
+ }
+
+ @Override
+ public void onItemUpdate(TodoList item) {
+ // Retrieve # completed and # tasks. Then forward the item.
+ setTaskCompletion(item);
+ mListener.onItemUpdate(item);
+ }
+
+ @Override
+ public void onItemDelete(String key) {
+ // Remove listeners for the # completed and # tasks. Then forward the item.
+ stopWatchTodoListTasks(key);
+ mListener.onItemDelete(key);
+ }
+ }));
+
+ mListener = listener;
+ mTodoListTaskListeners = new HashMap<>();
+ mTodoListTrackers = new HashMap<>();
}
@Override
@@ -36,10 +76,140 @@
@Override
public void deleteTodoList(String key) {
mTodoLists.child(key).removeValue();
+
+ // After deleting the list itself, delete all the orphaned tasks!
+ Firebase tasksRef = getFirebase().child(FirebaseTodoList.TASKS).child(key);
+ tasksRef.removeValue();
+ }
+
+ @Override
+ public void completeAllTasks(final TodoList todoList) {
+ // Update all child tasks for this key to have done = true.
+ Firebase tasksRef = getFirebase().child(FirebaseTodoList.TASKS).child(todoList.getKey());
+ 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(todoList.getKey()).mTasks.values()) {
+ Task tCopy = t.copy();
+ tCopy.setDone(true);
+ mutableData.child(t.getKey()).setValue(tCopy);
+ }
+ return Transaction.success(mutableData);
+ }
+
+ @Override
+ public void onComplete(FirebaseError firebaseError, boolean b, DataSnapshot dataSnapshot) {
+ }
+ });
+
+ // Further, update this todo list to set its last updated time.
+ mTodoLists.child(todoList.getKey()).setValue(new TodoList(todoList.getName()));
+ }
+
+ private void setTaskCompletion(TodoList todoList) {
+ TodoListTasksListener tracker = mTodoListTrackers.get(todoList.getKey());
+ tracker.swapTodoList(todoList);
+ }
+
+ private void startWatchTodoListTasks(final TodoList todoList) {
+ final String todoListKey = todoList.getKey();
+
+ Firebase taskRef = getFirebase().child(FirebaseTodoList.TASKS).child(todoListKey);
+ TodoListTasksListener tasksListener = new TodoListTasksListener(todoList);
+ ChildEventListener l = taskRef.addChildEventListener(
+ new ChildEventListenerAdapter<>(Task.class, tasksListener)
+ );
+ mTodoListTrackers.put(todoListKey, tasksListener);
+ mTodoListTaskListeners.put(todoListKey, l);
+ }
+
+ private void stopWatchTodoListTasks(String key) {
+ mTodoListTrackers.remove(key).disable(); // Disable; we don't want this listener anymore.
+ ChildEventListener l = mTodoListTaskListeners.remove(key);
+ getFirebase().removeEventListener(l);
}
@Override
public void close() {
getFirebase().removeEventListener(mTodoListsListener);
+ for (String key : mTodoListTaskListeners.keySet()) {
+ getFirebase().removeEventListener(mTodoListTaskListeners.get(key));
+ }
+ }
+
+ private class TodoListTasksListener implements ListEventListener<Task> {
+ TodoList mTodoList; // The list whose numCompleted and numTasks fields will be updated.
+ final Map<String, Task> mTasks;
+ boolean disabled = false;
+
+ TodoListTasksListener(TodoList todoList) {
+ mTodoList = todoList;
+ mTasks = new HashMap<>();
+ }
+
+ // 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() {
+ disabled = true;
+ }
+
+ public void swapTodoList(TodoList otherList) {
+ if (disabled) {
+ return;
+ }
+ assert mTodoList.getKey() == otherList.getKey();
+ otherList.numCompleted = mTodoList.numCompleted;
+ otherList.numTasks = mTodoList.numTasks;
+ mTodoList = otherList;
+ }
+
+ @Override
+ public void onItemAdd(Task item) {
+ if (disabled) {
+ return;
+ }
+ mTodoList.numTasks++;
+ if (item.getDone()) {
+ mTodoList.numCompleted++;
+ }
+ mTasks.put(item.getKey(), item);
+
+ mListener.onItemUpdate(mTodoList);
+ }
+
+ @Override
+ public void onItemUpdate(Task item) {
+ if (disabled) {
+ return;
+ }
+ Task oldItem = mTasks.get(item.getKey());
+ mTasks.put(item.getKey(), item);
+
+ if (oldItem.getDone() != item.getDone()) {
+ if (item.getDone()) {
+ mTodoList.numCompleted++;
+ } else {
+ mTodoList.numCompleted--;
+ }
+ mListener.onItemUpdate(mTodoList);
+ }
+ }
+
+ @Override
+ public void onItemDelete(String key) {
+ if (disabled) {
+ return;
+ }
+ mTodoList.numTasks--;
+ Task t = mTasks.remove(key);
+ if (t.getDone()) {
+ mTodoList.numCompleted--;
+ }
+
+ mListener.onItemUpdate(mTodoList);
+ }
}
}
diff --git a/projects/todos/app/src/main/java/io/v/todos/MainActivity.java b/projects/todos/app/src/main/java/io/v/todos/MainActivity.java
index 1f7467a..708ffff 100644
--- a/projects/todos/app/src/main/java/io/v/todos/MainActivity.java
+++ b/projects/todos/app/src/main/java/io/v/todos/MainActivity.java
@@ -74,16 +74,22 @@
RecyclerView recyclerView = (RecyclerView)findViewById(R.id.recycler);
recyclerView.setAdapter(adapter);
- // TODO(alexfandrianto): Very much copy-pasted between MainActivity and TodoListActivity.
new ItemTouchHelper(new SwipeableTouchHelperCallback() {
@Override
public void onSwiped(final RecyclerView.ViewHolder viewHolder, final int direction) {
+ String todoListKey = (String)viewHolder.itemView.getTag();
if (direction == ItemTouchHelper.RIGHT) {
- // TODO(alexfandrianto): This doesn't do anything yet. Should mark all child
- // Tasks as done.
- adapter.notifyDataSetChanged();
+ TodoList l = snackoosList.findByKey(todoListKey);
+ if (l != null && l.canCompleteAll()) {
+ mPersistence.completeAllTasks(l);
+ } else {
+ // TODO(alexfandrianto): Can we remove this? The bug when we don't have it
+ // here is that the swiped card doesn't return even though no data may have
+ // been affected.
+ adapter.notifyDataSetChanged();
+ }
} else if (direction == ItemTouchHelper.LEFT) {
- mPersistence.deleteTodoList((String)viewHolder.itemView.getTag());
+ mPersistence.deleteTodoList(todoListKey);
}
}
}).attachToRecyclerView(recyclerView);
@@ -92,27 +98,23 @@
@Override
public void onItemAdd(TodoList item) {
snackoosList.insertInOrder(item);
+
adapter.notifyDataSetChanged();
-
- // TODO(alexfandrianto): In order to capture the computed values for this TodoList,
- // we have to watch the Task's data.
-
setEmptyVisiblity();
}
@Override
public void onItemUpdate(TodoList item) {
snackoosList.updateInOrder(item);
+
adapter.notifyDataSetChanged();
}
@Override
public void onItemDelete(String key) {
snackoosList.removeByKey(key);
+
adapter.notifyDataSetChanged();
-
- // TODO(alexfandrianto): Stop watching the Task data for this TodoList.
-
setEmptyVisiblity();
}
});
diff --git a/projects/todos/app/src/main/java/io/v/todos/TodoList.java b/projects/todos/app/src/main/java/io/v/todos/TodoList.java
index 31f27a1..cb3179c 100644
--- a/projects/todos/app/src/main/java/io/v/todos/TodoList.java
+++ b/projects/todos/app/src/main/java/io/v/todos/TodoList.java
@@ -41,6 +41,7 @@
public boolean getDone() {
return numTasks > 0 && numCompleted == numTasks;
}
+ public boolean canCompleteAll() { return numCompleted < numTasks; }
public void setKey(String key) {
this.key = key;
}
diff --git a/projects/todos/app/src/main/java/io/v/todos/TodoListActivity.java b/projects/todos/app/src/main/java/io/v/todos/TodoListActivity.java
index 0cc91f7..0789c43 100644
--- a/projects/todos/app/src/main/java/io/v/todos/TodoListActivity.java
+++ b/projects/todos/app/src/main/java/io/v/todos/TodoListActivity.java
@@ -109,6 +109,7 @@
public void onItemUpdate(Task item) {
snackoosList.updateInOrder(item);
adapter.notifyDataSetChanged();
+ setEmptyVisiblity();
}
@Override
diff --git a/projects/todos/app/src/main/java/io/v/todos/persistence/MainPersistence.java b/projects/todos/app/src/main/java/io/v/todos/persistence/MainPersistence.java
index 1fbbef5..d59e4ac 100644
--- a/projects/todos/app/src/main/java/io/v/todos/persistence/MainPersistence.java
+++ b/projects/todos/app/src/main/java/io/v/todos/persistence/MainPersistence.java
@@ -9,4 +9,5 @@
public interface MainPersistence extends Persistence {
void addTodoList(TodoList todoList);
void deleteTodoList(String key);
+ void completeAllTasks(TodoList todoList);
}
diff --git a/projects/todos/app/src/main/res/layout/todo_list_row.xml b/projects/todos/app/src/main/res/layout/todo_list_row.xml
index 9006102..c76ce42 100644
--- a/projects/todos/app/src/main/res/layout/todo_list_row.xml
+++ b/projects/todos/app/src/main/res/layout/todo_list_row.xml
@@ -61,18 +61,18 @@
android:layout_weight = "1">
<TextView android:id="@+id/todo_list_completed"
- android:layout_width="wrap_content"
+ android:layout_width="fill_parent"
android:layout_weight = "1"
- android:layout_height="fill_parent"
+ android:layout_height="wrap_content"
android:gravity="center_vertical"
android:textSize="12sp"
android:textColor="#333333"
android:layout_margin="5dp" />
<TextView android:id="@+id/todo_list_time"
- android:layout_width="wrap_content"
+ android:layout_width="fill_parent"
android:layout_weight = "1"
- android:layout_height="fill_parent"
+ android:layout_height="wrap_content"
android:gravity="center_vertical"
android:textSize="12sp"
android:textColor="#333333"