深入解析Android Compose框架的主题切换机制
深入解析Android Compose框架的主题切换机制
本文将详细介绍Android开发中Compose框架的主题切换功能。通过分析Now in Android应用的具体实现,我们将深入探讨从UI交互到数据配置改变,再到最终主题更新的完整流程。
1. 切换主题的具体实现:从单选框选择UI到配置数据的改变
首先,我们找到UI部分,这是一个SettingDialog,具体位置在feature模块的settings模块中,包含两个主要文件:SettingsDialog和SettingsViewModel。
打开SettingsDialog,映入眼帘的是预览界面。让我们看看切换单选框时发生了什么。
SettingsPanel的点击事件触发了onChangeThemeBrand回调:
onChangeThemeBrand = viewModel::updateThemeBrand,
这里使用了Kotlin的函数引用语法。具体来说:
viewModel
是一个视图模型实例。updateThemeBrand
是这个视图模型对象中的一个方法。
整体表示对 viewModel
对象的 updateThemeBrand
方法的引用。这种用法常见于函数式编程场景中,例如在使用高阶函数时,将特定的方法作为参数传递给另一个函数,以便在合适的时候调用这个方法。
fun updateThemeBrand(themeBrand: ThemeBrand) {
viewModelScope.launch {
userDataRepository.setThemeBrand(themeBrand)
}
}
跟踪代码发现,UI改变后,调用了ViewModel里的方法,这个方法更新了DataStore里的数据。
2. 从配置数据到更改主题
接下来,我们来到MainActivity的setContent
方法:
setContent {
val darkTheme = shouldUseDarkTheme(uiState)
val appState = rememberNiaAppState(
networkMonitor = networkMonitor,
userNewsResourceRepository = userNewsResourceRepository,
timeZoneMonitor = timeZoneMonitor,
)
val currentTimeZone by appState.currentTimeZone.collectAsStateWithLifecycle()
CompositionLocalProvider(
LocalAnalyticsHelper provides analyticsHelper,
LocalTimeZone provides currentTimeZone,
) {
NiaTheme(
darkTheme = darkTheme,
androidTheme = shouldUseAndroidTheme(uiState),
disableDynamicTheming = shouldDisableDynamicTheming(uiState),
) {
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
NiaApp(appState)
}
}
}
这里的切换主要是Android主题和Default主题,我们关注shouldUseAndroidTheme
方法:
@Composable
private fun shouldUseAndroidTheme(
uiState: MainActivityUiState,
): Boolean = when (uiState) {
Loading -> false
is Success -> when (uiState.userData.themeBrand) {
ThemeBrand.DEFAULT -> false
ThemeBrand.ANDROID -> true
}
}
这个方法根据用户配置数据判断是否使用Android主题。
3. 主题的具体实现
定义一个ColorScheme,里面包含了各种颜色类型,如错误颜色、按钮颜色等。Now in Android直接使用了Material Design 3里的ColorScheme:
fun lightColorScheme(
primary: Color = ColorLightTokens.Primary,
onPrimary: Color = ColorLightTokens.OnPrimary,
primaryContainer: Color = ColorLightTokens.PrimaryContainer,
onPrimaryContainer: Color = ColorLightTokens.OnPrimaryContainer,
inversePrimary: Color = ColorLightTokens.InversePrimary,
secondary: Color = ColorLightTokens.Secondary,
onSecondary: Color = ColorLightTokens.OnSecondary,
secondaryContainer: Color = ColorLightTokens.SecondaryContainer,
onSecondaryContainer: Color = ColorLightTokens.OnSecondaryContainer,
tertiary: Color = ColorLightTokens.Tertiary,
onTertiary: Color = ColorLightTokens.OnTertiary,
tertiaryContainer: Color = ColorLightTokens.TertiaryContainer,
onTertiaryContainer: Color = ColorLightTokens.OnTertiaryContainer,
background: Color = ColorLightTokens.Background,
onBackground: Color = ColorLightTokens.OnBackground,
surface: Color = ColorLightTokens.Surface,
onSurface: Color = ColorLightTokens.OnSurface,
surfaceVariant: Color = ColorLightTokens.SurfaceVariant,
onSurfaceVariant: Color = ColorLightTokens.OnSurfaceVariant,
surfaceTint: Color = primary,
inverseSurface: Color = ColorLightTokens.InverseSurface,
inverseOnSurface: Color = ColorLightTokens.InverseOnSurface,
error: Color = ColorLightTokens.Error,
onError: Color = ColorLightTokens.OnError,
errorContainer: Color = ColorLightTokens.ErrorContainer,
onErrorContainer: Color = ColorLightTokens.OnErrorContainer,
outline: Color = ColorLightTokens.Outline,
outlineVariant: Color = ColorLightTokens.OutlineVariant,
scrim: Color = ColorLightTokens.Scrim,
surfaceBright: Color = ColorLightTokens.SurfaceBright,
surfaceContainer: Color = ColorLightTokens.SurfaceContainer,
surfaceContainerHigh: Color = ColorLightTokens.SurfaceContainerHigh,
surfaceContainerHighest: Color = ColorLightTokens.SurfaceContainerHighest,
surfaceContainerLow: Color = ColorLightTokens.SurfaceContainerLow,
surfaceContainerLowest: Color = ColorLightTokens.SurfaceContainerLowest,
surfaceDim: Color = ColorLightTokens.SurfaceDim,
): ColorScheme =
ColorScheme(
primary = primary,
onPrimary = onPrimary,
primaryContainer = primaryContainer,
onPrimaryContainer = onPrimaryContainer,
inversePrimary = inversePrimary,
secondary = secondary,
onSecondary = onSecondary,
secondaryContainer = secondaryContainer,
onSecondaryContainer = onSecondaryContainer,
tertiary = tertiary,
onTertiary = onTertiary,
tertiaryContainer = tertiaryContainer,
onTertiaryContainer = onTertiaryContainer,
background = background,
onBackground = onBackground,
surface = surface,
onSurface = onSurface,
surfaceVariant = surfaceVariant,
onSurfaceVariant = onSurfaceVariant,
surfaceTint = surfaceTint,
inverseSurface = inverseSurface,
inverseOnSurface = inverseOnSurface,
error = error,
onError = onError,
errorContainer = errorContainer,
onErrorContainer = onErrorContainer,
outline = outline,
outlineVariant = outlineVariant,
scrim = scrim,
surfaceBright = surfaceBright,
surfaceContainer = surfaceContainer,
surfaceContainerHigh = surfaceContainerHigh,
surfaceContainerHighest = surfaceContainerHighest,
surfaceContainerLow = surfaceContainerLow,
surfaceContainerLowest = surfaceContainerLowest,
surfaceDim = surfaceDim,
)
4. CompositionLocal的重要概念
它会为当前的Composeable域创建一个变量,是一个副本。这里的值可以改变,不响应我们的定义好的值。
MaterialTheme对象提供了三个CompositionLocal实例,即colors、typography和shapes。可以在任何地方拿到这些实例进行使用。具体来说,这些MaterialTheme的colors、shapes和typography属性就是访问LocalColors、LocalShapes和LocalTypography。
CompositionLocal实例的作用域限定为Composable的一部分,因此可以在结构树的不同级别提供不同的值。CompositionLocal的current值对应于Composable的某个父级提供的就近值。
如需为CompositionLocal提供新值,请使用CompositionLocalProvider及其provides infix函数,该函数将CompositionLocal键与value相关联。在访问CompositionLocal的current属性时,CompositionLocalProvider的content lambda将获取提供的值。提供新值后,Compose会重组读取CompositionLocal的组合部分。
这样,最外层的Theme的colorScheme,放到CompositionLocal里,里面所有的东西都能用了。而且提供新值后,Compose会重组,所有的颜色也就跟着改变了。
5. StateFlow
StateFlow状态的订阅,是一种特殊的SharedFlow。Now in Android,用来代替了LiveData给状态提供了订阅通知的功能。一处改变,到处通知。MainActtivityUIState和SettingsUiState都订阅了User数据仓库里的userData对象,当userData发生改变,就会通知到MainActivity和SettingDialog,这样,主题,就会改变,Settings的单选框状态也会改变。StateFlow的具体原理,有时间再单独写一篇文章给大家介绍。
6. 总的逻辑
总的逻辑,我画了图,根据图再去看代码,一看就能看明白。
7. 总结
Compose切换主题的主要逻辑,Composeable域,有一个全局变量,存储所有颜色,不过这个变量用CompositionLocal进行了包装,这样只影响自己的composeable域。这个地方不仅可以存储主题颜色之类的,其他的业务数据也可以存。
某些场景下,CompositionLocal可能不合适,甚至过度使用。
- 显式参数:在极简单逻辑情况,应尽量使用显示参数传递,且只传递有效参数,避免造成参数过多。
- 控制反转:另一种避免参数过多或无效参数的方法就是控制反转。一些逻辑可以不在子级页面进行,而应该转移到父级页面来进行。