はじめに
こんにちは!
先日、メモの追加・削除・永続化機能を実装し、僕のメモアプリも一通りのCRUD操作ができるようになりました。
しかし、今のアプリには致命的な欠陥があります。それは、もしデータベースへの保存や削除に失敗した時、アプリがユーザーに何も伝えず、ただ沈黙してしまうことです。これは、ユーザーに「あれ、保存されたのかな?」という不安を与え、最悪の場合データが消えているのに気づかせない、非常に不親切な設計です。
そこで今回は、この「沈黙のエラー」を解決し、ユーザーにフィードバックを返す、堅牢なエラー通知機能を実装するための要件定義と基本設計をまとめていきます。
この設計の過程で、Compose開発における超重要概念、「State」と「Event」の分離についても深く掘り下げてみます。
1. なぜ、エラー通知が必要なのか?(要件定義)
1.1. なぜやるのか? (Why)
-
現状の課題: 現在の実装では、
RepositoryでDB操作が失敗しても、ViewModelはprintlnで開発者向けのログを出力するだけです。ユーザーは、自分の操作が成功したのか失敗したのかを知る術がありません。 -
目指す姿: アプリに**「対話能力」**を持たせること。操作が失敗したという「悪い知らせ」であっても、それを正直にユーザーに伝えることで、信頼関係を築きたい。これが今回の機能追加の根本的な動機です。
1.2. 何をやるのか? (What)
上記の「Why」を実現するために、以下の機能要件を定義します。
-
エラー通知: メモの追加または削除に失敗した場合、ユーザーに対して「保存に失敗しました」などのエラーメッセージを画面上に表示すること。
-
一時的な表示: エラーメッセージは、数秒間表示された後、自動的に消えること。ユーザーの操作を妨げ続けないようにします。
2. 「どう作るか」の骨格を決める(基本設計)
2.1. UI/UXの選定:「Snackbar」が最適解
エラー通知の方法はいくつか(Toast, AlertDialogなど)ありますが、今回は**Snackbar**を採用します。
-
理由:
- 画面の下部に表示され、ユーザーの操作を完全に妨げることがない(非侵入的)。
- 数秒で自動的に消えるため、要件に合致している。
- Material Designの標準コンポーネントであり、Composeで簡単に実装できる。
2.2. アーキテクチャ設計と思考プロセス
ここが今回の設計の肝です。
「エラーメッセージをどうやってViewModelからUIに伝えるか?」と考えた時、多くの初学者が陥る罠があります。
❌ ありがちな失敗例:UiStateにエラーメッセージを入れる
// やってしまいがちな悪い例
data class Success(
val memos: List,
val errorMessage: String? = null // ← ここにエラーメッセージを追加
) : MemoUiState
一見良さそうですが、これには大きな問題があります。UiStateは**「画面の状態」**を表すものです。ここに一時的なエラーメッセージを入れてしまうと、画面回転(再コンポーズ)が起きた時に、同じエラーメッセージが何度も表示されてしまうのです。
🤔 思考プロセス:StateとEventを分離せよ
ここで、我々は2つの概念を明確に区別する必要があります。
-
State(状態):
- メモのリストなど、画面に永続的に表示され続けるべきデータ。
- 例えるなら: 天気予報の「今日の天気は晴れです」という情報。
-
Event(イベント):
- 「保存に失敗しました」という通知など、一度だけ処理されればよい出来事。
- 例えるなら: 「ただいま、ゲリラ豪雨が発生しました」という一回限りの緊急速報。
この「緊急速報(Event)」を、「今日の天気(State)」に混ぜて管理してはいけないのです。
✅ 解決策:イベント通知専用のパイプラインを作る
そこで、ViewModelの中に、UiStateとは別の、イベント通知専用のパイプラインを用意します。このパイプラインには**SharedFlow**という道具が最適です。
-
StateFlowとの違い:-
StateFlow: 最新の「状態」を一つだけ保持する。UIが接続した瞬間に、現在の状態を必ず受け取れる。 -
SharedFlow: イベントを次々と流すことができる。UIは、自分が接続した後に流れてきたイベントだけを受け取る(過去のイベントは受け取らない)。まさに、一度きりの通知に最適です。
-
2.3. コンポーネント間の連携
この設計思想に基づき、各コンポーネントの役割と連携を以下のように定義します。
-
ViewModelの改造:-
UiStateとは別に、イベントを流すためのSharedFlowをprivateで定義する (_eventFlow)。 - UIには、
publicなFlowとして公開する (eventFlow)。 -
addMemoやdeleteMemoのonFailureブロックで、_eventFlow.emit(...)を呼び出し、エラーイベントをパイプラインに流す。
-
-
MemoScreenの改造:-
Snackbarを表示・管理するためのSnackbarHostStateを用意する。 -
LaunchedEffectという特別なComposableを使い、ViewModelのeventFlowを一度だけ監視(collect)する。 -
eventFlowから新しいイベントが流れてきたら、snackbarHostState.showSnackbar(...)を呼び出して、Snackbarを表示する。
-
【情報の流れのイメージ】
[ DB操作失敗! in Repository ]
↓ Result.failure
[ ViewModel ] -- emits event --> [ SharedFlow (イベント用パイプライン) ]
↓ ↓ collects
[ UiState (データ用パイプライン) ] [ LaunchedEffect in MemoScreen ]
↓ ↓ shows
[ UI (リスト表示など) ] [ Snackbar (エラー表示) ]
このように、**「画面の状態」と「一回きりの通知」**の通り道を分けることで、クリーンで予測可能なUIを構築できます。
まとめ
というわけで、今回は「UIへのエラー通知」を実装するための設計を行いました。
-
Snackbarを使って、ユーザーに分かりやすくエラーを通知する。 -
State(状態)とEvent(イベント)を明確に分離する。 -
イベント通知には
SharedFlowを使い、UI側はLaunchedEffectで監視する。
この設計は、単にエラーを表示するだけでなく、Jetpack Composeにおける、より高度で堅牢なUIアーキテクチャを学ぶための、最高の練習問題になります。
次回は、この設計書をもとにした実装編に進んでいこうと思います!
