Flutterでアイデアを混ぜるアプリを作った際の10の小技

---
# :book: Flutterの記事を整理し本にしました :book:
https://zenn.dev/kazutxt/books/flutter_practice_introduction

* <font color="red"><b>本稿の記事を含む様々な記事を体系的に整理し本にまとめました</b></font>
* 今後はこちらを最新化するため、<font color="red"><b>最新情報はこちらをご確認くださ</b>い</font>
* 10万文字を超える超大作になっています(笑)

---

# はじめに
## 概要
* 生まれてはじめてスマホアプリを作りました
* 前々から興味はあったのですが、Flutterというものと出会い、初めてリリースまでたどり着きました
* 完全独学で、きれいな実装でないかもしれませんが、誰かの役に立てばと思い、使ったWidgetやtipsなどを公開したいと思います
    * アプリ「<font color='blue'><b><ins>アイデアマキネッタ</ins></b></font>」で使っているものを10個紹介します

* <font color='red'><b><ins>アプリの難易度は低い</ins></b></font>と思います。
    * 画面数は10程度です
    * DBを利用しています
    * バックエンドサーバなど外部との通信はありません。
    * 技術レベルとしては、公式ドキュメントの例＋α程度のレベルだと思います
* 是非、DLしてご意見/ご感想を頂ければと思います
    * 本アプリはアイデア創出を目的にしていますが、使い方によっては、優先度に応じた抽選ツールとしても利用できます

## 対象読者
* これからFlutterでアプリを作ってみたいと思っている方
* Flutterの実アプリでどんな技術が使われているか知りたい方

