- 作者:老汪软件技巧
- 发表时间:2024-09-28 07:01
- 浏览量:
上篇文章我们介绍了,Now in Android的架构,和具体功能介绍,在介绍功能的时候,里面有个切换主题功能,我觉得这个功能很nice,所以我们一起来看看是怎么实现的。
1、切换主题的具体实现,从单选框选择UI到配置数据的改变。
首先我们找到Ui部分,是一个SettingDialog,具体的位置是在feature 模块里面的settings模块,就两个文件,SettingsDialog,和SettingsViewModel
打开SettingsDialog映入眼帘的就是预览界面
我们再去看具体实现的代码。看看切换单选框的时候,发生了什么事情。
SettingsPanel点击事件,执行了onChangeThemBrand回调,
onChangeThemeBrand = viewModel::updateThemeBrand, Kotlin里面也有::的语法了么,不知道什么意思,猜测应该是调用viewModel里的updateThemeBrand方法,在 Kotlin 中,viewModel::updateNumber是一种函数引用的表达方式。
具体来说:
函数引用的概念
函数引用允许你直接引用一个已有函数(或方法),将其作为一个值进行传递。在某些情况下,它可以替代使用 lambda 表达式来传递一个可调用的代码块。
这里的具体含义
viewModel通常是一个对象,它可能是一个视图模型(ViewModel)实例。
updateNumber是这个视图模型对象中的一个方法。
所以,viewModel::updateNumber整体表示对viewModel对象的updateNumber方法的引用。
这种用法常见于一些函数式编程的场景中,例如在使用高阶函数时,将特定的方法作为参数传递给另一个函数,以便在合适的时候调用这个方法。比如,可能会有一个函数接收一个函数类型的参数,然后调用这个参数所代表的函数,就可以将viewModel::updateNumber作为实参传递给这个函数,以实现对updateNumber方法的间接调用。
fun updateThemeBrand(themeBrand: ThemeBrand) {
viewModelScope.launch {
userDataRepository.setThemeBrand(themeBrand)
}
}
我们根据跟踪代码,发现,UI改变后,调用了ViewModel里面的方法,而这个方法里,更新了DataStore里的数据,然后就没了,然后UI就发生了改变。
好家伙
我们分析了从UI,到数据折条线,点击UI更改了User数据仓库里面的东西,更改了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主题,所以我们就看shuoldUserAndroidTheme方法就可以了这个方法是个Copmoseable方法
@Composable
private fun shouldUseAndroidTheme(
uiState: MainActivityUiState,
): Boolean = when (uiState) {
Loading -> false
is Success -> when (uiState.userData.themeBrand) {
ThemeBrand.DEFAULT -> false
ThemeBrand.ANDROID -> true
}
}
拿到User配置数据,判断,返回一个Boolean值,这样就能知道用哪个主题了,是用Android的还是Default的呀,是用暗黑主题啊,还是明亮主题呀。
3、主题的具体实现。
定义一个ColorScheme,里面定义了主体内的颜色类型,比如error颜色,button颜色,之类的,Now in Android,直接使用了MaterialDesign3里的ColorScheme,我们在开发真实项目中,可以和UI小姐姐一起商量,定义我门自己的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,
)
大概就是这个样子,然后根据这个类
将所有主题的ColorScheme定义好,供我们使用,使用的时候直接MaterialTheme.colorScheme.surfaceVariant,颜色就设置好了,
4、重要的概念CompositionLocal
它会为当前的Composeable域创建一个变量,它是一个副本这里的值可以改变,不响应我们的定义好的值,MaterialTheme对象提供了三个CompositionLocal实例,即 colors、typography 和 shapes。可以在任何地方拿到这些实例进行使用。具体来说,这些MaterialTheme的colors、shapes和typography属性就是访问 LocalColors、LocalShapes 和 LocalTypography。CompositionLocal实例的作用域限定为Composable的一部分,因此可以在结构树的不同级别提供不同的值。CompositionLocal的current值对应于Composable的某个父级提供的就近值。
如需为CompositionLocal提供新值,请使用CompositionLocalProvider及其providesinfix 函数,该函数将CompositionLocal键与 value 相关联。在访问CompositionLocal的current属性时,CompositionLocalProvider的contentlambda 将获取提供的值。提供新值后,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可能不合适,甚至过度使用。
显式参数
在极简单逻辑情况,应尽量使用显示参数传递,且只传递有效参数,避免造成参数过多。
控制反转
另一种避免参数过多或无效参数的方法就是控制反转。一些逻辑可以不在子级页面进行,而应该转移到父级页面来进行。