Automatically switch to Dark Mode (and back) in Compose for Desktop
Tags: kotlin, ux
Published: 2020-12-29 10:48:28

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:

{% youtube Ej84WtSr59c %}

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? 

```kotlin
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
  }
}
```

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](https://github.com/tkuenneth/nativeparameterstoreaccess). It's easy to include it in your apps.

#### Maven

```xml
<dependency>
  <groupId>com.github.tkuenneth</groupId>
  <artifactId>nativeparameterstoreaccess</artifactId>
  <version>0.1.0</version>
</dependency>
```

#### Gradle

```
dependencies {
  implementation("com.github.tkuenneth:nativeparameterstoreaccess:0.1.0")
}
```

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()`.

```kotlin
fun main() {
  GlobalScope.launch {
    while (isActive) {
      val newMode = isSystemInDarkTheme()
      if (isInDarkMode != newMode) {
        isInDarkMode = newMode
      }
      delay(1000)
    }
  }
  …
```

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`?

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

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:

```kotlin
@Composable
fun TKDupeFinderContent() {
  var colors by remember { mutableStateOf(colors()) }
  onIsInDarkModeChanged = { _, _ ->
    colors = colors()
  }
  …
  DesktopMaterialTheme(colors = colors) {
    Surface {
  …
```

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:

```kotlin
private fun colors(): Colors = if (isInDarkMode) {
  darkColors()
} else {
  lightColors()
}
```

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...