baku/todos: adds basic CRUD features

Todo items can be added and removed from the list. Items can be completed or
removed by dragging to the left or right. This work still needs some polish
(animations etc.) and only the basic operations to get a working version are
in this CL.

Change-Id: I92e25ba96484fba17adf6bde73b817108831ad75
diff --git a/examples/todos/flutter.yaml b/examples/todos/flutter.yaml
index 5f2e40e..fe54c87 100644
--- a/examples/todos/flutter.yaml
+++ b/examples/todos/flutter.yaml
@@ -1,3 +1,8 @@
 name: todos
 material-design-icons:
+  - name: action/check_circle
+  - name: action/delete
   - name: content/add
+  - name: content/clear
+  - name: navigation/arrow_back
+  - name: navigation/close
diff --git a/examples/todos/lib/components/todo_collection.dart b/examples/todos/lib/components/todo_collection.dart
new file mode 100644
index 0000000..f3baf81
--- /dev/null
+++ b/examples/todos/lib/components/todo_collection.dart
@@ -0,0 +1,69 @@
+// Copyright 2015 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.
+
+import 'package:flutter/material.dart';
+
+import 'todo_item.dart';
+import 'todo_dialog.dart';
+import '../todo_model.dart';
+
+/// [TodoCollection] is a [StatefulComponent] component.
+///
+/// This component is responsible for holding a list of TodoModel instances
+/// and rendering the list appropriately. A Todo list fills the whole screen
+/// and is composed of a tool bar, a list view, and the floating action button
+/// for adding a new Todo item.
+class TodoCollection extends StatefulComponent {
+  TodoCollectionState createState() => new TodoCollectionState();
+}
+
+class TodoCollectionState extends State<TodoCollection> {
+  List<TodoModel> todos = new List<TodoModel>();
+
+  void removeTodo(TodoModel todo) {
+    setState(() {
+      todos.remove(todo);
+    });
+  }
+
+  void addTodo(String title) {
+    // Use `setState(fn)` to signal to the framework that some internal state
+    // has been changed and a needs to be rebuilt. If the state update is not
+    // wrapped in a `setState` call the change will not be displayed to the
+    // screen.
+    setState(() {
+      todos.add(new TodoModel(title: title));
+    });
+  }
+
+  Widget buildTodo(BuildContext context, int index) {
+    if (index >= todos.length) {
+      return null;
+    }
+
+    TodoModel todo = todos[index];
+
+    return new TodoItem(
+        // Key is required here to support a requirement of child
+        // ScrollableMixedWidgetList.
+        key: todo.key,
+        todo: todo,
+        onDelete: removeTodo);
+  }
+
+  Widget build(BuildContext context) {
+    return new Scaffold(
+        toolBar: new ToolBar(center: new Text('TODO List')),
+        // Avoiding MaterialList/ScrollableList here since children
+        // would be required to have a pre-defined height via the contstructor
+        // property itemExtent.
+        // SEE: http://goo.gl/cB5bOE
+        body: new ScrollableMixedWidgetList(
+            builder: buildTodo, token: todos.length),
+        floatingActionButton: new FloatingActionButton(
+            child: new Icon(icon: 'content/add'),
+            onPressed: () => showDialog(
+                context: context, child: new TodoDialog(onSave: addTodo))));
+  }
+}
diff --git a/examples/todos/lib/components/todo_dialog.dart b/examples/todos/lib/components/todo_dialog.dart
new file mode 100644
index 0000000..3c542b7
--- /dev/null
+++ b/examples/todos/lib/components/todo_dialog.dart
@@ -0,0 +1,81 @@
+// Copyright 2015 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.
+
+import 'package:flutter/material.dart';
+
+typedef void TodoHandleSave(Todo);
+
+class TodoDialog extends StatefulComponent {
+  TodoDialog({Key key, this.value: "", this.title: "Add Todo", this.onSave})
+      : super(key: key);
+
+  final String title;
+  final TodoHandleSave onSave;
+  final String value;
+
+  TodoDialogState createState() => new TodoDialogState();
+}
+
+class TodoDialogState extends State<TodoDialog> {
+  String value;
+
+  @override
+  void initState() {
+    super.initState();
+    value = config.value;
+  }
+
+  void handleInputChange(String update) {
+    value = update;
+  }
+
+  // The Input widget gets different values on change versus on submit, be
+  // sure to update the todo with the most recent value.
+  void handleInputSubmit(String value) {
+    handleInputChange(value);
+    save();
+  }
+
+  void close() {
+    Navigator.pop(context);
+  }
+
+  void save() {
+    close();
+    config.onSave(value);
+  }
+
+  static final GlobalKey inputKey = new GlobalKey(debugLabel: 'todo input');
+
+  Widget build(BuildContext context) {
+    Text label = new Text("What needs to get done?");
+    Input input = new Input(
+        key: inputKey,
+        initialValue: value,
+        onChanged: handleInputChange,
+        onSubmitted: handleInputSubmit);
+    List children = <Widget>[label, input];
+
+    return new Scaffold(
+        toolBar: buildToolbar(),
+        body: new Container(
+            margin: new EdgeDims.all(24.0), child: new Block(children)));
+  }
+
+  Widget buildToolbar() {
+    return new ToolBar(
+        left: new IconButton(icon: "navigation/close", onPressed: close),
+        center: new Text(config.title),
+        right: <Widget>[
+          // NOTE: The right spacing is off here.
+          new FlatButton(
+              child: new Text('SAVE'),
+              // NOTE: without this the button text color is black instead of
+              // white. Is there a correct/canonical way to reach into the
+              // theme and grab the toolbar text color to use here?
+              textColor: Colors.white,
+              onPressed: save)
+        ]);
+  }
+}
diff --git a/examples/todos/lib/components/todo_item.dart b/examples/todos/lib/components/todo_item.dart
new file mode 100644
index 0000000..51f4ce6
--- /dev/null
+++ b/examples/todos/lib/components/todo_item.dart
@@ -0,0 +1,252 @@
+// Copyright 2015 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.
+
+import 'dart:ui' as ui;
+import 'package:flutter/material.dart';
+import 'package:flutter/painting.dart';
+import 'package:flutter/rendering.dart';
+import 'package:flutter/gestures.dart';
+import 'package:flutter/animation.dart';
+
+import "../todo_model.dart";
+
+const Duration _todoSnapDuration = const Duration(milliseconds: 200);
+
+typedef void TodoHandleDelete(TodoModel);
+
+enum TodoStatus { idle, completing, deleting, }
+
+class TodoItem extends StatefulComponent {
+  TodoItem({Key key, this.todo, this.onDelete}) : super(key: key) {
+    assert(todo != null);
+    assert(onDelete != null);
+  }
+
+  final TodoModel todo;
+  final TodoHandleDelete onDelete;
+
+  TodoItemState createState() => new TodoItemState();
+}
+
+// TODO(jasoncampbell): Create a throttle animation so that dragging
+// appears to slow down as the item gets closer to the threshold.
+class TodoItemState extends State<TodoItem> {
+  HorizontalDragGestureRecognizer drag;
+  ValuePerformance<double> snapPerformance;
+
+  TodoItemState() {
+    drag = new HorizontalDragGestureRecognizer(
+        router: Gesturer.instance.pointerRouter,
+        gestureArena: Gesturer.instance.gestureArena)
+      ..onStart = handleDragStart
+      ..onUpdate = handleDragUpdate
+      ..onEnd = handleDragEnd;
+
+    snapPerformance = new ValuePerformance<double>(
+        variable: snapValue, duration: _todoSnapDuration)
+      ..addStatusListener(handleSnapAnimationStatus);
+  }
+
+  TodoModel todo;
+  TodoStatus status;
+  Size size = new Size(ui.window.size.width, 0.0);
+  double position = 0.0;
+  double lastDelta;
+
+  final double dragThreshold = ui.window.size.width / 2 * 0.75;
+
+  final AnimatedValue<double> snapValue =
+      new AnimatedValue<double>(1.0, end: 0.0, curve: Curves.easeOut);
+
+  /// Override `initState()`.
+  @override
+  void initState() {
+    super.initState();
+
+    todo = config.todo;
+  }
+
+  void dispose() {
+    snapPerformance?.stop();
+    super.dispose();
+  }
+
+  void handleSnapAnimationStatus(PerformanceStatus update) {
+    if (update == PerformanceStatus.completed) {
+      setState(() {
+        snapPerformance.progress = 0.0;
+        position = 0.0;
+
+        // Make sure the right thing happens after the snap animation.
+        switch (status) {
+          case TodoStatus.completing:
+            todo.completed = true;
+            status = TodoStatus.idle;
+            break;
+          case TodoStatus.deleting:
+            config.onDelete(todo);
+            break;
+          default:
+            status = TodoStatus.idle;
+        }
+      });
+    }
+  }
+
+  void handlePointerDown(PointerDownEvent event) {
+    drag.addPointer(event);
+  }
+
+  void handleDragStart(Point globalPosition) {
+    position = 0.0;
+  }
+
+  void handleDragUpdate(double delta) {
+    double update = position + delta;
+
+    // NOTE: My eyes might be playing tricks on me but I think this prevents
+    // some jitter while dragging a Todo horizonatlly.
+    if (position != update) {
+      bool pastThreshold = update.abs() > dragThreshold;
+
+      // Prevent the drag from moving past the treshold.
+      if (!pastThreshold) {
+        setState(() {
+          lastDelta = delta;
+          position = update;
+
+          double statusThreshold = dragThreshold / 2;
+
+          // Update status based on the current position.
+          if (position.abs() > statusThreshold) {
+            if (position.isNegative) {
+              // Dragging to the left (revealing the delete icon).
+              status = TodoStatus.deleting;
+            } else {
+              // Dragging to the right (revealing the complete icon).
+              status = TodoStatus.completing;
+            }
+          } else {
+            // When dragging back and forth between the original position and
+            // the status threshold return the item to the idle state.
+            status = TodoStatus.idle;
+          }
+        });
+      }
+    }
+  }
+
+  void handleDragEnd(Offset velocity) {
+    snapPerformance.play();
+  }
+
+  void handleResize(Size update) {
+    setState(() {
+      size = update;
+    });
+  }
+
+  Widget build(BuildContext context) {
+    Widget overlay = new BuilderTransition(
+        variables: <AnimatedValue<double>>[snapPerformance.variable],
+        performance: snapPerformance.view, builder: (BuildContext context) {
+      double left = snapValue.value.clamp(0.0, 1.0) * position;
+
+      return new Positioned(
+          width: size.width,
+          left: left,
+          child: new TodoItemBody(todo: todo, status: status));
+    });
+
+    List<Widget> children = [
+      // NOTE: This instance of TodoItemBody will never be seen, it is used
+      // entirely for sizing purposes and appears behind the action icons
+      // below. The visible instance is inside of a positioned element on top
+      // of the stack and as a result will lack a hight constraint.
+      new TodoItemBody(todo: todo, onResize: handleResize),
+    ];
+
+    Widget background = new Container(
+        decoration: new BoxDecoration(
+            backgroundColor: Theme.of(context).canvasColor,
+            border: new Border(
+                bottom: new BorderSide(
+                    color: Theme.of(context).dividerColor, width: 1.0))),
+        height: size.height,
+        child: new Row(<Widget>[
+          new Flexible(
+              child: new Container(
+                  child: new Align(
+                      child: new Icon(
+                          icon: "action/check_circle",
+                          color: IconThemeColor.white),
+                      alignment: const FractionalOffset(0.0, 0.5)),
+                  decoration: new BoxDecoration(
+                      backgroundColor: Colors.greenAccent[100]),
+                  padding: const EdgeDims.all(24.0))),
+          new Flexible(
+              child: new Container(
+                  child: new Align(
+                      child: new Icon(
+                          icon: "action/delete", color: IconThemeColor.white),
+                      alignment: const FractionalOffset(1.0, 0.5)),
+                  decoration: new BoxDecoration(
+                      backgroundColor: Colors.redAccent[100]),
+                  padding: const EdgeDims.all(24.0)))
+        ]));
+
+    children.add(background);
+    children.add(overlay);
+
+    return new Listener(
+        onPointerDown: handlePointerDown,
+        behavior: HitTestBehavior.translucent,
+        child: new Stack(children));
+  }
+}
+
+typedef void TodoHandleResize(Size);
+
+class TodoItemBody extends StatelessComponent {
+  TodoItemBody({Key key, this.todo, this.status, this.onResize});
+
+  final TodoModel todo;
+  final TodoHandleResize onResize;
+  final TodoStatus status;
+
+  void maybeCallResize(Size size) {
+    if (onResize != null) {
+      onResize(size);
+    }
+  }
+
+  Widget build(BuildContext context) {
+    Color backgroundColor;
+
+    switch (status) {
+      case TodoStatus.completing:
+        backgroundColor = Colors.greenAccent[100];
+        break;
+      case TodoStatus.deleting:
+        backgroundColor = Colors.redAccent[100];
+        break;
+      default:
+        backgroundColor = Theme.of(context).canvasColor;
+    }
+
+    String message = todo.title;
+
+    if (todo.completed) {
+      message = "COMPLETED: ${todo.title}";
+    }
+
+    return new SizeObserver(
+        onSizeChanged: maybeCallResize,
+        child: new Container(
+            decoration: new BoxDecoration(backgroundColor: backgroundColor),
+            child: new Padding(
+                padding: const EdgeDims.all(24.0),
+                child: new Text(message))));
+  }
+}
diff --git a/examples/todos/lib/main.dart b/examples/todos/lib/main.dart
index e0cf583..6e40cfa 100644
--- a/examples/todos/lib/main.dart
+++ b/examples/todos/lib/main.dart
@@ -4,6 +4,8 @@
 
 import 'package:flutter/material.dart';
 
