↩️
Compose Multiplatformでアプリのバックグラウンド切り替えを検知するには
こんにちは!sugitaniと申します。ブラックキャット・カーニバル(略称ブラキャニ)というCompose Multiplatformで作られたSNSアプリを開発しています。
本稿はブラキャニの開発で得られた"あれどうやるんだっけ" を備忘録も兼ねて共有していくシリーズの1作目です。
フォアグラウンド/バックグラウンド切り替えを検知する方法
バックグラウンド切り替えを検知する場合、iOS/Androidでアプローチが異なります。よってexpect/actualを使って個別に実装していくことになります
- iOS: NotificationCenterで
UIApplicationDidBecomeActiveNotification
やUIApplicationDidEnterBackgroundNotification
を監視します - Android:
registerActivityLifecycleCallbacks
を使いActivityLifecycleCallbacksを呼んでもらいます
状態が切り替わるたびにテキストが表示されていくアプリを実装してみましょう。
commonMainでの実装
package cc.bcc.cmpexamples.example001.lifecycle
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.compositionLocalOf
import kotlinx.coroutines.flow.StateFlow
interface AppLifecycleTracker {
fun start()
fun stop()
val isInForeground: StateFlow<Boolean>
}
@Composable
expect fun buildAppLifecycleTracker(): AppLifecycleTracker
@Composable
fun rememberAppLifecycleTracker(): AppLifecycleTracker {
val lifecycleTracker = buildAppLifecycleTracker()
DisposableEffect(lifecycleTracker) {
lifecycleTracker.start()
onDispose {
lifecycleTracker.stop()
}
}
return lifecycleTracker
}
val LocalAppLifecycleTracker =
compositionLocalOf<AppLifecycleTracker> {
throw Exception("LocalAppLifecycleTracker is not initialized")
}
やっていること
- 状態変更を追跡する
AppLifecycleTracker
のinterface定義を行います - そのインスタンス作成を行う
buildAppLifecycleTracker()
を定義します。Android側でContextが絡むので@Composable
をつけています - 作った
AppLifecycleTracker
インスタンスをよしなに開始終了するrememberAppLifecycleTracker()
を定義します - 作った
AppLifecycleTracker
インスタンスを全体に共有するためのLocalAppLifecycleTracker
を定義します
Androidの実装
package cc.bcc.cmpexamples.example001.lifecycle
import android.app.Activity
import android.app.Application
import android.os.Bundle
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
@Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING")
class AppLifecycleTrackerImpl(
private val activity: Activity,
) : AppLifecycleTracker {
private val _isInForeground = MutableStateFlow(true)
override val isInForeground: StateFlow<Boolean> = _isInForeground.asStateFlow()
private val activityLifecycleCallbacks =
object : Application.ActivityLifecycleCallbacks {
override fun onActivityStarted(activity: Activity) {
if (this@AppLifecycleTrackerImpl.activity == activity) {
_isInForeground.value = true
}
}
override fun onActivityStopped(activity: Activity) {
if (this@AppLifecycleTrackerImpl.activity == activity) {
_isInForeground.value = false
}
}
override fun onActivityCreated(
activity: Activity,
savedInstanceState: Bundle?,
) {
}
override fun onActivityResumed(activity: Activity) {}
override fun onActivityPaused(activity: Activity) {}
override fun onActivitySaveInstanceState(
activity: Activity,
outState: Bundle,
) {
}
override fun onActivityDestroyed(activity: Activity) {}
}
override fun start() {
activity.registerActivityLifecycleCallbacks(activityLifecycleCallbacks)
}
override fun stop() {
activity.unregisterActivityLifecycleCallbacks(activityLifecycleCallbacks)
}
}
@Composable
actual fun buildAppLifecycleTracker(): AppLifecycleTracker {
val activity = LocalContext.current as Activity
return remember(activity) {
AppLifecycleTrackerImpl(activity)
}
}
やっていること
-
registerActivityLifecycleCallbacks
でコールバックを呼び出してもらうようにします -
onActivityStarted
/onActivityStopped
を監視してStateFlowに状態を保存しています
iOS側の実装
@Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING")
class AppLifecycleTrackerImpl : AppLifecycleTracker {
private val _isInForeground =
MutableStateFlow(
UIApplication.sharedApplication.applicationState ==
UIApplicationState.UIApplicationStateActive,
)
override val isInForeground: StateFlow<Boolean> = _isInForeground.asStateFlow()
private var didBecomeActiveObserver: Any? = null
private var didEnterBackgroundObserver: Any? = null
override fun start() {
val notificationCenter = NSNotificationCenter.defaultCenter
didBecomeActiveObserver =
notificationCenter.addObserverForName(
UIApplicationDidBecomeActiveNotification,
null,
NSOperationQueue.mainQueue,
) { _ ->
_isInForeground.value = true
}
didEnterBackgroundObserver =
notificationCenter.addObserverForName(
UIApplicationDidEnterBackgroundNotification,
null,
NSOperationQueue.mainQueue,
) { _ ->
_isInForeground.value = false
}
}
override fun stop() {
val notificationCenter = NSNotificationCenter.defaultCenter
didBecomeActiveObserver?.let {
notificationCenter.removeObserver(it)
didBecomeActiveObserver = null
}
didEnterBackgroundObserver?.let {
notificationCenter.removeObserver(it)
didEnterBackgroundObserver = null
}
}
}
@Composable
actual fun buildAppLifecycleTracker(): AppLifecycleTracker = remember { AppLifecycleTrackerImpl() }
やっていること
- NotificationCenterを使って
UIApplicationDidBecomeActiveNotification
とUIApplicationDidEnterBackgroundNotification
を監視し、StateFlowを適宜更新しています
使い方
アプリのエントリポイントに近いところでAppLifecycleTrackerを準備しておきます
@Composable
internal fun App() {
val tracker = rememberAppLifecycleTracker()
CompositionLocalProvider(LocalAppLifecycleTracker provides tracker) {
TrackCheck()
}
}
あとは trackerの isInForeground
を監視するだけです
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun TrackCheck(modifier: Modifier = Modifier) {
val tracker = LocalAppLifecycleTracker.current
val logs = mutableStateListOf<String>()
LaunchedEffect(Unit) {
tracker.isInForeground.collect { isInForeground ->
val entry = "App is in foreground: $isInForeground"
println(entry)
logs.add(entry)
}
}
MaterialTheme {
Scaffold(
topBar = {
CenterAlignedTopAppBar(
title = { Text("App Lifecycle Tracker Example") },
)
},
modifier = modifier,
) { innerPadding ->
Column(
modifier =
Modifier
.padding(innerPadding)
.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
logs.forEach { log ->
Text(text = log, modifier = Modifier.padding(vertical = 4.dp))
}
}
}
}
}
注意点としてiOSでは collectAsState()
がバックグラウンドでは動作しないので、以下のようにしてしまうと正しく動作しません (参考: https://youtrack.jetbrains.com/issue/CMP-3889 )
// // This approach doesn't work on iOS with Compose Multiplatform.
// val isInForeground by tracker.isInForeground.collectAsState()
// LaunchedEffect(isInForeground) {
// val entry = "App is in foreground: $isInForeground"
// println(entry)
// logs.add(entry)
// }
サンプルプロジェクト
本稿のソースコード、および動作するコードは
にあります。免責事項
このコードはあくまで"自分はこう実装した"という例ですので、よりよい方法がある可能性があります。見つけたら教えてください!
以下宣伝
- コメントのしやすさともらいやすさに全力を注いだショートSNS ブラキャニ をお試しください
- ブラックキャット・カーニバル株式会社のPublicationではCompose Multiplatformに関する記事を沢山投稿しています
- Compose Multiplatformでアプリのバックグラウンド切り替えを検知するには
- Compose MultiplatformでコンポーネントをImageBitmapにするには
- Compose MultiplatformでKoinを使って環境別のDIを行う方法
- Compose Multiplatformでexpect/actialでContextをうまく扱う方法
- Compose Multiplatformで後から重ねられる&一部をくり抜いたコンポーネントを作るには
- Compose MultiplatformでImageBitmapとBitmap/UIImageを相互変換するには
- Compose MultiplatformでLiquid glass対応のタブ表示を行うには
Discussion