Skip to content

Commit 6db442e

Browse files
authored
Merge pull request #231 from amir1376/refactor/darkmode-detector
Refactor: Use an interface for dark mode detector
2 parents f1c9995 + c443012 commit 6db442e

6 files changed

Lines changed: 104 additions & 105 deletions

File tree

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package io.github.kdroidfilter.nucleus.darkmodedetector
2+
3+
import io.github.kdroidfilter.nucleus.core.runtime.Platform
4+
import io.github.kdroidfilter.nucleus.darkmodedetector.linux.LinuxPortalThemeDetector
5+
import io.github.kdroidfilter.nucleus.darkmodedetector.mac.MacOSThemeDetector
6+
import io.github.kdroidfilter.nucleus.darkmodedetector.windows.WindowsThemeDetector
7+
import java.util.function.Consumer
8+
9+
interface IDarkModeDetector {
10+
fun isDark(): Boolean
11+
12+
fun registerListener(listener: Consumer<Boolean>)
13+
14+
fun removeListener(listener: Consumer<Boolean>)
15+
}
16+
17+
object NoopDarkModeDetector : IDarkModeDetector {
18+
override fun isDark(): Boolean = false
19+
20+
override fun registerListener(listener: Consumer<Boolean>) = Unit
21+
22+
override fun removeListener(listener: Consumer<Boolean>) = Unit
23+
}
24+
25+
public fun getPlatformDarkModeDetector(): IDarkModeDetector =
26+
when (Platform.Current) {
27+
Platform.MacOS -> MacOSThemeDetector
28+
Platform.Windows -> WindowsThemeDetector
29+
Platform.Linux -> LinuxPortalThemeDetector
30+
else -> NoopDarkModeDetector
31+
}

darkmode-detector/src/main/kotlin/io/github/kdroidfilter/nucleus/darkmodedetector/IsSystemInDarkMode.kt

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@ package io.github.kdroidfilter.nucleus.darkmodedetector
22

33
import androidx.compose.foundation.isSystemInDarkTheme
44
import androidx.compose.runtime.Composable
5+
import androidx.compose.runtime.DisposableEffect
6+
import androidx.compose.runtime.mutableStateOf
7+
import androidx.compose.runtime.remember
58
import androidx.compose.ui.platform.LocalInspectionMode
6-
import io.github.kdroidfilter.nucleus.core.runtime.Platform
7-
import io.github.kdroidfilter.nucleus.darkmodedetector.linux.isLinuxInDarkMode
8-
import io.github.kdroidfilter.nucleus.darkmodedetector.mac.isMacOsInDarkMode
9-
import io.github.kdroidfilter.nucleus.darkmodedetector.windows.isWindowsInDarkMode
9+
import java.util.function.Consumer
1010

