Reputation: 1
there is a mainActivity in which the navHost is called and the drawer is called in it, and from it the settings screen
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
WindowCompat.setDecorFitsSystemWindows(window, false)
super.onCreate(savedInstanceState)
setContent {
val viewModel: SettingsViewModel = hiltViewModel()
val activityState by viewModel.appTheme.collectAsStateWithLifecycle()
LaunchedEffect(Unit) {
viewModel.getTheme(this@MainActivity)
}
AppTheme(
isDarkTheme = shouldUseDarkTheme(appTheme = activityState)
) {
TopNavHost()
}
}
}
}
viewModel for settings:
class SettingsViewModel: ViewModel() {
private val _appTheme = MutableStateFlow(AppTheme.System)
val appTheme: StateFlow<AppTheme> = _appTheme
fun getTheme(context: Context) {
viewModelScope.launch {
_appTheme.value = withContext(Dispatchers.IO) {
DataStoreManager(context).getTheme()
}
}
}
fun saveTheme(context: Context, newTheme: AppTheme) {
_appTheme.value = newTheme
viewModelScope.launch {
DataStoreManager(context).saveTheme(_appTheme.value.name)
}
}
}
and the settings screen itself
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SettingsScreen(
onBack: () -> Unit,
viewModel: SettingsViewModel = hiltViewModel()
) {
val context = LocalContext.current
LaunchedEffect(Unit) {
viewModel.getTheme(context)
}
Scaffold(topBar = {
TopAppBar(
title = {
Text(
text = stringResource(id = R.string.settings),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
},
navigationIcon = {
IconButton(onClick = { onBack() }) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = "back"
)
}
}
)
},
content = { innerPadding ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(innerPadding)
.padding(horizontal = 10.dp)
) {
ThemeSettings(viewModel.appTheme.collectAsStateWithLifecycle().value) {
viewModel.saveTheme(context, it)
}
HorizontalDivider(
modifier = Modifier.padding(top = 5.dp, bottom = 10.dp),
thickness = 1.dp
)
LanguageSettings()
HorizontalDivider(
modifier = Modifier.padding(top = 5.dp, bottom = 10.dp),
thickness = 1.dp
)
}
})
}
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun ThemeSettings(appTheme: AppTheme, onAppThemeChanged: (AppTheme) -> Unit) {
val themes = arrayOf(
stringResource(id = R.string.system_theme),
stringResource(id = R.string.dark_theme),
stringResource(id = R.string.light_theme)
)
Column(
modifier = Modifier.fillMaxWidth()
) {
Spacer(modifier = Modifier.height(5.dp))
Text(text = stringResource(id = R.string.theme))
FlowRow(
modifier = Modifier.fillMaxWidth()
) {
TextedRadioButton(
selected = appTheme == AppTheme.System,
onClick = {
if (appTheme != AppTheme.System)
onAppThemeChanged(AppTheme.System)
},
text = themes[0]
)
TextedRadioButton(
selected = appTheme == AppTheme.Light,
onClick = {
if (appTheme != AppTheme.Light)
onAppThemeChanged(AppTheme.Light)
},
text = themes[2]
)
TextedRadioButton(
selected = appTheme == AppTheme.Dark,
onClick = {
if (appTheme != AppTheme.Dark)
onAppThemeChanged(AppTheme.Dark)
},
text = themes[1]
)
}
}
}
If I call settingsScreen directly from MainActivity then the theme changes without re-entering the application. I understand that my MainActivity only starts when the application is turned on, but I don’t understand how I can change the theme directly from the settings screen.
here is the theme code:
@Composable
fun AppTheme(
isDarkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable () -> Unit
) {
val colorScheme = if (isDarkTheme) DarkColors else LightColors
if (Build.VERSION.SDK_INT >= 29) {
LocalView.current.isForceDarkAllowed = false
}
val systemUiController = rememberSystemUiController()
SideEffect {
systemUiController.setStatusBarColor(
color = Color.Transparent,
darkIcons = !isDarkTheme
)
systemUiController.setNavigationBarColor(
color = Color.Transparent,
darkIcons = !isDarkTheme
)
}
MaterialTheme(
colorScheme = colorScheme,
content = {
Surface(
modifier = Modifier.fillMaxSize(),
color = colorScheme.background,
contentColor = colorScheme.onBackground,
content = content
)
}
)
}
Upvotes: 0
Views: 122
Reputation: 1275
With code you provided it looks like there are 2 causes:
1. You are using two instances of settingsViewModel
In your code _appTheme.value = newTheme
is called only for SettingsViewModel
instance on SettingsScreen
, the other one (inside MainActivity
) remains unchenged.
There is nothing wrong about it but your DataStoreManager
should provide way how to share single AppTheme
on multiple screens.
2. You don't observing changes for your AppTheme
.
In your code are getting AppTheme
within LaunchedEffect
which is single time operation (in your code, not in general). That's why it's working only when you enter the app again.
Solution
I don't know your DataStoreManager
so i created this one. Using hilt
@Singleton
to make sure you have only one instance.
@Singleton
class DataStoreManager @Inject constructor(
@ApplicationContext
private val context: Context
) {
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "test data store")
val key = stringPreferencesKey("appTheme")
//Flow that gets updated every time you update theme
val themeFlow = context.dataStore.data.map { AppTheme.byName(it[key] ?: "") }
suspend fun saveTheme(name: String) {
context.dataStore.edit {
it[key] = name
}
}
}
With this, you can collect AppTheme
changes in your SettingsViewModel
and send it further. There is no need for getTheme
function now, theme is being updated.
@HiltViewModel
class SettingsViewModel @Inject constructor(
context: Application,
val dataStore: DataStoreManager
) : ViewModel() {
private val _appTheme = MutableStateFlow(AppTheme.System)
val appTheme: StateFlow<AppTheme> = _appTheme
init {
viewModelScope.launch {
//Collecting theme
dataStore.themeFlow.collect { newTheme ->
_appTheme.value = newTheme
}
}
}
fun saveTheme(newTheme: AppTheme) {
//Now this is not necessary because once you save theme
//_appTheme gets updated
//_appTheme.value = newTheme
viewModelScope.launch {
dataStore.saveTheme(newTheme.name)
}
}
}
With this, your theme change will be visible immediately.
Upvotes: 0