## 関連記事
* [Flutter系の記事のまとめ](https://qiita.com/kazutxt/items/524acb1c134015682ccf)
* [Flutterの"Widget of the Week"がめっちゃ勉強になったので、まとめてみた](https://qiita.com/kazutxt/items/10355961ee2fa3a65023)

## ダウンロード先
* [アイデアマキネッタ(GooglePlay)](https://play.google.com/store/apps/details?id=com.kazutxt.idea_macchinetta)
* [アイデアマキネッタ(AppStore)](https://apps.apple.com/us/app/%E3%82%A2%E3%82%A4%E3%83%87%E3%82%A2%E3%83%9E%E3%82%AD%E3%83%8D%E3%83%83%E3%82%BF/id1534560453)

## スタート(前提条件)
* Flutterの概念やDartの基本構文を知っていること

## ゴール(達成できること)
* アイデアマキネッタで使われている技術が理解できる

## スコープ外(ふれないもの)
* リリースの仕方や審査を通るためのノウハウ

## 開発環境

* OSのバージョン

``` .sh
macOS Catalina バージョン10.15.7
```
* Flutterのバージョン

``` .sh
Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel stable, 1.22.2, on Mac OS X 10.15.7 19H2, locale ja-JP)
 
[✓] Android toolchain - develop for Android devices (Android SDK version 30.0.1)
[✓] Xcode - develop for iOS and macOS (Xcode 11.7)
[✓] Android Studio (version 4.0)
[✓] IntelliJ IDEA Community Edition (version 2017.1.2)
[✓] VS Code (version 1.50.1)
[!] Connected device
    ! No devices available
```
* アプリバージョン

```
アイデアマキネッタ：1.0.2
```

---

# 本編
## 全体像
まずどんなアプリかを簡単に紹介し、その後使われている技術を紹介します。
実際にアプリをダウンロードして、動作と下記の解説を対比させながらご覧頂くと分かりやすいかと思います。 

## アプリ紹介
<img width="400" alt="pics_1.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/259228/933e2a64-7b2c-1bed-6f07-aaf61828b40b.png">
基本的には「まぜる」ボタンを押すだけで使えます。
画面に予め設定されたアイデアをランダムで表示します。
<img width="400" alt="pics_2.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/259228/2271afc8-371b-c1a0-4cbb-f534b2617125.png">
要素数を変更することもでき、1✕1から5✕5まで増やせます。
<img width="400" alt="pics_3.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/259228/eb8a3eb7-06ff-402c-48bd-ce83b62c822f.png">
カテゴリとアイデアは、あらかじめ設定されていますが、もちろんユーザが増やすこともできます。
優先度や色を付けて、わかりやすく、使いやすくカスタマイズできます。
<img width="400" alt="pics_4.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/259228/0484834a-800d-c472-89bb-c58c8b567eb1.png">
色の選択も直感的に選べます。
<img width="400" alt="pics_5.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/259228/6a819219-1ec4-a7a4-434b-c63a620e184c.png">
カテゴリだけでなく、要素にも優先度がつけれます。

## 使われている機能
### 1. スワイプで画面遷移をする
* スワイプで画面を移動します。
* PageControllerで実現しています。

#### イメージ
<img width="200" alt="pic1.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/259228/f219fc92-b397-1acc-a859-b12ac9ecfc11.png">

#### ソース
``` dart
class _MyHomePageState extends State<MyHomePage> {
  PageController _pageController;
  int _selectedIndex = 1; // 添字は0から始まるため左から2番目が最初

  // 遷移先画面の定義
  var _pages = [
    InfoPage(), //左にスワイプして表示される画面
    MacchinettaPage(), //最初に表示される画面
    CategoryPage(), //右にスワイプして表示される画面
  ];

  @override
  void initState() {
    super.initState();
    _pageController = PageController(initialPage: _selectedIndex);
  }

  @override
  void dispose() {
    super.dispose();
    _pageController.dispose();
  }

  void _onPageChanged(int index) {
    setState(() {
      _selectedIndex = index;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        body: PageView(
            controller: _pageController,
            onPageChanged: _onPageChanged,
            children: _pages));
  }
```

### 2. タップで画面遷移をする
* リストをタップすると別画面に遷移します
* Navigatorを使っています
* 戻る矢印とイベントは特段何もしなくても動作します

#### イメージ
<img width="400" alt="pic2.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/259228/4cdc364b-5e67-130b-7033-79f379c36c59.png">

#### ソース
``` dart
initDb() async {
  categoryList = ListView.builder(
    /* 中略 */
    itemBuilder: (BuildContext context, int i) {
    return _menuItem(cl[i]); // Listの中のイベントとして定義
    }
  );
}

Widget _menuItem(Category c) {
  return Container(
    /* 中略 */
    child: ListTile(
        leading: /* 中略 */
        title:  /* 中略 */
        trailing:  /* 中略 */
        onTap: () async {
          // コンテンツ画面へカテゴリ情報を持って遷移
          await Navigator.of(context)
              .push(MaterialPageRoute(builder: (context) {
            return ContentsPage(c);
          }));
        },
    )
  )
}
```

### 3. サイドバーをつける
* 画面右上のアイコンをタップしたときにサイドバーを出します
*  endDrawerにDrawerを設定します。
*  反対に左側に設定したい場合は、endDrawerではなく、drawerにします

#### イメージ
<img width="400" alt="pic3.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/259228/fb7ba735-9c92-901c-bc4c-68511bb0fc81.png">

#### ソース
``` dart
@override
Widget build(BuildContext context) {
  return Scaffold(
      appBar: /* 中略 */
      endDrawer: Drawer(
        child: ListView(children: <Widget>[
          // デフォルトでは高さが大きすぎるので、自作のContainerで調整
          Container(
              height: 100.0,
              child: DrawerHeader(
                child: Text('アプリ情報',
                    style: TextStyle(
                        fontWeight: FontWeight.bold,
                        fontSize: 20,
                        color: Colors.white)),
                decoration: BoxDecoration(
                  color: Colors.blue,
                ),
              ),
              margin: EdgeInsets.all(0.0),
              padding: EdgeInsets.all(0.0)),
          ListTile(
            title: Text("使い方"),
            trailing: Icon(Icons.arrow_forward),
          ),
          //以下同じように必要な分ListTileを作る
        ]),
      ),
      body: /* 中略 */
      );
}

```

### 4. 右上にゴミ箱を付け、イベントをつける
* 右上にアイコンを置いて、イベントをつけます
* appBarのactionsを使っています
* ダイアログ表示には、showDialogを使っています

#### イメージ
<img width="400" alt="pic4.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/259228/7337249d-c17f-88af-a180-4090b3913356.png">

#### ソース
``` dart
@override
Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(
      title: /* 中略 */
      actions: <Widget>[
          IconButton(
            icon: Icon(FontAwesomeIcons.trashAlt),
            onPressed: _delete,
          )
      ],
    ),
    body: /* 中略 */
  );
}
_delete() async {
  showDialog(
      context: context,
      builder: (BuildContext context) => AlertDialog(
              title: Text("削除してよろしいでしょうか"),
              actions: <Widget>[
                SimpleDialogOption(
                  child: Text("YES"),
                  onPressed: () {
                    Navigator.pop(context, "YES");
                  },
                ),
                SimpleDialogOption(
                  child: Text("NO"),
                  onPressed: () {
                    Navigator.pop(context, "NO");
                  },
                ),
              ])).then<void>((value) async {
    switch (value) {
      case "YES":
        await deleteCategory(); // データの削除を行う処理
        Navigator.of(context).pop();// 元の画面に戻る
        break;
      case "NO":
        break;
    }
  });
}
```

### 5.グリッドのタップで一部分を変える
* グリッドのタップで内容を変更するイベントを仕込みます
* GridTileとInkResponseを使っています

#### イメージ
<img width="400" alt="pic5.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/259228/2bae29f3-d954-7db2-e17c-b92511a0a48f.png">

#### ソース
``` dart
  // tileを作る
  createTile() { 
    /* 中略 (値の取得や 要素数が変わっていないかなどの確認などの処理 */
    // タイルリストを作る
    List<Widget> tileList = [];
    for (int i = 0; i < maxSize; i++) {
      Widget c = Container(
          /* 中略 (色や幅を設定) */
          child: GridTile(
              child: new InkResponse(
                  enableFeedback: true,
                  child: Center(
                      child: Text( /* 表示させる文字 */)
                  ),
                  onTap: () {
                    changeTap(i); // 作り直す
                  },
                  onLongPress: () {
                    showDialog(/* 要素の情報をダイアログ表示する */);
                  })));
      tileList.add(c);
    }
    resultChange(tileList);
  }

  changeTap(int i) async {
    //DBアクセス
    DbProvider dbProvider = new DbProvider();
    await dbProvider.init();

    /* 中略(index番目のコンテンツだけを作り直すロジック)　*/

    // タイルを作り直す
    createTile();
  }

  // 状態を反映させる
  resultChange(result) {
    Widget _glidArea = GridView.count(
      /* 中略 */
      children: result,
    );
    setState(() {
      glidArea = _glidArea;
    });
  }
}
```

### 6.フローティングアクションボタン(FAB)を複数つける
* floatingActionButtonにColumnを使い、複数の要素を設定しています
* 位置なども調整できます

#### イメージ
<img width="200" alt="pic6.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/259228/26e91965-626a-c50a-0e2a-4d041b6fb521.png">

#### ソース
``` dart

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: /* 中略 */
        endDrawer: /* 中略 */
        body: /* 中略 */
        floatingActionButton: Column(
          mainAxisAlignment: MainAxisAlignment.end,
          children: [
            // 上のFAB
            Align(
              child: FloatingActionButton.extended(
                onPressed: createGrid,
                icon: new Icon(Icons.refresh),
                label: Text("まぜる"),
              ),
            ),
            // 下のFAB
            Align(
              alignment: Alignment.bottomRight,
              child: FloatingActionButton(
                onPressed: _diceClick,
                child: rowText,
              ),
            ),
            // 底上げ
            Padding(
              padding: EdgeInsets.only(bottom: 60.0),
            )
          ],
        ));
  }
```

### 7.カラーピッカーを入れる
* パッケージを使い、showDialogと組み合わせてカラーピッカーのUIを構築しています

#### イメージ
<img width="400" alt="pic7.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/259228/6c6daf7d-1045-564c-6228-4dd019c5a968.png">

#### ソース
``` pubspec.yaml
dependencies:
flutter_material_color_picker: ^1.0.5
````


``` dart
showColorDialog() {
  Color bufferColor;
  showDialog(
      context: context,
      builder: (BuildContext context) => AlertDialog(
              title: Text("イメージカラー"),
              content: MaterialColorPicker(
                onColorChange: (Color color) {
                  bufferColor = color;
                },
                selectedColor: imageColor,
              ),
              actions: <Widget>[
                SimpleDialogOption(
                  child: Text("YES"),
                  onPressed: () {
                    Navigator.pop(context, "YES");
                  },
                ),
                SimpleDialogOption(
                  child: Text("NO"),
                  onPressed: () {
                    Navigator.pop(context, "NO");
                  },
                ),
              ])).then<void>((value) {
    switch (value) {
      case "YES":
        imageColor = bufferColor;
        setState(() {
          imageSample = Icon(Icons.check_circle, color: imageColor);
        });
        break;
      case "NO":
        break;
    }
  });
}
```

### 8.条件でウィジェットの表示/非表示を切り替える
* Widgetの構築のなかにif文が使えます
* 条件を満たすときだけ表示されます

#### イメージ
<img width="400" alt="pic8.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/259228/f5e1cb80-78f1-3990-531b-b9248907abc3.png">

``` dart
 Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: /* 中略 */
        actions: <Widget>[
          // カテゴリーIDが-1の時はIconButtonが表示されない
          if (category.id != -1) 
            IconButton(
              icon: Icon(FontAwesomeIcons.trashAlt),
              onPressed: _delete,
            )
        ],
      ),
      body: /* 中略 */
    );
 }

```

### 9.リストの最後にボタンを付ける
* リストの要素として、最後にボタンを追加します。
* リストに組み込みたいため、builderの要素を無理やり増やして追加しています
    * 強引に入れているので、もっと良い方法もあるかと思います。。。

#### イメージ
<img width="200" alt="pic9.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/259228/3aae17f2-ec57-ddb8-2f56-9da421cbad56.png">

#### ソース
``` dart
setState(() {
      categoryList = ListView.builder(
          physics: AlwaysScrollableScrollPhysics(),
          itemCount: cl.length + 1,
          itemBuilder: (BuildContext context, int i) {
            if (i < cl.length)
              return _menuItem(cl[i]);
            else  {
              return 
                  RaisedButton(
                      child: Text("データリセットボタン"),
                      color: Colors.blue,
                      onPressed: _resetClick);
            } 
          });
    });
```

### 10.Markdownで記載した文字を表示する
* パッケージを使い、markdown記法の情報を画面に表示させます
* スクロールなどもさせたいため、小技をいれてUIに配慮しています

#### イメージ
<img width="200" alt="pic10.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/259228/8624aaf8-a34d-c208-a5b5-7497050a56d0.png">

#### ソース
``` pubspec.yaml
dependencies:
  flutter_markdown: ^0.4.4
````


``` dart
Widget build(BuildContext context) {
    return Scaffold(
      appBar: /* 中略 */
      body: SingleChildScrollView(
          // スクロールできるようにしておくのと、パッディングをいれてみやすくしておく
          physics: AlwaysScrollableScrollPhysics(),
          child: Container(
              child: Padding(
                  padding: const EdgeInsets.all(10.0),
                  child: MarkdownBody(data: markdownSource)))),
    );
  }

final String markdownSource = '''
# アイデアマキネッタとは 
/* 中略 */
''';
```



## 所感
* Android開発は敷居が高い印象だったのですが、簡単でした。しかもiOSのアプリも同時に作れてしまうとはFlutterはすごい
    * 学習コストも小さい印象です
    
---

# 補足
## 備考
* ソースコードは可能な限り主要な要素を残すように記載しておりますが、単純コピーでは動作しないものが多いと思いますので、ご自身のコードに合わせてご活用ください
* 自分も勉強とTry&Errorをしながらなので、もっと良い実装方法があるかもしれません。
    * 一例として見ていただけると幸いです

## 参考文献
* [Flutter公式サイト](https://flutter.dev/)
* [アプリの公式サイト](https://smart-simple-tools.kazutxt.com/)

## 追記
* 2020/11/1 初稿/誤字脱字等修正