この記事は
Flutter #2 Advent Calendar 2019 の21日目です。
前回は
こちら
次回は
こちら
1日遅れてすいません。しかも解説は追記予定です。
ソースコードだけは置くので参考になれば幸いです。
(追記2020/2/16)
github にプロジェクトコードを置きました。
Firebaseで
デモ を置きました。
最近Dartを使っているので、Flutterを再び勉強するモチベーションが出てまいりました。Flutterは
以前少しだけ触って放置していた ので、改めてキャッチアップしてみると、なんとFlutter for Webというのがあったので興味本位で触ってみることにしました。
そういうわけでFlutter for Webで簡単なTodoリストを作ります。
やること
開発するときにみた情報源の話。作り始めのころの雰囲気を記録したいと思います。なので読みにくい点もあると思いますが、その時はもし良ければ「読みにくい」ってコメントをお願いします🙇♂️
やらないこと
設計周りの話はしません。今はクリーンなアーキテクチャよりもプログラミングを楽しむことに価値を置きたいと思います。
どういうのを作るのか
仕様を決めます。
ユーザーはタスクリストを見ることができる。
ユーザーはタスク名を入力し、タスクリストに追加できる。
ユーザーはタスクリストから終わったタスクをワンクリックで消すことができる。
ユーザーはタスクリストに書いたタスクを次回も見ることができる。
足りなければ追加するということで、とりあえずこれで行きます。
あとは画面のモックを作っておきます。
ハイクオリティな画面モック
では取り掛かっていきます。
環境構築
公式 の方法に従って導入します。
現在はbetaチャンネルなので今後stableに変わると思います。
というわけでプロジェクトを作ります。
flutter create --org com.akior todolist_app
cd todolist_app
code .
VSCodeであらかじめ設定しておけば これでプロジェクトで開けるはずです。
あと、VSCodeは
Flutterの拡張設定 は必要です。
flutter run -d chrome
これでweb画面が起動します。
ちなみにVSCodeからも起動できます。
Touch Barの実行ボタンでも起動できます。白い三角が実行、中抜き三角がデバッグ実行。
lib/main.dartを編集していきます。
完成
タスクを追加する処理です。
ユーザーはタスク名を入力し、タスクリストに追加できる
タスクを削除する処理です。
ユーザーはタスクリストから終わったタスクをワンクリックで消すことができる
更新しても画面は保持されたままです。
ユーザーはタスクリストに書いたタスクを次回も見ることができる
ここにソースコードを示します。
This file contains 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:flutter/material.dart';
import 'dart:html';
void main() => runApp(TodoApp());
class TodoApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: Scaffold(
appBar: AppBar(
title: Text('ToDoリスト'),
),
body: TodoList(),
),
);
}
}
class TodoList extends StatefulWidget {
@override
_TodoListState createState() => _TodoListState();
}
class _TodoListState extends State<TodoList> {
final GlobalKey<AnimatedListState> _listKey = GlobalKey();
List<TodoTask> todoList = List();
final myController = TextEditingController();
WebStrage strage = WebStrage();
@override
void initState() {
super.initState();
if (strage.getTitleList() == null || strage.getTitleList().isEmpty) {
return;
}
List<String> titleList = strage.getTitleList().split(',');
titleList.forEach((f) => todoList.add(TodoTask.initialize(f)));
}
@override
Widget build(BuildContext context) {
return Container(
child: Column(
children: <Widget>[
Card(
child: ListTile(
title: TextField(
controller: myController,
),
trailing: IconButton(
onPressed: () {
_addItems(
myController.text != '' ? myController.text : 'no title');
_saveToLocalStrage();
myController.clear();
},
icon: Icon(Icons.add_circle),
),
),
),
Expanded(
child: AnimatedList(
key: _listKey,
initialItemCount: todoList.length,
itemBuilder: (context, index, animation) {
return _buildItem(todoList[index], animation, index);
},
),
),
],
),
);
}
@override
void dispose() {
myController.dispose();
super.dispose();
}
Widget _buildItem(TodoTask todoTask, Animation animation, num index) {
return SizeTransition(
sizeFactor: animation,
child: _createCard(todoTask, index),
);
}
Widget _createCard(TodoTask todoTask, num index) {
return Card(
child: ListTile(
leading: InkWell(
splashColor: Colors.greenAccent,
borderRadius: BorderRadius.circular(45.0),
onTap: () {
todoTask.checkIn();
_removeItems(todoTask, index);
_saveToLocalStrage();
},
child: todoTask.isDone
? Icon(
Icons.check_circle,
color: Colors.greenAccent,
)
: Icon(Icons.check_circle),
),
title: Text(todoTask._title),
),
);
}
void _saveToLocalStrage() {
List<String> titleList = [];
todoList.forEach((f) => titleList.add(f._title));
strage.save(titleList);
}
void _addItems(String title) {
todoList.insert(0, TodoTask.initialize(title));
_listKey.currentState.insertItem(0);
}
void _removeItems(TodoTask todoTask, num index) {
TodoTask removeItem = todoList.removeAt(index);
AnimatedListRemovedItemBuilder builder = (context, animation) {
return _buildItem(removeItem, animation, index);
};
_listKey.currentState.removeItem(index, builder);
}
}
class TodoTask {
String _title;
bool _isDone;
TodoTask.initialize(this._title) {
_isDone = false;
}
String get title => _title;
bool get isDone => _isDone;
void checkIn() {
_isDone = true;
}
}
class WebStrage {
final Storage _storage = window.localStorage;
void save(List<String> titleList) {
_storage['todo'] = titleList.join(',');
}
String getTitleList() => _storage['todo'];
void invalidate() {
_storage.remove('todo');
}
}
解説
使ったのは
AnimatedList と
WebStorage です。
アプリならSharedPreferencesを使うところですが、WebなのでWebStorageを使うことができました。使うにはdart:htmlをインポートする必要があります。
おわりに
Flutter for Webで簡単なToDo リストを作成しました🎉
Flutterのお作法で悩みながら楽しく作れたと思います。Widgetツリーを意識しながら構造的に書くことは勉強になりました。
課題として、
ネットワークの要素を取り入れる(Firebaseなどと通信する)
PWAのように使えるのか調査する(hi fiveみたいにキャッシュにためたあと通信する機構など)
オリジナルのデザインをつかう
があげられそうです。
余談
最初にFlutter for Webを書き始めたときは12月始めで、そのときはdevチャンネルでした。
なので12日のFlutter Interactでbetaチャンネルに移行したときは飲み会で一芸を楽しみにしてる感じの声が出ました。
Flutterにsearch.array().