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)        │
│  - 数据获取(网络/数据库)              │
│  - 业务规则                              │
│  - 数据转换                              │
└─────────────────────────────────────────┘

核心优势

  1. 数据绑定:通过LiveData/StateFlow自动同步数据和UI
  2. 职责分离:UI逻辑与业务逻辑完全解耦
  3. 易于测试:ViewModel可独立进行单元测试
  4. 生命周期感知:自动处理配置更改,避免内存泄漏
  5. 可维护性:代码结构清晰,易于扩展和维护

📝 第一部分: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

📚 实战练习

  1. 基础练习:实现一个简单的设备列表页面,包含加载、刷新、搜索功能
  2. 进阶练习:添加设备详情页,实现编辑、删除功能
  3. 高级练习:实现Event播放器,包含播放控制、进度管理、播放列表
  4. 综合练习:实现Dashboard仪表盘,组合多个数据源显示统计信息

延伸学习

掌握本文内容,能够显著提升开发能力和实战水平,为构建高质量、高性能的应用打下坚实基础。


文档更新时间:2026-03-21

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