1111
/**
1212
* Composable function that returns whether the system is in dark mode.
@@ -18,11 +18,26 @@ fun isSystemInDarkMode(): Boolean {
1818
if (isInPreview) {
1919
return isSystemInDarkTheme()
2020
}
21+
val darkModeDetector = remember { getPlatformDarkModeDetector() }
22+
val darkModeState = remember { mutableStateOf(darkModeDetector.isDark()) }
2123

22-
return when (Platform.Current) {
23-
Platform.MacOS -> isMacOsInDarkMode()
24-
Platform.Windows -> isWindowsInDarkMode()
25-
Platform.Linux -> isLinuxInDarkMode()
26-
else -> false
24+
DisposableEffect(Unit) {
25+
debugln(TAG) { "Registering OS dark mode listener in Compose" }
26+
val listener =
27+
Consumer<Boolean> { newValue ->
28+
debugln(TAG) { "OS dark mode updated: $newValue" }
29+
darkModeState.value = newValue
30+
}
31+
32+
darkModeDetector.registerListener(listener)
33+
34+
onDispose {
35+
debugln(TAG) { "Removing OS dark mode listener in Compose" }
36+
darkModeDetector.removeListener(listener)
37+
}
2738
}
39+
40+
return darkModeState.value
2841
}
42+
43+
private const val TAG = "PlatformThemeDetectorCompose"
Lines changed: 5 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
package io.github.kdroidfilter.nucleus.darkmodedetector.linux
22

3-
import androidx.compose.runtime.Composable
4-
import androidx.compose.runtime.DisposableEffect
5-
import androidx.compose.runtime.mutableStateOf
6-
import androidx.compose.runtime.remember
3+
import io.github.kdroidfilter.nucleus.darkmodedetector.IDarkModeDetector
74
import io.github.kdroidfilter.nucleus.darkmodedetector.debugln
85
import java.util.function.Consumer
96

@@ -18,42 +15,19 @@ private const val TAG = "LinuxPortalThemeDetector"
1815
* The detector also monitors for SettingChanged signals in real-time via a background
1916
* D-Bus dispatch thread.
2017
*/
21-
internal object LinuxPortalThemeDetector {
18+
internal object LinuxPortalThemeDetector : IDarkModeDetector {
2219
init {
2320
debugln(TAG) { "Initializing Linux portal theme observer via JNI" }
2421
NativeLinuxBridge.nativeStartObserving()
2522
}
2623

27-
fun isDark(): Boolean = NativeLinuxBridge.nativeIsDark()
24+
override fun isDark(): Boolean = NativeLinuxBridge.nativeIsDark()
2825

29-
fun registerListener(listener: Consumer<Boolean>) {
26+
override fun registerListener(listener: Consumer<Boolean>) {
3027
NativeLinuxBridge.registerListener(listener)
3128
}
3229

33-
fun removeListener(listener: Consumer<Boolean>) {
30+
override fun removeListener(listener: Consumer<Boolean>) {
3431
NativeLinuxBridge.removeListener(listener)
3532
}
3633
}
37-
38-
/**
39-
* A helper composable function that returns the current Linux dark mode state
40-
* via the XDG Desktop Portal, updating automatically when the system theme changes.
41-
*/
42-
@Composable
43-
fun isLinuxInDarkMode(): Boolean {
44-
val darkModeState = remember { mutableStateOf(LinuxPortalThemeDetector.isDark()) }
45-
DisposableEffect(Unit) {
46-
debugln(TAG) { "Registering Linux portal dark mode listener in Compose" }
47-
val listener =
48-
Consumer<Boolean> { newValue ->
49-
debugln(TAG) { "Compose Linux portal dark mode updated: $newValue" }
50-
darkModeState.value = newValue
51-
}
52-
LinuxPortalThemeDetector.registerListener(listener)
53-
onDispose {
54-
debugln(TAG) { "Removing Linux portal dark mode listener in Compose" }
55-
LinuxPortalThemeDetector.removeListener(listener)
56-
}
57-
}
58-
return darkModeState.value
59-
}
Lines changed: 5 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
package io.github.kdroidfilter.nucleus.darkmodedetector.mac
22

3-
import androidx.compose.runtime.Composable
4-
import androidx.compose.runtime.DisposableEffect
5-
import androidx.compose.runtime.mutableStateOf
6-
import androidx.compose.runtime.remember
3+
import io.github.kdroidfilter.nucleus.darkmodedetector.IDarkModeDetector
74
import io.github.kdroidfilter.nucleus.darkmodedetector.debugln
85
import java.util.function.Consumer
96

@@ -14,42 +11,19 @@ private const val TAG = "MacOSThemeDetector"
1411
* to detect theme changes in macOS. It reads the system preference "AppleInterfaceStyle"
1512
* (which is "Dark" when in dark mode) from NSUserDefaults.
1613
*/
17-
internal object MacOSThemeDetector {
14+
internal object MacOSThemeDetector : IDarkModeDetector {
1815
init {
1916
debugln(TAG) { "Initializing macOS theme observer via JNI" }
2017
NativeDarkModeBridge.nativeStartObserving()
2118
}
2219

23-
fun isDark(): Boolean = NativeDarkModeBridge.nativeIsDark()
20+
override fun isDark(): Boolean = NativeDarkModeBridge.nativeIsDark()
2421

25-
fun registerListener(listener: Consumer<Boolean>) {
22+
override fun registerListener(listener: Consumer<Boolean>) {
2623
NativeDarkModeBridge.registerListener(listener)
2724
}
2825

29-
fun removeListener(listener: Consumer<Boolean>) {
26+
override fun removeListener(listener: Consumer<Boolean>) {
3027
NativeDarkModeBridge.removeListener(listener)
3128
}
3229
}
33-
34-
/**
35-
* A helper composable function that returns the current macOS dark mode state,
36-
* updating automatically when the system theme changes.
37-
*/
38-
@Composable
39-
internal fun isMacOsInDarkMode(): Boolean {
40-
val darkModeState = remember { mutableStateOf(MacOSThemeDetector.isDark()) }
41-
DisposableEffect(Unit) {
42-
debugln(TAG) { "Registering macOS dark mode listener in Compose" }
43-
val listener =
44-
Consumer<Boolean> { newValue ->
45-
debugln(TAG) { "Compose macOS dark mode updated: $newValue" }
46-
darkModeState.value = newValue
47-
}
48-
MacOSThemeDetector.registerListener(listener)
49-
onDispose {
50-
debugln(TAG) { "Removing macOS dark mode listener in Compose" }
51-
MacOSThemeDetector.removeListener(listener)
52-
}
53-
}
54-
return darkModeState.value
55-
}
Lines changed: 5 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
package io.github.kdroidfilter.nucleus.darkmodedetector.windows
22

3-
import androidx.compose.runtime.Composable
4-
import androidx.compose.runtime.DisposableEffect
5-
import androidx.compose.runtime.mutableStateOf
6-
import androidx.compose.runtime.remember
3+
import io.github.kdroidfilter.nucleus.darkmodedetector.IDarkModeDetector
74
import io.github.kdroidfilter.nucleus.darkmodedetector.debugln
85
import java.util.function.Consumer
96

@@ -18,45 +15,19 @@ private const val TAG = "WindowsThemeDetector"
1815
* The detector monitors the registry for changes in real-time via a native
1916
* background thread using RegNotifyChangeKeyValue in async mode.
2017
*/
21-
internal object WindowsThemeDetector {
18+
internal object WindowsThemeDetector : IDarkModeDetector {
2219
init {
2320
debugln(TAG) { "Initializing Windows theme observer via JNI" }
2421
NativeWindowsBridge.nativeStartObserving()
2522
}
2623

27-
fun isDark(): Boolean = NativeWindowsBridge.nativeIsDark()
24+
override fun isDark(): Boolean = NativeWindowsBridge.nativeIsDark()
2825

29-
fun registerListener(listener: Consumer<Boolean>) {
26+
override fun registerListener(listener: Consumer<Boolean>) {
3027
NativeWindowsBridge.registerListener(listener)
3128
}
3229

33-
fun removeListener(listener: Consumer<Boolean>) {
30+
override fun removeListener(listener: Consumer<Boolean>) {
3431
NativeWindowsBridge.removeListener(listener)
3532
}
3633
}
37-
38-
/**
39-
* Composable function that returns whether Windows is currently in dark mode.
40-
*/
41-
@Composable
42-
internal fun isWindowsInDarkMode(): Boolean {
43-
val darkModeState = remember { mutableStateOf(WindowsThemeDetector.isDark()) }
44-
45-
DisposableEffect(Unit) {
46-
debugln(TAG) { "Registering Windows dark mode listener in Compose" }
47-
val listener =
48-
Consumer<Boolean> { newValue ->
49-
debugln(TAG) { "Windows dark mode updated: $newValue" }
50-
darkModeState.value = newValue
51-
}
52-
53-
WindowsThemeDetector.registerListener(listener)
54-
55-
onDispose {
56-
debugln(TAG) { "Removing Windows dark mode listener in Compose" }
57-
WindowsThemeDetector.removeListener(listener)
58-
}
59-
}
60-
61-
return darkModeState.value
62-
}

docs/runtime/darkmode-detector.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,40 @@ dependencies {
1414

1515
## Usage
1616

17+
### Manual usage
18+
19+
This is the core functionality - it’s totally up to you how you want to use it. Just don’t forget to dispose its callback (`removeListener`) when you don’t need it anymore.
20+
21+
You can use Compose states, RxJava/Kotlin, or anything else you prefer.
22+
23+
Here we’re using a coroutines Flow wrapper just for demonstration.
24+
25+
```kt
26+
val isSystemDarkFlow: Flow<Boolean> = callbackFlow<Boolean> {
27+
val listener: Consumer<Boolean> = { isDark: Boolean ->
28+
trySend(isDark)
29+
}
30+
31+
val darkModeDetector = getPlatformDarkModeDetector()
32+
33+
// emit initial value
34+
trySend(darkModeDetector.isDark())
35+
36+
// listen to dark mode change events
37+
darkModeDetector.registerListener(listener)
38+
awaitClose {
39+
darkModeDetector.removeListener(listener)
40+
}
41+
}
42+
isSystemDarkFlow.collect {
43+
println("dark mode changes, isDark: $it")
44+
}
45+
```
46+
47+
### In compose
48+
49+
There is already a composable function available out of the box, which you can use it in your compose applications.
50+
1751
```kotlin
1852
@Composable
1953
fun App() {

0 commit comments

Comments
 (0)