+import './components/todo_collection.dart';
+
 /// Start the Todo application.
 ///
 /// Use the Flutter `runApp` function render this application's widget tree
@@ -21,10 +23,9 @@
 /// composition of the widget tree it can inherit from `StatelessComponent`.
 class App extends StatelessComponent {
   final ThemeData theme = new ThemeData(
-    brightness: ThemeBrightness.light,
-    primarySwatch: Colors.indigo,
-    accentColor: Colors.pinkAccent[200]
-  );
+      brightness: ThemeBrightness.light,
+      primarySwatch: Colors.indigo,
+      accentColor: Colors.pinkAccent[200]);
 
   /// Build a `MaterialApp` widget.
   ///
@@ -32,86 +33,8 @@
   /// only an empty Todo List is rendered as the single, default route.
   Widget build(BuildContext context) {
     return new MaterialApp(
-      title: 'TODO',
-      theme: theme,
-      routes: {'/': (RouteArguments args) => new TodoList()}
-    );
-  }
-}
-
-/// `TodoList` is a `StatefulComponent` component.
-///
-/// This component is responsible for holding a list of `Todo` items and
-/// rendering the list appropriately. A Todo list fills the whole screen
-/// and is composed of a tool bar, a list view, and the floating
-/// action button for adding a new Todo item.
-class TodoList extends StatefulComponent {
-  TodoListState createState() => new TodoListState();
-}
-
-class TodoListState extends State<TodoList> {
-  List<Todo> todos = new List<Todo>();
-
-  void addTodo() {
-    // Use `setState(fn)` to signal to the framework that some internal state
-    // has been changed and a needs to be rebuilt. If the state update is not
-    // wrapped in a `setState` call the change will not be displayed to the
-    // screen.
-    setState(() {
-      todos.add(new Todo(title: 'FAB Item #${todos.length}.'));
-    });
-  }
-
-  Widget build(BuildContext context) {
-    return new Scaffold(
-      toolBar: new ToolBar(
-        center: new Text('TODO List')
-      ),
-      body: new MaterialList(
-        type: MaterialListType.oneLine,
-        children: todos
-      ),
-      floatingActionButton: new FloatingActionButton(
-          child: new Icon(icon: 'content/add'),
-          onPressed: addTodo
-      )
-    );
-  }
-}
-
-class Todo extends StatefulComponent {
-  Todo({Key key, this.title, this.completed: false}) : super(key: key);
-
-  final String title;
-  final bool completed;
-
-  TodoState createState() => new TodoState();
-}
-
-class TodoState extends State<Todo> {
-  bool completed;
-
-  /// Override `initState()`.
-  ///
-  /// Initialize internal state value `completed` with the value passed into
-  /// the parent `Todo` constructor. The `config` property allows the state
-  /// instance to access the properties of the `Todo` component.
-  @override
-  void initState() {
-    super.initState();
-    completed = config.completed;
-  }
-
-  void toggle() {
-    setState(() {
-      completed = !completed;
-    });
-  }
-
-  Widget build(BuildContext context) {
-    return new ListItem(
-      center: new Text('${config.title} - done: $completed'),
-      onTap: toggle
-    );
+        title: 'TODO',
+        theme: theme,
+        routes: {'/': (RouteArguments args) => new TodoCollection()});
   }
 }
diff --git a/examples/todos/lib/todo_model.dart b/examples/todos/lib/todo_model.dart
new file mode 100644
index 0000000..37aecb4
--- /dev/null
+++ b/examples/todos/lib/todo_model.dart
@@ -0,0 +1,17 @@
+// Copyright 2015 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.
+
+import 'package:flutter/material.dart';
+import 'package:uuid/uuid.dart';
+
+class TodoModel {
+  TodoModel({this.id, this.title, this.completed: false}) {
+    id ??= new Uuid().v4();
+  }
+
+  String id;
+  String title;
+  bool completed;
+  Key get key => new ObjectKey(this);
+}
diff --git a/examples/todos/pubspec.yaml b/examples/todos/pubspec.yaml
index dc298bd..855c158 100644
--- a/examples/todos/pubspec.yaml
+++ b/examples/todos/pubspec.yaml
@@ -1,5 +1,6 @@
 name: todos
 description: A minimal Flutter project.
 dependencies:
+  uuid: 0.5.0
   flutter:
     path: ../../../../../../flutter/packages/flutter