【Flutter】Streamを一切使わず、riverpodで実装する通知バッジ

Dart

概要

どうも、@daiki1003です!

この記事は、Flutter #1 Advent Calendarの10日目の記事です。

こんなやつ実装します。
コード量的には200行ほどで出来てしまいます!
(自分でもびっくり)

実際の動作

今回の全コードはgist.github.comに載せています!
・main.dartにコピペ
・pubspec.yamlに依存関係を追加
だけで動くと思います。

ざっくりとした手順

// 機能
1. 表示するバッジ数を取得 (本記事では説明は割愛)
2. バッジ数を保持するためのStateProviderを定義
3. それぞれのバッジ数をStateProviderに保持
4. アプリの状態を監視する (本記事では説明は割愛)
5. バックグラウンドに行く際にアプリのバッジを設定

// UI
6. お知らせバッジWidget生成
7. タブアイコンWidget生成
8. BottomNavigationBarの生成

それではやっていきましょう!

機能

1. 表示するバッジ数を取得 (本記事では説明は割愛)

こちらは詳細は割愛しますが、APIか内部処理で取得することになるでしょう。

2. バッジ数を保持するためのStateProviderを定義

final _homeBadgeProvider = StateProvider<int>(
  (ref) => 0,
);
final _notificationBadgeProvider = StateProvider<int>(
  (ref) => 0,
);
final _myPageBadgeProvider = StateProvider<int>(
  (ref) => 0,
);

final _appBadgeProvider = Provider<int>(
  (ref) {
    final homeBadge = ref.watch(_homeBadgeProvider).state;
    final notificationBadge = ref.watch(_notificationBadgeProvider).state;
    final myPageBadge = ref.watch(_myPageBadgeProvider).state;

    return homeBadge + notificationBadge + myPageBadge;
  },
);

タブの分だけStateProviderを用意します。
後に通知数を更新する必要があるので、StateProviderを使用しています。

そして、最後アプリのアイコンに付けるバッジ数を
保持する_appBadgeProviderです。
アプリのタブに表示するバッジ数をref.watchで監視し、
それらが更新された時に勝手に再計算されます。
なので、僕らが意図的に値を更新することはありません。
そのため、Providerを使っています。

※ここでは、同じファイルで参照するのでprivateになっていますが、
別ファイルにpublicで定義してもOKです!

3. それぞれのバッジ数をStateProviderに保持

final badgesResponse = await http.get(url);
final json = json.decode(badgesResponse.body);

context.read(_homeBadgeProvider).state = json['home_count'];
context.read(_notificationBadgeProvider).state = json['notification_count'];
context.read(_myPageBadgeProvider).state = json['my_page_count'];

取得する部分は簡略化していますが、何らかの方法で取得したバッジ数を、
定義したStateProviderに保持していきます。

4. アプリの状態を監視する (本記事では説明は割愛)

WidgetsBindingObserver と言うmixinに準拠することによって
アプリの状態を監視する事が出来ます。
詳しくは別記事で解説しようと思っています。

5. バックグラウンドに行く際にアプリのバッジを設定

flutter_app_badgerと言うパッケージを使います。

flutter_app_badger | Flutter Package
Plugin to update the app badge on the launcher (both for Android and iOS)

パッケージのインストール方法に関してはこちらからどうぞ。

【超簡単!】Flutterのパッケージのインストール方法を説明するよ!
概要 Flutter(Dart)で画像のクリッピングするコードが書けますか? Flutter(Dart)でhttp通信するコードが書けますか? 大丈夫、書けなくて良いんです。 Flutterでは、色んな人が使いそ...

VSCodeを使っている方は圧倒的にこちらの方がおすすめです!

