Dart/FlutterのTips

Flutterを使って個人で以下のスマホ向けのおみくじアプリ（大御心アプリ）を開発・運用しています。

<img width="130" alt="Screenshot_home.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/76958/d4a6102b-53cd-eb2c-f333-5e23392e7f29.png">
<img width="130" alt="Screenshot_fortune_1.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/76958/ccc35e8e-f92e-308a-3fb8-8d194e2edc16.png">
<img width="130" alt="Screenshot_fortune_2.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/76958/c6959d28-9900-9789-84b1-d909607acc40.png">
<img width="130" alt="Screenshot_emperor_list.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/76958/34b47a38-04e2-1ac9-baf8-6da0e8848086.png">
<img width="130" alt="Screenshot_emperor_detail.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/76958/d68400a0-2926-1b1c-764f-76efaa7729e3.png">

[iOS]
https://apps.apple.com/us/app/%E5%A4%A7%E5%BE%A1%E5%BF%83%E3%82%A2%E3%83%97%E3%83%AA/id1627544916

[Android]
https://play.google.com/store/apps/details?id=jp.sikisimanomiti.oomigokoro

上記アプリを開発する過程で地味にはまり何とか解決し得た細かなDart＆FlutterのTipsを忘れないよう随時追記してまとめたいと思います。

