Android Jetpack Compose如何实现应用主题切换设置功能
Jetpack Compose 基于DataStore实现全局主题切换完整流程
以下是可直接跑通的从零实现步骤,没有额外复杂架构依赖:
1. 添加基础依赖
在模块级build.gradle.kts的dependencies块中加入必要依赖,Compose项目默认已带的依赖可以跳过:
// DataStore 偏好存储核心依赖 implementation("androidx.datastore:datastore-preferences:1.0.0") // 协程与Compose ViewModel依赖 implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3") implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.2")
2. 定义主题类型与DataStore存储实例
首先定义支持的主题模式枚举,覆盖绝大多数场景需求:
enum class AppThemeMode { LIGHT, // 浅色模式 DARK, // 深色模式 FOLLOW_SYSTEM // 跟随系统 }
然后写DataStore的顶层扩展配置,定义存储用的key:
// 全局DataStore实例,存应用通用偏好 private val Context.dataStore by preferencesDataStore(name = "app_settings") // 主题配置对应的存储key private val THEME_MODE_KEY = stringPreferencesKey("selected_theme_mode")
3. 封装主题配置读写逻辑
写一个简单的仓库类统一处理主题配置的存和读,不做依赖注入也能直接用:
class ThemeRepository(private val context: Context) { // 对外暴露主题状态流,读取出错时默认返回跟随系统 val themeModeFlow: Flow<AppThemeMode> = context.dataStore.data .catch { e -> if (e is IOException) emit(emptyPreferences()) else throw e } .map { prefs -> val savedModeName = prefs[THEME_MODE_KEY] ?: AppThemeMode.FOLLOW_SYSTEM.name runCatching { AppThemeMode.valueOf(savedModeName) } .getOrDefault(AppThemeMode.FOLLOW_SYSTEM) } // 保存用户选中的主题,挂起函数需在协程内调用 suspend fun saveThemeMode(mode: AppThemeMode) { context.dataStore.edit { prefs -> prefs[THEME_MODE_KEY] = mode.name } } }
4. 用ViewModel承载全局主题状态
ViewModel作为全局状态持有容器,保证配置变更(比如转屏)时状态不丢失:
class ThemeViewModel(application: Application) : AndroidViewModel(application) { private val repository = ThemeRepository(application.applicationContext) // 将Flow转换为Compose可直接观察的State val themeMode: State<AppThemeMode> = repository.themeModeFlow .stateIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(5000), initialValue = AppThemeMode.FOLLOW_SYSTEM ) // 对外暴露的主题切换方法,自动处理协程上下文 fun switchTheme(mode: AppThemeMode) { viewModelScope.launch { repository.saveThemeMode(mode) } } } // 定义全局CompositionLocal,避免ViewModel层层传参 val LocalThemeViewModel = staticCompositionLocalOf<ThemeViewModel> { error("请先在根节点提供ThemeViewModel实例") }
5. 根节点挂载动态主题
在MainActivity的setContent根节点观察主题状态,动态传入主题配置,实现全应用重组刷新:
class MainActivity : ComponentActivity() { private val themeViewModel: ThemeViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { val currentTheme = themeViewModel.themeMode.value // 根据当前配置判断是否启用深色模式 val useDarkTheme = when (currentTheme) { AppThemeMode.LIGHT -> false AppThemeMode.DARK -> true AppThemeMode.FOLLOW_SYSTEM -> isSystemInDarkTheme() } // 传入动态深色参数到你自己的项目主题,和普通Compose主题写法完全一致 YourAppTheme(darkTheme = useDarkTheme) { // 全局提供ViewModel实例 CompositionLocalProvider(LocalThemeViewModel provides themeViewModel) { // 这里放你的根导航/首页入口 AppRootContent() } } } } }
注:
YourAppTheme是项目自定义的主题封装,内部就是调用原生MaterialTheme传入配色、字体、形状参数,不需要额外修改逻辑。
6. 实现设置页切换交互
在设置页面直接拿到全局ViewModel,点击选项调用切换方法即可,状态变更后全应用主题会自动刷新,不需要重启Activity:
@Composable fun ThemeSettingScreen() { val themeVm = LocalThemeViewModel.current val selectedMode = themeVm.themeMode.value Column(modifier = Modifier.fillMaxSize().padding(16.dp)) { Text("应用主题", style = MaterialTheme.typography.titleLarge, modifier = Modifier.padding(bottom = 24.dp)) // 浅色模式选项 Row( modifier = Modifier .fillMaxWidth() .clickable { themeVm.switchTheme(AppThemeMode.LIGHT) } .padding(vertical = 12.dp), verticalAlignment = Alignment.CenterVertically ) { RadioButton(selected = selectedMode == AppThemeMode.LIGHT, onClick = { themeVm.switchTheme(AppThemeMode.LIGHT) }) Spacer(Modifier.width(8.dp)) Text("浅色模式") } // 深色模式选项 Row( modifier = Modifier .fillMaxWidth() .clickable { themeVm.switchTheme(AppThemeMode.DARK) } .padding(vertical = 12.dp), verticalAlignment = Alignment.CenterVertically ) { RadioButton(selected = selectedMode == AppThemeMode.DARK, onClick = { themeVm.switchTheme(AppThemeMode.DARK) }) Spacer(Modifier.width(8.dp)) Text("深色模式") } // 跟随系统选项 Row( modifier = Modifier .fillMaxWidth() .clickable { themeVm.switchTheme(AppThemeMode.FOLLOW_SYSTEM) } .padding(vertical = 12.dp), verticalAlignment = Alignment.CenterVertically ) { RadioButton(selected = selectedMode == AppThemeMode.FOLLOW_SYSTEM, onClick = { themeVm.switchTheme(AppThemeMode.FOLLOW_SYSTEM) }) Spacer(Modifier.width(8.dp)) Text("跟随系统") } } }
常见踩坑提示
- 不要在每个页面单独创建
ThemeViewModel实例,必须保证全应用用同一个实例,否则会出现状态不同步的问题 - DataStore读写是异步协程逻辑,不要用主线程阻塞方式调用,上面封装的方法已经处理了协程上下文,直接调用即可
- 如果项目接入了Hilt等依赖注入框架,可以把Repository和ViewModel的创建交给注入框架管理,核心逻辑不需要改动
- 主题切换完全依赖Compose的重组机制,只要根节点的主题参数跟着状态变化,所有子组件的颜色会自动更新,不需要做额外的刷新操作
内容的提问来源于stack exchange,提问作者Melom
相关产品推荐
相关产品推荐

