3

In my Android project, I'm using DataStore and StateFlow to store and change the UI state. The problem is, every time the app starts, because of the necessary initial value of StateFlow, the corresponding theme setting will always set to the initial value before set to the value read from DataStore, and this will lead to a visual hit(change from Light to Dark or Dark to light) for users.

The following text shows the detail on achieving that(I use this sample code as reference):

  1. Get the value from DataStore as a Flow and assign it to observeSelectedDarkMode(): Flow<String> function in my AppRepository class.
class AppRepository(
    val context: Context
) : AppRepository {

    private val dataStore = context.dataStore

    override fun observeSelectedDarkMode(): Flow<String> = dataStore.data.map { it[KEY_SELECTED_DARK_MODE] ?: DEFAULT_VALUE_SELECTED_DARK_MODE }

    ...
}
  1. Convert it to StateFlow using .stateIn() in my SettingsViewModel class.
class SettingsViewModel(
    private val appRepository: AppRepository
) : ViewModel() {

    val selectedDarkModel = appRepository.observeSelectedDarkMode().stateIn(
        viewModelScope,
        SharingStarted.WhileSubscribed(),
        AppDataStore.DEFAULT_VALUE_SELECTED_DARK_MODE
    )

    ...
}
  1. Use .collectAsState() in my CampusHelperTheme() fun to observe the change.
@Composable
fun CampusHelperTheme(
    appContainer: AppContainer,
    content: @Composable () -> Unit
) {
    val settingsViewModel: SettingsViewModel = viewModel(
        factory = SettingsViewModel.provideFactory(appContainer.appRepository)
    )
    val selectedDarkMode by settingsViewModel.selectedDarkModel.collectAsState()
    val darkTheme = when (selectedDarkMode) {
        "On" -> true
        "Off" -> false
        else -> isSystemInDarkTheme()
    }
    val context = LocalContext.current
    val colorScheme = if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)

    ...

    MaterialTheme(
        colorScheme = colorScheme,
        typography = Typography,
        content = content
    )
}

It seems that everything is good, but when the selectedDarkMode value is different from its default value AppDataStore.DEFAULT_VALUE_SELECTED_DARK_MODE(Follow System), the app will turn from Dark to Light or from Light to Dark, which would give users a bad experience.

So what I want is let the app skip the initial value of StateFlow and then the app can set to what the user choose once it launch, then there will not have a visual hit to users.

I search on this site, and found this ask: How to use DataStore with StateFlow and Jetpack Compose?, but it has no content about the initial value I am looking for, I also found this ask: Possible to ignore the initial value for a ReactiveObject?, but it is in C# and ReactiveUI, which is not suitable for me.

How should I achieve this?

founchoo
  • 41
  • 3
  • I think maybe you’re approaching the problem with the wrong sort of solution in mind. What are you going to show on the screen while waiting for the first item of the flow? Even if you skip it, you’ll have to put something on the screen. – Tenfour04 Oct 06 '22 at 03:53
  • One possibility would be to switch from a Boolean to an Enum for Light, Dark, and then Undetermined. Collect this state at the base level of your composition and if the value is Undetermined, show a full screen solid color that matches the background color of your window or splash screen, and skip creating the rest of the composition. . – Tenfour04 Oct 06 '22 at 03:58
  • I am not sure if this is the way to go. But I achieved it by using runBlocking to get the initial value. class SettingsViewModel( private val appRepository: AppRepository ) : ViewModel() { val selectedDarkModel = appRepository.observeSelectedDarkMode().stateIn( viewModelScope, SharingStarted.WhileSubscribed(), runBlocking{appRepository.observeSelectedDarkMode().first()} ) ... } – Diken Mhrz Mar 07 '23 at 07:20

2 Answers2

1

Use Flow.drop.

Like so:

    val selectedDarkMode by settingsViewModel.selectedDarkModel.drop(1).collectAsState()

This ignores the first value emitted by the flow.

Rollersteaam
  • 11
  • 1
  • 1
0

I am not sure if this is the way to go. But I achieved it by using runBlocking to get the initial value.

class SettingsViewModel(
 private val appRepository: AppRepository
 ) : ViewModel() {

 val selectedDarkModel = appRepository
               .observeSelectedDarkMode()
               .stateIn( 
                viewModelScope, 
                SharingStarted.WhileSubscribed(), 
                runBlocking{appRepository.observeSelectedDarkMode().first()}
               )
               ... 
  } 
Diken Mhrz
  • 327
  • 2
  • 14