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万文字を超える超大作になっています(笑)

---

# はじめに
## 概要
* アイデアマキネッタが楽しかったので、2つ目のアプリをリリースしました。
* 引き続き、完全独学できれいな実装でないかもしれませんが、誰かの役に立てばと思い、使ったWidgetやtipsなどを公開したいと思います
    * アプリ「<font color='blue'><b><ins>この日何の日??</ins></b></font>」で使っているものを10個紹介します
        * 前作の「[アイデアマキネッタ](https://qiita.com/kazutxt/items/08cf01c46eadc2b9a2e8)」についてはこちらです
        * 前作に含まれているものは除いて紹介します

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

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

## 関連記事
* [Flutter系の記事のまとめ](https://qiita.com/kazutxt/items/524acb1c134015682ccf)
* [Flutterの"Widget of the Week"がめっちゃ勉強になったので、まとめてみた](https://qiita.com/kazutxt/items/10355961ee2fa3a65023)
* [Flutterでアイデアを混ぜるアプリを作ってみた](https://qiita.com/kazutxt/items/08cf01c46eadc2b9a2e8)
    * <font color='red'><b><ins>前回の10個のノウハウはこちら</ins></b></font>

## ダウンロード先
* [この日何の日??(GooglePlay)](https://play.google.com/store/apps/details?id=com.kazutxt.whatstoday)
* [この日何の日??(AppStore)](https://apps.apple.com/us/app/%E3%81%93%E3%81%AE%E6%97%A5%E4%BD%95%E3%81%AE%E6%97%A5/id1537287010)

## スタート(前提条件)
* 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.1
```

---

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

## アプリ紹介
<img width="400" alt="pics1.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/259228/c874356f-1c87-d735-24d1-6e65781bb655.png">

* トップ画面では、本日の日付と今年の残り日数が表示されます
* 探したい日付を選び、ボタンを押すとその日について調べられます
* データは自動で取得されます。データは西暦の降順で表示されます
* 誕生日を入れると、リスト画面で何才のときか表示されます

<img width="400" alt="pics2.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/259228/d1471b18-1354-ea28-bae5-dc83d40c6704.png">

* 右側のアイコンで用語をgoogle/wikipediaで確認できます

<img width="400" alt="pics3.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/259228/76a77f2d-06f7-0e1b-70e4-ae951b7a0d2a.png">

* 画面右上のメニューから、項目の表示/非表示を変更できます
* 画面上部のアイコンから、30日前/1日前/任意選択/1日後/30日後を選べます

<img width="400" alt="pics4.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/259228/1f1277a2-074a-f41f-b13a-017cab2edeb8.png">

* ロングタップで内容をコピーできます(Androidのみ)

## 使われている機能

### 1. タップでブラウザを開く
* タップで目的のURLをブラウザで開きます
* urlLauncherのパッケージを使います
    * このパッケージはメールや電話など他にもいろいろ使えるようです

#### イメージ
<img width="400" alt="pic1.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/259228/6eacb7c6-00d9-098b-fce5-5f8b60874bd0.png">

#### ソース
``` pubspec.yaml
url_launcher: ^5.7.8
```

``` dart
import 'package:url_launcher/url_launcher.dart';
/* 中略 */
IconButton(
    icon: /*リンクを貼るアイコン*/,
    onPressed: () async {
        // cdata.linkには検索対象の文字列が入っています
        if (cdata.link.isNotEmpty) {
            String url = Uri.encodeFull(
                "https://www.google.co.jp/search?q=${cdata.link}");
            if (await canLaunch(url)) {
                await launch(url);
            }
        }
    }),
```

### 2. 多様なアイコンを使う
* AwesomeFontのパッケージを使います

#### イメージ
<img width="200" alt="pic2.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/259228/471a73f0-1e77-5d90-be36-4157f19a34a1.png">

#### ソース
``` pubspec.yaml
font_awesome_flutter: "^8.8.1"
``` 
``` dart
Icon(FontAwesomeIcons.gift, color: Colors.teal) // プレゼントマーク
Icon(FontAwesomeIcons.birthdayCake, color: Colors.pinkAccent) // 誕生日ケーキ
Icon(FontAwesomeIcons.bible, color: Colors.brown) // 聖書
Icon(FontAwesomeIcons.wikipediaW, color: Colors.lime) // Wikipedia
Icon(FontAwesomeIcons.globeAsia, color: Colors.cyan) // 地球儀
``` 

### 3. データを保存する
* SharedPreferenceのパッケージを使います

#### イメージ
<img width="200" alt="pic3.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/259228/d23217f2-1b45-2008-889c-5b4d636f798f.png">

#### ソース
``` pubspec.yaml
shared_preferences: ^0.5.12+4
```
``` dart
import 'package:shared_preferences/shared_preferences.dart';
class SaveData {
  // 保存
  static saveBarth(int year, int month, int day) async {
    SharedPreferences prefs = await SharedPreferences.getInstance();
    prefs.setInt('BARTH_YEAR', year);
    prefs.setInt('BARTH_MONTH', month);
    prefs.setInt('BARTH_DAY', day);
  }
  // 読み出し
  static loadBarth() async {
    SharedPreferences prefs = await SharedPreferences.getInstance();
    int year = prefs.getInt('BARTH_YEAR') ?? 2000;
    int month = prefs.getInt('BARTH_MONTH') ?? 1;
    int day = prefs.getInt('BARTH_DAY') ?? 1;
    return [year, month, day];
  }
```
* 保存できる型がある程度決まっているので注意
* 暗号化などはされず、直接見ることもできるとのことなので、セキュリティ的な意味でも注意

### 4. DBデータを使う
* SqlLiteのパッケージを使います

#### イメージ
<img width="200" alt="pic4.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/259228/7e22bed8-a2d2-6993-b55c-a9c7f8230ae5.png">

#### ソース
``` pubspec.yaml
sqflite:
path:
```
* 公式サイトの例でバージョン指定がないため、あえて空欄にしていますが、使うバージョンに合わせて指定をするべきかとおもいます。

``` dart
import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart';

class DbProvider {
  Database db;

  //DBの初期化
  Future<void> init() async {
    final path = join(await getDatabasesPath(), "database.db");
    db = await openDatabase(
      path,
      version: 1,
      onCreate: (Database newDb, int version) {
        // DBがpathに存在しなかった場合に onCreateメソッドが呼ばれます。
        newDb.execute(/* SQL分 */);
        },
    );
  }
  //DBの削除
  deleteDB() async {
    final path = join(await getDatabasesPath(), "idea.db");
    await deleteDatabase(path);
  }

    // DBアクセス
  Future<List<Contents>> selectContents(categoryId) async {
      // contentsテーブルをcategoryIdで取得する例
      // メソッドも準備されているが、複雑なものを書きたい時は生のSQL指定も可能
    List<Map<String, dynamic>> maps;
      maps = await db.query('contents',
          orderBy: "id desc", where: 'categoryId = ?', whereArgs: [categoryId]);
    }
}
```

### 5. プログレスバーで進捗率を表す
* Slackを使って、プログレスと文字を重ねて表現します

#### イメージ
<img width="200" alt="pic5.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/259228/093235c0-1f83-abd2-2b0e-1302d5498e66.png">

#### ソース

``` dart
Stack(children: [
    // プログレスバー
    SizedBox(
        child: LinearProgressIndicator(
        minHeight: 30.0,
        backgroundColor: Colors.blue,
        value: remainingDays,
    )),
    // 進捗率の文字
    Align(
        child: Text(
            "残り${((1 - remainingDays) * 100).toStringAsFixed(1)}%",
            style: TextStyle(
                fontSize: 20, color: Colors.white)),
        alignment: Alignment.center),
]),
```

### 6. サイドバーでスイッチリストを作る
* endDrawerにSwitchListTileを持ったListViewを設定します

#### イメージ
<img width="200" alt="picx.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/259228/99b79c8b-4548-5e45-67d3-b6c8c92ac338.png">

#### ソース
``` dart
endDrawer: Drawer(
  child: ListView(children: <Widget>[
    /* コンテナでヘッダ部分を作る */
    SwitchListTile(
    title: Text('記念日'),
    value: ifl.anivFlag,
    onChanged: (bool value) {
        setState(() {
        ifl.anivFlag = value;
        });
    },
    secondary: Icon(FontAwesomeIcons.gift, color: Colors.teal),
    ),
    /* SwitichListTileが続く */
    /* Dividerなど他のものが含まれてもOK */
    ]),
),
```

### 7. 日付選択をカレンダーではなく、ドラムで行う
* datetime_pickerのライブラリを使います
    * 本来は日付と時間を同時に設定するものですが、通常のdate_pickerはカレンダー形式でドラムのUIを使いたいため、あえて日付だけを使っています

#### イメージ
<img width="200" alt="picy.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/259228/f440c027-20fb-ef32-68e5-52995f77f971.png">

#### ソース

```  pubspec.yaml
flutter_datetime_picker: ^1.4.0
```

``` dart
import 'package:flutter_datetime_picker/flutter_datetime_picker.dart';

IconButton(
  icon: Icon(FontAwesomeIcons.calendarAlt,
  color: Colors.white, size: 30.0),
  onPressed: () {
    DatePicker.showDatePicker(context,
      showTitleActions: true,
      minTime: DateTime(1900, 1, 1),
      maxTime: DateTime(2049, 12, 31), 
      onConfirm: (date) {
        setState(() {
            targetday = date;
        });
        initData(); //新しい日付でデータを再取得する
      },
      currentTime: targetday, locale: LocaleType.jp
    );
  },
)
```

### 8. NowLoading画面を表示する
* データを読み込むまで、読み込み中を表すWidgetを設定し、データの準備が整ったらデータ本体(記念日のリスト)を描画します

#### イメージ
<img width="200" alt="pic8.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/259228/3e81e750-1369-670f-43bc-78cb3395c3fe.png">

#### ソース

``` dart
// 定義
static Widget getNowLoading(context) {
  return Column(children: [
    // CircularProgressIndicatorを使って読み出し中を表す
    Center(
      child: Container(
        padding: EdgeInsets.all(10.0),
        child: SizedBox(
          width: 100,
          height: 100,
          child: CircularProgressIndicator(
            strokeWidth: 10.0,
          )
        )
      )
    ),
    /* 以後文字などを追加 */
  ]);
}

//使う部分
initData() async {
  // 画面ロード時にまずNowLoadingをセット
  setState(() {
    dataViewList = Util.getNowLoading(context);
  });

  /* データを取得する */

  setState(() {
    dataViewList = /* 取得したデータを入れる(=NowLoadingを上書き) */
  });
}


```
### 9.ロングタップで文字をコピーする
* ロングタップでクリップボードに文字列をコピーし、flushbarを1秒だけ表示します
* flushbarはパッケージで実現します
    * SnackBarだと画面下部になり、広告と重なってしまうため、利用を見送りました

#### イメージ
<img width="200" alt="pic9.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/259228/d9dcbe0d-ee90-b044-871c-df8ffadf95a1.png">

#### ソース
```  pubspec.yaml
flushbar: ^1.10.4 
```

``` dart
// listViewのonLongPressイベント
onLongPress: () async {
  // ライブラリがAndroidしか動かない(iOSだとフリーズする)ので条件分岐で避ける
  if (Platform.isAndroid) { 
    // クリップボードに入れる
    final data = ClipboardData(text: cdata.content);
    await Clipboard.setData(data);
    // フラッシューバーを表示する
    Flushbar(
      message: "コピーしたよ",
      flushbarPosition: FlushbarPosition.TOP,
      duration: Duration(seconds: 1),
    )..show(context);
}

```
### 10.ColumnとListを同時に使う
* Listは無限の高さを持ちますが、Listを子に持つColumnはサイズを知らないとエラーとなるため、Expandedで回避しています

#### イメージ

<img width="200" alt="pic10.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/259228/b34ffb8f-4f04-7455-4bcd-7c21efc754d5.png">

#### 何もしない場合のエラー
```
The following assertion was thrown during performResize():
Vertical viewport was given unbounded height.
// 以下省略
```

#### ソース
``` dart
dataViewList = Column(children: [
/* Widgetのリスト */
]),
Expanded(child: createViewList()),
```

## 所感
* ちょっと調べると、色々できてFlutterの生産性や機能追加はかなり良さそうです。

---

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

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

## 追記
* 2020/11/12 初稿