01-07-01 MVVM架构完全指南
01-07-01 MVVM架构完全指南
🎯 问题背景
在deviceSecurity项目开发中,随着业务逻辑的增长,我们面临诸多架构挑战:
- 如何将UI展示、业务逻辑和数据管理清晰分离?
- 如何避免Activity/Fragment代码臃肿?
- 如何实现数据与界面的自动同步?
- 如何正确管理复杂的UI状态?
- 如何处理配置更改和内存泄漏问题?
MVVM(Model-View-ViewModel)架构模式提供了优雅的解决方案,本文将深入探讨MVVM架构的核心概念、ViewModel最佳实践和状态管理策略。
💡 核心概念
MVVM三层架构
MVVM通过三层结构实现清晰的职责分离:
┌─────────────────────────────────────────┐
│ View Layer │
│ (Activity/Fragment/Compose) │
│ - 显示UI │
│ - 监听用户操作 │
│ - 观察ViewModel状态 │
└──────────┬──────────────────────────────┘
│ observes (单向依赖)
↓
┌─────────────────────────────────────────┐
│ ViewModel Layer │
│ - 持有UI状态 (StateFlow) │
│ - 处理业务逻辑 │
│ - 调用UseCase/Repository │
│ - 不持有View引用(避免内存泄漏) │
└──────────┬──────────────────────────────┘
│ calls
↓
┌─────────────────────────────────────────┐
│ Model Layer │
│ (Repository/UseCase/DataSource) │
│ - 数据获取(网络/数据库) │
│ - 业务规则 │
│ - 数据转换 │
└─────────────────────────────────────────┘
核心优势
- 数据绑定:通过LiveData/StateFlow自动同步数据和UI
- 职责分离:UI逻辑与业务逻辑完全解耦
- 易于测试:ViewModel可独立进行单元测试
- 生命周期感知:自动处理配置更改,避免内存泄漏
- 可维护性:代码结构清晰,易于扩展和维护
📝 第一部分:MVVM基础实现
1.1 Model层实现
Model层负责数据和业务逻辑,包括数据模型、Repository和数据源。
// ========== 数据模型 ==========
data class Device(
val deviceId: String,
val name: String,
val status: DeviceStatus,
val battery: Int,
val lastSyncTime: Long
)
enum class DeviceStatus {
ONLINE, OFFLINE, UPDATING, ERROR
}
// ========== Repository接口 ==========
interface DeviceRepository {
suspend fun getDevices(): Result<List<Device>>
suspend fun getDeviceById(id: String): Result<Device>
suspend fun updateDevice(device: Device): Result<Unit>
suspend fun deleteDevice(id: String): Result<Unit>
fun observeDevices(): Flow<List<Device>>
}
// ========== Repository实现 ==========
class DeviceRepositoryImpl @Inject constructor(
private val api: DeviceApi,
private val dao: DeviceDao,
private val networkMonitor: NetworkMonitor
) : DeviceRepository {
override suspend fun getDevices(): Result<List<Device>> {
return withContext(Dispatchers.IO) {
runCatching {
if (networkMonitor.isOnline()) {
// 从网络获取
val remoteDevices = api.fetchDevices()
// 缓存到本地
dao.insertDevices(remoteDevices)
remoteDevices
} else {
// 从本地获取
dao.getAllDevices()
}
}
}
}
override suspend fun updateDevice(device: Device): Result<Unit> {
return withContext(Dispatchers.IO) {
runCatching {
// 先更新本地
dao.updateDevice(device)
// 如果有网络,同步到远程
if (networkMonitor.isOnline()) {
try {
api.updateDevice(device.deviceId, device)
} catch (e: Exception) {
// 网络失败不影响本地更新
Log.w("DeviceRepository", "Failed to sync to remote", e)
}
}
}
}
}
override fun observeDevices(): Flow<List<Device>> {
return dao.observeAllDevices()
.flowOn(Dispatchers.IO)
}
}
1.2 ViewModel层实现
ViewModel是MVVM的核心,负责持有UI状态和处理业务逻辑。
// ========== UI状态封装 ==========
data class DeviceListUiState(
val devices: List<Device> = emptyList(),
val isLoading: Boolean = false,
val isRefreshing: Boolean = false,
val error: String? = null,
val selectedDevice: Device? = null,
val searchQuery: String = ""
) {
// 派生状态
val filteredDevices: List<Device>
get() = if (searchQuery.isEmpty()) {
devices
} else {
devices.filter {
it.name.contains(searchQuery, ignoreCase = true)
}
}
val onlineCount: Int
get() = devices.count { it.status == DeviceStatus.ONLINE }
val lowBatteryCount: Int
get() = devices.count { it.battery < 20 }
val isEmpty: Boolean
get() = devices.isEmpty() && !isLoading
}
// ========== ViewModel实现 ==========
@HiltViewModel
class DeviceListViewModel @Inject constructor(
private val repository: DeviceRepository
) : ViewModel() {
// 单一状态源(StateFlow)
private val _uiState = MutableStateFlow(DeviceListUiState())
val uiState: StateFlow<DeviceListUiState> = _uiState.asStateFlow()
// 单次事件(Channel)
private val _events = Channel<DeviceEvent>(Channel.BUFFERED)
val events = _events.receiveAsFlow()
init {
loadDevices()
observeDevices()
}
// 初始加载设备
private fun loadDevices() {
viewModelScope.launch {
_uiState.update { it.copy(isLoading = true, error = null) }
repository.getDevices()
.onSuccess { devices ->
_uiState.update {
it.copy(
devices = devices,
isLoading = false
)
}
}
.onFailure { error ->
_uiState.update {
it.copy(
isLoading = false,
error = error.message
)
}
_events.send(DeviceEvent.ShowError(error.message ?: "加载失败"))
}
}
}
// 持续观察设备变化
private fun observeDevices() {
viewModelScope.launch {
repository.observeDevices()
.catch { error ->
_events.send(DeviceEvent.ShowError(error.message ?: "数据同步失败"))
}
.collect { devices ->
_uiState.update { it.copy(devices = devices) }
}
}
}
// 刷新设备列表
fun refreshDevices() {
viewModelScope.launch {
_uiState.update { it.copy(isRefreshing = true) }
repository.getDevices()
.onSuccess { devices ->
_uiState.update {
it.copy(devices = devices, isRefreshing = false)
}
_events.send(DeviceEvent.ShowMessage("刷新成功"))
}
.onFailure { error ->
_uiState.update { it.copy(isRefreshing = false) }
_events.send(DeviceEvent.ShowError("刷新失败"))
}
}
}
// 搜索设备
fun searchDevices(query: String) {
_uiState.update { it.copy(searchQuery = query) }
}
// 点击设备
fun onDeviceClick(device: Device) {
_uiState.update { it.copy(selectedDevice = device) }
viewModelScope.launch {
_events.send(DeviceEvent.NavigateToDetail(device.deviceId))
}
}
// 删除设备
fun deleteDevice(deviceId: String) {
viewModelScope.launch {
repository.deleteDevice(deviceId)
.onSuccess {
_events.send(DeviceEvent.ShowMessage("删除成功"))
}
.onFailure { error ->
_events.send(DeviceEvent.ShowError("删除失败: ${error.message}"))
}
}
}
}
// ========== 单次事件定义 ==========
sealed class DeviceEvent {
data class ShowError(val message: String) : DeviceEvent()
data class ShowMessage(val message: String) : DeviceEvent()
data class NavigateToDetail(val deviceId: String) : DeviceEvent()
}
1.3 View层实现(Fragment)
View层只负责UI展示和用户交互,不包含业务逻辑。
@AndroidEntryPoint
class DeviceListFragment : Fragment(R.layout.fragment_device_list) {
private val viewModel: DeviceListViewModel by viewModels()
private val binding by viewBinding(FragmentDeviceListBinding::bind)
private val adapter = DeviceListAdapter { device ->
viewModel.onDeviceClick(device)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupRecyclerView()
observeUiState()
observeEvents()
setupListeners()
}
private fun setupRecyclerView() {
binding.recyclerView.apply {
adapter = this@DeviceListFragment.adapter
layoutManager = LinearLayoutManager(context)
}
}
// 观察状态变化
private fun observeUiState() {
viewLifecycleOwner.lifecycleScope.launch {
viewModel.uiState.collect { state ->
// 更新设备列表
adapter.submitList(state.filteredDevices)
// 更新加载状态
binding.progressBar.isVisible = state.isLoading
binding.swipeRefresh.isRefreshing = state.isRefreshing
// 显示错误
binding.errorText.apply {
isVisible = state.error != null
text = state.error
}
// 空状态
binding.emptyView.isVisible = state.isEmpty
// 更新统计信息
binding.onlineCountText.text = "在线: ${state.onlineCount}"
binding.lowBatteryCountText.text = "低电量: ${state.lowBatteryCount}"
}
}
}
// 观察单次事件
private fun observeEvents() {
viewLifecycleOwner.lifecycleScope.launch {
viewModel.events.collect { event ->
when (event) {
is DeviceEvent.ShowError -> {
Toast.makeText(context, event.message, Toast.LENGTH_SHORT).show()
}
is DeviceEvent.ShowMessage -> {
Snackbar.make(binding.root, event.message, Snackbar.LENGTH_SHORT).show()
}
is DeviceEvent.NavigateToDetail -> {
findNavController().navigate(
DeviceListFragmentDirections.actionToDeviceDetail(event.deviceId)
)
}
}
}
}
}
private fun setupListeners() {
// 下拉刷新
binding.swipeRefresh.setOnRefreshListener {
viewModel.refreshDevices()
}
// 搜索
binding.searchView.addTextChangedListener { text ->
viewModel.searchDevices(text.toString())
}
// 重试按钮
binding.retryButton.setOnClickListener {
viewModel.refreshDevices()
}
}
}
1.4 View层实现(Compose)
使用Jetpack Compose实现更加简洁的声明式UI。
@Composable
fun DeviceListScreen(
viewModel: DeviceListViewModel = hiltViewModel(),
onNavigateToDetail: (String) -> Unit
) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
val context = LocalContext.current
// 处理单次事件
LaunchedEffect(Unit) {
viewModel.events.collect { event ->
when (event) {
is DeviceEvent.ShowError -> {
Toast.makeText(context, event.message, Toast.LENGTH_SHORT).show()
}
is DeviceEvent.ShowMessage -> {
Toast.makeText(context, event.message, Toast.LENGTH_SHORT).show()
}
is DeviceEvent.NavigateToDetail -> {
onNavigateToDetail(event.deviceId)
}
}
}
}
DeviceListContent(
uiState = uiState,
onRefresh = viewModel::refreshDevices,
onSearchQueryChange = viewModel::searchDevices,
onDeviceClick = viewModel::onDeviceClick,
onDeleteDevice = viewModel::deleteDevice
)
}
@Composable
private fun DeviceListContent(
uiState: DeviceListUiState,
onRefresh: () -> Unit,
onSearchQueryChange: (String) -> Unit,
onDeviceClick: (Device) -> Unit,
onDeleteDevice: (String) -> Unit
) {
Column(modifier = Modifier.fillMaxSize()) {
// 搜索栏
SearchBar(
query = uiState.searchQuery,
onQueryChange = onSearchQueryChange,
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
)
// 统计信息
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text("在线: ${uiState.onlineCount}")
Text("低电量: ${uiState.lowBatteryCount}")
}
Spacer(modifier = Modifier.height(8.dp))
// 内容区域
when {
uiState.error != null -> {
ErrorView(
message = uiState.error,
onRetry = onRefresh
)
}
uiState.isEmpty -> {
EmptyView()
}
else -> {
val pullRefreshState = rememberPullRefreshState(
refreshing = uiState.isRefreshing,
onRefresh = onRefresh
)
Box(Modifier.pullRefresh(pullRefreshState)) {
LazyColumn {
items(
items = uiState.filteredDevices,
key = { it.deviceId }
) { device ->
DeviceItem(
device = device,
onClick = { onDeviceClick(device) },
onDelete = { onDeleteDevice(device.deviceId) }
)
}
}
PullRefreshIndicator(
refreshing = uiState.isRefreshing,
state = pullRefreshState,
modifier = Modifier.align(Alignment.TopCenter)
)
}
}
}
// 加载指示器
if (uiState.isLoading) {
LinearProgressIndicator(modifier = Modifier.fillMaxWidth())
}
}
}
@Composable
private fun DeviceItem(
device: Device,
onClick: () -> Unit,
onDelete: () -> Unit
) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 8.dp)
.clickable(onClick = onClick)
) {
Row(
modifier = Modifier
.padding(16.dp)
.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Column(modifier = Modifier.weight(1f)) {
Text(
text = device.name,
style = MaterialTheme.typography.titleMedium
)
Text(
text = "电量: ${device.battery}%",
style = MaterialTheme.typography.bodySmall,
color = when {
device.battery < 20 -> Color.Red
device.battery < 50 -> Color.Yellow
else -> Color.Green
}
)
}
StatusBadge(status = device.status)
IconButton(onClick = onDelete) {
Icon(Icons.Default.Delete, contentDescription = "删除")
}
}
}
}
@Composable
private fun StatusBadge(status: DeviceStatus) {
val (text, color) = when (status) {
DeviceStatus.ONLINE -> "在线" to Color.Green
DeviceStatus.OFFLINE -> "离线" to Color.Gray
DeviceStatus.UPDATING -> "更新中" to Color.Blue
DeviceStatus.ERROR -> "错误" to Color.Red
}
Surface(
shape = RoundedCornerShape(4.dp),
color = color.copy(alpha = 0.2f)
) {
Text(
text = text,
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp),
color = color,
style = MaterialTheme.typography.labelSmall
)
}
}
📝 第二部分:ViewModel最佳实践
2.1 状态管理进阶
使用SavedStateHandle保存状态
SavedStateHandle可以在进程重建后恢复状态,非常适合保存用户输入和临时数据。
@HiltViewModel
class DeviceDetailViewModel @Inject constructor(
private val getDeviceUseCase: GetDeviceUseCase,
private val updateDeviceUseCase: UpdateDeviceUseCase,
private val savedStateHandle: SavedStateHandle
) : ViewModel() {
// 从Navigation参数获取deviceId
private val deviceId: String = savedStateHandle.get<String>("deviceId")
?: throw IllegalArgumentException("deviceId required")
// 使用SavedStateHandle保存编辑状态
var isEditMode: Boolean by savedStateHandle.saveable { mutableStateOf(false) }
var editingName: String by savedStateHandle.saveable { mutableStateOf("") }
// 保存滚动位置
var scrollPosition: Int
get() = savedStateHandle.get<Int>(KEY_SCROLL_POSITION) ?: 0
set(value) { savedStateHandle[KEY_SCROLL_POSITION] = value }
// 使用StateFlow从SavedStateHandle
val searchQuery: StateFlow<String> = savedStateHandle.getStateFlow(
key = KEY_SEARCH_QUERY,
initialValue = ""
)
private val _uiState = MutableStateFlow(DeviceDetailUiState())
val uiState: StateFlow<DeviceDetailUiState> = _uiState.asStateFlow()
companion object {
private const val KEY_SCROLL_POSITION = "scroll_position"
private const val KEY_SEARCH_QUERY = "search_query"
}
}
复杂状态组合
使用combine组合多个数据流,创建统一的UI状态。
@HiltViewModel
class DashboardViewModel @Inject constructor(
private val observeDevicesUseCase: ObserveDevicesUseCase,
private val observeEventsUseCase: ObserveEventsUseCase,
private val observeAlertsUseCase: ObserveAlertsUseCase
) : ViewModel() {
// 组合多个数据流
val uiState: StateFlow<DashboardUiState> = combine(
observeDevicesUseCase(),
observeEventsUseCase(limit = 20),
observeAlertsUseCase()
) { devices, events, alerts ->
DashboardUiState(
devices = devices,
recentEvents = events,
activeAlerts = alerts,
statistics = calculateStatistics(devices, events, alerts)
)
}.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = DashboardUiState()
)
private fun calculateStatistics(
devices: List<Device>,
events: List<Event>,
alerts: List<Alert>
): Statistics {
return Statistics(
totalDevices = devices.size,
onlineDevices = devices.count { it.status == DeviceStatus.ONLINE },
lowBatteryDevices = devices.count { it.battery < 20 },
todayEvents = events.count { it.isToday() },
activeAlerts = alerts.size
)
}
}
data class DashboardUiState(
val devices: List<Device> = emptyList(),
val recentEvents: List<Event> = emptyList(),
val activeAlerts: List<Alert> = emptyList(),
val statistics: Statistics = Statistics()
)
data class Statistics(
val totalDevices: Int = 0,
val onlineDevices: Int = 0,
val lowBatteryDevices: Int = 0,
val todayEvents: Int = 0,
val activeAlerts: Int = 0
)
2.2 协程管理
取消协程Job
对于需要手动控制的协程,保存Job引用以便取消。
@HiltViewModel
class EventPlayerViewModel @Inject constructor(
private val playEventUseCase: PlayEventUseCase
) : ViewModel() {
private val _uiState = MutableStateFlow(EventPlayerUiState())
val uiState: StateFlow<EventPlayerUiState> = _uiState.asStateFlow()
// 定时更新播放进度的Job
private var progressUpdateJob: Job? = null
fun play() {
_uiState.update { it.copy(isPlaying = true) }
startProgressUpdate()
}
fun pause() {
_uiState.update { it.copy(isPlaying = false) }
stopProgressUpdate()
}
private fun startProgressUpdate() {
stopProgressUpdate() // 先取消旧的Job
progressUpdateJob = viewModelScope.launch {
while (isActive) {
delay(100) // 每100ms更新一次
updateProgress()
}
}
}
private fun stopProgressUpdate() {
progressUpdateJob?.cancel()
progressUpdateJob = null
}
private fun updateProgress() {
_uiState.update { state ->
val newPosition = state.currentPosition + 100
if (newPosition >= state.duration) {
// 播放结束
state.copy(
currentPosition = state.duration,
isPlaying = false
)
} else {
state.copy(currentPosition = newPosition)
}
}
}
override fun onCleared() {
super.onCleared()
stopProgressUpdate()
// 释放其他资源
}
}
错误处理
使用CoroutineExceptionHandler集中处理协程异常。
@HiltViewModel
class DeviceSetupViewModel @Inject constructor(
private val setupDeviceUseCase: SetupDeviceUseCase
) : ViewModel() {
private val _uiState = MutableStateFlow(SetupUiState())
val uiState: StateFlow<SetupUiState> = _uiState.asStateFlow()
// 错误处理器
private val errorHandler = CoroutineExceptionHandler { _, throwable ->
_uiState.update {
it.copy(
isLoading = false,
error = when (throwable) {
is DeviceNotFoundException -> "未发现设备,请确保设备已开机"
is SetupTimeoutException -> "配网超时,请重试"
is NetworkException -> "网络错误,请检查网络连接"
else -> "配网失败: ${throwable.message}"
}
)
}
}
fun startSetup(ssid: String, password: String) {
viewModelScope.launch(errorHandler) {
_uiState.update { it.copy(isLoading = true, error = null) }
setupDeviceUseCase(ssid, password)
.catch { error ->
_uiState.update {
it.copy(isLoading = false, error = error.message)
}
}
.collect { state ->
when (state) {
is SetupState.Discovering -> {
_uiState.update {
it.copy(step = "正在发现设备...")
}
}
is SetupState.Connecting -> {
_uiState.update {
it.copy(step = "正在连接设备...")
}
}
is SetupState.Configuring -> {
_uiState.update {
it.copy(step = "正在配置网络...")
}
}
is SetupState.Success -> {
_uiState.update {
it.copy(isLoading = false, isSuccess = true)
}
}
}
}
}
}
}
2.3 Paging集成
使用Paging 3实现高效的分页加载。
@HiltViewModel
class EventListViewModel @Inject constructor(
private val getEventsUseCase: GetEventsUseCase
) : ViewModel() {
// 使用Paging 3
val events: Flow<PagingData<Event>> = getEventsUseCase()
.cachedIn(viewModelScope) // 缓存在ViewModel生命周期内
}
// 在Compose中使用
@Composable
fun EventListScreen(viewModel: EventListViewModel = hiltViewModel()) {
val events = viewModel.events.collectAsLazyPagingItems()
LazyColumn {
items(
count = events.itemCount,
key = { index -> events[index]?.id ?: index }
) { index ->
events[index]?.let { event ->
EventItem(event = event)
}
}
// 加载状态处理
when (events.loadState.refresh) {
is LoadState.Loading -> {
item {
Box(
modifier = Modifier.fillMaxWidth(),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator()
}
}
}
is LoadState.Error -> {
val error = (events.loadState.refresh as LoadState.Error).error
item {
ErrorView(
message = error.message ?: "加载失败",
onRetry = { events.retry() }
)
}
}
else -> {}
}
}
}
📝 第三部分:状态管理最佳实践
3.1 状态分层设计
将状态分为三层:UI状态、业务状态和临时状态。
// ========== UI State(ViewModel层)==========
data class DeviceDetailUiState(
// 数据状态
val device: Device? = null,
val recentEvents: List<Event> = emptyList(),
// 加载状态
val isLoading: Boolean = false,
val isRefreshing: Boolean = false,
val error: String? = null,
// 交互状态
val isEditMode: Boolean = false,
val showDeleteDialog: Boolean = false,
val editingName: String = ""
) {
// 派生状态
val canSave: Boolean
get() = editingName.isNotBlank() && editingName != device?.name
val batteryStatus: BatteryStatus
get() = when {
device == null -> BatteryStatus.UNKNOWN
device.battery < 10 -> BatteryStatus.CRITICAL
device.battery < 20 -> BatteryStatus.LOW
else -> BatteryStatus.NORMAL
}
val isOnline: Boolean
get() = device?.status == DeviceStatus.ONLINE
}
enum class BatteryStatus {
UNKNOWN, CRITICAL, LOW, NORMAL
}
// ========== Business State(Domain层)==========
data class Device(
val id: String,
val name: String,
val type: DeviceType,
val status: DeviceStatus,
val battery: Int
) {
// 领域逻辑
fun isLowBattery(): Boolean = battery < 20
fun canUpdate(): Boolean = status != DeviceStatus.UPDATING
fun requiresAttention(): Boolean = isLowBattery() || status == DeviceStatus.ERROR
}
// ========== Transient State(Compose层)==========
@Composable
fun DeviceDetailScreen(viewModel: DeviceDetailViewModel = hiltViewModel()) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
// 临时UI状态(不需要ViewModel管理)
var showBottomSheet by remember { mutableStateOf(false) }
var selectedTab by remember { mutableStateOf(0) }
var expandedCardId by remember { mutableStateOf<String?>(null) }
DeviceDetailContent(
uiState = uiState,
showBottomSheet = showBottomSheet,
selectedTab = selectedTab,
onTabChange = { selectedTab = it },
onToggleBottomSheet = { showBottomSheet = !showBottomSheet }
)
}
3.2 状态更新模式
模式1:同步状态更新
对于简单的UI状态变化,使用update直接更新。
@HiltViewModel
class DeviceDetailViewModel @Inject constructor(
private val repository: DeviceRepository
) : ViewModel() {
private val _uiState = MutableStateFlow(DeviceDetailUiState())
val uiState: StateFlow<DeviceDetailUiState> = _uiState.asStateFlow()
// 进入编辑模式
fun enterEditMode() {
_uiState.update { currentState ->
currentState.copy(
isEditMode = true,
editingName = currentState.device?.name ?: ""
)
}
}
// 更新编辑内容
fun updateEditingName(name: String) {
_uiState.update { it.copy(editingName = name) }
}
// 取消编辑
fun cancelEdit() {
_uiState.update {
it.copy(isEditMode = false, editingName = "")
}
}
// 切换对话框显示
fun toggleDeleteDialog() {
_uiState.update {
it.copy(showDeleteDialog = !it.showDeleteDialog)
}
}
}
模式2:异步状态更新
对于需要网络请求的操作,分步更新状态。
fun saveName() {
viewModelScope.launch {
val newName = _uiState.value.editingName
// Step 1: 标记加载中
_uiState.update { it.copy(isLoading = true) }
// Step 2: 执行异步操作
repository.updateDeviceName(deviceId, newName)
.onSuccess {
// Step 3: 成功后更新状态
_uiState.update {
it.copy(
isLoading = false,
isEditMode = false,
editingName = ""
)
}
_events.send(DeviceEvent.ShowMessage("更新成功"))
}
.onFailure { error ->
// Step 4: 失败后更新错误状态
_uiState.update {
it.copy(isLoading = false, error = error.message)
}
_events.send(DeviceEvent.ShowError(error.message ?: "更新失败"))
}
}
}
模式3:Flow持续更新
对于需要持续观察的数据,使用Flow。
init {
// 自动观察设备变化
viewModelScope.launch {
repository.observeDevice(deviceId)
.catch { error ->
_uiState.update {
it.copy(isLoading = false, error = error.message)
}
}
.collect { device ->
_uiState.update {
it.copy(device = device, isLoading = false, error = null)
}
}
}
// 同时观察最近事件
viewModelScope.launch {
repository.observeRecentEvents(deviceId, limit = 10)
.collect { events ->
_uiState.update { it.copy(recentEvents = events) }
}
}
}
3.3 状态验证与派生
表单验证
在State中实现验证逻辑,保持ViewModel简洁。
data class LoginState(
val username: String = "",
val password: String = "",
val isLoading: Boolean = false,
val error: String? = null
) {
// ========== 验证逻辑 ==========
val usernameError: String?
get() = when {
username.isBlank() -> "用户名不能为空"
username.length < 3 -> "用户名至少3个字符"
!username.matches(Regex("^[a-zA-Z0-9_]+$")) ->
"用户名只能包含字母、数字和下划线"
else -> null
}
val passwordError: String?
get() = when {
password.isBlank() -> "密码不能为空"
password.length < 6 -> "密码至少6个字符"
!password.any { it.isDigit() } -> "密码必须包含数字"
!password.any { it.isLetter() } -> "密码必须包含字母"
else -> null
}
// ========== 派生状态 ==========
val isValid: Boolean
get() = usernameError == null && passwordError == null
val canSubmit: Boolean
get() = isValid && !isLoading
val hasErrors: Boolean
get() = usernameError != null || passwordError != null
}
@HiltViewModel
class LoginViewModel @Inject constructor(
private val loginUseCase: LoginUseCase
) : ViewModel() {
private val _state = MutableStateFlow(LoginState())
val state: StateFlow<LoginState> = _state.asStateFlow()
fun updateUsername(username: String) {
_state.update { it.copy(username = username, error = null) }
}
fun updatePassword(password: String) {
_state.update { it.copy(password = password, error = null) }
}
fun submit() {
// 使用派生状态判断
if (!_state.value.canSubmit) return
viewModelScope.launch {
_state.update { it.copy(isLoading = true, error = null) }
loginUseCase(_state.value.username, _state.value.password)
.onSuccess {
// 导航到主页
}
.onFailure { error ->
_state.update {
it.copy(isLoading = false, error = error.message)
}
}
}
}
}
在Compose中使用验证
@Composable
fun LoginScreen(viewModel: LoginViewModel = hiltViewModel()) {
val state by viewModel.state.collectAsStateWithLifecycle()
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp)
) {
// 用户名输入
OutlinedTextField(
value = state.username,
onValueChange = viewModel::updateUsername,
label = { Text("用户名") },
isError = state.usernameError != null,
supportingText = {
state.usernameError?.let { Text(it) }
},
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(16.dp))
// 密码输入
OutlinedTextField(
value = state.password,
onValueChange = viewModel::updatePassword,
label = { Text("密码") },
isError = state.passwordError != null,
supportingText = {
state.passwordError?.let { Text(it) }
},
visualTransformation = PasswordVisualTransformation(),
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(24.dp))
// 登录按钮
Button(
onClick = viewModel::submit,
enabled = state.canSubmit,
modifier = Modifier.fillMaxWidth()
) {
if (state.isLoading) {
CircularProgressIndicator(
modifier = Modifier.size(24.dp),
color = Color.White
)
} else {
Text("登录")
}
}
// 错误提示
state.error?.let { error ->
Spacer(modifier = Modifier.height(16.dp))
Text(
text = error,
color = MaterialTheme.colorScheme.error,
style = MaterialTheme.typography.bodyMedium
)
}
}
}
3.4 复杂场景:Event播放器
实现一个功能完整的播放器状态管理。
// ========== 播放器状态 ==========
data class EventPlayerState(
// 数据
val event: Event? = null,
val playlist: List<Event> = emptyList(),
// 播放状态
val playbackState: PlaybackState = PlaybackState.IDLE,
val currentPosition: Long = 0,
val duration: Long = 0,
// 控制状态
val playbackSpeed: Float = 1.0f,
val volume: Float = 1.0f,
val isMuted: Boolean = false,
val repeatMode: RepeatMode = RepeatMode.OFF,
val shuffleEnabled: Boolean = false,
// UI状态
val showControls: Boolean = true,
val isFullscreen: Boolean = false,
val isBuffering: Boolean = false,
val error: String? = null
) {
// ========== 派生状态 ==========
val currentIndex: Int
get() = playlist.indexOf(event)
val hasNext: Boolean
get() = currentIndex < playlist.size - 1
val hasPrevious: Boolean
get() = currentIndex > 0
val progress: Float
get() = if (duration > 0) {
currentPosition.toFloat() / duration
} else 0f
val isPlaying: Boolean
get() = playbackState == PlaybackState.PLAYING
val canPlay: Boolean
get() = event != null && playbackState != PlaybackState.ERROR
val formattedPosition: String
get() = formatTime(currentPosition)
val formattedDuration: String
get() = formatTime(duration)
private fun formatTime(millis: Long): String {
val seconds = (millis / 1000) % 60
val minutes = (millis / 1000 / 60) % 60
val hours = millis / 1000 / 3600
return if (hours > 0) {
String.format("%d:%02d:%02d", hours, minutes, seconds)
} else {
String.format("%d:%02d", minutes, seconds)
}
}
}
enum class PlaybackState {
IDLE, PREPARING, PLAYING, PAUSED, ENDED, ERROR
}
enum class RepeatMode {
OFF, ONE, ALL
}
// ========== ViewModel实现 ==========
@HiltViewModel
class EventPlayerViewModel @Inject constructor(
private val playEventUseCase: PlayEventUseCase,
savedStateHandle: SavedStateHandle
) : ViewModel() {
private val _state = MutableStateFlow(EventPlayerState())
val state: StateFlow<EventPlayerState> = _state.asStateFlow()
private var playbackJob: Job? = null
private val eventId: String = savedStateHandle["eventId"]!!
init {
loadEvent()
}
private fun loadEvent() {
viewModelScope.launch {
_state.update { it.copy(isBuffering = true) }
playEventUseCase(eventId)
.onSuccess { eventInfo ->
_state.update {
it.copy(
event = eventInfo.event,
playlist = eventInfo.playlist,
duration = eventInfo.duration,
isBuffering = false,
playbackState = PlaybackState.PAUSED
)
}
}
.onFailure { error ->
_state.update {
it.copy(
isBuffering = false,
playbackState = PlaybackState.ERROR,
error = error.message
)
}
}
}
}
fun play() {
if (!_state.value.canPlay) return
_state.update { it.copy(playbackState = PlaybackState.PLAYING) }
startPlayback()
}
fun pause() {
_state.update { it.copy(playbackState = PlaybackState.PAUSED) }
stopPlayback()
}
fun seekTo(position: Long) {
val newPosition = position.coerceIn(0, _state.value.duration)
_state.update { it.copy(currentPosition = newPosition) }
}
fun setSpeed(speed: Float) {
val validSpeed = speed.coerceIn(0.5f, 2.0f)
_state.update { it.copy(playbackSpeed = validSpeed) }
}
fun setVolume(volume: Float) {
val validVolume = volume.coerceIn(0f, 1f)
_state.update { it.copy(volume = validVolume, isMuted = validVolume == 0f) }
}
fun toggleMute() {
_state.update { state ->
if (state.isMuted) {
state.copy(isMuted = false, volume = 1.0f)
} else {
state.copy(isMuted = true, volume = 0f)
}
}
}
fun toggleRepeatMode() {
_state.update { state ->
val newMode = when (state.repeatMode) {
RepeatMode.OFF -> RepeatMode.ONE
RepeatMode.ONE -> RepeatMode.ALL
RepeatMode.ALL -> RepeatMode.OFF
}
state.copy(repeatMode = newMode)
}
}
fun toggleShuffle() {
_state.update { it.copy(shuffleEnabled = !it.shuffleEnabled) }
}
fun playNext() {
val currentState = _state.value
val nextIndex = if (currentState.shuffleEnabled) {
// 随机播放
(0 until currentState.playlist.size).random()
} else {
// 顺序播放
currentState.currentIndex + 1
}
if (nextIndex < currentState.playlist.size) {
playEventAt(nextIndex)
} else if (currentState.repeatMode == RepeatMode.ALL) {
playEventAt(0)
} else {
pause()
_state.update { it.copy(playbackState = PlaybackState.ENDED) }
}
}
fun playPrevious() {
val prevIndex = _state.value.currentIndex - 1
if (prevIndex >= 0) {
playEventAt(prevIndex)
}
}
private fun playEventAt(index: Int) {
val playlist = _state.value.playlist
if (index !in playlist.indices) return
_state.update {
it.copy(
event = playlist[index],
currentPosition = 0,
playbackState = PlaybackState.PLAYING
)
}
startPlayback()
}
private fun startPlayback() {
stopPlayback()
playbackJob = viewModelScope.launch {
while (_state.value.isPlaying) {
delay(100)
_state.update { state ->
val newPosition = state.currentPosition + 100
if (newPosition >= state.duration) {
// 播放结束
when (state.repeatMode) {
RepeatMode.ONE -> {
// 重复当前
state.copy(currentPosition = 0)
}
RepeatMode.ALL, RepeatMode.OFF -> {
// 播放下一首或结束
playNext()
state
}
}
} else {
state.copy(currentPosition = newPosition)
}
}
}
}
}
private fun stopPlayback() {
playbackJob?.cancel()
playbackJob = null
}
override fun onCleared() {
super.onCleared()
stopPlayback()
}
}
⚡ 关键要点总结
1. MVVM架构原则
// ✅ 推荐:清晰的三层分离
// Model: 数据和业务逻辑
interface DeviceRepository {
suspend fun getDevices(): Result<List<Device>>
}
// ViewModel: UI状态和逻辑
@HiltViewModel
class DeviceListViewModel @Inject constructor(
private val repository: DeviceRepository
) : ViewModel()
// View: 纯UI展示
@Composable
fun DeviceListScreen(viewModel: DeviceListViewModel = hiltViewModel())
// ❌ 避免:在View中处理业务逻辑
@Composable
fun DeviceListScreen() {
val devices = repository.getDevices() // 错误!
}
2. ViewModel状态管理
// ✅ 推荐:使用UiState封装所有状态
data class UiState(
val data: List<Item>,
val isLoading: Boolean,
val error: String?
)
private val _uiState = MutableStateFlow(UiState())
val uiState: StateFlow<UiState> = _uiState.asStateFlow()
// ❌ 避免:多个独立的LiveData/StateFlow
private val _data = MutableStateFlow<List<Item>>(emptyList())
private val _isLoading = MutableStateFlow(false)
private val _error = MutableStateFlow<String?>(null)
3. 单次事件处理
// ✅ 推荐:使用Channel处理单次事件
private val _events = Channel<Event>(Channel.BUFFERED)
val events = _events.receiveAsFlow()
// 发送事件
_events.send(Event.ShowToast("操作成功"))
// ❌ 避免:用StateFlow处理单次事件(会在配置更改时重复触发)
private val _toastMessage = MutableStateFlow<String?>(null)
4. 不持有Context/View引用
// ✅ 推荐:使用Application Context或依赖注入
@HiltViewModel
class MyViewModel @Inject constructor(
@ApplicationContext private val appContext: Context,
private val resourceProvider: ResourceProvider
) : ViewModel()
// ❌ 避免:持有Activity/Fragment引用(内存泄漏)
class MyViewModel(
private val activity: Activity // 危险!
) : ViewModel()
5. 状态不可变性
// ✅ 推荐:使用data class的copy()
_uiState.update { it.copy(isLoading = true) }
// ❌ 避免:直接修改状态
_uiState.value.isLoading = true // 编译错误
6. 生命周期感知
// ✅ 推荐:在View中使用viewLifecycleOwner
viewLifecycleOwner.lifecycleScope.launch {
viewModel.uiState.collect { state ->
// 自动处理生命周期
}
}
// ✅ Compose中使用collectAsStateWithLifecycle
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
// ❌ 避免:使用lifecycleScope(在Fragment中可能泄漏)
lifecycleScope.launch {
viewModel.uiState.collect { } // 可能泄漏
}
7. 协程管理
// ✅ 推荐:使用viewModelScope,自动取消
viewModelScope.launch {
// 协程会在ViewModel清除时自动取消
}
// ✅ 需要手动控制时保存Job引用
private var job: Job? = null
fun start() {
job?.cancel()
job = viewModelScope.launch { /* ... */ }
}
override fun onCleared() {
super.onCleared()
job?.cancel()
}
8. Flow的使用
// ✅ 使用stateIn转换为StateFlow
val devices: StateFlow<List<Device>> = repository.observeDevices()
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = emptyList()
)
// ✅ 使用combine组合多个Flow
val uiState = combine(flow1, flow2, flow3) { a, b, c ->
UiState(a, b, c)
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), UiState())
🔗 相关知识点
- Blog #13-02: MVI架构完全指南
- Blog #13-03: Clean Architecture完全指南
- Blog #13-04: Repository模式完全指南
- Blog #271: StateFlow vs LiveData选择
- Blog #275: Flow的冷流与热流
- Blog #278: Channel vs SharedFlow
📚 实战练习
- 基础练习:实现一个简单的设备列表页面,包含加载、刷新、搜索功能
- 进阶练习:添加设备详情页,实现编辑、删除功能
- 高级练习:实现Event播放器,包含播放控制、进度管理、播放列表
- 综合练习:实现Dashboard仪表盘,组合多个数据源显示统计信息
延伸学习
掌握本文内容,能够显著提升开发能力和实战水平,为构建高质量、高性能的应用打下坚实基础。
文档更新时间:2026-03-21
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)