Commit 9820b154 authored by Brian Egan's avatar Brian Egan

Vanilla app working with a good amount of functionality to test different concepts

parent 35867dba
File added
......@@ -7,4 +7,5 @@ pubspec.lock
# Directory created by dartdoc
doc/api/
.idea
\ No newline at end of file
.idea
/intl_en.arb
.PHONY: clean localize all check_env
FILES := $(shell find . -name '*.arb' | xargs)
all: localize
localize: check_env clean
pub run intl_translation:extract_to_arb --output-dir=./ --no-transformer lib/src/localization.dart
mv intl_messages.arb intl_en.arb
pub run intl_translation:generate_from_arb lib/src/localization.dart $(FILES)
mv messages*.dart lib/src/localizations
clean:
rm -f *.arb
check_env:
ifndef FLUTTER_ROOT
$(error FLUTTER_ROOT is undefined. Please export a FLUTTER_ROOT pointing to the installation of Flutter.)
endif
\ No newline at end of file
import 'package:flutter/material.dart';
import 'package:flutter_mvc/flutter_mvc.dart';
import 'package:vanilla/data/todos_service.dart';
import 'package:vanilla/models.dart';
import 'package:vanilla/screens/add_edit_screen.dart';
import 'package:vanilla/screens/tabs_screen.dart';
import 'package:vanilla/localization.dart';
class VanillaApp extends StatefulWidget {
final TodosService service;
VanillaApp({this.service});
@override
State<StatefulWidget> createState() {
return new VanillaAppState();
}
}
class VanillaAppState extends State<VanillaApp> {
AppState appState = new AppState.loading();
@override
void initState() {
super.initState();
widget.service.loadTodos().then((loadedTodos) {
setState(() {
appState = new AppState(todos: loadedTodos);
});
}).catchError((err) {
setState(() {
appState.isLoading = false;
});
});
}
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: new VanillaLocalizations().appTitle,
theme: FlutterMvcTheme.theme,
localizationsDelegates: [
new ArchitectureLocalizationsDelegate(),
new VanillaLocalizationsDelegate(),
],
routes: {
FlutterMvcRoutes.home: (context) {
return new TabsScreen(
appState: appState,
updateFiler: updateFilter,
updateTodo: updateTodo,
updateTab: updateTab,
addTodo: addTodo,
removeTodo: removeTodo,
toggleAll: toggleAll,
clearCompleted: clearCompleted,
);
},
FlutterMvcRoutes.addTodo: (context) {
return new AddEditScreen(
addTodo: addTodo,
updateTodo: updateTodo,
);
},
},
);
}
void toggleAll() {
setState(() {
appState.toggleAll();
});
}
void clearCompleted() {
setState(() {
appState.clearCompleted();
});
}
void updateTab(AppTab tab) {
setState(() {
appState.activeTab = tab;
});
}
void updateFilter(VisibilityFilter filter) {
setState(() {
appState.activeFilter = filter;
});
}
void addTodo(Todo todo) {
setState(() {
appState.todos.add(todo);
});
}
void removeTodo(Todo todo) {
setState(() {
appState.todos.remove(todo);
});
}
void updateTodo(
Todo todo, {
bool complete,
String id,
String note,
String task,
}) {
setState(() {
todo.complete = complete ?? todo.complete;
todo.id = id ?? todo.id;
todo.note = note ?? todo.note;
todo.task = task ?? todo.task;
});
}
@override
void setState(VoidCallback fn) {
super.setState(fn);
widget.service.saveTodos(appState.todos);
}
}
import 'dart:async';
import 'dart:io';
import 'dart:convert';
import 'package:flutter_mvc/src/models.dart';
import 'package:path_provider/path_provider.dart';
import 'package:vanilla/models.dart';
/// Create an abstract class that we can use for both the concrete
/// implementation below and a Mock implementation for testing.
abstract class FileStorage {
Future<List<Todo>> loadTodos();
Future<File> saveTodos(List<Todo> todos);
Future<FileSystemEntity> clean();
}
class FlutterMvcFileStorage {
/// Loads and saves a List of Todos using a text file stored on the device.
class VanillaFileStorage implements FileStorage {
final String tag;
FlutterMvcFileStorage(this.tag);
VanillaFileStorage(this.tag);
Future<AppState> loadAppState() async {
Future<List<Todo>> loadTodos() async {
final file = await _getLocalFile();
final string = await file.readAsString();
return AppState.fromJson(new JsonDecoder().convert(string));
return AppState.fromJson(new JsonDecoder().convert(string)).todos;
}
Future saveAppState(AppState state) async {
Future<File> saveTodos(List<Todo> todos) async {
final file = await _getLocalFile();
return file.writeAsString(new JsonEncoder().convert(state.toJson()));
return file.writeAsString(
new JsonEncoder().convert(new AppState(todos: todos).toJson()));
}
Future<File> _getLocalFile() async {
......@@ -27,4 +37,10 @@ class FlutterMvcFileStorage {
return new File('${dir.path}/FlutterMvcFileStorage__$tag.json');
}
Future<FileSystemEntity> clean() async {
final file = await _getLocalFile();
return file.delete();
}
}
import 'dart:async';
import 'dart:core';
import 'package:vanilla/data/file_storage.dart';
import 'package:vanilla/data/web_service.dart';
import 'package:vanilla/models.dart';
/// A class that glues together our
class TodosService {
final FileStorage fileStorage;
final WebService webService;
TodosService({this.fileStorage, this.webService});
/// Loads todos first from File storage. If they don't exist or encounter an
/// error, it attempts to load the Todos from a Web Service.
Future<List<Todo>> loadTodos() async {
try {
return (await fileStorage.loadTodos());
} catch (e) {
return webService.fetchTodos();
}
}
/// Attempts to persist the given todos to File Storage and the Web service.
/// Returns true if everything worked, false if not.
Future<bool> saveTodos(List<Todo> todos) async {
try {
// Run the operations in parallel.
await Future.wait([
fileStorage.saveTodos(todos),
webService.postTodos(todos),
]);
return true;
} catch (e) {
return false;
}
}
}
import 'dart:async';
import 'package:vanilla/models.dart';
/// A class that is meant to represent a Web Service you would call to fetch
/// and persist Todos to and from the cloud.
///
/// Since we're trying to keep this example simple, it is a Mock implementation.
class WebService {
WebService();
/// Mock that "fetches" some Todos from a "web service" after a short delay
Future<List<Todo>> fetchTodos() async {
return new Future.delayed(
new Duration(milliseconds: 1200),
() => [
new Todo('Hey', note: 'Ho, let\'s go!'),
new Todo('Wonderwall', complete: true),
new Todo('Everything', note: 'in its right place'),
new Todo('Cheeseburger in Paradise'),
new Todo('If you like', note: 'Pina Coladas', complete: true),
]);
}
/// Mock that returns true or false for success or failure. In this case,
/// it will "Always Succeed"
Future<bool> postTodos(List<Todo> todos) async {
return new Future.value(true);
}
}
import 'dart:async';
import 'package:flutter/material.dart';
class VanillaLocalizations {
static VanillaLocalizations of(BuildContext context) {
return Localizations.of<VanillaLocalizations>(
context, VanillaLocalizations);
}
String get appTitle => "Vanilla Example";
}
class VanillaLocalizationsDelegate
extends LocalizationsDelegate<VanillaLocalizations> {
@override
Future<VanillaLocalizations> load(Locale locale) =>
new Future(() => new VanillaLocalizations());
@override
bool shouldReload(VanillaLocalizationsDelegate old) => false;
}
This diff is collapsed.
import 'dart:math';
import 'package:flutter_mvc/flutter_mvc.dart';
class AppState {
final List<Todo> todos;
final VisibilityFilter activeFilter;
final bool isLoading;
final AppTab activeTab;
VisibilityFilter activeFilter;
AppTab activeTab;
bool isLoading;
List<Todo> todos;
AppState({
this.todos = const [],
this.activeFilter = VisibilityFilter.all,
this.isLoading = false,
this.activeTab = AppTab.todos,
this.isLoading = false,
this.todos = const [],
});
factory AppState.loading(List<Todo> todos, VisibilityFilter activeFilter) =>
new AppState(
todos: todos,
activeFilter: activeFilter,
isLoading: true,
);
AppState copyWith({
List<Todo> todos,
VisibilityFilter activeFilter,
bool isLoading,
AppTab activeTab,
}) =>
new AppState(
todos: todos ?? this.todos,
activeFilter: activeFilter ?? this.activeFilter,
isLoading: isLoading ?? this.isLoading,
activeTab: activeTab ?? this.activeTab,
);
// AppState toggleAll() {
// final allCompleted = this.allComplete;
//
// return copyWith(
// todos:
// todos.map((todo) => todo.copyWith(complete: !allCompleted)).toList(),
// );
// }
//
// AppState toggleOne(Todo todo, bool isComplete) {
// return copyWith(
// todos: todos
// .map((t) => t == todo ? t.copyWith(complete: isComplete) : t)
// .toList(),
// );
// }
//
// AppState clearCompleted() =>
// copyWith(todos: todos.where((todo) => !todo.complete).toList());
bool get hasTodos => todos.isNotEmpty;
factory AppState.loading() => new AppState(isLoading: true);
bool get allComplete => todos.every((todo) => todo.complete);
bool get allActive => todos.every((todo) => !todo.complete);
bool get hasCompletedTodos => !allActive;
int get numCompleted =>
todos.fold(0, (sum, todo) => todo.complete ? ++sum : sum);
int get numActive =>
todos.fold(0, (sum, todo) => !todo.complete ? ++sum : sum);
List<Todo> get filteredTodos => todos.where((todo) {
if (activeFilter == VisibilityFilter.all) {
return true;
......@@ -77,6 +27,41 @@ class AppState {
}
}).toList();
bool get hasCompletedTodos => todos.any((todo) => todo.complete);
@override
int get hashCode =>
todos.hashCode ^
activeFilter.hashCode ^
isLoading.hashCode ^
activeTab.hashCode;
int get numActive =>
todos.fold(0, (sum, todo) => !todo.complete ? ++sum : sum);
int get numCompleted =>
todos.fold(0, (sum, todo) => todo.complete ? ++sum : sum);
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is AppState &&
runtimeType == other.runtimeType &&
todos == other.todos &&
activeFilter == other.activeFilter &&
isLoading == other.isLoading &&
activeTab == other.activeTab;
void clearCompleted() {
todos.removeWhere((todo) => todo.complete);
}
void toggleAll() {
final allCompleted = this.allComplete;
todos.forEach((todo) => todo.complete = !allCompleted);
}
Map<String, Object> toJson() {
return {
"todos": todos.map((todo) => todo.toJson()).toList(),
......@@ -85,6 +70,11 @@ class AppState {
};
}
@override
String toString() {
return 'AppState{todos: $todos, activeFilter: $activeFilter, isLoading: $isLoading, activeTab: $activeTab}';
}
static AppState fromJson(Map<String, Object> json) {
final todos = (json["todos"] as List<Map<String, Object>>)
.map((todoJson) => Todo.fromJson(todoJson))
......@@ -93,7 +83,10 @@ class AppState {
final activeTab = getTabFromString(json["activeTab"] as String);
return new AppState(
todos: todos, activeFilter: activeFilter, activeTab: activeTab);
todos: todos,
activeFilter: activeFilter,
activeTab: activeTab,
);
}
static VisibilityFilter getFilterFromString(String activeFilterAsString) {
......@@ -116,54 +109,41 @@ class AppState {
return AppTab.todos;
}
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is AppState &&
runtimeType == other.runtimeType &&
todos == other.todos &&
activeFilter == other.activeFilter &&
isLoading == other.isLoading &&
activeTab == other.activeTab;
@override
int get hashCode =>
todos.hashCode ^
activeFilter.hashCode ^
isLoading.hashCode ^
activeTab.hashCode;
@override
String toString() {
return 'AppState{todos: $todos, activeFilter: $activeFilter, isLoading: $isLoading, activeTab: $activeTab}';
void addTodo(Todo todo) {
todos.add(todo);
}
AppState addTodo(Todo todo) {
return copyWith(
todos: ([]
..addAll(todos)
..add(todo)));
void removeTodo(Todo todo) {
todos.remove(todo);
}
}
AppState updateTodo(Todo old, Todo replacement) {
return copyWith(
todos: todos.map((todo) => todo == old ? replacement : todo).toList());
}
enum AppTab { todos, stats }
AppState removeTodo(Todo todoToRemove) {
return copyWith(
todos: todos.where((todo) => todo != todoToRemove).toList());
}
}
enum ExtraAction { toggleAllComplete, clearCompleted }
class Todo {
bool complete;
String task;
String note;
String id;
String note;
String task;
Todo(this.task, {this.complete = false, this.note = '', String id})
: this.id = id ?? (new Uuid().generateV4());
: this.id = id ?? new Uuid().generateV4();
@override
int get hashCode =>
complete.hashCode ^ task.hashCode ^ note.hashCode ^ id.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is Todo &&
runtimeType == other.runtimeType &&
complete == other.complete &&
task == other.task &&
note == other.note &&
id == other.id;
Map<String, Object> toJson() {
return {
......@@ -174,6 +154,11 @@ class Todo {
};
}
@override
String toString() {
return 'Todo{complete: $complete, task: $task, note: $note, id: $id}';
}
static Todo fromJson(Map<String, Object> json) {
return new Todo(
json["task"] as String,
......@@ -182,62 +167,6 @@ class Todo {
id: json["id"] as String,
);
}
@override
String toString() {
return 'Todo{complete: $complete, task: $task, note: $note, id: $id}';
}
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is Todo &&
runtimeType == other.runtimeType &&
complete == other.complete &&
task == other.task &&
note == other.note &&
id == other.id;
@override
int get hashCode =>
complete.hashCode ^ task.hashCode ^ note.hashCode ^ id.hashCode;
}
/// A UUID generator. This will generate unique IDs in the format:
///
/// f47ac10b-58cc-4372-a567-0e02b2c3d479
///
/// The generated uuids are 128 bit numbers encoded in a specific string format.
///
/// For more information, see
/// http://en.wikipedia.org/wiki/Universally_unique_identifier.
class Uuid {
final Random _random = new Random();
/// Generate a version 4 (random) uuid. This is a uuid scheme that only uses
/// random numbers as the source of the generated uuid.
String generateV4() {
// Generate xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx / 8-4-4-4-12.
final int special = 8 + _random.nextInt(4);
return '${_bitsDigits(16, 4)}${_bitsDigits(16, 4)}-'
'${_bitsDigits(16, 4)}-'
'4${_bitsDigits(12, 3)}-'
'${_printDigits(special, 1)}${_bitsDigits(12, 3)}-'
'${_bitsDigits(16, 4)}${_bitsDigits(16, 4)}${_bitsDigits(16, 4)}';
}
String _bitsDigits(int bitCount, int digitCount) =>
_printDigits(_generateBits(bitCount), digitCount);
int _generateBits(int bitCount) => _random.nextInt(1 << bitCount);
String _printDigits(int value, int count) =>
value.toRadixString(16).padLeft(count, '0');
}
enum AppTab { todos, stats }
enum VisibilityFilter { all, active, completed }
enum ExtraAction { toggleAllComplete, clearCompleted }
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_mvc/flutter_mvc.dart';
import 'package:vanilla/models.dart';
import 'package:vanilla/widgets/typedefs.dart';
class AddEditScreen extends StatelessWidget {
static final GlobalKey<FormState> formKey = new GlobalKey<FormState>();
static final GlobalKey<FormFieldState<String>> taskKey =
new GlobalKey<FormFieldState<String>>();
static final GlobalKey<FormFieldState<String>> noteKey =
new GlobalKey<FormFieldState<String>>();
final Todo todo;
final TodoAdder addTodo;
final TodoUpdater updateTodo;
AddEditScreen({
Key key,
@required this.addTodo,
@required this.updateTodo,
this.todo,
})
: super(key: key);
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text(isEditing
? ArchitectureLocalizations.of(context).editTodo
: ArchitectureLocalizations.of(context).addTodo),
),
body: new Padding(
padding: new EdgeInsets.all(16.0),
child: new Form(
key: formKey,
autovalidate: false,
onWillPop: () {
return new Future(() => true);
},
child: new ListView(
children: [
new TextFormField(
initialValue: todo != null ? todo.task : '',
key: taskKey,
autofocus: isEditing ? false : true,
style: Theme.of(context).textTheme.headline,
decoration: new InputDecoration(
hintText:
ArchitectureLocalizations.of(context).newTodoHint),
validator: (val) => val.trim().isEmpty
? ArchitectureLocalizations.of(context).emptyTodoError
: null,
),
new TextFormField(
initialValue: todo != null ? todo.note : '',
key: noteKey,
maxLines: 10,
style: Theme.of(context).textTheme.subhead,
decoration: new InputDecoration(
hintText: ArchitectureLocalizations.of(context).notesHint,
),
)
],
),
),
),
floatingActionButton: new FloatingActionButton(
tooltip: isEditing
? ArchitectureLocalizations.of(context).saveChanges
: ArchitectureLocalizations.of(context).addTodo,
child: new Icon(isEditing ? Icons.check : Icons.add),
onPressed: () {
final form = formKey.currentState;
if (form.validate()) {
final task = taskKey.currentState.value;
final note = noteKey.currentState.value;
if (isEditing) {
updateTodo(todo, task: task, note: note);
} else {
addTodo(new Todo(
task,
note: note,
));
}
Navigator.pop(context);
}
}),
);
}
bool get isEditing => todo != null;
}
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_mvc/flutter_mvc.dart';
import 'package:vanilla/models.dart';
import 'package:vanilla/screens/add_edit_screen.dart';
import 'package:vanilla/widgets/typedefs.dart';
class DetailScreen extends StatelessWidget {
final Todo todo;
final Function onDelete;
final TodoAdder addTodo;