概要
昔は当たり前であった実装方法が現在では、推奨されなくなってたりします。たとえば、onResume()
などのライフサイクルメソッドでもオーバーライドを現在ではしなくなっています。
公式には以下のように「アクティビティやフラグメントのライフサイクルメソッドをオーバーライドしない」というのを強く推奨してます。
対応方法は書いてあるのですが、ここで、callbackFlow
を使って、ライフサイクルもFlow
として管理すると使いやすいかもね。という話です。
公式の対応方法
書いてある通りです。LifecycleObserverで対応しましょうって、Androidエンジニアだったらすぐにわかるでしょう。
もちろん、ActivityでもFragmentでもほぼ同じです。
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycle.addObserver(object : DefaultLifecycleObserver {
override fun onResume(owner: LifecycleOwner) {
Log.d("lifecycle","AndroidView")
}
})
・・・
}
コンポーザブル関数で書くとこんな感じです。
@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
val lifecycleOwner = LocalLifecycleOwner.current
DisposableEffect(lifecycleOwner) {
val lifecycleObserver = object : DefaultLifecycleObserver {
override fun onResume(owner: LifecycleOwner) {
Log.d("lifecycle","DisposableEffect")
}
}
lifecycleOwner.lifecycle.addObserver(lifecycleObserver)
onDispose {
lifecycleOwner.lifecycle.removeObserver(lifecycleObserver)
}
}
もうひとつの方法
上記の方法だとオーバーライドしたときと、可読性は、そんなに変わらないと思います。
で、もうひとつの方法として、人によっては昔からやってるよ的なやつだと思いますが、ライフサイクルイベントもFlow
にして扱うという話です。
処理の内容によっては可読性は上がると思います。
fun Lifecycle.events(): Flow<Lifecycle.Event> = callbackFlow {
val observer = LifecycleEventObserver { _, event -> trySend(event) }
addObserver(observer)
awaitClose { removeObserver(observer) }
}
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycle.events().filter { it == Lifecycle.Event.ON_RESUME }.onEach {
Log.d("lifecycle","callbackFlow")
}.launchIn(lifecycleScope)
...
repeatOnLifecycle
やcollectAsStateWithLifecycle
を使えば、こんなの使うことはないと思うかもしれませんが、複数のFlow
をマージして、それを1つで購読したい場合などでは便利かもしれません。
何を言ってるかわからないので、次にユースケースのサンプルを挙げます。
こんなときに使えるかも
たとえば、フィードで動画プレイヤーを利用している場合です。
- ライフサイクルのSTARTでは、プレイ・イベントを送る
- ライフサイクルのSTOPでは、ストップ・イベントを送る
-
RecyclerView.OnScrollListener
もcallbackFlow
でFlow
として管理していて、動画のアイテムが見えたときに、プレイ・イベントを送る。逆に隠れたらストップ・イベントを送る - 動画プレイヤーのクリック・イベントも
SharedFlow
で扱い、ViewHolder内のsetOnClickListenerで、emitし、プレイやストップのイベントを送る - それらのイベントを購読して、イベントが来たら、Playerを再生/停止をする
これも何をいってるかわからないかもなので、こんな感じです。(雰囲気で書いてるので動かない実装ですが、イメージとして見ていただけると)
以下は、ライフサイクルの実装で、スタートとストップのイベントを送っています。
fun Lifecycle.viewEvents(): Flow<PlayerEvent> {
return events()
.filter { event -> event == Lifecycle.Event.ON_START || event == Lifecycle.Event.ON_STOP }
.map { event ->
when (event) {
Lifecycle.Event.ON_START -> PlayerEvent.Start
Lifecycle.Event.ON_STOP -> PlayerEvent.Stop
else -> {}
}
}
}
以下はRecyclerViewのスクロールリスナーで動画が表示されたときにプレイイベント、非表示になったときにストップイベントを送る実装をします。
fun RecyclerView.visible(): Flow<PlayerEvent> = callbackFlow<PlayerEvent> {
val scrollListener = object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
val layoutManager = layoutManager
// 色々実装しないといけないけど、こんな雰囲気。
・・・・
// 動画プレイヤーが全部でたら
trySend(PlayerEvent.Start) // TODO 本当は、トラックのindexを渡して動かす動画を特定する必要があるが割愛
・・・・
// 動画プレイヤーが消えたら
trySend(PlayerEvent.Stop) // TODO 本当は、トラックのindexを渡して止める動画を特定する必要があるが割愛
・・・・
}
}
addOnScrollListener(scrollListener)
awaitClose {
removeOnScrollListener(scrollListener)
}
}
以下はクリックしたときのイベントを投げる処理
class RecyclerViewHolder(
private val binding: ItemBinding,
) : RecyclerView.ViewHolder(binding.root) {
private val clicks = MutableSharedFlow<PlayerEvent>(extraBufferCapacity = 1)
fun clicks() = clicks.asSharedFlow()
init {
// TODO 本当は動画が動いてる時だけ、Stopで、逆の場合はPlayにする必要があるが割愛
binding.root.setOnClickListener { clicks.tryEmit(PlayerEvent.Play) }
}
・・・
上の3つをひとつのフローで管理
val events = merge(
lifecycle.viewEvents(),
binding.recyclerView.visible(),
adapter.viewEvents()
).onEach {
when(it) {
PlayerEvent.Play -> player.play()
PlayerEvent.Stop -> player.stop()
}
}.launchIn(lifecycleScope)
こんな感じで実装していけば、プレイヤーの処理を一箇所で管理できると思います。
上記はAndroid Viewで書いてますが、Composeでもほとんど同じです。
NewsPicksでは、最近、人気の番組をショート動画として、本編を見るきっかけになるような機能をリリースしています。
内容が面白いのでぜひ、見てほしいです。
で、実際は、これとは違う仕組みで実装しましたが、こんな感じでも良さそうという話でした。(動くコードは気が向いたら書こうと思います)