Flutter初心者がメモアプリを作ってみた

# はじめに

Flutter入門としてメモアプリを作成したので記事にしてみました。
Flutterはこんなものだよ！という感じで簡単にまとめているので、
これから始めてみたいという方や初心者の方の参考になればと思います。

# アプリ概要

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1076680/9c7441e1-5354-f2a3-1377-738e7fa5f6d3.png" width=30%>　<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1076680/dc032d87-da2e-9b91-a29e-e2873b512640.png" width=30%>　<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1076680/2ec7e84a-dd1c-218c-8346-635c57d195b9.png" width=30%>

**メモリスト画面（TOP画面）**
| ボタン / アイコン | できること |
|:-|:-:|
| ＋ | メモ追加画面へ遷移 |
| 削除（ゴミ箱） | メモを削除する |
| 編集（えんぴつ） | メモ編集画面へ遷移 |

**メモ追加画面**
| ボタン | できること |
|:-|:-:|
| Save | メモを追加し、メモリスト画面に戻る |
| Cancel | メモの追加をキャンセルし、メモリスト画面に戻る |

**メモ編集画面**
| ボタン | できること |
|:-|:-:|
| Edit | メモを更新し、メモリスト画面に戻る |
| Cancel | メモの更新をキャンセルし、メモリスト画面に戻る |


# アプリ実装
Flutterプロジェクトを作成した際に作られる `main.dart`ファイル に書き込みます。

## 共通処理
```dart
import 'package:flutter/material.dart';
import 'package:flutter_slidable/flutter_slidable.dart';

void main() {
  runApp(MyMemoApp());
}

class MyMemoApp extends StatelessWidget {
  // テーマカラーをブラウンにするために設定
  static Map<int, Color> color = {
    50: Color(0xFFEFEBE9),
    100: Color(0xFFD7CCC8),
    200: Color(0xFFBCAAA4),
    300: Color(0xFFA1887F),
    400: Color(0xFF8D6E63),
    500: Color(0xFF895548),
    600: Color(0xFF6D4C41),
    700: Color(0xFF5D4037),
    800: Color(0xFF4E342E),
    900: Color(0xFF3E2723),
  };
  final MaterialColor primeColor = MaterialColor(0xFFA1887F, color);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      // アプリ名
      title: "Memo App",
      theme: ThemeData(
        // テーマカラー設定
        primarySwatch: primeColor,
        accentColor: primeColor
      ),
      // メモリスト画面を表示
      home: MemoListPage(),
    );
  }
}
```


## 1. メモリスト画面
- メモリストWidget
```dart
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('MemoList'),
      ),
      // データを元にListViewを作成
      body: ListView.builder(
        itemCount: memoList.length,
        itemBuilder: (context, index) {
          // インデックスに対応するメモを取得する
          return Slidable(
            // 右方向にリストアイテムをスライドした場合の設定
            startActionPane: ActionPane(
              ...
              children: [
                SlidableAction(
                  onPressed: (context) async {
                    // スライドして表示された編集ボタンを押下した際の動き
                    ...
                  },
                  backgroundColor: Colors.brown,
                  icon: Icons.edit,
                ),
              ],
            ),
            child: Card(
              child: ListTile(
                title: Text(memoList[index]),
                trailing: IconButton(
                  icon: Icon(Icons.delete),
                  iconSize: 20,
                  onPressed: () async {
                    // リストの削除ボタンを押下した際の処理
                    ...
                  },
                ),
              ),
            ),
          );
        },
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () async {
          // ＋ボタンを押下した際の処理
          // メモ追加画面で追加した際の値をリストへ追加する
          ...
        },
        child: Icon(Icons.add),
      ),
    );
  }
```

<details><summary>コード詳細</summary><div>

