Flutter初心者がFlutterでTodoアプリを作った際に調べてわかったこと

# 前置き
* 最近 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
```

# できたもの
## ソースコード
https://github.com/popy1017/flutter-simple-todo-app

## 画面
![flutter-simple-todo-app.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/526994/05a3cea5-6080-ae79-e099-c3c384138ca3.gif)

* 各Todoは、「タイトル」、「締切日」、「メモ」の3つのフィールドを持つ
* フローティングアクションボタンでTodo新規作成画面に遷移
* 各TodoタップでTodo編集画面に遷移
* 各Todoを左右にスワイプで完了（＝リストから削除）
* 今回はローカルストレージは使わない（＝アプリを停止するとTodoがすべて初期化される）

# 調べてわかったこと
## ファイル・フォルダ構成
こちらの記事を参考にさせていただきました。
https://qiita.com/tanakeiQ/items/2c4a7fcb8e95b9aa55ad

最終的には以下のようなフォルダ構成になりました。

```sh
$ 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でネストが深くなってしまう問題をどのように対応するか - 虎視眈々と](https://www.shogogeek.com/entry/2018/08/13/000000)

以下は上記のブログの引用
> * columを使った時はその中のWidgetは全て別メソッドにする
> * 別メソッドに分けたWidgetは上からViewの順番通りにおく
> * Viewとロジックは明確に分ける
> * できるだけif文やfor文は書かない

## Dart関連
### コンストラクタの定義
Dartでは、コンストラクタのオーバーロードができない(？)かわりに、名前をつけて複数のコンストラクタを定義できる。
[Dartのコンストラクタについて](https://dev.classmethod.jp/client-side/about_dart_constructors/#item_01-02)

```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とか)の場合は後者の記法を使う。

```dart
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を作成する
やり方がネットにたくさん載っているので、ここはそんなに困らなかった。

* [FlutterでListView](https://qiita.com/tasogarei/items/d41e4d0d3cc5dfae4314)
* [リストを表示する | わかりやすいFlutterリファレンス エヌ次元株式会社 (n次元 エヌジゲン)](https://nzigen.com/flutter-reference/2018-04-17-list-view.html)

簡単のためまずは、`itemCount`で要素数を指定し、その分だけ`itemBuilder`で要素を生成する方式を採用。  
※ 最終的には要素を追加したり編集したりする動的なリスト表示が必要になるので、それに対応した方式に変更する必要がある。

```dart
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}");
        },
      ),
    );
  }
}
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/526994/9096760b-772a-7aa4-eebe-5f6c18bac706.png)

## 各要素をカード形式で表示する
リスト表示する各要素を、Todoの各情報を含むCard形式で表示するよう修正する。
Flutterには、カード形式で表示するためのCard ウィジェットが用意されている。
[Card class - material library - Dart API](https://api.flutter.dev/flutter/material/Card-class.html)

Cardウィジェット自体にはタップを検知する機能がないため、タップを検知するためには、<b>InkWell</b>か<b>GestureDetector</b>、または<b>ListTile</b>を使う（他にもあるかも）。
明確な違いはわかっていないが、今回は<b>ListTile</b>を使う。  
> InkWellとGestureDetectorの共通点は、`onTap`や`onLongPress`等のタップを検出する機能を備えていることである。InkWellはタップした際にエフェクトがかかるが、GestureDetectorではそれがない代わりにドラッグ等のより細かい制御ができる。組み合わせて使うこともできるらしい。
https://stackoverflow.com/questions/56725308/flutter-inkwell-vs-gesturedetector-what-is-the-difference

> ListTileはListViewの中でだけ使えるInkWellのテンプレート的な何かだと思っている。

```dart
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,
            )
          );
        },
      ),
    );
  }
}
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/526994/84dde169-aa83-045a-4eb3-eea32e2a9804.png)

## FloatingActionButtonをつける
Todoの新規作成ボタンとして、画面右下に配置するFloatingActionButtonを使う。  
Flutterでは、Scaffoldウィジェットのフィールド[floatingActionButton](https://api.flutter.dev/flutter/material/FloatingActionButton-class.html)を使って簡単にボタンを追加できる。

```dart
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),
      ),
    );
  }
}
```
## 画面遷移
画面遷移の方法についてはこちらの記事参考にさせていただきました。

[Flutter：画面遷移  |  さはらのぶろぐ](https://saharablog.com/mobile-app/flutter-app/navigation1)

ざっくり言うと、Navigator.push(遷移先の画面)で遷移、Navigator.popで戻って来られる。

```dart
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](https://qiita.com/sekitaka_1214/items/17cccc94a9e7150cdf3a)

## 状態管理
Flutterでは状態管理のやり方がたくさんあり、どれを使うか迷っていたところ、下記のブログを発見。
[FlutterのWidget生成のパフォーマンスを改善する３つのポイント｜shogo yamada｜note](https://note.com/shogoyamada/n/n3b752f2adf2e)
どうやら、パフォーマンスの観点から、チュートリアルで出てきた StatefulWidgetとsetState を使うのはやめて、StatelessWidgetの中でStreamBuilderやFutureBuilderを使うべきらしい(setStateでは大きな範囲での再描画になりがちになってしまう)。
再描画しないWidgetにconstつけるとか、細かくWidgetをわけるといった細工をすることで、簡単なアプリだったらsetStateでも対応できそうだったが、StreamBuilderを使った方が個人的にはわかりやすかったのでこちらを採用。
※現状の使い方だと、リストの要素が更新されるたびにリスト全体を再描画している(と思っている)ので、setStateでもStreamBuilderでもパフォーマンスはそんなに変わらなそう。

```dart
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](https://medium.com/flutter-community/using-sqlite-in-flutter-187c1a82e8b) 
後半にBlocパターンとStreamBuilderを使ったサンプルがあります。
#### [Flutterの状態管理いろいろ比較 〜グローバル変数StateからBLoCパターンまで〜 - Qiita](https://qiita.com/agajo/items/50d5d7497d28730de1d3)
複数ある状態管理方法をサンプルコード付きでわかりやすく解説してくださっています。
#### [listview - When removing items from futurebuilder it doesn't update - Stack Overflow](https://stackoverflow.com/questions/57714092/when-removing-items-from-futurebuilder-it-doesnt-update)
最初はFutureBuilderを使っていて動きはしていたけど、削除した要素がちらつく(意味不明)現象に悩んでいたらこちらのQAに出会った。StreamBuilder使おう。


## スワイプして消すやつ
DismissibleというWidgetを使う。

```dart
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でもそんなに難しくはない。