第六周创新实训记录-背单词软件功能主题模式设计
根据之前的软件框架搭建接下来就是相关功能设计以及IDEA中使用git时的问题解决
涉及到的有一下文件
一、themeMode.kt
此文件是一个枚举类型文件,主要用于通过枚举类型,定义了应用支持的多种主题模式,并且每种模式都关联了一个中文描述字符串,便于在界面显示,在应用中,可以直接通过这个枚举来切换以及存储主题模式
enum class ThemeMode(val cnValue: String) {
LIGHT("浅色"),
DARK("深色"),
EYE_CARE("护眼模式"),
VIBRANT("彩色模式"),
DEFAULT("按系统设定")
}
二、theme.kt
相关颜色方案的定义以及主题包装函数
//彩色模式下的各个组件的颜色----颜色方案
private val VibrantColors = lightColorScheme(
primary = Color(0xFF2196F3), // 亮蓝色
onPrimary = Color.White,
background = Color(0xFFE3F2FD), // 浅蓝色背景
onBackground = Color.Black,
surface = Color.White,
onSurface = Color.Black,
secondary = Color(0xFFFF9800), // 橙色
onSecondary = Color.White
)
主题包装函数
@Composable
fun LandingAppTheme(
themeMode: ThemeMode = ThemeMode.DEFAULT, // 使用 ThemeMode 枚举,默认值为ThemeMode.DEFAULT
content: @Composable () -> Unit
) {
// 选择颜色方案
val colors = when (themeMode) {
ThemeMode.LIGHT -> LightColors
ThemeMode.DARK -> DarkColors
ThemeMode.EYE_CARE -> EyeCareColors
ThemeMode.VIBRANT -> VibrantColors
ThemeMode.DEFAULT -> if (isSystemInDarkTheme()) DarkColors else DefaultColors
}
// 设置系统 UI 颜色
val systemUiController = rememberSystemUiController() //获取一个系统 UI 控制器
systemUiController.setSystemBarsColor(
color = Color.Transparent, //状态栏颜色设置成透明
darkIcons = themeMode == ThemeMode.LIGHT || themeMode == ThemeMode.DEFAULT || themeMode == ThemeMode.VIBRANT //判断是否使用深色图标,浅色、默认、彩色模式下显示深色
)
// 应用 MaterialTheme
MaterialTheme(
colorScheme = colors, //将前面的选择颜色方案传入,将整个应用统一
content = content //包裹要显示的UI内容,将子组件使用该主题配置
)
}
三、PreferencesRepository.kt
用于主题模式持久化
首先是有关主题存储键的定义,用于在本地持久化存储中保存和读取用户选择的主题模式
private val themeValue = stringPreferencesKey(THEME_MODE_PREF)
接着是存储主题模式,将用户选择的主题模式存储到 DataStore 中
异步操作--使用 suspend 关键字和 dataStore.edit {} 进行异步存储
数据存储--将枚举值转换为字符串:themeMode.name,并保存到 DataStore 中
异常捕获--捕获 IOException 和其他异常,返回对应的错误类型
suspend fun setTheme(themeMode: ThemeMode): DataResult<String> {
return try {
dataStore.edit { pref ->
pref[themeValue] = themeMode.name
}
DataResult.Success("")
} catch (exception: Exception) {
if (exception is IOException) {
DataResult.Error(code = DataResult.Error.Code.IO)
} else {
DataResult.Error(code = DataResult.Error.Code.UNKNOWN)
}
}
}
最后是读取主题模式,从 DataStore 中读取当前的主题模式,并将其转换为 Flow 流
数据流--dataStore.data用于返回一个流,代表存储中的数据流更新
异常处理--catch {}用于捕获 IOException 异常,并返回 DataResult.Error
数据提取和转换--通过 pref[themeValue] 获取主题值,若不存在,则返回默认主题(ThemeMode.DEFAULT.name),使用 ThemeMode.valueOf(data) 将字符串还原为 ThemeMode 枚举值,最后返回 DataResult.Success(themeMode) 封装结果
fun getThemeValueFlow(): Flow<DataResult<ThemeMode>> {
return dataStore.data
.catch {
DataResult.Error(code = DataResult.Error.Code.IO)
}
.map { pref ->
val data = pref[themeValue] ?: ThemeMode.DEFAULT.name
val themeMode = ThemeMode.valueOf(data)
DataResult.Success(themeMode)
}
}
四、MainViewModel.kt
此文件用于管理应用程序的主界面状态,包括主题设置和用户协议相关逻辑
在实现主题功能时,它通过注入 PreferencesRepository 来访问持久化存储的数据,包括主题模式和用户协议接受状态
首先主题流的获取--调用 PreferencesRepository 中的 getThemeValueFlow() 方法,获取主题模式的数据流(Flow),该流包含用户设置的主题,例如:浅色、深色、护眼模式、彩色模式等
val themeFlow = preferencesRepository.getThemeValueFlow()
使用 combine 将用户协议流和主题流组合在一起,生成联合流
agreementFlow.combine(themeFlow) { agreement, theme ->
agreement to theme
}
接着处理主题值
when (themeValue) {
is DataResult.Error -> {
mainUiState.value = MainUiState.Error(
code = themeValue.code
) //如果获取主题失败,更新UI状态为Error
}
is DataResult.Success -> {
mainUiState.value = MainUiState.Success(
startDestination = LandingDestination.Main.Home.route,
themeMode = themeValue.data
) //成功获取主题,更新UI状态为Success,并携带用户设置的主题模式
}
}
最后时UI状态的更新
mainUiState.value = MainUiState.Success(
startDestination = LandingDestination.Main.Home.route, //启动页(跳转不同页面)
themeMode = themeValue.data //用户设置的主题模式
)
更新后的 mainUiState 会自动通知UI 层进行更新,切换主题
五、MainActivity.kt
用于在应用程序启动时正确设置和应用用户选择的主题模式
即根据主题设置动态应用主题,在整个应用程序范围内统一管理主题,即使在加载界面和错误界面,也保持主题一致性,通过这种方法,即使用户在设置中切换主题,整个应用程序的界面都会立刻响应变化,实现实时主题切换
Activity 生命周期设置主题
splashScreen.setKeepOnScreenCondition {
mainViewModel.uiState.value == MainUiState.Loading
}
设置内容和主题
动态主题应用--使用 LandingAppTheme(themeMode = uiState.themeMode) 来包裹整个导航图(LandingNavGraphMain),themeMode 通过 ViewModel 获取(即用户选择的主题)
不同主题设置--如果用户选择了深色模式,会传递 isDarkMode = true,否则为 false,将主题与导航图绑定,使整个应用根据主题模式切换
setContent {
val navHostController = rememberNavController()
when (val uiState = mainViewModel.uiState.value) {
is MainUiState.Success -> {
LandingAppTheme(themeMode = uiState.themeMode) {
LandingNavGraphMain(
isDarkMode = uiState.themeMode == ThemeMode.DARK,
playPron = sound::playAudio,
navHostController = navHostController,
exitApp = this@MainActivity::finish,
startDestination = uiState.startDestination
)
}
}
...
}
}
显示加载界面时的主题--通过 LandingAppTheme 包裹,确保加载界面也有统一的主题风格
is MainUiState.Loading -> {
LandingAppTheme {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.background(MaterialTheme.colorScheme.background)
.windowInsetsPadding(insets = WindowInsets.systemBars)
.fillMaxSize()
) {
CircularProgressIndicator(
modifier = Modifier.size(24.dp, 24.dp)
)
}
}
}
显示错误界面时的主题--通过 LandingAppTheme 包裹,确保错误界面风格统一
is MainUiState.Error -> {
LandingAppTheme {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.background(MaterialTheme.colorScheme.background)
.windowInsetsPadding(insets = WindowInsets.systemBars)
.fillMaxSize()
) {
ErrorNotice(code = uiState.code)
}
}
}
即该文件整体的流程为启动时,显示启动画面,直到 ViewModel 加载完成主题设置;主题加载成功会根据用户设置(或者系统默认),应用正确的主题模式;在应用主题中,使用 LandingAppTheme 包裹整个应用的导航图,确保所有界面都符合主题风格;在界面切换上,即使在错误界面和加载界面,也使用同样的主题风格,保持一致性
六、主题模式功能主要流程
MainActivity 启动并显示启动画面
MainViewModel 初始化并通过 PreferencesRepository 获取主题设置
PreferencesRepository 获取和返回保存的主题模式和协议状态
MainViewModel 根据返回的数据(协议状态、主题模式)更新 UI 状态
MainActivity 根据 UI 状态更新界面,应用主题
LandingAppTheme 负责根据当前主题设置来应用主题模式
七、实现功能图
模式选择
浅色模式
护眼模式
彩色模式
八、IDEA中git使用
首先需要在IDEA中下载git,点开从VCS获取
下面的界面是导入github仓库,当点击下面克隆会IDEA弹出下载git的提示(以及点击下载即可)
其中我当时是从git官网上下载的git,一直使用命令行运行,因为IDEA自动下载的git和之前版本不一致,因此它刚开始提示我下载错误,之后又提示我已经存在git
解决方案--打开设置->版本控制->git
上面的可执行文件选择之前下载的git.exe文件,成功导入
之后可以直接在IDEA里面实现相关git操作