はじめに
Flutter で よく見かけるようなTodoアプリを作成したので、備忘録としていくつかの記事に分けて残します。
今回の記事では全体の構成とタスク一覧画面について書いていきます。
開発環境
- Windows10
- Flutter 3.0.1
- Dart 2.17.1
- Android Studio Chipmunk|2021.2.1
アプリの画面構成
アプリ起動時にログイン画面が表示され、「Firebase Authentication」を使用した認証を行います。
認証が通ったらタスクの一覧画面に遷移します。
登録したタスクは、「Firestore」上にデータが保存されます。

タスク一覧画面
ハンバーガーメニュー
スマートフォンアプリでよく見かける以下のような、ハンバーガーメニューのやり方です。

Drawer
ハンバーガーメニューを実装するには「Drawer」を使います。
「ListTile」を増やすことによって、メニューを追加することができます。
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Home'),
),
drawer: Drawer(
child: ListView(
padding: EdgeInsets.zero,
children: <Widget>[
const DrawerHeader(
decoration: BoxDecoration(
color: Colors.amber,
),
child: Text(
'Todoアプリ',
style: TextStyle(
color: Colors.white,
fontSize: 24,
),
),
),
ListTile(
leading: const Icon(Icons.flag_rounded, color: Colors.grey),
title: const Text('フラグ'),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) {
return TodoListFlagPage(userId: widget.userId);
}),
);
},
),
ListTile(
leading: const Icon(Icons.circle_outlined, color: Colors.grey),
title: const Text('未完了'),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) {
return TodoListNoDonePage(userId: widget.userId);
}),
);
},
),
ListTile(
leading: const Icon(Icons.logout, color: Colors.grey),
title: const Text('ログアウト'),
onTap: () async {
FirebaseAuth.instance.signOut();
Navigator.push(
context,
MaterialPageRoute(builder: (context) {
return const Login();
}),
);
},
),
],
),
),
body: Column(
//以下省略
タスク一覧
タスク一覧画面では「ListTile」を使って実装しています。。

body: Column(
children: [
Expanded(
// FutureBuilder
child: StreamBuilder<QuerySnapshot>(
stream: FirebaseFirestore.instance
.collection('User')
.doc(widget.userId)
.collection('Todo')
.orderBy('date')
.snapshots(),
builder: (context, snapshot) {
// データが取得できた場合
if (snapshot.hasData) {
final List<DocumentSnapshot> documents = snapshot.data!.docs;
// 取得した投稿メッセージ一覧を元にリスト表示
return ListView(
children: documents.map((document) {
return Card(
child: Slidable(
key: ObjectKey(FirebaseFirestore.instance
.collection('User')
.doc(widget.userId)
.collection('Todo')
.doc(document.id)),
startActionPane: ActionPane(
motion: const ScrollMotion(),
dismissible: DismissiblePane(onDismissed: () {
setState(() async {
await FirebaseFirestore.instance
.collection('User')
.doc(widget.userId)
.collection('Todo')
.doc(document.id)
.delete();
});
}),
children: [
SlidableAction(
onPressed: (context) async {
setState(() async {
await FirebaseFirestore.instance
.collection('User') // コレクションID
.doc(widget.userId)
.collection('Todo')
.doc(document.id)
.delete();
});
},
backgroundColor: const Color(0xFFFE4A49),
foregroundColor: Colors.white,
icon: Icons.delete,
label: '削除',
),
SlidableAction(
onPressed: (context) async {
await Navigator.of(context).push(
// タスク編集画面に遷移
MaterialPageRoute(builder: (context) {
return TodoEditPage(
userId: widget.userId,
documentId: document.id,
title: document['title'],
memo: document['memo'],
startDay: document['startDay'].toDate(),
endDay: document['endDay'].toDate());
}),
);
},
backgroundColor: const Color(0xFF21B7CA),
foregroundColor: Colors.white,
icon: Icons.edit,
label: '編集',
),
],
),
child: ListTile(
title: Text(document['title'],
// チェックありの時に、タスクに取り消し線をつける
style: document['done']
? const TextStyle(
decoration: TextDecoration.lineThrough)
: const TextStyle(
decoration: TextDecoration.none)),
leading: Checkbox(
activeColor: Colors.blue,
value: document['done'],
shape: const CircleBorder(),
onChanged: (value) async {
await FirebaseFirestore.instance
.collection('User')
.doc(widget.userId)
.collection('Todo')
.doc(document.id)
.update({'done': !document['done']}); // データ
},
),
// お気に入りの状態で、アイコンを変化させる
trailing: document['flag']
? IconButton(
icon: const Icon(Icons.flag_rounded,
color: Colors.redAccent),
tooltip: 'フラグあり',
onPressed: () async {
await FirebaseFirestore.instance
.collection('User')
.doc(widget.userId)
.collection('Todo')
.doc(document.id)
.update({
'flag': !document['flag']
}); // データ
},
)
: IconButton(
icon: const Icon(Icons.flag_rounded,
color: null),
tooltip: 'フラグなし',
onPressed: () async {
await FirebaseFirestore.instance
.collection('User')
.doc(widget.userId)
.collection('Todo')
.doc(document.id)
.update({
'flag': !document['flag']
}); // データ
},
),
onTap: () async {
await Navigator.of(context).push(
// タスク詳細画面に遷移
MaterialPageRoute(builder: (context) {
return TodoRecordView(
documentId: document.id,
userId: widget.userId,
title: document['title'],
memo: document['memo'],
startDay: document['startDay'].toDate(),
endDay: document['endDay'].toDate());
}),
);
},
),
),
);
}).toList(),
);
}
// データが読込中の場合
return const Center(
child: Text('読込中...'),
);
},
),
),
],
),
おわり
実装は手を動かしながらやるのでさくさく進めることができましたが、記事書くのはつまらないので書くスピードが落ちてしまっているので、次回以降はスムーズに書けるように工夫したいと思ってます。