【Flutter】SNSアプリの投稿の「いいね」状態のNavigation間の共有方法
はじめに
SNSアプリ開発において、いいねやファボボタンはモダンSNSであればほぼ必ず実装します。
投稿データをもつPost型から、いいねのオン/オフ状態をStateとして持つ投稿ウィジェット(PostWidget)を描画したとして、このまま画面遷移を行うとStateが破棄され、サーバーで持っているオン/オフ状態とズレが生じるため快適なUXが阻害されてしまいます。
このスクラップにはこの問題に対する一応の解決策と改良案、検討結果を載せて行こうと思います。
皆さんのご意見も絶賛お待ちしております。
サーバーからのレスポンス
いいねボタンを押しオン/オフを切り替える場合、サーバーからはresult
(変更に成功したか)とstate
(変更後の状態)がレスポンスされます。
{
"liking" : {
"result": true,
"state": true
}
}
この場合、いいねの状態変更に成功し、いいね中になったことを意味します。
バージョン
・Flutter 2.0.4
・Dart 2.12.2
一応の解決策
一応申し分なく動きはする解決策として以下のように実装しました。おそらく最善のアーキテクチャではないのでこれ以降少しずつ改善していく様子をこのスクラップに残す予定です。
アーキテクチャ
Provider
によるMVVMアーキテクチャを採用しています。
ページ間のPostWidgetの状態共有
アプリの最上部にChangeNotifier
を配置し、投稿ID(String
)と投稿データの変更を検知するValueNotifier
(PostNotifier
)をMap
で保持します。
class AppViewModel extends ChangeNotifier {
final Map<String, PostNotifier> _postNotifiers = {}; // Viewから直接参照はしないのでprivateにしておきます
// PostNotifierをPost.idを基に取得。(存在しなければ作成して取得)
PostNotifier getPostNotifier(Post post) {
if (_postNotifiers[post.id] == null) { _createPostNotifier(post); }
return _postNotifiers[post.id];
}
// 新規のPostNotifierを作成し_postNotifiersに追加
void _createPostNotifier(Post post) {
_postNotifier[post.id] = PostNotifier(post);
}
// メモリリークを防ぐ為すべてのPostNotifierをdisposeする
void clearAllNotifier() {
_postNotifiers.forEach((postID, notifier) {
notifier.dispose();
});
_postNotifiers.clear();
}
}
class PostNotifier extends ValueNotifier<Post> {
PostNotifier(Post post):super(post);
void updateIsLiking(bool v) {
value = value.copyWith(isLiking: v);
}
}
投稿ウィジェット(PostWidget)側で変更検知
PostWidget内のValueListenableBuilder
でProvider.of<AppViewModel>(context).getPostNotifier(post.id)
を購読することでアプリ内のどこで状態の変更が発生しても検知できるようにします。
これでNavigationにより画面遷移をしても状態を共有できます。
いいねボタンを押したときの挙動
- サーバーにいいねイベントをリクエスト
- サーバーは「サーバーからのレスポンス」で定義した通りにレスポンスする
-
result
がtrue
の場合、Provider.of<AppViewModel>(context).getPostNotifier(post.id).updateIsLiking(liking.state)
を実行し、状態を変更します。 - ValueListenableBuilderが状態変更を検知しいいねのオン/オフを切り替えてくれます
PostNotifierの破棄タイミング
PostNotifierはValueNotifierなので適切なタイミングで破棄しなければなりません。
ユーザーがログアウトすれば投稿のいいね状態は不要になるのでログアウト時にProvider.of<AppViewModel>(context).clearAllNotifier
を実行したくなりますが、ログアウトの画面遷移のタイミングまでValueListenableBuilderが購読していると、dispose後の購読ということでエラーが吐かれてしまいます。
そこでユーザーが切り替われば投稿のいいね状態は不要になると解釈を変えて、ログイン時に破棄することにします。
完成…?
これでエラーを吐かれる問題も解決し、一応本題の実装が完了しました。
ここからはこのアーキテクチャの改良や試行錯誤の過程を記していきます。皆様のご意見を参考にしたいのでご意見あれば遠慮なく書き連ねてください。
動作せず
ユーザー情報とJWTを持つViewModelを個別に切り出す作業などをやっていたところ上記の方法で動作しなくなりました。前回のcommitから随分と作業してしまった為原因の調査が難しいのでいろいろ試行錯誤しています。
ほぼ原因特定、解決
状態変更自体は検知されていたのでLottie製の いいねボタンWidget(LottieLikeButton)で変化を検知してアニメーションさせる必要があった
void didUpdateWidget(old) {
super.didUpdateWidget(old);
widget.value? _animationController?.forward(): _animationController?.reset();
}
これで解決