Last active
September 26, 2018 18:10
-
-
Save AlexKorovyansky/066f69bba0b7fa5affed49f4b444e6e4 to your computer and use it in GitHub Desktop.
Blinking todo-app
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'package:cloud_firestore/cloud_firestore.dart'; | |
import 'package:flutter/foundation.dart'; | |
import 'package:flutter/material.dart'; | |
import 'package:firebase_database/firebase_database.dart'; | |
import 'package:flutter/rendering.dart'; | |
import 'package:rxdart/rxdart.dart'; | |
import 'dart:async'; | |
void main() { | |
Firestore.instance.enablePersistence(true); | |
FirebaseDatabase.instance.setPersistenceEnabled(false); | |
runApp(MaterialApp(title: "Happy.do", home: HappyDo())); | |
} | |
class HappyDo extends StatelessWidget { | |
@override | |
Widget build(BuildContext context) { | |
return DefaultTabController( | |
length: 3, | |
child: Scaffold( | |
appBar: AppBar( | |
title: Text('Happy.do'), | |
bottom: TabBar( | |
tabs: [ | |
Tab( | |
text: 'Today', | |
), | |
Tab(text: 'This week'), | |
Tab(text: 'Next week'), | |
], | |
)), | |
body: TabBarView( | |
children: <Widget>[ | |
ReordableTodoListView( | |
stream: Observable<QuerySnapshot>(Firestore.instance | |
.collection('todos') | |
.where('date', isEqualTo: '2018-09-25') | |
.where('weekly', isEqualTo: false) | |
.snapshots()), | |
), | |
ReordableTodoListView( | |
stream: Observable<QuerySnapshot>(Firestore.instance | |
.collection('todos') | |
.where('date', isGreaterThanOrEqualTo: '2018-09-24') | |
.where('date', isLessThanOrEqualTo: '2018-09-30') | |
.where('weekly', isEqualTo: false) | |
.snapshots()), | |
), | |
ReordableTodoListView( | |
stream: Observable<QuerySnapshot>(Firestore.instance | |
.collection('todos') | |
.where('date', isGreaterThanOrEqualTo: '2018-10-01') | |
.where('date', isLessThanOrEqualTo: '2018-10-07') | |
.where('weekly', isEqualTo: true) | |
.snapshots()), | |
), | |
], | |
), | |
floatingActionButton: FloatingActionButton( | |
child: Icon(Icons.add), | |
onPressed: () async { | |
await Firestore.instance.collection('todos').document().setData({ | |
'order': -DateTime.now().millisecondsSinceEpoch.toDouble(), | |
'todo': '', | |
'date': '2018-09-25', | |
'weekly': false, | |
'checked': false | |
}); | |
}, | |
), | |
), | |
); | |
} | |
} | |
class ReordableTodoListView extends StatelessWidget { | |
final Observable<QuerySnapshot> stream; | |
ReordableTodoListView({Key key, @required this.stream}) : super(key: key); | |
@override | |
Widget build(BuildContext context) { | |
return StreamBuilder<List<DocumentSnapshot>>( | |
stream: stream.map((s) => s.documents | |
..sort((d1, d2) => | |
((d1.data['order'] ?? 0) - (d2.data['order'] ?? 0)).toInt())), | |
builder: (context, documents) { | |
if (documents.data == null) return Container(); | |
final orderMutation = List<double>(); | |
// If we want to drag&drop element X between elements Y&Z | |
// that's enough to change only X.order to (Y.order+Z.order)*0.5 | |
// | |
// If we want to move element to first or to last position | |
// we use -current_time and +current as not the ideal but | |
// prototype valid solution. | |
// | |
// The approach itself is inspired by Trello API. | |
for (var i = 0; i < documents.data.length - 1; i++) { | |
final order1 = documents.data[i]['order'] ?? 0.0; | |
final order2 = documents.data[i + 1]['order'] ?? 0.0; | |
orderMutation.add((order1 + order2) * 0.5); | |
} | |
orderMutation.insert(0, -DateTime.now().millisecondsSinceEpoch.toDouble()); | |
orderMutation.insert(orderMutation.length, | |
DateTime.now().millisecondsSinceEpoch.toDouble()); | |
debugPrint('hackList = $orderMutation'); | |
documents.data.toList().fold<List<double>>( | |
[-DateTime.now().millisecondsSinceEpoch.toDouble()].toList(), | |
(list, order) { | |
return list; | |
}); | |
return Container( | |
child: ReorderableListView( | |
padding: EdgeInsets.all(8.0), | |
onReorder: (oldIndex, newIndex) async { | |
debugPrint('$oldIndex, $newIndex'); | |
await Firestore.instance | |
.collection('todos') | |
.document(documents.data[oldIndex].documentID) | |
.updateData({'order': orderMutation[newIndex]}); | |
}, | |
children: documents.data | |
.map((document) { return TodoWidget( | |
key: ValueKey(document.documentID), | |
checked: document['checked'], | |
text: document['todo'], | |
documentId: document | |
.documentID, //todo: try to pass text & checked here? | |
);}) | |
.toList()), | |
// child: Column( | |
// children: documents.data | |
// .map((document) => TodoWidget( | |
// key: GlobalObjectKey(document.documentID.hashCode), | |
// documentId: document.documentID, | |
// )) | |
// .toList()), | |
); | |
}); | |
} | |
} | |
class TodoListWidget extends StatefulWidget { | |
final String title; | |
final Observable<List<dynamic>> stream; | |
final Function addPressed; | |
TodoListWidget({Key key, this.title, this.stream, this.addPressed}) | |
: super(key: key); | |
@override | |
_TodoListWidgetState createState() => _TodoListWidgetState(); | |
} | |
class _TodoListWidgetState extends State<TodoListWidget> { | |
@override | |
Widget build(BuildContext context) { | |
return LayoutBuilder( | |
builder: (BuildContext context, BoxConstraints constraints) { | |
return Column( | |
crossAxisAlignment: CrossAxisAlignment.start, | |
children: <Widget>[ | |
Padding( | |
padding: const EdgeInsets.only(left: 16.0), | |
child: Row( | |
children: <Widget>[ | |
Flexible( | |
child: Text( | |
widget.title, | |
style: TextStyle( | |
fontSize: 20.0, | |
fontWeight: FontWeight.bold, | |
), | |
), | |
), | |
IconButton( | |
icon: Icon(Icons.add), | |
onPressed: () { | |
widget.addPressed(); | |
}, | |
), | |
], | |
), | |
), | |
StreamBuilder<List<dynamic>>( | |
stream: widget.stream, | |
builder: (context, documents) { | |
if (documents.data == null) return Container(); | |
return Padding( | |
padding: const EdgeInsets.symmetric(vertical: 16.0), | |
child: Column( | |
mainAxisAlignment: MainAxisAlignment.center, | |
children: List<Widget>() | |
..addAll(documents.data | |
.map((documentSnapshot) { | |
return TodoWidget( | |
key: Key(documentSnapshot.documentID), | |
checked: documentSnapshot["checked"], | |
documentId: documentSnapshot.documentID); | |
}) | |
.fold<List<Widget>>( | |
List<Widget>()..add(Text('123')), | |
(current, widget) => | |
current..addAll([widget, Text('123')])) | |
.toList()))); | |
}, | |
), | |
], | |
); | |
}); | |
} | |
} | |
class TodoWidget extends StatefulWidget { | |
final String documentId; | |
final String text; | |
final bool checked; | |
TodoWidget({Key key, this.documentId, this.text = "", this.checked = false}) : super(key: key); | |
@override | |
_TodoWidgetState createState() => _TodoWidgetState(); | |
} | |
class _TodoWidgetState extends State<TodoWidget> { | |
MyTextEditingController outController = MyTextEditingController(text: ''); | |
BehaviorSubject<String> subject; | |
StreamSubscription<DocumentSnapshot> subscription; | |
bool checked = false; | |
@override | |
void initState() { | |
super.initState(); | |
this.checked = widget.checked; | |
this.outController.text = widget.text; | |
debugPrint('initState $widget.documentId, $checked, ${widget.text}'); | |
subscription = Firestore.instance | |
.collection('todos') | |
.document(widget.documentId) | |
.snapshots() | |
.listen((documentSnapshot) { | |
setState(() { | |
outController.text = | |
documentSnapshot.exists ? documentSnapshot["todo"] : "[none]"; | |
checked = documentSnapshot.exists | |
? documentSnapshot["checked"] ?? false | |
: false; | |
}); | |
}); | |
subject = BehaviorSubject<String>(); | |
subject.debounce(Duration(milliseconds: 750)).listen((text) { | |
Firestore.instance | |
.collection('todos') | |
.document(widget.documentId) | |
.updateData({'todo': text, 'checked': checked}); | |
}); | |
} | |
@override | |
void didUpdateWidget(TodoWidget oldWidget) { | |
debugPrint('didUpdateWidget ' + | |
widget.documentId + | |
' from ' + | |
oldWidget.documentId); | |
super.didUpdateWidget(oldWidget); | |
} | |
@override | |
void dispose() { | |
debugPrint('dispose ' + widget.documentId); | |
subject.close(); | |
subscription.cancel(); | |
super.dispose(); | |
} | |
@override | |
Widget build(BuildContext context) { | |
return _buildTodoRow(); | |
} | |
Row _buildTodoRow() { | |
return Row( | |
children: <Widget>[ | |
Checkbox( | |
value: (outController.text.trim() != '') ? checked : false, | |
onChanged: (changedChecked) async { | |
if (outController.text.trim() != '') { | |
// TODO: cancel deferred events on subject | |
await Firestore.instance | |
.collection('todos') | |
.document(widget.documentId) | |
.updateData( | |
{'checked': changedChecked, 'todo': outController.text}); | |
} | |
}, | |
), | |
Flexible( | |
child: TextField( | |
controller: outController, | |
onChanged: (text) { | |
subject.add(text); | |
}, | |
style: ((outController.text.trim() != '') ? checked : false) | |
? TextStyle( | |
color: Colors.black, decoration: TextDecoration.lineThrough) | |
: TextStyle(color: Colors.black), | |
enabled: !(((outController.text.trim() != '') ? checked : false)), | |
decoration: InputDecoration( | |
border: InputBorder.none, hintText: "enter new todo here"), | |
), | |
), | |
IconButton( | |
icon: Icon(Icons.code), | |
), | |
IconButton( | |
icon: Icon(Icons.delete), | |
onPressed: () async { | |
await Firestore.instance | |
.collection('todos') | |
.document(widget.documentId) | |
.delete(); | |
}, | |
) | |
], | |
); | |
} | |
} | |
/// workaround for https://github.com/flutter/flutter/issues/22171 | |
class MyTextEditingController extends TextEditingController { | |
MyTextEditingController({String text}) : super(text: text); | |
set text(String newText) { | |
try { | |
value = value.copyWith( | |
text: newText); | |
} catch (e) { | |
value = value.copyWith( | |
text: newText, | |
selection: TextSelection.collapsed(offset: -1), | |
composing: TextRange.empty); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment