1. はじめに
今回は Android で簡易的な EventBus の実装方法として、StateFlow を使ったイベント通知実装を紹介したいと思います。
背景としては、私の担当するプロジェクトでアプリケーションの Foregroud、Backgroud を検知で実現する機能を実装する上で今回の実装が必要になったことにあります。Android の EventBus としては EventBus などのライブラリを利用する方法もあるのですが、ライブラリ導入はその後のメンテナンスコストや脆弱性が生じる可能性も考慮する必要があります。また、ライブラリの導入は、今回実現したかった機能に対してオーバースペックだったため、チームとして独自実装で実現することにしました。
2. まず考えたこと
まず最初に大まかな実現方法をイメージしてみました。
- Application Class で Foreground, Background を監視する
- Singleton で状態管理するクラスを作る
- Activity / Fragment で状態管理クラスから変更を監視する(通知を受ける)
この仕組みの中で、状態管理をどう実現するかがポイントかなという印象でした。
3. 実装編
では、上記の検討事項をベースに実装の検討を進めたいと思います。
(1) Application Class の LifeCycle を監視する
Application の Lifecycle の監視については、懸念なくDefaultLifecycleObserver
で実現可能だと考えました。DefaultLifecycleObserver は、公式ドキュメントにも記載されているよう LifecycleObserver により、LifecycleOwner の状態変更の Callback を受けられるようになります。
事前準備としては、gradle に依存関係を追加します。
dependencies {
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version"
}
合わせて Application class を拡張して interface を追加します。
class MyApplication : Application(), DefaultLifecycleObserver {
override fun onCreate() {
super<Application>.onCreate()
ProcessLifecycleOwner.get().lifecycle.addObserver(this)
}
override fun onStart(owner: LifecycleOwner) {
super.onStart(owner)
// TODO: Emit Application State
}
override fun onStop(owner: LifecycleOwner) {
super.onStop(owner)
// TODO: Emit Application State
}
}
通知を受ける Lifecycle は、onResume
とonPause
でも良いと思うので、アプリケーションに応じて使い分けてもらうのが良いと思います。
参考:DefaultLifecycleObserver | Android Developers
(2) Singleton で状態管理するクラスを作る
アプリケーションの状態を管理するクラスを作成するの前に、状態区分が必要になるため、enum で定義します。必要な状態区分識別は、 Foreground と Background なので、シンプルな定美にしています。
enum class AppState {
NONE,
FOREGROUND,
BACKGROUND
}
次に、状態をコミットしていくデータホルダークラスを設定する必要があるのですが、
- LiveData
- SharedFlow
- StateFlow
候補としては3つくらいあったので、実際に検証してみました。
実用にあたっては、状態変更の通知をどう受けられるかが重要なポイントです。
LiveDataで実現する
EvetBus ライブラリで Singleton 設計を採用しているというのもありますが、アプリケーションの状態を横断的に管理するという点からだと Sigleton が良いと考えました。LiveData は MVVM アーキテクトを採用する上で欠かせないデータホルダークラスで、更新に関する情報をアクティブなオブザーバーにのみ通知してくれます。
class EventBus {
companion object {
private var instance: EventBus = EventBus()
fun sharedInstance(): EventBus {
return instance
}
}
private val _bus: MutableLiveData<AppStatus> = MutableLiveData<AppStatus>().apply { value = NONE }
val bus: LiveData<AppStatus> = _bus
fun postValue(status: Status) {
_bus.postValue(status)
}
}
このように LiveData を使った場合、呼び出しとオブザーバーはこのようになります。
(以後、前後の実装は割愛します)
override fun onStart(owner: LifecycleOwner) {
EventBus.sharedInstance().postValue(AppState.FOREGROUND)
}
override fun onStop(owner: LifecycleOwner) {
EventBus.sharedInstance().postValue(AppState.BACKGROUND)
}
override fun onResume() {
Handler(Looper.getMainLooper()).post {
EventBus.sharedInstance().bus.observeForever {
// TODO:
}
}
}
LiveData のイベント通知でアプリケーションの状態通知は受けることができたのですが、LiveDataの使い方とオブザーバー実装の強引さは否めません。
SharedFlowで実現する
次は、LiveData の代わりに SharedFlow を使って実装検証します。
SharedFlow は Kotlinx coroutines の公式ドキュメントにも EventBus の実装が記載されており、これを利用するのが良いように思えます。
参考: kotlinx.coroutines 1.6.0-SNAPSHOT | SharedFlow
class SampleEventBus {
private val _events = MutableSharedFlow<Event>()
val events = _events.asSharedFlow()
suspend fun produceEvent(event: Event) {
_events.emit(event) // suspends until all subscribers receive it
}
}
suspends until all subscribers receive it
と記載されているように、DefaultLifecycleObserver の override methods でproduceEvent()
を直接呼び出すことができないので、今回のシーンでは断念。
StateFlow で実現する
今度は、Flow から状態の最新情報を適切に出力するための API を提供している StateFlow を検討します。
StateFlow は、状態保持用の監視可能な Flow で、現在の状態や状態更新の情報をコレクタに出力します。現在の状態の値は、その value プロパティから読み取ることもできます。状態を更新してこの Flow に送信するには、MutableStateFlow クラスの value プロパティに新しい値を割り当てます。公式ドキュメントにも以下の記載があり、今回の機能の実現の最適解と考えました。
Android において、StateFlow は状態を変更可能かつ監視可能に維持する必要のあるクラスに最適です
参考: Android Developers | StateFlow と SharedFlow
上記の Singleton の実装と合わせると、状態管理はこのようになります。使う側の実装は、LiveData の実装例と同様になります。
private val _state = MutableStateFlow(AppState.NONE)
val state = _state.asStateFlow()
fun postValue(newValue: AppState) {
_state.value = newValue
}
通知を受ける側は。CoroutineScope で通知を受けることができます。
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
lifecycleScope.launchWhenStarted {
EventBus.sharedInstance().state.collect { state ->
// TODO:
}
}
}
lifecycleScope.launchWhenStarted
を利用するためには、以下の依存関係の追加が必要になります。
dependencies {
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_runtime_version"
}
この CoroutineScope で通知を受ける方法が一点注意点があります。それは、launchWhenStarted が廃止予定になっており、代わりに Lifecycle.repeatOnLifecycle にする必要があります。ライブラリの正式バージョンとしては、v2.4.0 から導入されますが、targetSdkVersion: 31 が要求されます。repeatOnLifecycle で実装する場合は、このようになります。
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
EventBus.sharedInstance().state.collect { state ->
}
}
}
}
参考:[Android Developers | launchWhenStarted](https://developer.android.com/reference/kotlin/androidx/lifecycle/LifecycleCoroutineScope#launchwhenstarted
-), AndroidX Tech Artifacts
4. まとめ
以上のように Android EventBus を独自実装する方法を紹介しました。Android は iOS と異なり、クラスを跨いだ通知実装が課題になる場面も少なくないかと思います。今回は StateFlow を使った簡易実装ですが、Android の通知実装の実現に参考になれば幸いです。