Hi guys, How do you handle theme changes programma...
# compose
h
Hi guys, How do you handle theme changes programmatically in a compose app, so that it considers the drawables from the drawable-night folder? The
isSystemInDarkTheme()
is just used for colors. For the drawables, I should use
AppCompatDelegate.setDefaultNightMode.
But for now, it breaks my app UI on Android 35. Idk what the issue is with it. Is there any other option?
r
isSystemInDarkTheme()
is checking for the device dark mode. If you change it manually and don't follow the system theme, you should set it manually with AppCompatDelegate
👍 1
h
Yeaah exactly, thanks for the answer. Idk why, but changing it via
AppCompatDelegate
breaks my app UI, the system bars get black. My guess is that there may be some composable state issues because of the configuration change. But it happens only on emulators with android 15. Tried with 2-3 real devices, works fine. The fix for now is to use
UiModeManager
for android 12 and above.
👍 1
m
That makes sense @Hayk. The
ComponentActivity.enableEdgeToEdge
function has an auto setting that pays attention to the system mode to set your status bars to either dark or light colors. On android 15, this is automatically triggered. On all versions below android 15, you have to do this yourself. I would recommend you update all your activities to call this function in the onCreate, before you set any view content (xml or compose) so that you get the same behavior on all versions of android. And if you have application specific logic as to whether or not are in dark or light mode, you can pass that to the function as well.
Copy code
internal fun Context.getComponentActivity(): ComponentActivity? = when (this) {
    is ComponentActivity -> this
    is ContextWrapper -> baseContext.getComponentActivity()
    else -> null
}

@Composable
fun EdgeToEdgeSideEffect(
    isStatusBarLight: Boolean,
    isNavigationBarLight: Boolean
) {
    val activity = LocalContext.current.getComponentActivity()

    activity?.let {
        DisposableEffect(
            isStatusBarLight, isNavigationBarLight
        ) {
            activity.enableEdgeToEdge(
                statusBarStyle =
                if (isStatusBarLight) {
                    SystemBarStyle.light(
                        Color.TRANSPARENT,
                        Color.TRANSPARENT
                    )
                } else {
                    SystemBarStyle.dark(
                        Color.TRANSPARENT
                    )
                },
                navigationBarStyle =
                if (isNavigationBarLight) {
                    SystemBarStyle.light(
                        Color.TRANSPARENT,
                        Color.TRANSPARENT
                    )
                } else {
                    SystemBarStyle.dark(
                        Color.TRANSPARENT
                    )
                }
            )
            onDispose { }
        }
    }
}
❤️ 1
h
@mattinger Thanks a lot, that makes sense. So, should I call this composable inside my
setContent
before I create my other composables? Though does it make sense, cause it's in a DisposableEffect?
m
Yes, you put this as the first thing in your composable. If you were using an xml based screen, you'd do it in the onCreate before you inflate and set your xml content.
Personally, i have this setup in my theme wrapper, since the theme itself knows whether it's light or dark, so this gets applied based there.
h
I do apply colors to my system bar icons, but in a different way
Copy code
DisposableEffect(systemBarColors, isInDarkMode) {
                with(WindowCompat.getInsetsController(window, window.decorView)) {
                    isAppearanceLightStatusBars = systemBarColors.statusBarDarkIcons
                    isAppearanceLightNavigationBars = systemBarColors.navigationBarDarkIcons
                }

                if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
                    window.navigationBarColor = systemBarColors.navigationBarColor.toArgb()
                }

                onDispose { }
            }
Actually I don't think it's connected with edgeToEdge, though it only happens on Android 15. Below that works fine. I tried using your way, but it didn't work 🥲
And it breaks when I set the theme to the opposite of the system theme. I think AppCompatDelegate just doesn't apply the theme again if it's the same as system, so that's why it doesn't break
And on real devices it works fine, breaks only on emulators (yet 😅)
m
Yeah, emulators have some issues.
And i always use transparent for the colors, because it won't apply colors on android 15 anyway. I use the root container to apply the background colors, along with the proper paddings.
h
I'm gonna wait and see if this issue arises on real devices, for now I'll leave it as it is, thanks a lot btw 🔥
Yesss, they deprecated all the methods, so even if you want to apply a color, it's impossible 😅 You just have to get the insets and draw a Box or something by yourself)))
m
Yeah, i fought with the emulator for a day or two before i tried it on a real device and realized it was just an emulator issue.
🫠 1
Yeah, i draw the box myself, though we always use a light navigation bar at the moment, since we don't support dark mode just yet. We just support changing the status bar color from a white to some other dark color
Copy code
Surface(
        color = statusBarColor,
        modifier = Modifier
            .fillMaxSize()
    ) {
        Box(
            modifier = Modifier
                .fillMaxSize()
                .statusBarsPadding()
                .displayCutoutPadding()
                .background(backgroundColor)
                .navigationBarsPadding()
        ) {
            content()
        }
    }
❤️ 1
but you could easily re-work this to have your navigation bar have it's own color.
And again, i also have a wrapper function for the most common case where i call the side effect, make this surface + box and then pass it content to render. Makes my life easier
👍 1
h
Yeaah, I had just a single specific screen where I should have to apply colors, I did it in this way. For other screens it's fully immersive
And with newer compose versions, you won't need this code anymore
Context.getComponentActivity
as they already have
LocalActivity.current
m
Yes, that's there, but you still have to cast it to ComponentActivity.
👍 1