任意のWidgetを通知みたいに出せるin_app_notificationがv1.1.0になりました
これは何?
これは自分が作成したFlutter用packageの紹介もとい宣伝記事です。公開自体は結構前にしていたのですが、そういえば日本語媒体でちゃんと宣伝をしてなかった(Twitterくらい)のと、先日大規模なリファクタリングを伴うアップデートを行ったので、ノリで記事を書こうかなと思った次第です。
どんなパッケージ?
タイトルにも書いてありますが、これは任意のWidgetをまるでプッシュ通知のように画面に登場させることができるpackageです。
READMEにも貼ってあるgifをここにも貼っておきます。
そしてこれまたREADMEにも書いてありますが、使い方は非常にシンプル。
InAppNotification
Widgetをアプリ内に配置して、
return InAppNotification(
child: MaterialApp(
title: 'In-App Notification Demo',
home: const HomePage(),
),
);
InAppNotification.show()
メソッドを呼ぶだけです。
InAppNotification.show(
child: SomeCoolWidget(),
context: context,
onTap: () => print('Notification tapped!'),
);
Widgetの大きさも自由なので[1]、上のgif画像でいう Count:N
のテキストの大きさを通知を出すたびに自由に変更しても問題ありません[2]。
InAppNotification.show()
メソッドでは表示するWidget、必須のBuildContext
以外に以下の引数を指定できます(v1.1.0時点)。
- 通知をタップしたときのコールバック関数
onTap
- 通知を表示し続ける時間
duration
- 通知を表示するときのアニメーションカーブ
curve
- 通知を非表示にするときのアニメーションカーブ
dismissCurve
表示した通知は、以下の方法で非表示にすることができます。
- 通知をタップする(
onTap
コールバックを指定している場合のみ) - 通知を上にスワイプする
- 通知を左右にスワイプする
v1.1.0までの道のり
packageの機能紹介自体はREADMEで大体できてるので、最初のリリースからここまで何があったかを掘り起こしながら書いていきます。
初回リリース v0.1.0
当初このpackageは弊社CBcloudで開発しているプロダクト(これもまた宣伝)で実装した機能を、可能な限り一般化してpackage化したものになります。
一番最初のバージョンのコードでは、Navigatorで遷移する画面一つ一つを InAppNotification
で囲む必要がある、というなんとも言えない微妙なインターフェースでした🤔
とりあえず動くものができているので、フィードバックを求めるのを兼ねて公開、そしてRedditへの投稿も行いました。
嬉しいことにこの投稿は30以上のupvoteを頂き、コメントでアドバイスも頂きました。
Very excited to see this library!
I think it's a missed opportunity to display using overlay entries. > That way there's no requirement to wrap the entire app.
(意訳:overlay entries(OverlayEntryのこと)を使ったほうがいいと思うぜ!)
恥ずかしいことに、当初OverlayEntryの存在をすっかり忘れていた(知ってはいた)ため、見た瞬間に「アッッッ」ってなりました。そういうことってありますよね。
このとき通知を表示する仕組みとしては、配置したInAppNotification
の中にStack
が入っており、渡したWidgetをそのStack
に乗せて、あとはアニメーション付きで表示/非表示するといった感じでした。
通常のアプリのWidgetの中、Navigatorよりも下位でWidgetの重ね合わせを実現するのがStack
なのに対して、OverlayはNavigatorよりも上位でWidgetの重ね合わせを実現する仕組みになります。OverlayEntry
もWidgetツリーの中には入りますし内部的にはStackを使っているので大した差はないですが、Overlayの場合はNavigatorの画面スタックに関係なく上からWidgetを表示できるので今回のような用途にはうってつけです(他にはWidgetを長押しして出すツールチップなどがOverlayの作例としてあります)。
普段の業務の合間合間で作っていたpackageではありますが、すぐに直したくなってしまったのでちょっと業務時間の振り方を変えたりして、v0.1.0公開から10日後にv0.3.0を公開しました。
v0.3.0
v0.3.0では、v0.1.0で指摘されていたようにOverlayEntry
を使うようにした他、アプリコードから渡すようにしていた minAlertHeight
や safeAreaPadding
といったプロパティを内部で取得できるように変更しました。これによってInAppNotification
をより手軽に使えるようになり、ついでに内部的な作りもいい感じになりました。
ここらでv1.0.0にしても良かったんですが、ロードマップ的にはここから横スワイプでの非表示も追加したかったので、それができるまでメジャーバージョンはお預けしてました。
v1.0.0
予定していた横スワイプでの非表示を実装し、ついにメジャーバージョンを迎えました🎉
横スワイプの実装は「これでいいのか?」という感じでしたが、なんとかなったのでそのまま通した、という感じです。
大まかには、GestureDetector
でスワイプを検知しておき、スワイプが終わったときの指の速度と位置(速度がしきい値を超えているか、あるいは位置がしきい値を超えているか)で通知を非表示にするアニメーションを再生する/通知が元の位置に戻るアニメーションを再生するといった処理をしています。コードで言うとこのあたりになります。
また、地味に重要な改善も同時に行いました。InAppNotification
は StatefulWidget
を使っており、その状態(OverlayEntry
を操作するためのインスタンス保持や、通知自体の位置やAnimationController
の保持)はそのState
に保持しています。
InAppNotification.show()
メソッドを呼んだとき、StatefulWidget
からState
を呼び出す方法が必要ですが、その方法としてBuildContext
にあるfindAncestorStateOfType
メソッドを使っています。
これはInheritedWidget
のようにインスタンスをWidgetツリーから探索するメソッドですが、InheritedWidget
で使うgetElementForInheritedWidgetOfExactType
がfindAncestorStateOfType
は
すなわち、このメソッドはWidgetツリーが深くなればなるほど(今回だと深い階層から呼ぶと)パフォーマンスが悪くなります。
そこで、findAncestorStateOfType
を使って取得するState
をキャッシュするようにしました。
これにより、毎回
…とはいえそこまで深いWidgetツリーを持ったアプリにまだ触れたことがないので、観測範囲ではそう困ることもなかったですが…
v1.1.0
そしてなんやかんやあり、v1.1.0までやってきました。
v1.0.0からの変更点としては、細かいバグ修正のほか以下の変更を行っています。
- 大きくリファクタリング
- ジェスチャー起点のアニメーションを
InteractAnimationController
インターフェースで操作できるように定義、VerticalInteractAnimationController
とHorizontalInteractAnimation
を用意してロジックをまとめた -
InAppNotification
をStatefulWidget
からStatelessWidget
に変更- 状態は
InheritedWidget
として持つように
- 状態は
- ジェスチャー起点のアニメーションを
-
InAppNotification.dismiss()
メソッドを追加- コードから通知を非表示にする方法がなかったので追加した
-
dismissCurve
引数を追加- タップ等で非表示にするときのアニメーションカーブを指定できるように
リファクタリングにより通知を操作するときのロジック(GestureDetector
に渡している諸メソッド)の可読性が上がりました。また、StatefulWidget
からの脱却をしたため
、先述したfindAncestorStateOfType
ではなくgetElementForInheritedWidgetOfExactType
を使うようしました。
その結果通知の状態を探索するときの計算量がすべて
これから
無事いい感じに使えるようになったInAppNotification
ですが、まだいくつかの課題があります。
- リファクタリングしたとはいえ、状態をmutableに扱っている
- その点で可読性がイマイチな部分がある
- 無理にimmutableにする必要もない気はしているが…
- 通知を表示するとき、初回のみ少しガタつく?
-
StatefulWidget
からの脱却により治ったかと思いきやそうでもなさそう
-
というわけで、まだまだ改良の余地はありそうなので今後もメンテナンスを続けていきたいと思っています。
個人的には結構便利なpackageになっているんじゃないかなと思うので、よければぜひ使ってみてください👐 なにか気づいた点や改良点があったら、お気軽にissueやPull Request投げていただければと思います。
-
縦横ともに画面の大きさの範囲内という制限はあります。 ↩︎
-
v1.1.0のexampleには通知の大きさを変更するUIが実装されてます。 ↩︎
Discussion