You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.28 14:36:23