【Flutter/Dart】情報管理アプリを作ろう

# 1.はじめに
これまでに7つのAndroidStudio&Flutter&Dart記事を書いている。以下は内容、

- 【Flutter/Dart】環境構築方法の紹介
https://qiita.com/my_programming/items/ad1493e3fef2fb63e036

- 【Flutter/Dart】簡単なプロジェクトの設定と共有方法の紹介
https://qiita.com/my_programming/items/cec09574a3b6e39be277

- 【Flutter/Dart】エミュレータの言語設定をかえよう
https://qiita.com/my_programming/items/6c67fed22c624fd9494b

- 【Flutter/Dart】Dartの文法について学ぼう
https://qiita.com/my_programming/items/9ba25114ef217d077ca5

- **【Flutter/Dart】タスク管理アプリを作ろう（今回説明する内容）**
- 【Flutter/Dart】Google地図アプリを作ろう
https://qiita.com/my_programming/items/26b9ac6f0d2b3d1bd766

- 【Flutter/Dart】情報管理アプリ＋Google地図アプリを作ろう
https://qiita.com/my_programming/items/5bd1bb7b789b642635b4

今回はflutterアプリの入門編として情報管理アプリを作成する。
-ボタンを押すと登録画面が開く機能
-登録画面で記入したタスクがトップ画面でリスト表示される機能

# 2.情報管理アプリを作ろう
## 2-1.完成イメージ
アプリの完成イメージと操作方法を説明する。
①プロジェクトを実行するとタスクの描画画面が開く。タスクを追加するためにプラスボタンを押す。
<img width="100" alt="task_app0.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/892339/a1728e8a-e096-cfe1-036b-95edb53beff7.png">

②テキストフィールドにタスク名称を書き、リスト追加ボタンを押す。
<img width="100" alt="task_app1.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/892339/5336ee4c-d352-ebde-db1c-2adb33f8b125.png">

③3回追加した後の画面が以下。タスクが追加した順番に表示されている。
<img width="100" alt="task_app2.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/892339/08b86ecc-83cc-cb85-b231-e098fc7160a5.png">

## 2-2.コードの説明
①まず、task_appという名前の新しいプロジェクトを作成する。
<img width="300" alt="task_app3.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/892339/c899baf6-0f9e-11f0-8f19-05959ad9f320.png">

②libフォルダにあるmain.dartファイルを開く。以下のコードを記述。これから作るアプリの全コードを含む。
<br>

``` main.dart
import 'package:flutter/material.dart';

void main() {
  // 最初に表示するWidget
  runApp(MyTodoApp());
}

class MyTodoApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      // 右上に表示される"debug"ラベルを消す
      debugShowCheckedModeBanner: false,
      // アプリ名
      title: 'My Todo App',
      theme: ThemeData(
        // テーマカラー
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      // リスト一覧画面を表示
      home: TodoListPage(),
    );
  }
}

// リスト一覧画面用Widget
class TodoListPage extends StatefulWidget {
  @override
  _TodoListPageState createState() => _TodoListPageState();
}

class _TodoListPageState extends State<TodoListPage> {
  // Todoリストのデータ
  List<String> todoList = [];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      // AppBarを表示し、タイトルも設定
      appBar: AppBar(
        title: Text('リスト一覧'),
      ),
      // データを元にListViewを作成
      body: ListView.builder(
        itemCount: todoList.length,
        itemBuilder: (context, index) {
          return Card(
            child: ListTile(
              title: Text(todoList[index]),
            ),
          );
        },
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () async {
          // "push"で新規画面に遷移
          // リスト追加画面から渡される値を受け取る
          final newListText = await Navigator.of(context).push(
            MaterialPageRoute(builder: (context) {
              // 遷移先の画面としてリスト追加画面を指定
              return TodoAddPage();
            }),
          );
          if (newListText != null) {
            // キャンセルした場合は newListText が null となるので注意
            setState(() {
              // リスト追加
              todoList.add(newListText);
            });
          }
        },
        child: Icon(Icons.add),
      ),
    );
  }
}

class TodoAddPage extends StatefulWidget {
  @override
  _TodoAddPageState createState() => _TodoAddPageState();
}

class _TodoAddPageState extends State<TodoAddPage> {
  // 入力されたテキストをデータとして持つ
  String _text = '';

  // データを元に表示するWidget
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('リスト追加'),
      ),
      body: Container(
        // 余白を付ける
        padding: EdgeInsets.all(64),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            // 入力されたテキストを表示
            Text(_text, style: TextStyle(color: Colors.blue)),
            const SizedBox(height: 8),
            // テキスト入力
            TextField(
              // 入力されたテキストの値を受け取る（valueが入力されたテキスト）
              onChanged: (String value) {
                // データが変更したことを知らせる（画面を更新する）
                setState(() {
                  // データを変更
                  _text = value;
                });
              },
            ),
            const SizedBox(height: 8),
            Container(
              // 横幅いっぱいに広げる
              width: double.infinity,
              // リスト追加ボタン
              child: ElevatedButton(
                onPressed: () {
                  // "pop"で前の画面に戻る
                  // "pop"の引数から前の画面にデータを渡す
                  Navigator.of(context).pop(_text);
                },
                child: Text('リスト追加', style: TextStyle(color: Colors.white)),
              ),
            ),
            const SizedBox(height: 8),
            Container(
              // 横幅いっぱいに広げる
              width: double.infinity,
              // キャンセルボタン
              child: TextButton(
                // ボタンをクリックした時の処理
                onPressed: () {
                  // "pop"で前の画面に戻る
                  Navigator.of(context).pop();
                },
                child: Text('キャンセル'),
              ),
            ),
          ],
        ),
      ),
    );
  }
}
```