1. [日付の書式変換](#日付の書式変換)
1. [テキストを左寄せにする](#テキストを左寄せにする)
1. [BottomNavigationBarにBottomNavigationBarItemが4個以上ある場合消えてしまう](#BottomNavigationBarにBottomNavigationBarItemが4個以上ある場合消えてしまう)
1. [SQLiteでレコード件数をカウントする](#SQLiteでレコード件数をカウントする)
1. [アイコンにBadgeを表示する](#アイコンにBadgeを表示する)


## 日付の書式変換
SQLiteに作成日時として例えば「2022-04-18T00:00:00+09:00」と保存しているが、画面に表示するのは「yyyy/MM/dd(E) HH:mm」の形式で表示したい。

```
import 'package:intl/date_symbol_data_local.dart';
import "package:intl/intl.dart";

String createdAt = '2022-04-18T00:00:00+09:00'

DateFormat('yyyy/MM/dd(E) HH:mm', 'ja_JP').format(DateTime.parse(message.createdAt).toLocal());
```

「2022-04-18T00:00:00+09:00」を「2022/04/18(月) 00:00」へ変換できました。

### 参考
[Dart の DateTime の扱いあれこれ](https://medium.com/flutter-jp/datetime-17e618c8e26e)
[ISO 8601](https://ja.wikipedia.org/wiki/ISO_8601)

## テキストを左寄せにする
テキストを表示するために下記のように実装したが、中央揃えでテキストを表示した。
```
Column(
    children: [
      Text(
          'タイトル',
          style: const TextStyle(
              fontWeight: FontWeight.bold
          ),
          textAlign: TextAlign.left
      ),
      Text('本文', textAlign: TextAlign.left),
    ],
)
```

ググって調べると、textAlignは、Textを含む領域が実際のテキストより大きい場合に、Textを含む領域にテキストを整列します。

そして、Columnの内部では、Textは最小限のスペースしか取らなくて、またColumnはcrossAxisAlignment（デフォルトはcenter）を使用するので、上記実装では中央揃えになってしまいました。

下記のようTextをラップして実装することで意図した整列（今回は左寄せ）ができるようになります。

```
Column(
    children: [
      SizedBox(
        width: double.infinity,
        child: Text(
          'タイトル',
          style: const TextStyle(
              fontWeight: FontWeight.bold
          ),
          textAlign: TextAlign.left
        ),
      ),
      SizedBox(
        width: double.infinity,
        child: Text('本文', textAlign: TextAlign.left),
      )
    ],
)
```

### 参考
[Under which circumstances textAlign property works in Flutter?](https://stackoverflow.com/questions/51638176/under-which-circumstances-textalign-property-works-in-flutter)

## BottomNavigationBarにBottomNavigationBarItemが4個以上ある場合消えてしまう
現在（2022/7/30時点）、アップデートなどを通知するため、新しくお知らせ機能を実装しています。単純にBottomNavigationBarに「お知らせ」アイコンを追加したら「お知らせ」アイコンが消えてしまい、「お知らせ」アイコンをクリックすると、「お知らせ」アイコンは表示するようになりましたが、「ホーム」アイコンが消えてしまいました。

<img width="300" alt="Screenshot_20220730_141632.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/76958/d25fbe3f-3e79-2d71-4c82-d8f5d4d327c5.png">

<img width="300" alt="Screenshot_20220730_141647.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/76958/19cfea77-c65a-700c-8e1d-dcb019a9789b.png">

[公式ドキュメント](https://api.flutter.dev/flutter/material/BottomNavigationBar-class.html)を見ると、

> The bottom navigation bar's type changes how its items are displayed. If not specified, then it's automatically set to BottomNavigationBarType.fixed when there are less than four items, and BottomNavigationBarType.shifting otherwise.
>
> The length of items must be at least two and each item's icon and title/label must not be null.
> > BottomNavigationBarType.fixed, the default when there are less than four items. The selected item is rendered with the selectedItemColor if it's non-null, otherwise the theme's ColorScheme.primary color is used for Brightness.light themes and ColorScheme.secondary for Brightness.dark themes. If backgroundColor is null, The navigation bar's background color defaults to the Material background color, ThemeData.canvasColor (essentially opaque white).
> > 
> > BottomNavigationBarType.shifting, the default when there are four or more items. If selectedItemColor is null, all items are rendered in white. The navigation bar's background color is the same as the BottomNavigationBarItem.backgroundColor of the selected item. In this case it's assumed that each item will have a different background color and that background color will contrast well with white.

とありました。

BottomNavigationBarにBottomNavigationBarItemが4個以上ある（BottomNavigationBarのtypeに指定がない）場合、BottomNavigationBarのtypeが自動的にBottomNavigationBarType.shiftingを設定して、BottomNavigationBarType.shiftingが白色でレンダリングして消えてしまったような状態になっていました。

BottomNavigationBarのtypeにBottomNavigationBarType.fixedを設定して無事「お知らせ」アイコンを表示できるようになりました。

<img width="300" alt="Screenshot_20220730_143612.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/76958/c5c945d3-44c6-9f7b-3d3b-50b21c862bb4.png">


### 参考
[[Flutter]BottomNavigationBarで4つ以上BottomNavigationBarItemがあると白く消えてしまう問題](https://nobushiueshi.com/flutterbottomnavigationbar%E3%81%A74%E3%81%A4%E4%BB%A5%E4%B8%8Abottomnavigationbaritem%E3%81%8C%E3%81%82%E3%82%8B%E3%81%A8%E7%99%BD%E3%81%8F%E6%B6%88%E3%81%88%E3%81%A6%E3%81%97%E3%81%BE%E3%81%86/)


## SQLiteでレコード件数をカウントする
上記アプリではAPIからお知らせのデータを取得してローカルのSQLiteに保存し、既読/未読管理を行っています。未読のお知らせがあれば、未読のお知らせの件数をアイコンにバッジで表示して新着のお知らせがあることをユーザーに分かるように実装しています。

<img width="300" alt="Screenshot_20220730_150341.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/76958/43895cef-6e16-90d2-5487-646b82c2f7bf.png">

SQLiteから未読のお知らせの件数を取得するために、Dart/Flutterでは[sqflite](https://pub.dev/packages/sqflite)パッケージを使って以下のように`Sqflite.firstIntValue`メソッドを使って取得することができました。

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

Future<int?> countUnreadMessage(Database db) async {
  return Sqflite.firstIntValue(await db.rawQuery('SELECT COUNT(*) FROM messages where read_at IS NULL;'));
}
```

### 参考
[How to get the number of rows in a database table with Flutter SQFlite](https://stackoverflow.com/questions/54097847/how-to-get-the-number-of-rows-in-a-database-table-with-flutter-sqflite)


## アイコンにBadgeを表示する
上記のお知らせ機能を実現するために通知アイコンにBadgeで表示してみました。Badgeを表示するために以下のパッケージ

[Badges for Flutter](https://pub.dev/packages/badges)

をインストールしました。しかし、デフォルトの設定のまま使ってしまうと、未読件数が10件や100件あると、アイコンが隠れてしまいました。

<img width="300" alt="Screenshot_20220731_004523.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/76958/2343a99d-37d4-6356-1ee3-9491822cd462.png">

<img width="300" alt="Screenshot_20220731_004505.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/76958/efea6f5b-7e21-7e2b-4e5e-b3067161c0b3.png">

上記パッケージは設定を変更できるので、アイコンが隠れないよう余白や位置、テキストのサイズを調整しました。無事Badgeを良い感じに表示できました。

```
  @override
  Widget build(BuildContext context) {
    BadgePosition position = BadgePosition.topEnd();
    if (_unreadMessageCounter! >= 10 && _unreadMessageCounter! < 100) {
      position = BadgePosition.topEnd(top: -8, end: -8);
    } else if (_unreadMessageCounter! >= 100) {
      position = BadgePosition.topEnd(top: -8, end: -12);
    }

    return Scaffold(
      appBar: AppBar(
        centerTitle: true,
        title: 'タイトル',
        backgroundColor: const Color(0xFFF2D64B),
      ),
      backgroundColor: const Color(0xFFF4F2EC),
      body: const HomePage(),
      bottomNavigationBar: BottomNavigationBar(
        currentIndex: 0,
        selectedItemColor: const Color(0xFFC4A50D),
          type: BottomNavigationBarType.fixed,
          items: <BottomNavigationBarItem>[
          BottomNavigationBarItem(
            icon: Badge(
              badgeContent: Text(_unreadMessageCounter.toString(), style: const TextStyle(color: Colors.white, fontSize: 10),),
              padding: const EdgeInsets.only(top: 3, bottom: 5, left: 3, right: 3),
              position: position,
              showBadge: _unreadMessageCounter! > 0 ? true : false,
              key: const Key('news-icon'),
              child: const Icon(Icons.notifications_outlined),
            ),
            activeIcon: Badge(
              badgeContent: Text(_unreadMessageCounter.toString(), style: const TextStyle(color: Colors.white, fontSize: 10)),
              padding: const EdgeInsets.only(top: 3, bottom: 5, left: 3, right: 3),
              position: position,
              showBadge: _unreadMessageCounter! > 0 ? true : false,
              child: const Icon(Icons.notifications_outlined),
            ),
            label: 'お知らせ',
          ),
        ]
      ),
    );
  }
```

<img width="300" alt="Screenshot_20220731_005408.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/76958/8c630276-ef7c-c863-7abe-a8e1cfdda64b.png">

<img width="300" alt="Screenshot_20220731_005422.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/76958/25f05061-5dee-e601-3fe8-ca65960b1149.png">


### 参考
[[Flutter]アイコン(Icon)にバッジを表示する方法](https://www.choge-blog.com/programming/flutter-icon-badge/)
[In flutter Chip Widget, how can I move the deleteIcon to be before the label](https://stackoverflow.com/questions/56786593/in-flutter-chip-widget-how-can-i-move-the-deleteicon-to-be-before-the-label)