• 作者:老汪软件技巧
  • 发表时间: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可能不合适,甚至过度使用。

显式参数

在极简单逻辑情况,应尽量使用显示参数传递,且只传递有效参数,避免造成参数过多。

控制反转

另一种避免参数过多或无效参数的方法就是控制反转。一些逻辑可以不在子级页面进行,而应该转移到父级页面来进行。