Skip to content

Instantly share code, notes, and snippets.

@AlexKorovyansky
Last active September 26, 2018 18:10
Show Gist options
  • Save AlexKorovyansky/066f69bba0b7fa5affed49f4b444e6e4 to your computer and use it in GitHub Desktop.
Save AlexKorovyansky/066f69bba0b7fa5affed49f4b444e6e4 to your computer and use it in GitHub Desktop.
Blinking todo-app
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