```dart
// メモリスト画面用Widget
class MemoListPage extends StatefulWidget {
  @override
  _MemoListPageState createState() => _MemoListPageState();
}

class _MemoListPageState extends State<MemoListPage> {
  // メモデータ
  List<String> memoList = [];

  @override
  void initState() {
    super.initState();
    init();
  }

  // アプリ起動時に保存したデータを読み込む
  void init() async {
    final prefs = await SharedPreferences.getInstance();
    setState(() {
      memoList = prefs.getStringList("memoList")!;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      // AppBarを表示し、タイトルも設定
      appBar: AppBar(
        title: Text("MemoList"),
      ),
      // データを元にListViewを作成
      body: ListView.builder(
        itemCount: memoList.length,
        itemBuilder: (context, index) {
          // インデックスに対応するメモを取得する
          return Slidable(
            // 右方向にリストアイテムをスライドした場合の設定
            startActionPane: ActionPane(
              motion: const ScrollMotion(),
              extentRatio: 0.25,
              children: [
                SlidableAction(
                  onPressed: (context) async {
                    final prefs = await SharedPreferences.getInstance();
                    // "push"で新規画面に遷移
                    // メモ編集画面から渡される値を受け取る
                    final editText = await Navigator.of(context).push(
                      MaterialPageRoute(builder: (context) => MemoEditPage(memoList[index])),
                    );
                    if (editText != null) {
                      // キャンセルした場合は、newListText が null となるため注意
                      setState(() {
                        // リスト追加
                        memoList[index] = editText;
                      });
                    }
                    prefs.setStringList("memoList", memoList);
                  },
                  backgroundColor: Colors.brown,
                  icon: Icons.edit,
                ),
              ],
            ),
            child: Card(
              child: ListTile(
                title: Text(memoList[index]),
                trailing: IconButton(
                  icon: Icon(Icons.delete),
                  iconSize: 20,
                  onPressed: () async {
                    final prefs = await SharedPreferences.getInstance();
                    setState(() {
                      memoList.removeAt(index);
                      prefs.setStringList("memoList", memoList);
                    });
                  },
                ),
              ),
            ),
          );
        },
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () async {
          final prefs = await SharedPreferences.getInstance();
          // "push"で新規画面に遷移
          // メモ追加画面から渡される値を受け取る
          final newListText = await Navigator.of(context).push(
            MaterialPageRoute(builder: (context) {
              // 遷移先の画面としてメモ追加画面を指定
              return MemoAddPage();
            }),
          );
          if (newListText != null) {
            // キャンセルした場合は、newListText が null となるため注意
            setState(() {
              // リスト追加
              memoList.add(newListText);
            });
          }
          prefs.setStringList("memoList", memoList);
        },
        child: Icon(Icons.add),
      ),
    );
  }
}
```
</div></details>

## 2. メモ追加画面
- メモ追加Widget
```dart
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('MemoAdd'),
      ),
      body: Container(
        ...
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            // 入力されたテキストを表示
            Text(_text, style: TextStyle(color: Colors.brown[300])),
            const SizedBox(height: 8),
            // テキスト入力箇所
            TextField(
              onChanged: (String value) {
                setState(() {
                  // データを変更したことを知らせる(画面を更新する)
                  _text = value;
                });
              },
            ),
            const SizedBox(height: 8),
            Container(
              ...
              // Saveボタン
              child: ElevatedButton(
                onPressed: () {
                  // ボタン押下で前の画面に戻り、前の画面にデータを渡す
                  Navigator.of(context).pop(_text);
                },
                child: Text('Save', style: TextStyle(color: Colors.white)),
              ),
            ),
            const SizedBox(height: 8),
            Container(
              ...
              // Cancelボタン
              child: TextButton(
                onPressed: () {
                  // ボタンを押下で前の画面に戻る
                  Navigator.of(context).pop();
                },
                child: Text('Cancel'),
              ),
            ),
          ],
        ),
      ),
    );
  }
```

<details><summary>コード詳細</summary><div>

```dart
// メモリスト追加画面用Widget
class MemoAddPage extends StatefulWidget {
  @override
  _MemoAddPageState createState() => _MemoAddPageState();
}

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

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

## 3. メモ編集画面
- メモ編集Widget
```dart
  @override
  Widget build(BuildContext context) {
    // 初期値を設定する
    final controller = TextEditingController(text: _text);
    // TextEditingControllerに設定したテキストのlengthをカーソルの位置に設定する
    controller.selection = TextSelection.fromPosition(
      TextPosition(offset: controller.text.length),
    );

    return Scaffold(
      appBar: AppBar(
        title: Text('MemoEdit'),
      ),
      body: Container(
        // メモ追加画面と同じ処理のため省略
        ...
      ),
    );
  }
```

<details><summary>コード詳細</summary><div>

```dart
// メモリスト編集画面用Widget
class MemoEditPage extends StatefulWidget {
  MemoEditPage(this.text);
  String text;
  @override
  _MemoEditPageState createState() => _MemoEditPageState();
}

class _MemoEditPageState extends State<MemoEditPage> {
  String _text = "";

  @override
  void initState() {
    super.initState();
    // 画面遷移時に渡された値を初期値に設定する
    _text = widget.text;
  }

  // データを元に表示するWidget
  @override
  Widget build(BuildContext context) {
    // 初期値を設定する
    final controller = TextEditingController(text: _text);
    // TextEditingControllerに設定したテキストのlengthをカーソルの位置に設定する
    controller.selection = TextSelection.fromPosition(
      TextPosition(offset: controller.text.length),
    );

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


# さいごに
今回は簡単にアプリの動きを紹介したいため、全てを`main.dart`ファイルに記載してしまったので、フォルダ構成を見直して機能ごとにファイルを分ける必要がありますね。。

また、アプリを終了した際にデータをローカルに保持しておく方法を別の記事で紹介したいと思います！


# 参考サイト
- [Todoアプリ概要 | Flutterで始めるアプリ開発](https://www.flutter-study.dev/todo-app/about-todo-app)
- [flutter_slidable | Flutter Package - Pub.dev](https://pub.dev/packages/flutter_slidable)
- [【Flutter】リストの要素を横にスライドさせたい](https://zenn.dev/ryota_iwamoto/articles/slidable_list_like_iphone_mail)