
みなさまこんにちは〜!
メモリアインクのすだです。
今回は、イベント通知処理の実装に便利な「SharedFlow」と「Channel」について、その違いとそれぞれの使い方をわかりやすく解説していきます。
この記事を読んでわかること…
・SharedFlowとChannelの仕様
・SharedFlowとChannelの比較
環境
- Kotlin (ver 1.9.0)
- Android Studio (Giraffe | 2022.3.1 Patch 3)
SharedFlow と Channel の基本的な理解
Kotlinには、非同期でデータを送受信するための機能として SharedFlow
と Channel
があります。
どちらもコルーチンを活用した非同期プログラミングにおいて重要な役割を果たしますが、用途や動作の違いを理解することで、より適切な設計が可能になります。
SharedFlow
SharedFlow
は、同じデータを複数の購読者(コルーチン)に同時に配信できる仕組みです。
たとえるなら、「拡声器」のようなものです。発信されたメッセージは、それを聞いているすべての人に一斉に届きます。
Channel
Channel
は1対1の通信を目的としたデータの受け渡しの仕組みです。
こちらは「手渡しの郵便」に近いイメージで、1つのデータを1人にだけ確実に渡します。
SharedFlow と Channel の使い分け
SharedFlowよりChannelを使ったほうがいい例
たとえば SharedFlow
を使ってしまっている以下の処理があるとします。
例)ユーザー情報を表示する画面の表示処理で、ユーザー情報を取得する&「ようこそ〜〜さん」のトースト表示をする
class UserViewModel : ViewModel() {
private val _userEvent = MutableSharedFlow<String>() // replay = 0
val userEvent = _userEvent.asSharedFlow()
init {
viewModelScope.launch {
val userName = "Taro" // 即時取得できたと仮定
_userEvent.emit(userName)
}
}
}
class UserFragment : Fragment() {
private val viewModel: UserViewModel by viewModels() // これで取得
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleScope.launch {
viewModel.userEvent.collect { name ->
Toast.makeText(requireContext(), "ようこそ $name さん", Toast.LENGTH_SHORT).show()
}
}
}
}
この構成では、UserViewModel のインスタンスが生成されるとすぐに init ブロックが実行され、
userNameのemitが発生します。
しかし、Fragment側でのcollect 処理はまだ始まっていないため、この時点ではSharedFlow
に購読者が存在しません。
加えてSharedFlow
のreplayが0に設定されている場合、過去に発行された値は保持されません。
そのため、イベントはそのまま流れて失われてしまい、結果としてトーストは表示されないという事態になります
これを防ぐことができるのが Channel
の仕様です。▼
class UserViewModel : ViewModel() {
private val _userEvent = Channel<String>(Channel.BUFFERED)
val userEvent = _userEvent.receiveAsFlow()
init {
viewModelScope.launch {
val userName = "Taro"
_userEvent.send(userName)
}
}
}
Channelの特徴として、まだ購読者がいなかった場合に待ってくれます。
そして購読者が現れたタイミングで1回のみイベントを送信してくれるのです。
ChannelよりSharedFlowを使ったほうがいい例
たとえば Channel
を使ってしまっている以下の処理があるとします。
例)「保存」ボタンを押下した際に画面にトーストを表示し、同時にバックグラウンドでトラッキングを送信
class SaveViewModel : ViewModel() {
private val _saveEvent = Channel<String>(Channel.BUFFERED)
val saveEvent = _saveEvent.receiveAsFlow()
fun onSaveSuccess() {
viewModelScope.launch {
_saveEvent.send("保存に成功しました")
}
}
}
class SaveFragment : Fragment() {
private val viewModel: SaveViewModel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.saveButton.setOnClickListener {
viewModel.onSaveSuccess()
}
// ① UIトースト通知
viewLifecycleOwner.lifecycleScope.launch {
viewModel.saveEvent.collect { message ->
Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show()
}
}
// ② トラッキング送信
viewLifecycleOwner.lifecycleScope.launch {
viewModel.saveEvent.collect { message ->
sendTrackingEvent("save_success", mapOf("message" to message))
}
}
}
}
この構成では、ボタン押下により ViewModel側でイベントがsend()され、
それをFragment側で2つのcollectが受け取ろうとします
しかし Channel
は「最初に値を受け取った1人だけが処理を行い、それ以外には届かない(消費される)」という性質を持つため、
- ①のトースト表示 または
- ②のトラッキング送信
のどちらか一方しか実行されず、もう一方はスルーされてしまうという問題が発生します。
これを防ぐことができるのがSharedFlow
の仕様です▼
class SaveViewModel : ViewModel() {
private val _saveEvent = MutableSharedFlow<String>(replay = 0)
val saveEvent = _saveEvent.asSharedFlow()
fun onSaveSuccess() {
viewModelScope.launch {
_saveEvent.emit("保存に成功しました")
}
}
}
SharedFlowの特徴として、複数購読者に同じイベントを同時に届けることができます。
値は消費されずどのcollectにも通知が届くので、イベントの多重実行が必要な場合はSharedFlowを使うと良いでしょう。
まとめ
おつかれさまでした。いかがでしたでしょうか!
一度だけ、かつひとつの処理だけに通知したいイベントには Channel
を、
複数の処理に同時に通知したいイベントには SharedFlow
を使うのが適切です。
この使い分けをぜひ覚えておいてください。



技術者としてのキャリアパスを次のレベルへと進めたい皆様、
<未経験からIT・Webエンジニアを目指すなら【ユニゾンキャリア】>
自分の市場価値をさらに向上させてみませんか?
それではまた次回の記事でお会いしましょう!
コメント