はじめに
最近ようやくSSOTという概念を知った初心者ではありますが、現時点でわかったことをまとめてみました。
MVVM前提でのSSOTの話になります。
SSOTとは
SSOT(Single Source of Truth)とは、「唯一の信頼できる情報源」のことで、
どういうことかというと、
MVVMだとViewに紐づくViewModelの中に表示用する内容のDataがあると思いますが、このDataをViewModelから分離させて、一元管理しようというものです。
例えば、MVVMにおいて、「投稿」という機能があったとすると、
・投稿一覧画面(PostListView)
・投稿詳細画面(PostDetailView)
があると思います。
SSOTでないPJは、PostListViewとPostDetailViewでは、それぞれに別の一覧のPostのDataと、詳細のPostのDataを持っていると思います。
SSOTでは、このPostのDataはStoreとして、Post一覧とPost詳細のDataを同じものとして一元管理します。
具体例を挙げると、
SSOTでないMVVMのPJで特にそれ用の処理を入れていない場合、投稿詳細画面でいいねを押した後、popで画面を戻って投稿一覧画面に戻った時、つけたはずのいいねが消えていると思います。
これがSSOTだとPostのDataは一元管理されているので、詳細画面でいいねをつけた時に一覧画面のUIも更新する処理を入れるだけで、詳細画面から一覧画面に戻ったとき、いいねがついている状態にできるということになります。
具体例のコード
SSOTで一元管理するDataは、Storeクラスとして管理するのが本来のSSOTのよう?なんですが、RiverPodの場合はStoreクラスとして一元管理するより、Provider内の変数としてStoreを入れて、直接ViewからwatchするというのがRiverPod的には良いようです。
以下はStateNotifierProviderでの例です。
@freezed
class PostState with _$PostState {
const factory PostState({
@Default(false) bool isInit,
@Default([]) List<int> listPostId, // 一覧UI更新用
int? detailPost, // 詳細UI更新用
}) = _PostState;
}
final postProvider = StateNotifierProvider<PostProvider, PostState>(
(ref) => PostProvider(ref: ref),
);
class PostProvider extends StateNotifier<PostState> {
PostProvider({required Ref ref})
: _ref = ref,
super(const PostState()) {
init();
}
final Ref _ref;
Map<int, Post> _postStore = {};
void init() {
state = state.copyWith(isInit: true);
}
/// 投稿一覧取得
Future<void> fetchPosts() async {
try {
final postList = await PostRepository.fetchPostList();
// --- SSOT (_postStore) 更新 ---
for (final p in postList) {
_postStore[p.id] = p;
}
// --- UI 用 state 更新 ---
state = state.copyWith(
listPostId: postList.map((e) => e.id).toList(),
);
} catch (e) {
LogUtil.d('投稿一覧取得失敗: ${e.toString()}');
}
}
/// 投稿詳細取得
Future<void> fetchPost(String postId) async {
try {
final post = await PostRepository.fetchPost(postId);
// --- SSOT (postStore) 更新 ---
_postStore[post.id] = post;
// --- UI 用 state 更新 ---
state = state.copyWith(detailPost: post.id);
} catch (e) {
LogUtil.d('投稿詳細取得失敗: ${e.toString()}');
}
}
//一覧画面更新
void reloadPostList() {
state = state.copyWith(
listPostId: [...state.listPostId], // 新しい List を作ってUI更新
);
}
}
ListのViewModel側の呼び出しコード
final postState = ref.watch(postProvider);
final postStore = ref.read(postProvider.notifier).postStore;
final posts = postState.listPostId.map((id) => postStore[id]).toList();
DetailのViewModel側の呼び出しコード
final postState = ref.watch(postProvider);
final postStore = ref.read(postProvider.notifier).postStore;
final postId = postState.detailPost;
final post = (postId != null) ? postStore[postId] : null;
SSOTのデメリット
投稿一覧画面にページネーションがあった場合、管理が少し難しくなります。
_postStoreにデータを溜めていく(表示されているページ外の表示済みPostのDataも持ち続ける)のは変わらないと思うんですが、
この場合、画面側ではなく上記のPostStateとPostProviderにページの情報持たせるのがいいのかなーと思います。
あとがき
もともとRiverPodは設計思想的にMVVMよりもSSOTが向いているようです。
SSOT的には、ページで持たせたい状態はViewModelにではなくStatefulWidgetで持たせましょうね、ということらしいです。
すでにMVVMに慣れてしまっているとSSOTをこれから取り入れていくのはコスト高いかもしれませんが、データの一元管理というのはすっきりとした綺麗なアーキテクチャに近付くものだと思うので、ぜひ取り入れてみてください。
SSOTでないMVVMのPJで特にそれ用の処理を入れていない場合、投稿詳細画面でいいねを押した後、popで画面を戻って投稿一覧画面に戻った時、つけたはずのいいねが消えていると思います。
これがSSOTだとPostのデータは一元管理されているので、詳細画面でいいねをつけると一覧画面のDataも更新されるので、詳細画面から一覧画面に戻ったとき、いいねがついている状態にできるということになります。
上記でこのように書いたので、MVVMでこのいいねを更新する方法を挙げると、
・pop時にthenで値を前の画面に渡して更新
・UI部品で画面が表示されたことを検知してAPIを叩き直す
・DetailViewModelからListViewModelのstateを変更する
このような方法があるかなと思います。
この中だと、DetailViewModelからListViewModelのstateを変更するのが一番楽かなと思います。
手間としてはSSOTの詳細でいいねつけたときのList画面用の更新処理入れるのと変わらないんですが、SSOTの方だと参照しているデータが同じなので一元管理されていて綺麗かなとは思います。