refactor(examples/todos): update flutter code
This is an initial pass to get the todo example into a condition that allows
multiple contributors to work on it.
* Updates flutter code to match version: cc93170894
* Adds rough screens based on the Vanadium Todos example
Remaing tasks are being tracked in vanadium/baku#1 and will follow as separate
CLs.
Change-Id: I2b1fb33fb5addfbdb844ae0163c56b724ed72ffa
diff --git a/examples/todos/.gitignore b/examples/todos/.gitignore
index dd20bb4..14c7d4c 100644
--- a/examples/todos/.gitignore
+++ b/examples/todos/.gitignore
@@ -4,5 +4,6 @@
.packages
.pub/
build/
+ios/.generated/
packages
pubspec.lock
diff --git a/examples/todos/README.md b/examples/todos/README.md
index 1e1caa6..2df1b5e 100644
--- a/examples/todos/README.md
+++ b/examples/todos/README.md
@@ -1,6 +1,8 @@
# todos
-Details coming soon.
+A new flutter project.
- pub get
- flutter start && flutter logs
+## Getting Started
+
+For help getting started with Flutter, view our online
+[documentation](http://flutter.io/).
diff --git a/examples/todos/apk/AndroidManifest.xml b/examples/todos/android/AndroidManifest.xml
similarity index 66%
rename from examples/todos/apk/AndroidManifest.xml
rename to examples/todos/android/AndroidManifest.xml
index 91febc0..cbef8cc 100644
--- a/examples/todos/apk/AndroidManifest.xml
+++ b/examples/todos/android/AndroidManifest.xml
@@ -1,15 +1,18 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
- package="com.example.todos">
+ package="com.yourcompany.todos"
+ android:versionCode="1"
+ android:versionName="0.0.1">
<uses-sdk android:minSdkVersion="16" android:targetSdkVersion="21" />
<uses-permission android:name="android.permission.INTERNET"/>
- <application android:name="org.domokit.sky.shell.SkyApplication" android:label="todos">
+ <application android:name="org.domokit.sky.shell.SkyApplication" android:label="todos" android:icon="@mipmap/ic_launcher">
<activity android:name="org.domokit.sky.shell.SkyActivity"
- android:launchMode="singleTask"
+ android:launchMode="singleTop"
android:theme="@android:style/Theme.Black.NoTitleBar"
- android:configChanges="orientation|keyboardHidden|keyboard|screenSize"
- android:hardwareAccelerated="true">
+ android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection"
+ android:hardwareAccelerated="true"
+ android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
diff --git a/examples/todos/android/res/mipmap-hdpi/ic_launcher.png b/examples/todos/android/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..f0a55cd
--- /dev/null
+++ b/examples/todos/android/res/mipmap-hdpi/ic_launcher.png
Binary files differ
diff --git a/examples/todos/android/res/mipmap-mdpi/ic_launcher.png b/examples/todos/android/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..eb98dc4
--- /dev/null
+++ b/examples/todos/android/res/mipmap-mdpi/ic_launcher.png
Binary files differ
diff --git a/examples/todos/android/res/mipmap-xhdpi/ic_launcher.png b/examples/todos/android/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..f1783db
--- /dev/null
+++ b/examples/todos/android/res/mipmap-xhdpi/ic_launcher.png
Binary files differ
diff --git a/examples/todos/android/res/mipmap-xxhdpi/ic_launcher.png b/examples/todos/android/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..46828a2
--- /dev/null
+++ b/examples/todos/android/res/mipmap-xxhdpi/ic_launcher.png
Binary files differ
diff --git a/examples/todos/android/res/mipmap-xxxhdpi/ic_launcher.png b/examples/todos/android/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..a527671
--- /dev/null
+++ b/examples/todos/android/res/mipmap-xxxhdpi/ic_launcher.png
Binary files differ
diff --git a/examples/todos/flutter.yaml b/examples/todos/flutter.yaml
index fe54c87..3e5efa7 100644
--- a/examples/todos/flutter.yaml
+++ b/examples/todos/flutter.yaml
@@ -1,8 +1,2 @@
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
+uses-material-design: true
diff --git a/examples/todos/ios/Assets.xcassets/AppIcon.appiconset/Contents.json b/examples/todos/ios/Assets.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 0000000..363d1c4
--- /dev/null
+++ b/examples/todos/ios/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,142 @@
+{
+ "images" : [
+ {
+ "size" : "29x29",
+ "idiom" : "iphone",
+ "filename" : "Icon-Small@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "iphone",
+ "filename" : "Icon-Small@3x.png",
+ "scale" : "3x"
+ },
+ {
+ "size" : "40x40",
+ "idiom" : "iphone",
+ "filename" : "Icon-Small-40@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "40x40",
+ "idiom" : "iphone",
+ "filename" : "Icon-Small-40@3x.png",
+ "scale" : "3x"
+ },
+ {
+ "size" : "60x60",
+ "idiom" : "iphone",
+ "filename" : "Icon-60@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "60x60",
+ "idiom" : "iphone",
+ "filename" : "Icon-60@3x.png",
+ "scale" : "3x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "ipad",
+ "filename" : "Icon-Small.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "ipad",
+ "filename" : "Icon-Small@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "40x40",
+ "idiom" : "ipad",
+ "filename" : "Icon-Small-40.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "40x40",
+ "idiom" : "ipad",
+ "filename" : "Icon-Small-40@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "76x76",
+ "idiom" : "ipad",
+ "filename" : "Icon-76.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "76x76",
+ "idiom" : "ipad",
+ "filename" : "Icon-76@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "83.5x83.5",
+ "idiom" : "ipad",
+ "filename" : "Icon-83.5@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "16x16",
+ "idiom" : "mac",
+ "filename" : "icon_16x16.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "16x16",
+ "idiom" : "mac",
+ "filename" : "icon_16x16@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "32x32",
+ "idiom" : "mac",
+ "filename" : "icon_32x32.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "32x32",
+ "idiom" : "mac",
+ "filename" : "icon_32x32@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "128x128",
+ "idiom" : "mac",
+ "filename" : "icon_128x128.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "128x128",
+ "idiom" : "mac",
+ "filename" : "icon_128x128@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "256x256",
+ "idiom" : "mac",
+ "filename" : "icon_256x256.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "256x256",
+ "idiom" : "mac",
+ "filename" : "icon_256x256@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "512x512",
+ "idiom" : "mac",
+ "filename" : "icon_512x512.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "512x512",
+ "idiom" : "mac",
+ "filename" : "icon_512x512@2x.png",
+ "scale" : "2x"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/examples/todos/ios/Assets.xcassets/AppIcon.appiconset/Icon-60@2x.png b/examples/todos/ios/Assets.xcassets/AppIcon.appiconset/Icon-60@2x.png
new file mode 100644
index 0000000..9996f5e
--- /dev/null
+++ b/examples/todos/ios/Assets.xcassets/AppIcon.appiconset/Icon-60@2x.png
Binary files differ
diff --git a/examples/todos/ios/Assets.xcassets/AppIcon.appiconset/Icon-60@3x.png b/examples/todos/ios/Assets.xcassets/AppIcon.appiconset/Icon-60@3x.png
new file mode 100644
index 0000000..7a543ed
--- /dev/null
+++ b/examples/todos/ios/Assets.xcassets/AppIcon.appiconset/Icon-60@3x.png
Binary files differ
diff --git a/examples/todos/ios/Assets.xcassets/AppIcon.appiconset/Icon-76.png b/examples/todos/ios/Assets.xcassets/AppIcon.appiconset/Icon-76.png
new file mode 100644
index 0000000..05a8268
--- /dev/null
+++ b/examples/todos/ios/Assets.xcassets/AppIcon.appiconset/Icon-76.png
Binary files differ
diff --git a/examples/todos/ios/Assets.xcassets/AppIcon.appiconset/Icon-76@2x.png b/examples/todos/ios/Assets.xcassets/AppIcon.appiconset/Icon-76@2x.png
new file mode 100644
index 0000000..bfbca28
--- /dev/null
+++ b/examples/todos/ios/Assets.xcassets/AppIcon.appiconset/Icon-76@2x.png
Binary files differ
diff --git a/examples/todos/ios/Assets.xcassets/AppIcon.appiconset/Icon-83.5@2x.png b/examples/todos/ios/Assets.xcassets/AppIcon.appiconset/Icon-83.5@2x.png
new file mode 100644
index 0000000..c924b8e
--- /dev/null
+++ b/examples/todos/ios/Assets.xcassets/AppIcon.appiconset/Icon-83.5@2x.png
Binary files differ
diff --git a/examples/todos/ios/Assets.xcassets/AppIcon.appiconset/Icon-Small-40.png b/examples/todos/ios/Assets.xcassets/AppIcon.appiconset/Icon-Small-40.png
new file mode 100644
index 0000000..3006f1d
--- /dev/null
+++ b/examples/todos/ios/Assets.xcassets/AppIcon.appiconset/Icon-Small-40.png
Binary files differ
diff --git a/examples/todos/ios/Assets.xcassets/AppIcon.appiconset/Icon-Small-40@2x.png b/examples/todos/ios/Assets.xcassets/AppIcon.appiconset/Icon-Small-40@2x.png
new file mode 100644
index 0000000..5f7d22d
--- /dev/null
+++ b/examples/todos/ios/Assets.xcassets/AppIcon.appiconset/Icon-Small-40@2x.png
Binary files differ
diff --git a/examples/todos/ios/Assets.xcassets/AppIcon.appiconset/Icon-Small-40@3x.png b/examples/todos/ios/Assets.xcassets/AppIcon.appiconset/Icon-Small-40@3x.png
new file mode 100644
index 0000000..9996f5e
--- /dev/null
+++ b/examples/todos/ios/Assets.xcassets/AppIcon.appiconset/Icon-Small-40@3x.png
Binary files differ
diff --git a/examples/todos/ios/Assets.xcassets/AppIcon.appiconset/Icon-Small.png b/examples/todos/ios/Assets.xcassets/AppIcon.appiconset/Icon-Small.png
new file mode 100644
index 0000000..3433da1
--- /dev/null
+++ b/examples/todos/ios/Assets.xcassets/AppIcon.appiconset/Icon-Small.png
Binary files differ
diff --git a/examples/todos/ios/Assets.xcassets/AppIcon.appiconset/Icon-Small@2x.png b/examples/todos/ios/Assets.xcassets/AppIcon.appiconset/Icon-Small@2x.png
new file mode 100644
index 0000000..f7f9f16
--- /dev/null
+++ b/examples/todos/ios/Assets.xcassets/AppIcon.appiconset/Icon-Small@2x.png
Binary files differ
diff --git a/examples/todos/ios/Assets.xcassets/AppIcon.appiconset/Icon-Small@3x.png b/examples/todos/ios/Assets.xcassets/AppIcon.appiconset/Icon-Small@3x.png
new file mode 100644
index 0000000..e9c2360
--- /dev/null
+++ b/examples/todos/ios/Assets.xcassets/AppIcon.appiconset/Icon-Small@3x.png
Binary files differ
diff --git a/examples/todos/ios/Assets.xcassets/AppIcon.appiconset/icon_128x128.png b/examples/todos/ios/Assets.xcassets/AppIcon.appiconset/icon_128x128.png
new file mode 100644
index 0000000..dd5fccb
--- /dev/null
+++ b/examples/todos/ios/Assets.xcassets/AppIcon.appiconset/icon_128x128.png
Binary files differ
diff --git a/examples/todos/ios/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png b/examples/todos/ios/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png
new file mode 100644
index 0000000..1909e27
--- /dev/null
+++ b/examples/todos/ios/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png
Binary files differ
diff --git a/examples/todos/ios/Assets.xcassets/AppIcon.appiconset/icon_16x16.png b/examples/todos/ios/Assets.xcassets/AppIcon.appiconset/icon_16x16.png
new file mode 100644
index 0000000..40a701b
--- /dev/null
+++ b/examples/todos/ios/Assets.xcassets/AppIcon.appiconset/icon_16x16.png
Binary files differ
diff --git a/examples/todos/ios/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png b/examples/todos/ios/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png
new file mode 100644
index 0000000..06c1a80
--- /dev/null
+++ b/examples/todos/ios/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png
Binary files differ
diff --git a/examples/todos/ios/Assets.xcassets/AppIcon.appiconset/icon_256x256.png b/examples/todos/ios/Assets.xcassets/AppIcon.appiconset/icon_256x256.png
new file mode 100644
index 0000000..1909e27
--- /dev/null
+++ b/examples/todos/ios/Assets.xcassets/AppIcon.appiconset/icon_256x256.png
Binary files differ
diff --git a/examples/todos/ios/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png b/examples/todos/ios/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png
new file mode 100644
index 0000000..69c96ef
--- /dev/null
+++ b/examples/todos/ios/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png
Binary files differ
diff --git a/examples/todos/ios/Assets.xcassets/AppIcon.appiconset/icon_32x32.png b/examples/todos/ios/Assets.xcassets/AppIcon.appiconset/icon_32x32.png
new file mode 100644
index 0000000..06c1a80
--- /dev/null
+++ b/examples/todos/ios/Assets.xcassets/AppIcon.appiconset/icon_32x32.png
Binary files differ
diff --git a/examples/todos/ios/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png b/examples/todos/ios/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png
new file mode 100644
index 0000000..0f94371
--- /dev/null
+++ b/examples/todos/ios/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png
Binary files differ
diff --git a/examples/todos/ios/Assets.xcassets/AppIcon.appiconset/icon_512x512.png b/examples/todos/ios/Assets.xcassets/AppIcon.appiconset/icon_512x512.png
new file mode 100644
index 0000000..69c96ef
--- /dev/null
+++ b/examples/todos/ios/Assets.xcassets/AppIcon.appiconset/icon_512x512.png
Binary files differ
diff --git a/examples/todos/ios/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png b/examples/todos/ios/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png
new file mode 100644
index 0000000..1d6355f
--- /dev/null
+++ b/examples/todos/ios/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png
Binary files differ
diff --git a/examples/todos/ios/Info.plist b/examples/todos/ios/Info.plist
new file mode 100644
index 0000000..7758887
--- /dev/null
+++ b/examples/todos/ios/Info.plist
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>CFBundleDevelopmentRegion</key>
+ <string>en</string>
+ <key>CFBundleExecutable</key>
+ <string>Runner</string>
+ <key>CFBundleIdentifier</key>
+ <string>com.yourcompany.todos</string>
+ <key>CFBundleInfoDictionaryVersion</key>
+ <string>6.0</string>
+ <key>CFBundleName</key>
+ <string>todos</string>
+ <key>CFBundlePackageType</key>
+ <string>APPL</string>
+ <key>CFBundleShortVersionString</key>
+ <string>1.0</string>
+ <key>CFBundleSignature</key>
+ <string>????</string>
+ <key>CFBundleVersion</key>
+ <string>1</string>
+ <key>LSRequiresIPhoneOS</key>
+ <true/>
+ <key>UILaunchStoryboardName</key>
+ <string>LaunchScreen</string>
+ <key>UIRequiredDeviceCapabilities</key>
+ <array>
+ <string>arm64</string>
+ </array>
+ <key>UISupportedInterfaceOrientations</key>
+ <array>
+ <string>UIInterfaceOrientationPortrait</string>
+ <string>UIInterfaceOrientationLandscapeLeft</string>
+ <string>UIInterfaceOrientationLandscapeRight</string>
+ </array>
+ <key>UISupportedInterfaceOrientations~ipad</key>
+ <array>
+ <string>UIInterfaceOrientationPortrait</string>
+ <string>UIInterfaceOrientationPortraitUpsideDown</string>
+ <string>UIInterfaceOrientationLandscapeLeft</string>
+ <string>UIInterfaceOrientationLandscapeRight</string>
+ </array>
+ <key>UIViewControllerBasedStatusBarAppearance</key>
+ <false/>
+</dict>
+</plist>
diff --git a/examples/todos/ios/LaunchScreen.storyboard b/examples/todos/ios/LaunchScreen.storyboard
new file mode 100644
index 0000000..78686cd
--- /dev/null
+++ b/examples/todos/ios/LaunchScreen.storyboard
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="9531" systemVersion="15C50" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" initialViewController="01J-lp-oVM">
+ <dependencies>
+ <deployment identifier="iOS"/>
+ <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="9529"/>
+ </dependencies>
+ <scenes>
+ <!--View Controller-->
+ <scene sceneID="EHf-IW-A2E">
+ <objects>
+ <viewController id="01J-lp-oVM" sceneMemberID="viewController">
+ <layoutGuides>
+ <viewControllerLayoutGuide type="top" id="Llm-lL-Icb"/>
+ <viewControllerLayoutGuide type="bottom" id="xb3-aO-Qok"/>
+ </layoutGuides>
+ <view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
+ <rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
+ <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+ <color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
+ </view>
+ </viewController>
+ <placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
+ </objects>
+ <point key="canvasLocation" x="53" y="375"/>
+ </scene>
+ </scenes>
+</document>
diff --git a/examples/todos/lib/components/todo_collection.dart b/examples/todos/lib/components/todo_collection.dart
deleted file mode 100644
index f3baf81..0000000
--- a/examples/todos/lib/components/todo_collection.dart
+++ /dev/null
@@ -1,69 +0,0 @@
-// 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
deleted file mode 100644
index 3c542b7..0000000
--- a/examples/todos/lib/components/todo_dialog.dart
+++ /dev/null
@@ -1,81 +0,0 @@
-// 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
deleted file mode 100644
index 51f4ce6..0000000
--- a/examples/todos/lib/components/todo_item.dart
+++ /dev/null
@@ -1,252 +0,0 @@
-// 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/home.dart b/examples/todos/lib/home.dart
new file mode 100644
index 0000000..48da1fa
--- /dev/null
+++ b/examples/todos/lib/home.dart
@@ -0,0 +1,74 @@
+// 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.
+
+import 'package:flutter/material.dart';
+
+import 'task_list_data.dart';
+import 'task_list_dialog.dart';
+import 'task_list_row.dart';
+
+/// [Home] is a [StatefulWidget].
+///
+/// This widget is responsible for holding a
+/// collection of Task Lists and rendering them appropriately. The home screen
+/// is composed of a tool bar, a list view, and the floating action button for
+/// adding a new Lists.
+class Home extends StatefulWidget {
+ final Map<String, TaskListData> lists;
+
+ Home(this.lists);
+
+ @override
+ HomeState createState() => new HomeState();
+}
+
+class HomeState extends State<Home> {
+ @override
+ Widget build(BuildContext context) {
+ // TODO(jxson): Look into using a widget that accepts a builder function
+ // for rendering rows in the scrollable list.
+ //
+ // SEE: https://git.io/vrzFH
+ List<Widget> children = config.lists.keys.map((String uuid) {
+ TaskListData list = config.lists[uuid];
+ return new TaskListRow(list, _delete);
+ }).toList();
+
+ return new Scaffold(
+ appBar: new AppBar(title: new Text('Todo Lists')),
+ // NOTE: Using any kind of scrollable list with a height extent prevents
+ // dismissible height animations from occurring.
+ body: new Block(children: children),
+ floatingActionButton: new FloatingActionButton(
+ tooltip: 'Add new list.',
+ child: new Icon(icon: Icons.add),
+ onPressed: _showNewTaskListDialog));
+ }
+
+ void _showNewTaskListDialog() {
+ TaskListData list = new TaskListData();
+ Widget dialog = new TaskListDialog(list: list);
+
+ // TODO(jxson): Use a callback instead of the return future from
+ // `showDialog`. There is too much indirection with the current method
+ // since the dialog needs to use `Navigator.pop(...)` to get values back
+ // to this widget.
+ //
+ // SEE: https://git.io/vrzFH
+ showDialog(context: context, child: dialog).then(_add);
+ }
+
+ void _add(TaskListData list) {
+ if (list == null) return;
+
+ // Use `setState(fn)` to signal to that some internal state has changed
+ // and the widget needs to be rebuilt. If the state updates are not
+ // wrapped in a `setState` call the change will not be displayed.
+ setState(() => config.lists[list.uuid] = list);
+ }
+
+ void _delete(String uuid) {
+ setState(() => config.lists.remove(uuid));
+ }
+}
diff --git a/examples/todos/lib/main.dart b/examples/todos/lib/main.dart
index 6e40cfa..e9e7eb3 100644
--- a/examples/todos/lib/main.dart
+++ b/examples/todos/lib/main.dart
@@ -1,40 +1,71 @@
-// Copyright 2015 The Vanadium Authors. All rights reserved.
+// 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.
import 'package:flutter/material.dart';
-import './components/todo_collection.dart';
+import 'home.dart';
+import 'task_list_data.dart';
+import 'task_list_view.dart';
-/// Start the Todo application.
+/// Start the application.
///
-/// Use the Flutter `runApp` function render this application's widget tree
-/// to the screen.
+/// Create an instance of [App] and use the Flutter [runApp] function render
+/// this application to the screen.
void main() {
runApp(new App());
}
-/// Subclass `StatelessComponent` to build the top-level component.
+/// Subclass [StatefulWidget] to build the top-level component.
///
-/// The `App` component composes other, lower-level components in it's
-/// `build` function. This top-level component, `App` does not need to hold
-/// any state since that is managed by components lower down in the tree like
-/// `TodoList`. Because this component is stateless, and only manages
-/// composition of the widget tree it can inherit from `StatelessComponent`.
-class App extends StatelessComponent {
+/// The top-level [App] widget manages the composition of the enitre
+/// applications's widget tree based on it's state. [AppState] holds an
+/// in-memory [Map] of Task Lists. It is critical this state is held at the
+/// top level (and not further down the widget tree) to prevent user created
+/// values from disappearing between navigation changes.
+class App extends StatefulWidget {
+ @override
+ AppState createState() => new AppState();
+}
+
+class AppState extends State<StatefulWidget> {
+ final Map<String, TaskListData> lists = <String, TaskListData>{};
final ThemeData theme = new ThemeData(
brightness: ThemeBrightness.light,
primarySwatch: Colors.indigo,
accentColor: Colors.pinkAccent[200]);
- /// Build a `MaterialApp` widget.
+ /// Build a [MaterialApp] widget.
///
- /// The `MaterialApp` widget allows the inheritance of theme data. For now
- /// only an empty Todo List is rendered as the single, default route.
+ /// The [MaterialApp] widget allows the inheritance of [ThemeData].
+ @override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'TODO',
theme: theme,
- routes: {'/': (RouteArguments args) => new TodoCollection()});
+ routes: <String, WidgetBuilder>{
+ '/': (BuildContext context) => new Home(lists)
+ },
+ onGenerateRoute: handleRoute);
+ }
+
+ // TODO(jxson): look into creating a more ergonomic routing system with
+ // clear route definitions and ways to explicitly handle not found errors.
+ Route<Null> handleRoute(RouteSettings settings) {
+ List<String> path = settings.name.split('/');
+
+ if (!settings.name.startsWith('/')) return null;
+
+ switch (path[1]) {
+ case 'lists':
+ TaskListData list = lists[path[2]];
+ return new MaterialPageRoute<Null>(
+ settings: settings,
+ builder: (BuildContext context) {
+ return new TaskListView(list: list);
+ });
+ default:
+ return null;
+ }
}
}
diff --git a/examples/todos/lib/task_data.dart b/examples/todos/lib/task_data.dart
new file mode 100644
index 0000000..89c44b9
--- /dev/null
+++ b/examples/todos/lib/task_data.dart
@@ -0,0 +1,25 @@
+// 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.
+
+import 'package:flutter/material.dart';
+import 'package:uuid/uuid.dart';
+
+class TaskData {
+ String uuid;
+ String description;
+ bool completed;
+ int createdAt;
+ int updatedAt;
+
+ // TODO(jxson): implement task.<from/to>JSON.
+ TaskData({this.uuid}) {
+ int now = new DateTime.now().millisecondsSinceEpoch;
+ uuid ??= new Uuid().v4();
+ completed = false;
+ createdAt = now;
+ updatedAt = now;
+ }
+
+ Key get key => new ObjectKey(this);
+}
diff --git a/examples/todos/lib/task_dialog.dart b/examples/todos/lib/task_dialog.dart
new file mode 100644
index 0000000..d8c7d94
--- /dev/null
+++ b/examples/todos/lib/task_dialog.dart
@@ -0,0 +1,83 @@
+// 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.
+
+import 'package:flutter/material.dart';
+import 'dart:async';
+
+import 'task_data.dart';
+
+/// [TaskDialog] handles the creation and editing of individual Tasks.
+class TaskDialog extends StatefulWidget {
+ final TaskData task;
+
+ TaskDialog({Key key, this.task}) : super(key: key) {
+ assert(task != null);
+ }
+
+ @override
+ TaskDialogState createState() => new TaskDialogState();
+}
+
+class TaskDialogState extends State<TaskDialog> {
+ @override
+ void initState() {
+ super.initState();
+
+ // HACK(jxson): Trigger an immediate state change to coerce autofocus on
+ // the [Input].
+ //
+ // TODO(jxson): Add an issue to flutter/flutter about this hack to get the
+ // autofocus to work.
+ new Timer(new Duration(milliseconds: 1), () {
+ setState(() {});
+ });
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final ThemeData theme = Theme.of(context);
+
+ Widget save = new FlatButton(
+ child: new Text('SAVE',
+ style: theme.textTheme.body1.copyWith(color: Colors.white)),
+ onPressed: _save);
+
+ // TODO(jxson): Clean up the form & input interactions.
+ //
+ // SEE: https://git.io/vrzFH
+ Widget input = new Input(
+ autofocus: true,
+ hintText: 'What needs to happen?',
+ labelText: 'Task Description',
+ formField:
+ new FormField<String>(setter: _update, validator: _validate));
+
+ return new Scaffold(
+ appBar: new AppBar(
+ leading: new IconButton(icon: Icons.clear, onPressed: _cancel),
+ title: new Text('New List'),
+ actions: <Widget>[save]),
+ body: new Form(
+ onSubmitted: _save, child: new Block(children: <Widget>[input])));
+ }
+
+ void _save() {
+ Navigator.pop(context, config.task);
+ }
+
+ void _cancel() {
+ Navigator.pop(context);
+ }
+
+ String _validate(String value) {
+ if (value.isEmpty)
+ return 'List name is required';
+ else
+ return null;
+ }
+
+ void _update(String value) {
+ config.task.description = value;
+ }
+}
diff --git a/examples/todos/lib/task_list_data.dart b/examples/todos/lib/task_list_data.dart
new file mode 100644
index 0000000..e4f2d9b
--- /dev/null
+++ b/examples/todos/lib/task_list_data.dart
@@ -0,0 +1,25 @@
+// 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.
+
+import 'package:flutter/material.dart';
+import 'package:uuid/uuid.dart';
+
+import 'task_data.dart';
+
+class TaskListData {
+ String uuid;
+ String name;
+ int createdAt;
+ int updatedAt;
+ final Map<String, TaskData> tasks = <String, TaskData>{};
+
+ TaskListData({this.uuid}) {
+ int now = new DateTime.now().millisecondsSinceEpoch;
+ uuid ??= new Uuid().v4();
+ createdAt = now;
+ updatedAt = now;
+ }
+
+ Key get key => new ObjectKey(this);
+}
diff --git a/examples/todos/lib/task_list_dialog.dart b/examples/todos/lib/task_list_dialog.dart
new file mode 100644
index 0000000..5534f94
--- /dev/null
+++ b/examples/todos/lib/task_list_dialog.dart
@@ -0,0 +1,77 @@
+// 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.
+
+import 'package:flutter/material.dart';
+import 'dart:async';
+
+import 'task_list_data.dart';
+
+/// [TaskListDialog] handles the creation and editing of Task Lists.
+class TaskListDialog extends StatefulWidget {
+ final TaskListData list;
+
+ TaskListDialog({Key key, this.list}) : super(key: key) {
+ assert(list != null);
+ }
+
+ @override
+ TaskListDialogState createState() => new TaskListDialogState();
+}
+
+class TaskListDialogState extends State<TaskListDialog> {
+ @override
+ void initState() {
+ super.initState();
+
+ // HACK(jxson): Trigger an immediate state change to coerce autofocus on
+ // the [Input].
+ new Timer(new Duration(milliseconds: 1), () {
+ setState(() {});
+ });
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final ThemeData theme = Theme.of(context);
+
+ Widget save = new FlatButton(
+ child: new Text('SAVE',
+ style: theme.textTheme.body1.copyWith(color: Colors.white)),
+ onPressed: _save);
+
+ Widget input = new Input(
+ autofocus: true,
+ hintText: 'What should this list be called?',
+ labelText: 'List Name',
+ formField:
+ new FormField<String>(setter: _update, validator: _validate));
+
+ return new Scaffold(
+ appBar: new AppBar(
+ leading: new IconButton(icon: Icons.clear, onPressed: _cancel),
+ title: new Text('New List'),
+ actions: <Widget>[save]),
+ body: new Form(
+ onSubmitted: _save, child: new Block(children: <Widget>[input])));
+ }
+
+ void _save() {
+ Navigator.pop(context, config.list);
+ }
+
+ void _cancel() {
+ Navigator.pop(context);
+ }
+
+ String _validate(String value) {
+ if (value.isEmpty)
+ return 'List name is required';
+ else
+ return null;
+ }
+
+ void _update(String value) {
+ config.list.name = value;
+ }
+}
diff --git a/examples/todos/lib/task_list_row.dart b/examples/todos/lib/task_list_row.dart
new file mode 100644
index 0000000..9f5385f
--- /dev/null
+++ b/examples/todos/lib/task_list_row.dart
@@ -0,0 +1,67 @@
+// 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.
+
+import 'package:flutter/material.dart';
+
+import 'task_list_data.dart';
+
+typedef void TaskListRowHandleDelete(String uuid);
+
+/// [TaskListRow] renders individual rows of Task Lists.
+class TaskListRow extends StatefulWidget {
+ final TaskListData list;
+ final TaskListRowHandleDelete onDelete;
+
+ TaskListRow(this.list, this.onDelete);
+
+ @override
+ TaskListRowState createState() => new TaskListRowState();
+}
+
+class TaskListRowState extends State<TaskListRow> {
+ @override
+ Widget build(BuildContext context) {
+ final ThemeData theme = Theme.of(context);
+
+ // TODO(jxson): create a flutter issue about the ink effect going away
+ // when using Dismissable.
+ return new Dismissable(
+ key: new ObjectKey(config.list),
+ direction: DismissDirection.endToStart,
+ onDismissed: _handleDismiss,
+ background: new Container(
+ decoration: new BoxDecoration(
+ backgroundColor: theme.canvasColor,
+ border: new Border(
+ bottom: new BorderSide(color: theme.dividerColor))),
+ child: new ListItem(trailing: new Icon(icon: Icons.delete))),
+ child: new Container(
+ decoration: new BoxDecoration(
+ backgroundColor: theme.cardColor,
+ boxShadow: kElevationToShadow[2],
+ border: new Border(
+ bottom: new BorderSide(color: theme.dividerColor))),
+ child: new ListItem(
+ isThreeLine: true,
+ title: new Text(config.list.name),
+ subtitle: new Text(config.list.uuid),
+ onTap: _open)));
+ }
+
+ void _open() {
+ Navigator.pushNamed(context, '/lists/${config.list.uuid}');
+ }
+
+ void _handleDismiss(DismissDirection direction) {
+ switch (direction) {
+ // Swipe from right to left to delete.
+ case DismissDirection.endToStart:
+ config.onDelete(config.list.uuid);
+ break;
+ default:
+ throw "Not implemented.";
+ break;
+ }
+ }
+}
diff --git a/examples/todos/lib/task_list_view.dart b/examples/todos/lib/task_list_view.dart
new file mode 100644
index 0000000..97fd161
--- /dev/null
+++ b/examples/todos/lib/task_list_view.dart
@@ -0,0 +1,65 @@
+// 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.
+
+import 'package:flutter/material.dart';
+
+import 'task_data.dart';
+import 'task_row.dart';
+import 'task_list_data.dart';
+import 'task_dialog.dart';
+
+/// [TaskListView] renders a page showing all the Tasks associated to a given
+/// `list`.
+class TaskListView extends StatefulWidget {
+ final TaskListData list;
+
+ TaskListView({Key key, this.list}) : super(key: key) {
+ assert(list != null);
+ }
+
+ @override
+ TaskListViewState createState() => new TaskListViewState();
+}
+
+class TaskListViewState extends State<TaskListView> {
+ @override
+ Widget build(BuildContext context) {
+ List<Widget> children = config.list.tasks.keys.map((String uuid) {
+ TaskData task = config.list.tasks[uuid];
+ return new TaskRow(task: task, onDelete: _delete, onComplete: _complete);
+ }).toList();
+
+ return new Scaffold(
+ appBar: new AppBar(title: new Text('List: ${config.list.name}')),
+ body: new Block(children: children),
+ floatingActionButton: new FloatingActionButton(
+ onPressed: _showNewTaskDialog,
+ tooltip: 'Add new list.',
+ child: new Icon(icon: Icons.add)));
+ }
+
+ void _showNewTaskDialog() {
+ TaskData task = new TaskData();
+ Widget dialog = new TaskDialog(task: task);
+
+ showDialog(context: context, child: dialog).then(_add);
+ }
+
+ void _add(TaskData task) {
+ if (task == null) return;
+
+ setState(() => config.list.tasks[task.uuid] = task);
+ }
+
+ void _delete(String uuid) {
+ setState(() => config.list.tasks.remove(uuid));
+ }
+
+ void _complete(String uuid) {
+ setState(() {
+ TaskData task = config.list.tasks[uuid];
+ task.completed = true;
+ });
+ }
+}
diff --git a/examples/todos/lib/task_row.dart b/examples/todos/lib/task_row.dart
new file mode 100644
index 0000000..5721237
--- /dev/null
+++ b/examples/todos/lib/task_row.dart
@@ -0,0 +1,84 @@
+// 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.
+
+import 'package:flutter/material.dart';
+
+import 'task_data.dart';
+
+typedef void TaskRowHandleDelete(String uuid);
+typedef void TaskRowHandleComplete(String uuid);
+
+/// [TaskRow] renders individual rows of Tasks .
+class TaskRow extends StatefulWidget {
+ final TaskData task;
+ final TaskRowHandleDelete onDelete;
+ final TaskRowHandleComplete onComplete;
+
+ TaskRow({Key key, this.task, this.onDelete, this.onComplete})
+ : super(key: key) {
+ assert(task != null);
+ }
+
+ @override
+ TaskRowState createState() => new TaskRowState();
+}
+
+class TaskRowState extends State<TaskRow> {
+ @override
+ Widget build(BuildContext context) {
+ final ThemeData theme = Theme.of(context);
+ final Text description = config.task.completed
+ ? new Text(config.task.description,
+ style: new TextStyle(
+ color: theme.disabledColor,
+ decoration: TextDecoration.lineThrough))
+ : new Text(config.task.description);
+
+ // TODO(jxson): create a flutter issue about the ripple effect going away
+ // when using Dismissable.
+ return new Dismissable(
+ // NOTE: Key needs to change based on completion.
+ key: new ObjectKey("${config.task.uuid}-${config.task.completed}"),
+ direction: DismissDirection.horizontal,
+ onDismissed: _handleDismiss,
+ background: new Container(
+ decoration: new BoxDecoration(
+ backgroundColor: theme.canvasColor,
+ border: new Border(
+ bottom: new BorderSide(color: theme.dividerColor))),
+ child: new ListItem(leading: new Icon(icon: Icons.check))),
+ secondaryBackground: new Container(
+ decoration: new BoxDecoration(
+ backgroundColor: theme.canvasColor,
+ border: new Border(
+ bottom: new BorderSide(color: theme.dividerColor))),
+ child: new ListItem(trailing: new Icon(icon: Icons.delete))),
+ child: new Container(
+ decoration: new BoxDecoration(
+ backgroundColor: theme.cardColor,
+ boxShadow: kElevationToShadow[2],
+ border: new Border(
+ bottom: new BorderSide(color: theme.dividerColor))),
+ child: new ListItem(
+ isThreeLine: true,
+ title: description,
+ subtitle: new Text(config.task.uuid))));
+ }
+
+ void _handleDismiss(DismissDirection direction) {
+ switch (direction) {
+ // Swipe from right to left to delete.
+ case DismissDirection.endToStart:
+ config.onDelete(config.task.uuid);
+ break;
+ // Swipe from left to right to complete.
+ case DismissDirection.startToEnd:
+ config.onComplete(config.task.uuid);
+ break;
+ default:
+ throw "Not implemented.";
+ break;
+ }
+ }
+}
diff --git a/examples/todos/lib/todo_model.dart b/examples/todos/lib/todo_model.dart
deleted file mode 100644
index 37aecb4..0000000
--- a/examples/todos/lib/todo_model.dart
+++ /dev/null
@@ -1,17 +0,0 @@
-// 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 855c158..0fe0f08 100644
--- a/examples/todos/pubspec.yaml
+++ b/examples/todos/pubspec.yaml
@@ -1,6 +1,12 @@
name: todos
-description: A minimal Flutter project.
+description: A Baku example project.
dependencies:
uuid: 0.5.0
flutter:
- path: ../../../../../../flutter/packages/flutter
+ path: ../../../../../../flutter/packages/flutter
+dev_dependencies:
+ test: any
+ flutter_test:
+ path: ../../../../../../flutter/packages/flutter_test
+ flutter_driver:
+ path: ../../../../../../flutter/packages/flutter_driver