Pantrix UI

Temayı izleyen sistem çubukları

Compose Multiplatform'da kendi koyu tema düğmesi olan bir uygulamada sistemin çizdiği parçaların, Android'in durum ve gezinme çubuklarının, iOS'un durum çubuğunun, klavyesinin ve kendi kontrollerinin neden sistemin koyu modunda kaldığı, kütüphanenin bunun için ne verdiği ve nasıl test edildiği. Sorun Pantrix'e özgü değildir: kendi temasını seçtiren her Compose Multiplatform uygulamasında aynı biçimde görünür.

Burada Compose ve platformlar hakkında söylenenler androidx.activity'nin EdgeToEdge.kt'sinde, androidx.core'un WindowInsetsControllerCompat'ında ve UIKit'in UIViewController.overrideUserInterfaceStyle belgesinde okundu. Doğrulanamayanlar 7. bölümde sayılır.

1. Sorun

Belirti

Neden

Çubukların ve sistem parçalarının görünüşü uygulamanın temasında değil, platformun bir nesnesindedir:

PlatformNe belirlerVarsayılan
AndroidWindowInsetsControllerCompat.isAppearanceLightStatusBars ve isAppearanceLightNavigationBars (koyu simgeli çubuğa "açık çubuk" denir)enableEdgeToEdge() bunları sistemin gece moduna göre ayarlar (SystemBarStyle.auto)
iOSGörünüm denetleyicisinin traitCollection.userInterfaceStyle'ı; durum çubuğu, klavye ve UIKit kontrolleri onu izlerSistemin modu
MasaüstüPencerenin çerçevesi işletim sisteminindirSistemin teması
WebTarayıcının kendi çubuklarıTarayıcının teması

Compose'un ortak kodunda bunlardan hiçbirine ulaşan bir API yoktur: isSystemInDarkTheme() yalnızca sistemin modunu okur, uygulamanın seçimini sisteme söylemez.

2. Çözüm

En dıştaki tema, ekranın koyu mu açık mı olduğunu bilen tek yerdir. Her kompozisyondan sonra o bilgiyi platforma söyler: Android'de pencerenin çubuk simgelerini, iOS'ta uygulamanın görünüm denetleyicisinin arayüz stilini ayarlar. İçteki bir tema (açık bir ekrandaki koyu bir kart) çubuklara dokunmaz: pencerenin çubukları ekranın bütününündür.

3. Pantrix ile

PantrixTheme bunu kendisi yapar; uygulamanın bir şey yazması gerekmez. Okurun seçtiği koyuluk temaya verilir, çubuklar onu izler:

val dark = settings.dark ?: isSystemInDarkTheme()
PantrixTheme(preset = Brand, colors = rememberPantrixColors(Brand, darkTheme = dark)) {
    App()
}

3.1. Çubukları kendisi ayarlayan uygulama

Çubuklarını kendi renkleriyle boyayan (kenardan kenara çizmeyen) ya da simgelerini kendisi seçen bir uygulama, temaya bunu bırakmasını söyler:

PantrixTheme(preset = Brand, followSystemBars = false) {
    App()
}

Pantrix'in kendi katmanları (sheet, dialog) kendi pencerelerinin simgelerini yine uygulamanın penceresinden okur; uygulamanın seçimi onlara da geçer.

4. Pantrix olmadan

4.1. Ortak kod (commonMain)

/** Sistemin çizdiği parçaları [dark] temayı izletir; en dıştaki temanın içinde bir kez çağrılır. */
@Composable
expect fun FollowSystemBars(dark: Boolean)

Uygulamanın kökünde, temanın hemen içinde:

MyTheme(dark = dark) {
    FollowSystemBars(dark)
    App()
}

4.2. Platform karşılıkları

// androidMain
@Composable
actual fun FollowSystemBars(dark: Boolean) {
    val view = LocalView.current
    if (view.isInEditMode) return
    SideEffect {
        val window = view.context.activity()?.window ?: return@SideEffect
        WindowCompat.getInsetsController(window, view).apply {
            // Android koyu simgeli çubuğa açık çubuk der.
            isAppearanceLightStatusBars = !dark
            isAppearanceLightNavigationBars = !dark
        }
    }
}

private tailrec fun Context.activity(): Activity? = when (this) {
    is Activity -> this
    is ContextWrapper -> baseContext.activity()
    else -> null
}
// iosMain
@Composable
actual fun FollowSystemBars(dark: Boolean) {
    val controller = LocalUIViewController.current
    SideEffect {
        controller.overrideUserInterfaceStyle =
            if (dark) UIUserInterfaceStyle.UIUserInterfaceStyleDark else UIUserInterfaceStyle.UIUserInterfaceStyleLight
    }
}
// jvmMain ve webMain
@Composable
actual fun FollowSystemBars(dark: Boolean) = Unit

5. Test etmek

Android'de çubukların durumu Robolectric'te okunur: bir host testi temayı koyu ve açık kurar, etkinliğin penceresinden simgeleri sorar.

@Test
fun the_bars_icons_follow_the_theme_as_it_changes() = runComposeUiTest {
    var dark by mutableStateOf(true)
    var activity: Activity? = null
    setContent {
        activity = LocalActivity.current
        PantrixTheme(darkTheme = dark) { Box(Modifier.size(8.dp)) }
    }
    val window = checkNotNull(activity).window
    val bars = WindowCompat.getInsetsController(window, window.decorView)
    assertFalse(bars.isAppearanceLightStatusBars)
    dark = false
    waitForIdle()
    assertTrue(bars.isAppearanceLightStatusBars)
}

iOS'un testleri bir uygulama olmadan, düz bir Compose sahnesinde koşar: orada görünüm denetleyicisi yoktur ve LocalUIViewController okununca test düşer. Pantrix bu yüzden önce uygulamanın olup olmadığına bakar; iOS'taki davranış simülatörde denenir.

6. Tuzaklar

7. Doğrulama

Testler

Simülatörde görülenler

Elle bozulan davranışlar

Doğrulanmayanlar

8. Bu depoda nerede