Skip to content

Commit f32f3b9

Browse files
authored
Merge pull request #15 from Javernaut/refactoring/theme-management
Theme management refactoring
2 parents 0ee2009 + 2ae13f0 commit f32f3b9

File tree

19 files changed

+236
-109
lines changed

19 files changed

+236
-109
lines changed

app/build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,11 +135,11 @@ dependencies {
135135
implementation(libs.bundles.androidx.compose)
136136

137137
implementation(libs.bundles.androidx.lifecycle)
138-
implementation(libs.androidx.appcompat)
139138
implementation(libs.androidx.palette)
140139
implementation(libs.androidx.savedstate)
141140
implementation(libs.androidx.browser)
142141
implementation(libs.androidx.window)
142+
implementation(libs.androidx.datastore.preferences)
143143
implementation(libs.androidx.navigation.compose)
144144

145145
implementation(libs.mediafile)
Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,7 @@
11
package com.javernaut.whatthecodec
22

33
import android.app.Application
4-
import com.javernaut.whatthecodec.settings.ThemeManager
54
import dagger.hilt.android.HiltAndroidApp
65

76
@HiltAndroidApp
8-
class App : Application() {
9-
10-
override fun onCreate() {
11-
super.onCreate()
12-
ThemeManager.initNightModePreference(this)
13-
}
14-
15-
}
7+
class App : Application()

app/src/main/java/com/javernaut/whatthecodec/compose/common/Common.kt

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,16 @@
11
package com.javernaut.whatthecodec.compose.common
22

3+
import android.content.BroadcastReceiver
4+
import android.content.Context
5+
import android.content.Intent
6+
import android.content.IntentFilter
37
import androidx.compose.foundation.layout.BoxWithConstraints
48
import androidx.compose.runtime.Composable
9+
import androidx.compose.runtime.DisposableEffect
10+
import androidx.compose.runtime.getValue
11+
import androidx.compose.runtime.rememberUpdatedState
512
import androidx.compose.ui.Modifier
13+
import androidx.compose.ui.platform.LocalContext
614

715
@Composable
816
fun OrientationLayout(
@@ -18,3 +26,32 @@ fun OrientationLayout(
1826
}
1927
}
2028
}
29+
30+
@Composable
31+
fun SystemBroadcastReceiver(
32+
systemAction: String,
33+
onSystemEvent: (intent: Intent?) -> Unit
34+
) {
35+
// Grab the current context in this part of the UI tree
36+
val context = LocalContext.current
37+
38+
// Safely use the latest onSystemEvent lambda passed to the function
39+
val currentOnSystemEvent by rememberUpdatedState(onSystemEvent)
40+
41+
// If either context or systemAction changes, unregister and register again
42+
DisposableEffect(context, systemAction) {
43+
val intentFilter = IntentFilter(systemAction)
44+
val broadcast = object : BroadcastReceiver() {
45+
override fun onReceive(context: Context?, intent: Intent?) {
46+
currentOnSystemEvent(intent)
47+
}
48+
}
49+
50+
context.registerReceiver(broadcast, intentFilter)
51+
52+
// When the effect leaves the Composition, remove the callback
53+
onDispose {
54+
context.unregisterReceiver(broadcast)
55+
}
56+
}
57+
}

app/src/main/java/com/javernaut/whatthecodec/compose/playground/ComposePlaygroundActivity.kt

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
package com.javernaut.whatthecodec.compose.playground
22

33
import android.os.Bundle
4+
import androidx.activity.ComponentActivity
45
import androidx.activity.compose.setContent
56
import androidx.activity.enableEdgeToEdge
6-
import androidx.appcompat.app.AppCompatActivity
77
import androidx.compose.foundation.layout.Box
88
import androidx.compose.foundation.layout.fillMaxSize
99
import androidx.compose.foundation.layout.padding
@@ -24,18 +24,20 @@ import androidx.lifecycle.viewmodel.compose.viewModel
2424
import com.javernaut.whatthecodec.R
2525
import com.javernaut.whatthecodec.compose.theme.WhatTheCodecTheme
2626
import com.javernaut.whatthecodec.home.ui.screen.ObserveAsEvents
27+
import dagger.hilt.android.AndroidEntryPoint
2728
import kotlinx.coroutines.channels.Channel
2829
import kotlinx.coroutines.flow.receiveAsFlow
2930
import kotlinx.coroutines.launch
3031

