问小白 wenxiaobai
资讯
历史
科技
环境与自然
成长
游戏
财经
文学与艺术
美食
健康
家居
文化
情感
汽车
三农
军事
旅行
运动
教育
生活
星座命理

深入解析Android Compose框架的主题切换机制

创作时间:
作者:
@小白创作中心

深入解析Android Compose框架的主题切换机制

引用
1
来源
1.
https://juejin.cn/post/7418767882618781706

本文将详细介绍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可能不合适,甚至过度使用。

  • 显式参数:在极简单逻辑情况,应尽量使用显示参数传递,且只传递有效参数,避免造成参数过多。
  • 控制反转:另一种避免参数过多或无效参数的方法就是控制反转。一些逻辑可以不在子级页面进行,而应该转移到父级页面来进行。
© 2023 北京元石科技有限公司 ◎ 京公网安备 11010802042949号