DEV Community

loading...
Cover image for Automatically switch to Dark Mode (and back) in Compose for Desktop

Automatically switch to Dark Mode (and back) in Compose for Desktop

tkuenneth profile image Thomas Kuenneth ・2 min read

Desktop apps written in the default programming language and ui framework of the platform usually respond to changes of the color scheme immediately. Wouldn't it be cool if your Compose for Desktop app did that, too? Allow me to whet your appetite:

On Android we can detect Dark Mode using isSystemInDarkTheme() (androidx.compose.foundation), but currently this function is not available in Compose for Desktop. So let's do this on our own, shall we?

fun isSystemInDarkTheme(): Boolean {
  return when {
    IS_WINDOWS -> {
      val result = getWindowsRegistryEntry(
          "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize",
          "AppsUseLightTheme")
      result == 0x0
    }
    IS_MACOS -> {
      val result = getDefaultsEntry("AppleInterfaceStyle")
      result == "Dark"
    }
    else -> false
  }
}
Enter fullscreen mode Exit fullscreen mode

As you can see, the color mode is stored in the Windows Registry and the macOS Defaults database. To access both in Java or Kotlin I have written a tiny open source library called Native Parameter Store Acess. You can find it on GitHub. It's easy to include it in your apps.

Maven

<dependency>
  <groupId>com.github.tkuenneth</groupId>
  <artifactId>nativeparameterstoreaccess</artifactId>
  <version>0.1.0</version>
</dependency>
Enter fullscreen mode Exit fullscreen mode

Gradle

dependencies {
  implementation("com.github.tkuenneth:nativeparameterstoreaccess:0.1.0")
}
Enter fullscreen mode Exit fullscreen mode

Now that we have the library in place, when do we call isSystemInDarkTheme() and (more importantly) what do we do then? To detect changes while your app is running I suggest using a coroutine that repeatedly invokes isSystemInDarkTheme().

fun main() {
  GlobalScope.launch {
    while (isActive) {
      val newMode = isSystemInDarkTheme()
      if (isInDarkMode != newMode) {
        isInDarkMode = newMode
      }
      delay(1000)
    }
  }
  
Enter fullscreen mode Exit fullscreen mode

Usually GlobalScope may be too broad, but given the detection should happen as long as the app is running I find it quite appropriate for this use case. What about isInDarkMode?

private var isInDarkMode by observable(false) { _, oldValue, newValue ->
  onIsInDarkModeChanged?.let { it(oldValue, newValue) }
}
private var onIsInDarkModeChanged: ((Boolean, Boolean) -> Unit)? = null
Enter fullscreen mode Exit fullscreen mode

It is an observable, which calls code referenced by onIsInDarkModeChanged. As long as this is null, nothing happens. Here is where it is actually set:

@Composable
fun TKDupeFinderContent() {
  var colors by remember { mutableStateOf(colors()) }
  onIsInDarkModeChanged = { _, _ ->
    colors = colors()
  }
  
  DesktopMaterialTheme(colors = colors) {
    Surface {
  
Enter fullscreen mode Exit fullscreen mode

The detection itself cannot be part of a composable as it needs to be done concurrently. But a change needs to trigger a recomposition. This is achieved through colors = colors() because colors is a remembered state. Here is where the colors come from:

private fun colors(): Colors = if (isInDarkMode) {
  darkColors()
} else {
  lightColors()
}
Enter fullscreen mode Exit fullscreen mode

If you watched the clip at the beginning of this article you surely have noticed that the window decoration and the menubar are not changed. As Compose for Desktop relies on Swing we should be able to fix this by using a Dark Mode-aware Swing look and feel. I may be followig up on this later...

Discussion (0)

pic
Editor guide