【Flutter】3秒でパッケージ追加完了!?VSCode使いに絶対に入れて欲しい拡張機能「Pubspec Assist」を紹介するよ!
Flutterでの開発には、世界中のデベロッパが作ってくれたパッケージのインストールが欠かせません。 あなたはどの様にパッケージを追加していますか? 実は、以前パッケージのインストール記事を書きました。 pub...
@override
void didChangeAppLifecycleState(AppLifecycleState state) async {
  super.didChangeAppLifecycleState(state);

  if (state != AppLifecycleState.inactive) {
    return;
  }
  
  if (!await FlutterAppBadger.isAppBadgeSupported()) {
    return;
  }
  
  FlutterAppBadger.updateBadgeCount(
    context.read(_appBadgeProvider),
  );

上記メソッドはアプリの状態が変化した時に呼ばれます。

AppLifecyccleStateは

enum AppLifecycleState {
  // アプリ画面が表示されており、ユーザのインプットに反応できる状態
  resumed,

  // ユーザのインプットに反応出来ないが、Flutterのホストビューは動いている状態
  // 認証画面が出ていたり、電話がかかってきたりアップスイッチャーが立ち上がっていたりといった状態
  inactive,

  // アプリ画面は表示されておらず、ユーのインプットに反応もせず、バックグラウンドで動いている状態
  paused,

  // Flutterエンジン上で動いているが、、ホストビューからは切り離されている状態
  detached,
}

の様に定義されています。

今回はアプリがバックグラウンドに行く際に、アイコンにバッジを付けたいので
inactiveのみ監視します。(pausedではない)

次に、「アプリがバッジをサポートしているか」
つまりユーザがプッシュ通知に対して許可をしているかを確認します。

最後に、アプリのバッジ数を更新をします。

ここまでで、機能面の実装は終わりました。

タブにバッジを付けるUI部分を実装していきましょう。

UI

6. お知らせバッジWidget生成

class NotificationBadge extends StatelessWidget {
  const NotificationBadge({
    @required this.badgeCount,
  });
      
  final int badgeCount;
      
  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4),
      decoration: BoxDecoration(
        color: Color.fromRGBO(250, 47, 49, 1.0),
        shape: BoxShape.circle,
      ),
      child: Text(
        badgeCount.toString(),
        style: const TextStyle(
          color: Colors.white,
          fontSize: 10,
          fontWeight: FontWeight.bold,
        ),
      ),
    );
  }
}

特筆すべきことはありません、バッジ数を取得して赤背景で表示するだけです。

7. タブアイコンWidget生成

enum TabType {
  home,
  notification,
  myPage,
}

extension TabTypeEx on TabType {
  ...
  ...
  /// choose provider for observing
  StateProvider get provider {
    switch (this) {
      case TabType.home:
        return _homeBadgeProvider;
      case TabType.notification:
        return _notificationBadgeProvider;
      case TabType.myPage:
        return _myPageBadgeProvider;
    }
    throw UnimplementedError();
  }  
}

class TabItem extends HookWidget {
  const TabItem(
    this.type, {
    @required this.selected,
  });

  final TabType type;
  final bool selected;

  @override
  Widget build(BuildContext context) {
    final badgeCount = useProvider(type.provider).state;
    final showBadge = 0 < badgeCount;
    return Stack(
      overflow: Overflow.visible,
      children: [
        Icon(type.iconData),
        if (showBadge)
          Positioned(
            top: -7,
            right: -12,
            child: NotificationBadge(badgeCount: badgeCount),
          ),
      ],
    );
  }
}

TabTypeはタブの種類を表すenumです。
extensionを使ってそれぞれのタブに応じたデータを返す様にしています。

肝は

final badgeCount = useProvider(type.provider).state;

の部分。

3の手順で、stateを更新した際にこの TabItemクラスがリビルドされ
バッジ数が更新される様になっています。
そして、バッジ数が0以上の時にバッジが表示されると言う流れです。

ビューの部分に関して少しだけ補足。
NotificationBadgeを右上にはみ出る様に設置しています。
このはみ出た部分も含めて正しく描画するために、Stackにoverflowプロパティを設定しています。

8. BottomNavigationBarの生成

class Sample extends HookWidget {
  @override
  Widget build(BuildContext context) {
    final tabType = useState(TabType.home);
    return Scaffold(
      bottomNavigationBar: BottomNavigationBar(
        currentIndex: tabType.value.index,
        items: TabType.values
            .map(
              (tabType) => BottomNavigationBarItem(
                icon: TabItem(tabType, selected: false),
                activeIcon: TabItem(tabType, selected: true),
                label: tabType.title,
              ),
            )
            .toList(),
        onTap: (value) => tabType.value = TabType.values[value],
      ),
    );
  }
}

今回の内容ではないので詳細は省きますが、
useStateを使うことによって値が更新された時にリビルドされる様にしています。

BottomNavigationBarItemを使って
非アクティブ時、アクティブ時のアイコンをそれぞれTabItemを指定します。

最後に

いかがでしたでしょうか?
記事執筆現在(2020/12/10)ではバッジをriverpodで実装した例は
見当たらないので(僕のググラビリティが低いだけの可能性ありw)、
皆様の参考になれば幸いです。

誰かのお役に立てば。

Twitterフォローお願いします

「次回以降も記事を読んでみたい!」
「この辺分からなかったから質問したい!」

そんな時は、是非@daiki1003のフォローお願いします♪

また、記事がとても役に立ったと思う人は
コーヒーを奢っていただけると非常に嬉しいです!

コメント

タイトルとURLをコピーしました