【正社員】還元率83%【フリーランス】マージン一律5万円で案件をご紹介させていただきます。 詳細はこちら

【Android】<Kotlin>SharedFlowでスマートに!Jetpack Compose×ViewModelイベント連携

【Android】<Kotlin>SharedFlowでスマートに!Jetpack Compose×ViewModelイベント連携
すだ

みなさまこんにちは〜!
メモリアインクのすだです。

今回は、ViewModel+SharedFlow+Composeを利用して、イベント処理を綺麗に実装する方法をわかりやすく解説していきます。

この記事を読んでわかること…
・Composeと非同期イベントの関係
・意図しない副作用を防ぐには

目次

環境

  • Kotlin (ver 1.9.0)
  • Android Studio (Giraffe | 2022.3.1 Patch 3)

Composeと非同期イベントの関係

Jetpack Compose は「視覚的な状態(State)を表示に変換する」UIフレームです。

しかし実際は、

  • データの保存完了メッセージ
  • Toastやダイアログの表示
  • エラーの通知

などの「状態ではなく」1回限りの “動作”が必要です。

Composeは再コンポーズされると Composable 内の処理が何度も実行されることがあります。
その結果、「トーストが何度も表示される」「ローディングが何回も走る」といった 意図しない副作用 が起きることがあります。

こうした問題を防ぐためには、

  • ViewModel側でイベントを一度きりにする仕組み(SharedFlowなど)を用意すること
  • @Composable側では LaunchedEffect などを使って再実行のタイミングを制御すること

が重要です。

ViewModel 経由のイベント処理を実装する

ViewModel の内部に「一回限実行されるイベント流し管理」を作ります。

以下のように役割分担をしていきます。

・ViewModel … イベントを発生させて SharedFlow に emit
・SharedFlow … UI と ViewModel 間の中継機構
・LaunchedEffect … Compose UI 側でイベントを受信・処理

class MyViewModel : ViewModel() {
    private val _event = MutableSharedFlow<String>(replay = 0)
    val event: SharedFlow<String> = _event           

    fun onButtonClick() {
        viewModelScope.launch {
            // 擬似保存処理
            delay(1000)
            _event.emit("保存が完了しました") // 1回だけトーストを出す
        }
    }
}

まず ViewModel 側でイベントを発生させる処理を書き、SharedFlow を使ってそのイベントを emit() によって UI 側へ送信します。

MutableSharedFlowの引数にあるreplyは、あとから来た人(collect)に「過去のイベントを何件届けるか」という意味を持ちます。
・replay = 0 → 「覚えてない」=その場にいた人だけが受け取る
・replay = 1 → 「1件だけ覚えてる」=あとから来た人にも最新の1件だけ配る
・replay = n → n件まで履歴を覚える

つまり使い分けとしては、
・replay = 0 → トースト通知、1回だけのイベントに最適
・replay = 1以上 → 過去の状態も受け取りたいとき(例:選択肢や設定の初期値)

といった感じです。
今回は一回のみ実行されるイベントの実装を実現したいので replyを0にします。
これにより、emitした瞬間にcollectしてた人だけが受け取り、「一度だけ」「今いる画面だけに」伝えることができます。

@Composable
fun MyScreen(viewModel: MyViewModel) {
    val context = LocalContext.current

    // 初回表示時だけ開始してイベントを受け取る
    LaunchedEffect(Unit) {
        viewModel.event.collect { message ->
            Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
        }
    }

    Button(onClick = { viewModel.onButtonClick() }) {
        Text("保存")
    }
}

そして、Compose の UI 側では LaunchedEffect(Unit) を使って、
初回表示時に一度だけ collect を開始し、そのイベントを受信します。

このように、ViewModel側とCompose側の“両方”で工夫することで、イベントが「一回だけ(= ボタン押下時のみ)」実行されるように制御されているんです。
(たとえ 再コンポーズが何度起きてもトーストが勝手に再発火されることはありません。)

✔ イベントを ViewModel 側から SharedFlow で一回だけ発行し、
✔ UI側は LaunchedEffect + collectLatest で一度だけ受け取る

まとめ

おつかれさまでした。いかがでしたでしょうか!

Composeでは、意図しない副作用を防ぐために、
UI部分では描画に集中し、副作用的処理はLaunchedEffectなどで分離して管理することが非常に大切です。

すだ

技術者としてのキャリアパスを次のレベルへと進めたい皆様、
未経験からIT・Webエンジニアを目指すなら【ユニゾンキャリア】
を通じて、
自分の市場価値をさらに向上させてみませんか?

それではまた次回の記事でお会いしましょう!

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

弊社テックブログをご愛読いただきありがとうございます。
当テックブログを運用している株式会社メモリアインクは、
【正社員】還元率83%
【フリーランス】マージン一律5万円で案件のご紹介
と、エンジニアの皆様に分かりやすい形で稼げる仕組みを構築し提供させていただいております。

コメント

コメントする

目次