Dotingo
Dotingo

Reputation: 1

Why doesn’t the theme change when changing the settings, but only when re-entering the application? Jetpack compose

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

Answers (1)

Miroslav H&#253;bler
Miroslav H&#253;bler

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

Related Questions