如何在Compose首次重组前设置应用主题并消除加载延迟?
为什么会有延迟?
DataStore 是基于协程和 Flow 的异步磁盘IO方案,首次读取数据需要从磁盘加载,加上线程调度的开销,冷启动时会出现几百到近一秒的延迟。你的ViewModel中通过viewModelScope.launch异步加载DataStore数据,所以UI会先拿到StateFlow的默认值,之后才会收到实际存储的主题值,导致主题切换。
你遗漏了什么?
没有处理DataStore加载的异步性,也没有设计加载完成前的过渡状态,直接用默认值渲染UI,导致后续主题切换的突兀感;另外,手动通过协程emit数据的方式,不如直接将DataStore的Flow转换为StateFlow高效。
解决方案
1. 优化ViewModel中主题Flow的实现
直接将DataStore的Flow转换为StateFlow,避免手动启动协程emit,让Flow自动处理数据更新:
首先修改SettingsRepository,暴露主题的Flow:
class SettingsRepository @Inject constructor(private val dataStore: DataStore<Preferences>) { private val COLOR_SCHEME_KEY = intPreferencesKey("color_scheme") // 直接返回DataStore的Flow val colorSchemeFlow: Flow<Int> = dataStore.data .map { preferences -> preferences[COLOR_SCHEME_KEY] ?: 0 // 默认值对应ColorScheme.AUTO } suspend fun saveColorScheme(colorScheme: ColorScheme) { dataStore.edit { preferences -> preferences[COLOR_SCHEME_KEY] = colorScheme.ordinal } } }
然后修改SettingsViewModel,用stateIn将Repository的Flow转换为StateFlow:
@HiltViewModel class SettingsViewModel @Inject constructor( private val settingsRepository: SettingsRepository ): ViewModel() { val colorScheme: StateFlow<ColorScheme> = settingsRepository.colorSchemeFlow .map { ColorScheme.values()[it] } .stateIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(5000), // 订阅者消失后保持5秒缓存 initialValue = ColorScheme.AUTO ) fun setColorScheme(colorScheme: ColorScheme){ viewModelScope.launch { settingsRepository.saveColorScheme(colorScheme) } } }
这种方式让StateFlow自动订阅DataStore的Flow,数据加载完成后会自动更新UI,相比手动emit更简洁高效,能略微缩短延迟。
2. 等待主题加载完成再显示主界面
如果不想让用户看到主题切换的闪烁,可以在ViewModel中添加加载状态,等待主题加载完成后再渲染主界面:
修改SettingsViewModel:
@HiltViewModel class SettingsViewModel @Inject constructor( private val settingsRepository: SettingsRepository ): ViewModel() { private val _colorScheme = MutableStateFlow<ColorScheme?>(null) // 初始值设为null,表示未加载 val colorScheme: StateFlow<ColorScheme?> = _colorScheme init { viewModelScope.launch { val savedOrdinal = settingsRepository.loadColorScheme() _colorScheme.emit(ColorScheme.values()[savedOrdinal]) } } // ... 其他方法不变 }
然后在MainActivity的Composable中处理加载状态:
@AndroidEntryPoint class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { val viewModel: SettingsViewModel = hiltViewModel() val colorScheme = viewModel.colorScheme.collectAsState() colorScheme.value?.let { scheme -> val darkTheme = when(scheme){ ColorScheme.AUTO -> isSystemInDarkTheme() ColorScheme.LIGHT -> false ColorScheme.DARK -> true } MyAppTheme(darkTheme = darkTheme) { MainScreen() } } ?: run { // 显示启动屏,可替换为品牌Logo或空白页 Box( modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center ) { Text("Loading...") } } } } }
这种方式会先显示启动屏,直到主题数据加载完成后再切换到主界面,完全避免主题闪烁。
3. 添加主题切换的平滑动画
如果不想等待加载,可通过Crossfade动画让主题切换更平滑,减少突兀感:
@AndroidEntryPoint class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { val viewModel: SettingsViewModel = hiltViewModel() val colorScheme = viewModel.colorScheme.collectAsState() Crossfade(targetState = colorScheme.value, animationSpec = tween(300)) { currentScheme -> val darkTheme = when(currentScheme){ ColorScheme.AUTO -> isSystemInDarkTheme() ColorScheme.LIGHT -> false ColorScheme.DARK -> true } MyAppTheme(darkTheme = darkTheme) { MainScreen() } } } } }
这样主题切换时会有300ms的淡入淡出动画,用户感知会更自然。
内容的提问来源于stack exchange,提问作者Mikhail

