在 Android 应用中使用 androidx.compose.ui 框架实现夜间模式支持,可以通过以下步骤完成。我们将实现以下功能:
- 默认跟随系统:应用的主题模式自动跟随系统的夜间模式设置。
- 手动切换:用户可以在应用中手动切换夜间模式和日间模式。
1. 添加依赖项
确保你的项目中已经添加了 Compose 和 Material Design 的依赖项:
androidx.compose.ui:ui
androidx.compose.material3:material3
androidx.datastore:datastore-preferences
androidx.compose.runtime:runtime-livedata
androidx.lifecycle:lifecycle-viewmodel-compose
2. 定义主题模式
创建一个 ThemeMode 枚举类来表示应用的主题模式:
// ThemeMode.kt
package com.nemo.notes.ui.theme
enum class ThemeMode {
LIGHT, // 日间模式
DARK, // 夜间模式
SYSTEM // 跟随系统
}
创建主题配置,
package com.nemo.notes.ui.theme
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.material3.Typography
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
import com.nemo.notes.viewmodel.ThemeViewModel
// 颜色定义, 用于暗色主题
private val DarkColorScheme = darkColorScheme(
primary = Purple80,
secondary = PurpleGrey80,
tertiary = Pink80
)
// 颜色定义, 用于浅色主题,
private val LightColorScheme = lightColorScheme(
primary = Purple40,
secondary = PurpleGrey40,
tertiary = Pink40
)
// 其他颜色定义
private val typography = Typography(
bodyLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 16.sp,
lineHeight = 24.sp,
letterSpacing = 0.5.sp
),
// 其他样式定义
)
// 主题定义, 用于设置主题, 颜色, 字体等
// 用于整个应用, 通过MaterialTheme使用
// 传入darkTheme参数, 用于判断当前主题
@Composable
fun NotesAppTheme(
themeViewModel: ThemeViewModel,
content: @Composable () -> Unit
) {
val themeMode by themeViewModel.themeMode.collectAsState()
val isDarkTheme = when (themeMode) {
ThemeMode.LIGHT -> false
ThemeMode.DARK -> true
ThemeMode.SYSTEM -> isSystemInDarkTheme()
}
MaterialTheme(
colorScheme = if (isDarkTheme) DarkColorScheme else LightColorScheme,
content = content
)
}
3. 创建 ViewModel
创建一个 ThemeViewModel,用于管理应用的主题模式状态。并设置初始状态跟随系统。
-
在
ThemePreference中:- 添加了
initializeTheme()方法来确保首次启动时设置为系统主题
- 添加了
-
在
ThemeViewModel中:- 初始化时调用
initializeTheme() - 将
stateIn的initialValue设置为ThemeMode.SYSTEM
- 初始化时调用
-
在
MainActivity中:- 添加了
Surface组件来确保正确应用主题背景色
- 添加了
这些确保了:
- 首次安装后默认跟随系统主题,应用首次启动时默认使用系统主题
- 在后续启动时使用了用户的保存的主题
- 如果出现任何问题,即数据存储出现问题,会默认使用系统主题
- 主题设置的持久化和恢复都能正常工作
- 整个应用界面都能正确响应主题变化
// ThemeViewModel.kt
package com.nemo.notes.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.nemo.notes.repository.ThemePreference
import com.nemo.notes.ui.theme.ThemeMode
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
class ThemeViewModel(
private val themePreference: ThemePreference
) : ViewModel() {
init {
// 在 ViewModel 初始化时确保主题被初始化
viewModelScope.launch {
themePreference.initializeTheme()
}
}
val themeMode: StateFlow<ThemeMode> = themePreference.themeMode
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = ThemeMode.SYSTEM // 设置初始值为 SYSTEM )
fun setThemeMode(mode: ThemeMode) {
viewModelScope.launch {
themePreference.setThemeMode(mode)
}
}
}
创建主题设置类ThemePreference, 用于管理主题设置。
package com.nemo.notes.repository
import android.content.Context
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import com.nemo.notes.ui.theme.ThemeMode
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
// 创建 ThemePreference 类, 用于管理主题设置
private val Context.dataStore by preferencesDataStore(name = "theme_settings")
/**
* 主题设置类, 用于管理主题设置,
* 包括获取和设置主题模式, 初始化主题设置
*
* @property context Context
* @constructor 创建 ThemePreference 实例
*/
class ThemePreference(private val context: Context) {
// 创建主题设置的 key, 用于存储主题设置
private val themeKey = stringPreferencesKey("theme_mode")
// 获取主题模式,默认返回 SYSTEM val themeMode: Flow<ThemeMode> = context.dataStore.data.map { preferences ->
try {
preferences[themeKey]?.let { ThemeMode.valueOf(it) } ?: ThemeMode.SYSTEM
} catch (e: Exception) {
ThemeMode.SYSTEM
}
}
// 设置主题模式
suspend fun setThemeMode(mode: ThemeMode) {
context.dataStore.edit { preferences ->
preferences[themeKey] = mode.name
}
}
// 添加初始化方法,如果没有设置则默认设置为 SYSTEM suspend fun initializeTheme() {
context.dataStore.edit { preferences ->
if (!preferences.contains(themeKey)) {
preferences[themeKey] = ThemeMode.SYSTEM.name
}
}
}
}
4. 监听系统夜间模式
在 Activity 中监听系统的夜间模式设置,并更新 ThemeViewModel 的状态。
package com.nemo.notes.ui
import android.annotation.SuppressLint
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.ui.Modifier
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import com.nemo.notes.repository.ThemePreference
import com.nemo.notes.ui.theme.NotesAppTheme
import com.nemo.notes.viewmodel.NoteViewModel
import com.nemo.notes.viewmodel.ThemeViewModel
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
@SuppressLint("UnrememberedMutableState", "StateFlowValueCalledInComposition")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val themePreference = ThemePreference(this)
setContent {
// 主题设置 ViewModel val themeViewModel: ThemeViewModel = viewModel { ThemeViewModel(themePreference) }
// 是否为暗色主题
var isDarkTheme = isSystemInDarkTheme()
// 主题设置
NotesAppTheme(themeViewModel) {
// 使用 Surface 确保整个应用使用正确的背景色
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
// 创建导航控制器
val navController = rememberNavController()
val viewModel: NoteViewModel = hiltViewModel()
// 设置导航控制器
NavHost(navController = navController, startDestination = "noteList") {
// 定义 noteList 跳转页
composable("noteList") {
NoteListScreen(
navController, viewModel, isDarkTheme,
onThemeChange = {
// 切换主题
isDarkTheme = it
themeViewModel.setThemeMode(
if (it) com.nemo.notes.ui.theme.ThemeMode.DARK else com.nemo.notes.ui.theme.ThemeMode.LIGHT
)
}
)
}
// 定义 noteEdit 跳转页
composable("noteEdit/{noteId}") { backStackEntry ->
val noteId =
backStackEntry.arguments?.getString("noteId")?.toLongOrNull()
NoteEditScreen(navController, viewModel, noteId)
}
}
}
}
}
}
}
5. 实现主题切换
在 NoteListScreen 中添加一个主题切换的按钮,实现主题切换功能,并根据 ThemeViewModel 的状态应用对应的主题。
package com.nemo.notes.ui
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.DarkMode
import androidx.compose.material.icons.filled.LightMode
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.navigation.NavHostController
import com.nemo.notes.viewmodel.NoteViewModel
/**
* 笔记列表界面
* @param navController 导航控制器
* @param viewModel 笔记ViewModel
* @param isDarkTheme 是否为暗色主题
* @param onThemeChange 切换主题回调
*
*/@Composable
fun NoteListScreen(
navController: NavHostController,
viewModel: NoteViewModel,
isDarkTheme: Boolean,
onThemeChange: (Boolean) -> Unit
) {
// 收集所有笔记的状态
val notes by viewModel.filteredNotes.collectAsState(initial = emptyList())
// 新增搜索字段
var searchQuery by remember { mutableStateOf("") }
Column(modifier = Modifier.padding(16.dp)) {
// 搜索框
TextField(
value = searchQuery,
onValueChange = {
searchQuery = it
viewModel.setSearchQuery(it)
},
label = { Text("Search") },
modifier = Modifier.fillMaxWidth()
)
// 添加间距
Spacer(modifier = Modifier.height(16.dp))
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
// 显示标题
Text(text = "My Notes", style = MaterialTheme.typography.headlineMedium)
// 添加切换主题按钮
IconButton(onClick = { onThemeChange(!isDarkTheme) }) {
Icon(
// 根据主题显示不同图标
imageVector = if (isDarkTheme) Icons.Default.DarkMode else Icons.Default.LightMode,
contentDescription = "Toggle Theme"
)
}
} // 显示笔记列表
LazyColumn {
items(notes) { note ->
// 每个笔记项使用 Card 显示
Card(
onClick = { navController.navigate("noteEdit/${note.id}") },
modifier = Modifier.padding(8.dp)
) {
Column(modifier = Modifier.padding(16.dp)) {
// 显示笔记标题
Text(text = note.title, style = MaterialTheme.typography.titleMedium)
// 显示笔记内容
Text(text = note.content, style = MaterialTheme.typography.bodyMedium)
// 显示笔记标签
if (note.tags.isNotEmpty()) {
Text(
text = "Tags: ${note.tags.joinToString(", ")}",
style = MaterialTheme.typography.bodySmall
)
}
}
} } } // 添加间距
Spacer(modifier = Modifier.height(16.dp))
// 添加新笔记按钮
Button(
onClick = { navController.navigate("noteEdit/null") },
modifier = Modifier.fillMaxWidth()
) {
Text("Add New Note")
}
}
}
6. 测试
- 默认跟随系统:确保应用的主题模式与系统的夜间模式设置一致。
- 手动切换:点击按钮切换主题模式,验证应用的主题是否正确更新。
通过以上步骤,我们实现了基于 androidx.compose.ui 框架的夜间模式支持,包括默认跟随系统和手动切换功能。你可以根据实际需求进一步优化和扩展此功能。
这个实现方案具有以下特点:
- 使用 DataStore 持久化存储主题设置
- 使用 ViewModel 管理主题状态
- 支持系统主题跟随、浅色模式和深色模式
- 使用 Material3 主题系统
- 主题切换即时生效,无需重启应用