🐈

onUserLeaveHintとかをComposable関数にしたい

2023/08/14に公開

やったこと

Activty#onUserLeaveHintなどをComposable内で使用しやすくする方法を考えてみました。

動機

最近はJetpack Composeの導入事例が増えてきて、自分の周りではSingle Activityで各画面はComposable関数で開発することが多くなってきました。
そんな中特定の画面だけPIPをサポートしたいケースなど、特定の画面だけでonUserLeaveHintとかをハンドリングしたいケースが出てきました。
BackHandlerみたいにOnUserLeaveHint()みたいなのあると便利かも!と思い、夏休みの自由研究がてら作成してみたので備忘として残しておこうと思いました。

やり方

1. Activtyの固有イベント用のDispatcherを作成する

次の様なクラスを作成します

object ActivitySpecificEventDispatcher {
    private val callbackList = mutableListOf<ActivitySpecificEventCallback>()

    fun addCallback(callback: ActivitySpecificEventCallback){
        callbackList.add(callback)
    }

    fun removeCallback(callback: ActivitySpecificEventCallback){
        callbackList.remove(callback)
    }

    fun dispatchPictureInPictureModeChanged(
        isInPictureInPictureMode: Boolean,
        newConfig: Configuration
    ){
        callbackList.forEach {
            it.onPictureInPictureModeChanged(
                isInPictureInPictureMode,
                newConfig
            )
        }
    }

    fun dispatchUserLeaveHint(){
        callbackList.forEach {
            it.onUserLeaveHint()
        }
    }
}

abstract class ActivitySpecificEventCallback {
    open fun onPictureInPictureModeChanged(
        isInPictureInPictureMode: Boolean,
        newConfig: Configuration
    ){}
    open fun onUserLeaveHint(){}
}

OnBackPressedDispatcherをイメージしています。
今回はonUserLeaveHintとonPictureInPictureModeChangedを対象にしてみましたが、他にもハンドリングしたいものがあれば随時増やしてもらえればと思います。

2. ActivtySpecificEventDispatcherのCompsitionLocalを作成する

作成したDispatcherのCompsitionLocalを次の様に作成します

// ActivitySpecificEventDispatcher.kt
val LocalActivitySpecificEventDispatcher = compositionLocalOf { ActivitySpecificEventDispatcher }

作成したCompsitionLocalの次の様に値を設定します。
今回は

// MainActtivy.kt

private val activitySpecificEventDispatcher = ActivitySpecificEventDispatcher

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            AppTheme {
                CompositionLocalProvider(
                    LocalActivitySpecificEventDispatcher provides activitySpecificEventDispatcher
                ) {
                    App()
                }
            }
        }
    }

さらにMainActtivyではonUserLeaveHintをオーバーライドし、ActivitySpecificEventDispatcher#dispatchUserLeaveHint呼んであげる様にします。

// MainActtivy.kt
override fun onUserLeaveHint() {
    activitySpecificEventDispatcher
        .dispatchUserLeaveHint()
}

override fun onPictureInPictureModeChanged(
    isInPictureInPictureMode: Boolean,
    newConfig: Configuration
) {
    super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
    activitySpecificEventDispatcher
        .dispatchPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
}

今回は省略しましたが、ActivitySpecificEventDispatcherはDagger Hiltなどを用いてDIしてあげるのがよいでしょう。

ここまでが下準備です

3. OnUserLeaveHint()を作成する

本題のonUserLeaveHintをハンドリングできるComposable関数を作成します。

@Composable
fun OnUseLeaveHint(action: () -> Unit){
    val dispatcher = LocalActivitySpecificEventDispatcher.current
    DisposableEffect(dispatcher){
        val callback = object : ActivitySpecificEventCallback(){
            override fun onUserLeaveHint() {
                action()
            }
        }
        dispatcher.addCallback(callback)
        onDispose {
            dispatcher.removeCallback(callback)
        }
    }
}

基本的にDispatcherによってハンドリングしたいイベントが発生した時はCallbackの該当メソッドが呼ばれるのでそれを通知してあげるだけです。
ついでにOnPictureInPictureModeChangedも作成しますが、こちらも同じ原理です。

@Composable
fun OnPictureInPictureModeChanged(
    action: (
        isInPictureInPictureMode: Boolean,
        newConfig: Configuration
    ) -> Unit
){
    val dispatcher = LocalActivitySpecificEventDispatcher.current
    DisposableEffect(dispatcher){
        val callback = object : ActivitySpecificEventCallback(){
            override fun onPictureInPictureModeChanged(
                isInPictureInPictureMode: Boolean,
                newConfig: Configuration
            ) {
                action(isInPictureInPictureMode, newConfig)
            }
        }
        dispatcher.addCallback(callback)
        onDispose {
            dispatcher.removeCallback(callback)
        }
    }
}

4. 使用する

実際に使用するとこんな感じです

@Composable
fun VideoPlayerScreen() {
    val viewModel: PlayerViewModel = hiltViewModel()
    val uiState: PlayerUiState by viewModel.uiState.collectAsStateWithLifecycle()

    BackHandler {
        viewModel.onBackPress()
    }

    OnUseLeaveHint {
        viewModel.onUseLeaveHint()
    }

    OnPictureInPictureModeChanged { isChanged, config ->
        viewModel.onPictureInPictureModeChanged(
            isInPictureInPictureMode = isChanged,
            newConfig = config
        )
    }

    Player(
        uiState = uiState
    )
}

これでBackHandlerのようにonUseLeaveHintなどを使える様になり、Composable関数で作成された特定の画面でだけイベントをハンドリングすることができます。
この仕組みを利用するとハインドリングしたいイベントが増えた時はDispatcherやCallbackに該当イベントをハンドリングするメソッドを生やしてあげて、対応するComposable関数を作成するだけです。

最後に

夏休みの自由研究がてら作成してみましたが、不備あれば優しく教えていただければと思います!
参考になれば幸いです!

Discussion