## 2-2.main.dartのコード解説
本アプリではStatelessWidgetとStatefulWidgetの両方を使っている。
<img width="200" alt="task_app4.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/892339/d1175f42-ed04-e360-e559-5a07ffa41b53.png">
StatelessWidgetはアプリで最初に1度だけ実行され、1度構築されると状態を変更できない。つまり、変数、アイコン、ボタンを変更したり、データを取得したりしても、アプリの状態を変更することはできない。StatefulWidgetはアプリで何度でも実行される。データを受信したときに外観を変更したり、トリガーされたイベントに応じて外観を変更したりできることを意味する。

<img width="200" alt="task_app5.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/892339/987422df-9237-8a33-2e62-6b78d84287e0.png">

最初にStatelessWidgetで初期画面を描画した後、タスクの追加と画面への反映などにはStatefulWidgetで処理を実行する。本アプリでは1つのStatelessWidgetのクラスと4つのStatefulWidgetのクラスを利用する。StatelessWidgetは呼び出されるとbuild()が実行されてStatelessWidgetの内容が1度だけ処理される。StatefulWidgetは呼び出されるとcreateState()がまず実行された後、build()が実行されてStatefulWidgetの内容が処理される。

参考）StatefulWidgetが2つに分かれている理由をしっかりと理解したい方は[こちら](https://tech-rise.net/why-stateful-widget-separates-two-classes/)を一読！


実行単位であるWidgetをツリー構造で示すと以下の様になる。MyTodoApp WidgetはMaterialApp Widgetを呼び出しThemeData WidgetとStatefulWeidgetであるTodoListPage Widgetを呼び出す。
<img width="100" alt="task_app6.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/892339/19b6c0d1-9da3-1146-bd10-eaa0f1d6f47e.png">

TodoListPageはScaffold Widgetを呼び出した後、タイトルバーであるAppBar Widgetとタスクをリスト表示するListView Widgetとタスク追加画面を表示するためのFloatingActionButton Widgetを呼び出す。ListView Widgetではリスト表示に適したCard Widget とListTile Widgetを使って簡単に整ったUIを作成する。FloatingActionButton Widgetはもう一つのStatefulWeidgetであるTodoAddPage Widgetを呼び出す。
<img width="200" alt="task_app7.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/892339/666a30aa-ac71-0b08-7186-c1eb1910f197.png">

TodoAddPage WidgetではScaffold Widgetを呼び出した後、タイトルバーであるAppBar Widgetとタスク追加を行うためのUIのレイアウトを縦に連ねる表示形式とするためColumn Widgetを呼び出す。Column WidgetはText, SizeBo, TextField, Containerが図のように複数並べて描かれる。
<img width="370" alt="task_app8.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/892339/3c362145-1faf-739e-e0e4-0dbe8f974d79.png">

それでは情報管理アプリを作るためにStatelessWidgetを使ってホーム画面を作成しよう。最初のコードの実行結果は以下。
<img width="100" alt="task_app9.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/892339/7496aa6c-d3b6-28e5-17eb-1fb8eb84288a.png">
最初に必要なコードは以下。StatelessWidgetを使ってMyTodoAppという名前のクラスを作成。戻り値にMaterialApp Widgetを書き内部でリスト一覧という文字を表示。MaterialApp Widgetの内部はテスト用なので後で削除する。
``` main.dart
import 'package:flutter/material.dart';

void main() {
  // 最初に表示するWidget
  runApp(MyTodoApp());
}

class MyTodoApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      // ここは後で消す
      home: Scaffold(
        appBar: AppBar(
          title: Text('リスト一覧'),
        ),
      )
      // 右上に表示される"debug"ラベルを消す
      // debugShowCheckedModeBanner: false,
      // アプリ名
      // title: 'My Todo App',
      // theme: ThemeData(
      //   // テーマカラー
      //   primarySwatch: Colors.blue,
      //   visualDensity: VisualDensity.adaptivePlatformDensity,
      // ),
      // リスト一覧画面を表示
      // home: TodoListPage(),
    );
  }
}
```
次にStatefulWidgetを使ってTodoListPageのクラスを作成する。新たに追加したコードを含むアプリの実行結果は以下。
<img width="100" alt="task_app10.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/892339/44d27f6f-ea37-6c41-0fd3-cbc9c77b7412.png">
MyTodoApp Widgetの中身をStatefulWidgetのTodoListPageを呼び出せるように書き換える。また、アプリ画面右上のdebug表記をなくすために`debugShowCheckedModeBanner: false`を入れておく。
``` main.dart
import 'package:flutter/material.dart';

void main() {
  // 最初に表示するWidget
  runApp(MyTodoApp());
}

class MyTodoApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      // 右上に表示される"debug"ラベルを消す
      debugShowCheckedModeBanner: false,
      // アプリ名
      title: 'My Todo App',
      theme: ThemeData(
        // テーマカラー
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      // リスト一覧画面を表示
      home: TodoListPage(),
    );
  }
}
```
StatefulWidgetを使ってcreateStateするためのTodoListPageクラスおよびbuildするための_TodoListPageStateクラスの二つを作成。_TodoListPageStateクラスの最初で画面に表示するタスクとして'テスト'と'flutter'と'アンドロイド'を含むtodoList変数を定義後、Scaffold Widget内部でAppBar WidgetとListView Widgetを呼び出し、ListView Widgetの中でCard WidgetとListTile Widgetを使ってtodoList変数を表示。itemCount: todoList.lengthの長さだけ、itemBuilder: (context, index)のindexがインクリメントされてtodoListの中身がひとつずつ画面にTextとして表示される。アプリ画面右下にはFloatingActionButton Widgetがあるものの内容はすべてコメントアウトされている。現在は、FloatingActionButtonを押しても何も実行されない。

``` main.dart
// class MyTodoApp extends StatelessWidget {
// ・・・
// }の続く位置に以下をコピー

// リスト一覧画面用Widget
class TodoListPage extends StatefulWidget {
  @override
  _TodoListPageState createState() => _TodoListPageState();
}

class _TodoListPageState extends State<TodoListPage> {
  // Todoリストのデータ
  List<String> todoList = ['テスト', 'flutter', 'アンドロイド'];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      // AppBarを表示し、タイトルも設定
      appBar: AppBar(
        title: Text('リスト一覧'),
      ),
      // データを元にListViewを作成
      body: ListView.builder(
        itemCount: todoList.length,
        itemBuilder: (context, index) {
          return Card(
            child: ListTile(
              title: Text(todoList[index]),
            ),
          );
        },
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () async {
        //   // "push"で新規画面に遷移
        //   // リスト追加画面から渡される値を受け取る
        //   final newListText = await Navigator.of(context).push(
        //     MaterialPageRoute(builder: (context) {
        //       // 遷移先の画面としてリスト追加画面を指定
        //       return TodoAddPage();
        //     }),
        //   );
        //   if (newListText != null) {
        //     // キャンセルした場合は newListText が null となるので注意
        //     setState(() {
        //       // リスト追加
        //       todoList.add(newListText);
        //     });
        //   }
        },
        child: Icon(Icons.add),
      ),
    );
  }
}

```
最後にStatefulWidgetを使ってTodoAddPageクラスを作成する。新たに追加したコードを含むアプリの実行結果は以下。
<img width="100" alt="task_app11.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/892339/e2819272-ac71-793c-2f57-9c6196144eb2.png">
まず`List<String> todoList = []`となるように、前述のtodoListリストの右辺を空値`[]`で初期化するように書き直す。次にFloatingActionButton Widget内部にあるコメントアウトについて、関数の説明文を除いて全て外す。ボタンが押されるとonPressed Widgetの内部が実行される。TodoAddPage Widgetから追加するタスクのテキスト情報を取得し、`todoList.add(newListText)`でnewListTextをtodoList変数に追加する。
``` main.dart
class _TodoListPageState extends State<TodoListPage> {
  // Todoリストのデータ
  List<String> todoList = [];
  
  //・・・

      floatingActionButton: FloatingActionButton(
        onPressed: () async {
          // "push"で新規画面に遷移
          // リスト追加画面から渡される値を受け取る
          final newListText = await Navigator.of(context).push(
            MaterialPageRoute(builder: (context) {
              // 遷移先の画面としてリスト追加画面を指定
              return TodoAddPage();
            }),
          );
          if (newListText != null) {
            // キャンセルした場合は newListText が null となるので注意
            setState(() {
              // リスト追加
              todoList.add(newListText);
            });
          }
        },
        child: Icon(Icons.add),
      ),

//　・・・

```
StatefulWidgetを使ってcreateStateするためのTodoAddPageクラスおよびbuildするための_TodoAddPageStateクラスの二つを作成。_TodoAddPageStateクラスの最初で_text変数を空値`''`で初期化。Scaffold WidgetにてAppbar WidgetとContainer Widgetを呼び出す。Container WidgetではColumn Widgetを呼び出し、Widgetを記述した順番にUIに反映される用にする。ここではText、SizedBox、TextField、SizedBox、Container（追加ボタン用）、SizedBox、Container（キャンセルボタン用）の各Widgetを縦に並べてUIを表示する。SizedBox Widgetには数字を引数にとることができ、ここでは8ピクセルの隙間を作るために使われる。まずTextField Widgetにて_text変数にテキストを格納し、次にText Widgetに渡されて入力内容がテキストフィールドの上部に表示される。さらに追加ボタンが押されるとElevatedButton WidgetのonPressed内部で_textがFloatingActionButton WidgetのnewListText変数に対して出力される。

``` main.dart
//class _TodoListPageState extends State<TodoListPage> {
//　・・・
// }の続く位置に以下をコピー

class TodoAddPage extends StatefulWidget {
  @override
  _TodoAddPageState createState() => _TodoAddPageState();
}

class _TodoAddPageState extends State<TodoAddPage> {
  // 入力されたテキストをデータとして持つ
  String _text = '';

  // データを元に表示するWidget
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('リスト追加'),
      ),
      body: Container(
        // 余白を付ける
        padding: EdgeInsets.all(64),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            // 入力されたテキストを表示
            Text(_text, style: TextStyle(color: Colors.blue)),
            const SizedBox(height: 8),
            // テキスト入力
            TextField(
              // 入力されたテキストの値を受け取る（valueが入力されたテキスト）
              onChanged: (String value) {
                // データが変更したことを知らせる（画面を更新する）
                setState(() {
                  // データを変更
                  _text = value;
                });
              },
            ),
            const SizedBox(height: 8),
            Container(
              // 横幅いっぱいに広げる
              width: double.infinity,
              // リスト追加ボタン
              child: ElevatedButton(
                onPressed: () {
                  // "pop"で前の画面に戻る
                  // "pop"の引数から前の画面にデータを渡す
                  Navigator.of(context).pop(_text);
                },
                child: Text('リスト追加', style: TextStyle(color: Colors.white)),
              ),
            ),
            const SizedBox(height: 8),
            Container(
              // 横幅いっぱいに広げる
              width: double.infinity,
              // キャンセルボタン
              child: TextButton(
                // ボタンをクリックした時の処理
                onPressed: () {
                  // "pop"で前の画面に戻る
                  Navigator.of(context).pop();
                },
                child: Text('キャンセル'),
              ),
            ),
          ],
        ),
      ),
    );
  }
}
```
以上で情報管理アプリは完成。

発展として、上記のコードにタスクを編集＆削除する機能をつけてみるとより実用的になるだろう。興味のある人は試してみよう。

ここまでに使ったコードをgithubに置いたのでリンクを紹介
https://github.com/MY-CODE-1981/task_app

# 3.まとめ
flutterアプリの入門編として情報管理アプリを作成した。

# 4.スマホアプリ開発コースの概要
https://techgym.jp/about/app_course/