Merge "baku/todos: adds basic CRUD features"
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