5
3

現在のAndroid開発ではライフサイクルメソッドのオーバーライドは推奨されてないけど、もっといい方法ないかな話

Last updated at Posted at 2023-12-19

概要

昔は当たり前であった実装方法が現在では、推奨されなくなってたりします。たとえば、onResume()などのライフサイクルメソッドでもオーバーライドを現在ではしなくなっています。

公式には以下のように「アクティビティやフラグメントのライフサイクルメソッドをオーバーライドしない」というのを強く推奨してます。

スクリーンショット 2023-12-17 2.59.01.png

対応方法は書いてあるのですが、ここで、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)
    ...

repeatOnLifecyclecollectAsStateWithLifecycleを使えば、こんなの使うことはないと思うかもしれませんが、複数のFlowをマージして、それを1つで購読したい場合などでは便利かもしれません。
何を言ってるかわからないので、次にユースケースのサンプルを挙げます。

こんなときに使えるかも

たとえば、フィードで動画プレイヤーを利用している場合です。

  • ライフサイクルのSTARTでは、プレイ・イベントを送る
  • ライフサイクルのSTOPでは、ストップ・イベントを送る
  • RecyclerView.OnScrollListenercallbackFlowFlowとして管理していて、動画のアイテムが見えたときに、プレイ・イベントを送る。逆に隠れたらストップ・イベントを送る
  • 動画プレイヤーのクリック・イベントも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では、最近、人気の番組をショート動画として、本編を見るきっかけになるような機能をリリースしています。
内容が面白いのでぜひ、見てほしいです。

で、実際は、これとは違う仕組みで実装しましたが、こんな感じでも良さそうという話でした。(動くコードは気が向いたら書こうと思います)

5
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
3