31-
class ComposePlaygroundActivity : AppCompatActivity() {
32+
@AndroidEntryPoint
33+
class ComposePlaygroundActivity : ComponentActivity() {
3234

3335
override fun onCreate(savedInstanceState: Bundle?) {
3436
enableEdgeToEdge()
3537
super.onCreate(savedInstanceState)
3638

3739
setContent {
38-
WhatTheCodecTheme {
40+
WhatTheCodecTheme.Dynamic {
3941
PlaygroundScreen()
4042
}
4143
}

app/src/main/java/com/javernaut/whatthecodec/compose/preference/ListPreference.kt

Lines changed: 10 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,10 @@ import androidx.compose.material3.Text
2323
import androidx.compose.material3.TextButton
2424
import androidx.compose.runtime.Composable
2525
import androidx.compose.runtime.getValue
26+
import androidx.compose.runtime.mutableIntStateOf
2627
import androidx.compose.runtime.mutableStateOf
2728
import androidx.compose.runtime.remember
29+
import androidx.compose.runtime.saveable.rememberSaveable
2830
import androidx.compose.runtime.setValue
2931
import androidx.compose.ui.Alignment
3032
import androidx.compose.ui.Modifier
@@ -36,24 +38,15 @@ import com.javernaut.whatthecodec.compose.common.WtcDialog
3638

3739
@Composable
3840
fun ListPreference(
39-
key: String,
40-
defaultValue: String,
4141
title: String,
4242
displayableEntries: List<String>,
43-
entriesCodes: List<String>,
44-
onNewCodeSelected: (String) -> Unit
43+
selectedItemIndex: Int,
44+
onNewCodeSelected: (Int) -> Unit
4545
) {
46-
val applicationContext = LocalContext.current.applicationContext
47-
val defaultSharedPreferences =
48-
PreferenceManager.getDefaultSharedPreferences(applicationContext)
49-
val selectedItemCode = defaultSharedPreferences.getString(key, defaultValue)
50-
51-
val currentlySelectedItemIndex = entriesCodes.indexOf(selectedItemCode)
52-
53-
var dialogOpened by remember { mutableStateOf(false) }
46+
var dialogOpened by rememberSaveable { mutableStateOf(false) }
5447
Preference(
5548
title = title,
56-
summary = displayableEntries[currentlySelectedItemIndex]
49+
summary = displayableEntries[selectedItemIndex]
5750
) {
5851
dialogOpened = true
5952
}
@@ -63,14 +56,9 @@ fun ListPreference(
6356
title = title,
6457
dismissRequest = { dialogOpened = false },
6558
items = displayableEntries,
66-
currentlySelectedIndex = currentlySelectedItemIndex
59+
currentlySelectedIndex = selectedItemIndex
6760
) {
68-
val newValueToSet = entriesCodes[it]
69-
defaultSharedPreferences
70-
.edit()
71-
.putString(key, newValueToSet)
72-
.apply()
73-
onNewCodeSelected(newValueToSet)
61+
onNewCodeSelected(it)
7462
}
7563
}
7664
}
@@ -83,7 +71,7 @@ fun SingleChoicePreferenceDialog(
8371
currentlySelectedIndex: Int,
8472
clickListener: (Int) -> Unit
8573
) {
86-
var selectedIndex by remember { mutableStateOf(currentlySelectedIndex) }
74+
var selectedIndex by rememberSaveable { mutableIntStateOf(currentlySelectedIndex) }
8775
PreferenceDialog(title, dismissRequest,
8876
applyRequest = {
8977
clickListener(selectedIndex)
@@ -123,7 +111,7 @@ fun MultiSelectListPreference(
123111
selectedItemCodes.contains(entriesCodes[it])
124112
}
125113

126-
var dialogOpened by remember { mutableStateOf(false) }
114+
var dialogOpened by rememberSaveable { mutableStateOf(false) }
127115
Preference(
128116
title = title,
129117
summary = summaryBuilder(displayableEntries.filterIndexed { index, s ->

app/src/main/java/com/javernaut/whatthecodec/compose/preference/Preference.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ private fun PreferenceTitle(@StringRes title: Int) {
100100
@Preview
101101
@Composable
102102
private fun PreferencePreview() {
103-
WhatTheCodecTheme {
103+
WhatTheCodecTheme.Static {
104104
PreferenceGroup {
105105
Preference(
106106
title = "Title",
@@ -114,7 +114,7 @@ private fun PreferencePreview() {
114114
@PreviewLightDark
115115
@Composable
116116
private fun PreviewPreferenceGroup() {
117-
WhatTheCodecTheme {
117+
WhatTheCodecTheme.Static {
118118
Column {
119119
PreferenceGroup(R.string.settings_title) {
120120
Preference(title = "Title", summary = "Summary") {

app/src/main/java/com/javernaut/whatthecodec/compose/theme/Theme.kt

Lines changed: 0 additions & 18 deletions
This file was deleted.
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package com.javernaut.whatthecodec.compose.theme
2+
3+
import android.app.Activity
4+
import android.os.Build
5+
import android.os.PowerManager
6+
import androidx.compose.foundation.isSystemInDarkTheme
7+
import androidx.compose.material3.MaterialTheme
8+
import androidx.compose.runtime.Composable
9+
import androidx.compose.runtime.SideEffect
10+
import androidx.compose.runtime.collectAsState
11+
import androidx.compose.runtime.getValue
12+
import androidx.compose.runtime.mutableStateOf
13+
import androidx.compose.runtime.remember
14+
import androidx.compose.runtime.setValue
15+
import androidx.compose.ui.platform.LocalContext
16+
import androidx.compose.ui.platform.LocalView
17+
import androidx.core.content.getSystemService
18+
import androidx.core.view.WindowCompat
19+
import androidx.lifecycle.viewmodel.compose.viewModel
20+
import com.javernaut.whatthecodec.compose.common.SystemBroadcastReceiver
21+
import com.javernaut.whatthecodec.compose.theme.dynamic.AppTheme
22+
import com.javernaut.whatthecodec.compose.theme.dynamic.ThemeViewModel
23+
24+
object WhatTheCodecTheme {
25+
@Composable
26+
fun Static(
27+
darkTheme: Boolean = isSystemInDarkTheme(),
28+
content: @Composable () -> Unit
29+
) {
30+
val colorScheme = if (darkTheme) darkScheme else lightScheme
31+
MaterialTheme(
32+
colorScheme = colorScheme,
33+
content = content
34+
)
35+
}
36+
37+
@Composable
38+
fun Dynamic(
39+
themeViewModel: ThemeViewModel = viewModel(),
40+
content: @Composable () -> Unit
41+
) {
42+
val appTheme by themeViewModel.appTheme.collectAsState()
43+
val darkTheme = when (appTheme) {
44+
AppTheme.Light -> false
45+
AppTheme.Dark -> true
46+
AppTheme.Auto ->
47+
// isSystemInDarkTheme() on Android S+ considers the Battery Saver mode
48+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) isSystemInDarkTheme()
49+
else isPowerSaveMode()
50+
}
51+
52+
val view = LocalView.current
53+
if (!view.isInEditMode) {
54+
SideEffect {
55+
val window = (view.context as Activity).window
56+
WindowCompat.getInsetsController(window, view).apply {
57+
isAppearanceLightStatusBars = !darkTheme
58+
isAppearanceLightNavigationBars = !darkTheme
59+
}
60+
}
61+
}
62+
63+
Static(
64+
darkTheme = darkTheme,
65+
content = content
66+
)
67+
}
68+
}
69+
70+
@Composable
71+
private fun isPowerSaveMode(): Boolean {
72+
val powerManager = LocalContext.current.getSystemService<PowerManager>()!!
73+
var result by remember {
74+
mutableStateOf(
75+
powerManager.isPowerSaveMode
76+
)
77+
}
78+
79+
SystemBroadcastReceiver(PowerManager.ACTION_POWER_SAVE_MODE_CHANGED) {
80+
result = powerManager.isPowerSaveMode
81+
}
82+
83+
return result
84+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package com.javernaut.whatthecodec.compose.theme.dynamic
2+
3+
enum class AppTheme {
4+
Light,
5+
Dark,
6+
Auto
7+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package com.javernaut.whatthecodec.compose.theme.dynamic
2+
3+
import android.content.Context
4+
import androidx.datastore.core.DataStore
5+
import androidx.datastore.preferences.core.Preferences
6+
import androidx.datastore.preferences.core.edit
7+
import androidx.datastore.preferences.core.intPreferencesKey
8+
import androidx.datastore.preferences.preferencesDataStore
9+
import dagger.hilt.android.qualifiers.ApplicationContext
10+
import kotlinx.coroutines.flow.Flow
11+
import kotlinx.coroutines.flow.map
12+
import javax.inject.Inject
13+
import javax.inject.Singleton
14+
15+
@Singleton
16+
class AppThemeRepository @Inject constructor(
17+
@ApplicationContext context: Context
18+
) {
19+
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "theme_settings")
20+
private val keyTheme = intPreferencesKey("selected_theme")
21+
22+
private val dataStore = context.dataStore
23+
24+
val selectedTheme: Flow<AppTheme> = context.dataStore.data
25+
.map { preferences ->
26+
preferences[keyTheme] ?: AppTheme.Auto.ordinal
27+
}.map {
28+
AppTheme.entries[it]
29+
}
30+
31+
suspend fun setSelectedTheme(newAppTheme: AppTheme) {
32+
dataStore.edit { settings ->
33+
settings[keyTheme] = newAppTheme.ordinal
34+
}
35+
}
36+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package com.javernaut.whatthecodec.compose.theme.dynamic
2+
3+
import androidx.lifecycle.ViewModel
4+
import androidx.lifecycle.viewModelScope
5+
import dagger.hilt.android.lifecycle.HiltViewModel
6+
import kotlinx.coroutines.flow.SharingStarted
7+
import kotlinx.coroutines.flow.stateIn
8+
import kotlinx.coroutines.launch
9+
import javax.inject.Inject
10+
11+
@HiltViewModel
12+
class ThemeViewModel @Inject constructor(
13+
private val appThemeRepository: AppThemeRepository
14+
) : ViewModel() {
15+
16+
val appTheme = appThemeRepository.selectedTheme.stateIn(
17+
viewModelScope, SharingStarted.Eagerly, AppTheme.Auto
18+
)
19+
20+
fun setAppTheme(newAppTheme: AppTheme) {
21+
viewModelScope.launch {
22+
appThemeRepository.setSelectedTheme(newAppTheme)
23+
}
24+
}
25+
}

0 commit comments

Comments
 (0)