前置き
- 最近 SwitftUIでTodoアプリを作ってみて、最近流行っている(?)Flutterとはどこが似ていて、どこが違うのか気になったので、同じくTodoアプリを作ってみることに。
- Flutterチュートリアルは経験済み(ほぼ写経)
- Flutterのインストール方法や詳しい使い方は記載しません
開発環境
- MacBookPro Catalina 10.15.2
- Xcode 11.3.1
- VS Code
$ flutter --version
Flutter 1.12.13+hotfix.5 • channel stable •
https://github.com/flutter/flutter.git
Framework • revision 27321ebbad (5 weeks ago) • 2019-12-10 18:15:01 -0800
Engine • revision 2994f7e1e6
Tools • Dart 2.7.0
できたもの
ソースコード
画面
- 各Todoは、「タイトル」、「締切日」、「メモ」の3つのフィールドを持つ
- フローティングアクションボタンでTodo新規作成画面に遷移
- 各TodoタップでTodo編集画面に遷移
- 各Todoを左右にスワイプで完了(=リストから削除)
- 今回はローカルストレージは使わない(=アプリを停止するとTodoがすべて初期化される)
調べてわかったこと
ファイル・フォルダ構成
こちらの記事を参考にさせていただきました。
https://qiita.com/tanakeiQ/items/2c4a7fcb8e95b9aa55ad
最終的には以下のようなフォルダ構成になりました。
$ tree ./lib
./lib
├── components
│ ├── app.dart
│ ├── todo_edit
│ │ └── todo_edit_view.dart # 新規作成・編集画面
│ └── todo_list
│ └── todo_list_view.dart # Todoリスト画面
├── configs
│ └── const_text.dart
├── main.dart
├── models
│ └── todo.dart
└── repositories
└── todo_bloc.dart
ネスト深くなってしまう問題
こちらのブログ記事がとても参考になりました。
Flutterでネストが深くなってしまう問題をどのように対応するか - 虎視眈々と
以下は上記のブログの引用
- columを使った時はその中のWidgetは全て別メソッドにする
- 別メソッドに分けたWidgetは上からViewの順番通りにおく
- Viewとロジックは明確に分ける
- できるだけif文やfor文は書かない
Dart関連
コンストラクタの定義
Dartでは、コンストラクタのオーバーロードができない(?)かわりに、名前をつけて複数のコンストラクタを定義できる。
Dartのコンストラクタについて
class Todo {
String id;
String title;
DateTime dueDate;
String note;
Todo(this.title, this. dueDate, this.note);
// クラス名.関数名 で異なるコンストラクタを定義できる
Todo.newTodo() {
title = "";
dueDate = DateTime.now();
note = "";
}
}
文字列内での変数展開
文字列内の変数展開は$変数名
か、${変数名}
を使う。メンバ変数等(todo.titleとか)の場合は後者の記法を使う。
final String name = "apple";
print("This is $name"); // -> "This is apple"
print("This is ${name}"); // -> "This is apple"
日付型
Dartの日付型はDateTimeクラス。DateTime.now()で現在時刻のインスタンスを取得できる。
アクセス制御
Dartだと _(アンダーバー) で始まる名前で宣言したクラスとか変数名はprivateになる。
引数の渡し方
基本的には参照渡しとなる。
値渡しだと思い込んで値を更新したりするとわけわからにことになるので注意。
(あまり確かなソースが見つからなかったので間違っていたらすいません。)
要素をリスト形式で並べる
複数の要素をリスト形式で表示するためのListViewを作成する
やり方がネットにたくさん載っているので、ここはそんなに困らなかった。
簡単のためまずは、itemCount
で要素数を指定し、その分だけitemBuilder
で要素を生成する方式を採用。
※ 最終的には要素を追加したり編集したりする動的なリスト表示が必要になるので、それに対応した方式に変更する必要がある。
class TodoListView extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(ConstText.todoListView)),
body: ListView.builder(
itemCount: 10,
itemBuilder: (BuildContext context, int index) {
Todo todo = Todo(
"タイトル $index",
DateTime.now(),
"タイトル $index のメモです。タイトル $index のメモです。"
);
return Text("${todo.title}");
},
),
);
}
}
各要素をカード形式で表示する
リスト表示する各要素を、Todoの各情報を含むCard形式で表示するよう修正する。
Flutterには、カード形式で表示するためのCard ウィジェットが用意されている。
Card class - material library - Dart API
Cardウィジェット自体にはタップを検知する機能がないため、タップを検知するためには、InkWellかGestureDetector、またはListTileを使う(他にもあるかも)。
明確な違いはわかっていないが、今回はListTileを使う。
InkWellとGestureDetectorの共通点は、
onTap
やonLongPress
等のタップを検出する機能を備えていることである。InkWellはタップした際にエフェクトがかかるが、GestureDetectorではそれがない代わりにドラッグ等のより細かい制御ができる。組み合わせて使うこともできるらしい。
https://stackoverflow.com/questions/56725308/flutter-inkwell-vs-gesturedetector-what-is-the-difference
ListTileはListViewの中でだけ使えるInkWellのテンプレート的な何かだと思っている。
class TodoListView extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(ConstText.todoListView)),
body: ListView.builder(
itemCount: 10,
itemBuilder: (BuildContext context, int index) {
Todo todo = Todo(
"タイトル $index",
DateTime.now(),
"タイトル $index のメモです。タイトル $index のメモです。"
);
// 以下を変更
return Card(
child: ListTile(
onTap: (){}, //TODO Tapしたときの動作は後で実装
title: Text("${todo.title}"),
subtitle: Text("${todo.note}"),
trailing: Text("${todo.dueDate.toLocal().toString()}"),
isThreeLine: true,
)
);
},
),
);
}
}
FloatingActionButtonをつける
Todoの新規作成ボタンとして、画面右下に配置するFloatingActionButtonを使う。
Flutterでは、ScaffoldウィジェットのフィールドfloatingActionButtonを使って簡単にボタンを追加できる。
class TodoListView extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(ConstText.todoListView)),
body: ListView.builder(
itemCount: 10,
itemBuilder: (BuildContext context, int index) {
Todo todo = Todo(
"タイトル $index",
DateTime.now(),
"タイトル $index のメモです。タイトル $index のメモです。"
);
return Card(
child: ListTile(
onTap: (){}, //TODO Tapしたときの動作は後で実装
title: Text("${todo.title}"),
subtitle: Text("${todo.note}"),
trailing: Text("${todo.dueDate.toLocal().toString()}"),
isThreeLine: true,
)
);
},
),
// 以下を追加
floatingActionButton: FloatingActionButton(
onPressed: (){}, // ボタンを押した時の処理は後で実装
child: Icon(Icons.add, size: 40),
),
);
}
}
画面遷移
画面遷移の方法についてはこちらの記事参考にさせていただきました。
ざっくり言うと、Navigator.push(遷移先の画面)で遷移、Navigator.popで戻って来られる。
class TodoListView extends StatelessWidget {
// 画面遷移のための関数
_moveToEditView() => Navigator.push(
context,
MaterialPageRoute(builder: (context) => TodoEditView())
);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(ConstText.todoListView)),
body: ListView.builder(
itemCount: 10,
itemBuilder: (BuildContext context, int index) {
Todo todo = Todo(
"タイトル $index",
DateTime.now(),
"タイトル $index のメモです。タイトル $index のメモです。"
);
return Card(
child: ListTile(
onTap: (){ _moveToEditView(); }, // <= 追記
title: Text("${todo.title}"),
subtitle: Text("${todo.note}"),
trailing: Text("${todo.dueDate.toLocal().toString()}"),
isThreeLine: true,
)
);
},
),
// 以下を追加
floatingActionButton: FloatingActionButton(
onPressed: (){ _moveToEditView(); }, // <= 追記
child: Icon(Icons.add, size: 40),
),
);
}
}
TextFieldにラベル付けるやつ
こちらの記事を参考にさせていただきました。
flutterでTextFieldにヒントやラベルを表示する - Qiita
状態管理
Flutterでは状態管理のやり方がたくさんあり、どれを使うか迷っていたところ、下記のブログを発見。
FlutterのWidget生成のパフォーマンスを改善する3つのポイント|shogo yamada|note
どうやら、パフォーマンスの観点から、チュートリアルで出てきた StatefulWidgetとsetState を使うのはやめて、StatelessWidgetの中でStreamBuilderやFutureBuilderを使うべきらしい(setStateでは大きな範囲での再描画になりがちになってしまう)。
再描画しないWidgetにconstつけるとか、細かくWidgetをわけるといった細工をすることで、簡単なアプリだったらsetStateでも対応できそうだったが、StreamBuilderを使った方が個人的にはわかりやすかったのでこちらを採用。
※現状の使い方だと、リストの要素が更新されるたびにリスト全体を再描画している(と思っている)ので、setStateでもStreamBuilderでもパフォーマンスはそんなに変わらなそう。
class TodoListView extends StatelessWidget {
@override
Widget build(BuildContext context) {
final _bloc = Provider.of<TodoBloc>(context, listen: false);
return Scaffold(
appBar: AppBar(title: Text(ConstText.todoListView)),
body: StreamBuilder<List<Todo>>(
stream: _bloc.todoStream, // <= こいつに変更があるとここだけ再描画してくれる
builder: (BuildContext context, AsyncSnapshot<List<Todo>> snapshot) {
if (snapshot.hasData) {
return ListView.builder(
itemCount: snapshot.data.length,
itemBuilder: (BuildContext context, int index) {
Todo todo = snapshot.data[index];
// 略
}
他に参考にしたサイト
Using SQLite in Flutter - Flutter Community - Medium
後半にBlocパターンとStreamBuilderを使ったサンプルがあります。
Flutterの状態管理いろいろ比較 〜グローバル変数StateからBLoCパターンまで〜 - Qiita
複数ある状態管理方法をサンプルコード付きでわかりやすく解説してくださっています。
listview - When removing items from futurebuilder it doesn't update - Stack Overflow
最初はFutureBuilderを使っていて動きはしていたけど、削除した要素がちらつく(意味不明)現象に悩んでいたらこちらのQAに出会った。StreamBuilder使おう。
スワイプして消すやつ
DismissibleというWidgetを使う。
class TodoListView extends StatelessWidget {
@override
Widget build(BuildContext context) {
final _bloc = Provider.of<TodoBloc>(context, listen: false);
return Scaffold(
appBar: AppBar(title: Text(ConstText.todoListView)),
body: StreamBuilder<List<Todo>>(
stream: _bloc.todoStream,
builder: (BuildContext context, AsyncSnapshot<List<Todo>> snapshot) {
if (snapshot.hasData) {
return ListView.builder(
itemCount: snapshot.data.length,
itemBuilder: (BuildContext context, int index) {
Todo todo = snapshot.data[index];
return Dismissible( // スワイプして消すやつ
key: Key(todo.id),
background: _backgroundOfDismissible(),
secondaryBackground: _secondaryBackgroundOfDismissible(),
onDismissed: (direction) {
_bloc.delete(todo.id); // ちゃんとデータからも消さないといけない
},
child: Card(
child: ListTile(
onTap: (){
_moveToEditView(context, _bloc, todo);
},
title: Text("${todo.title}"),
subtitle: Text("${todo.note}"),
trailing: Text("${todo.dueDate.toLocal().toString()}"),
isThreeLine: true,
)
),
);
},
);
}
return Center(child: CircularProgressIndicator());
},
),
floatingActionButton: FloatingActionButton(
onPressed: (){ _moveToCreateView(context, _bloc); },
child: Icon(Icons.add, size: 40),
),
);
}
所感
- コードでUIを作っていく感じがかなりSwiftUIと似ている
- SwiftUIより公式ドキュメントや解説記事が多いので、詰まっても解決しやすい
- (両方でTodoアプリを作ってみて)SwiftUIは変なバグ(TextFieldとかDatePickerとか)が多い、Flutterではあんまり出会わなかった。
- 状態管理はどちらも理解は難しい。使うだけならそんなに難しくはない。
- 画面遷移はFlutterのほうが楽そう。SwiftUIでもそんなに難しくはない。