AB

Kotlin Compose 的经典代码结构

2025-11-03



简述一下 Compose 的状态管理

tldr

经典的 Kotlin Compose 代码结构自顶向下主要可以分为三层,每层的主要构成如下:

  • UI 层:Compose、ViewModel(UiState、UiEvent)
  • 逻辑层/业务层:Service/UseCase、Repository、Model
  • 数据层:RepositoryImpl、DAO、Entity

一、常见目录结构和说明

---- ui/      视图层
  ---- screens/    页面级 Compose
  ---- navigation/ 页面路由
  ---- components/ 组件级 Compose
  ---- viewmodel/  用于管理页面状态和事件
  ---- state/      声明状态和事件枚举
---- domain/  逻辑层 or 业务层
  ---- services/   以某个 Model 为核心的业务逻辑
  ---- usecase/    以某个场景为核心的业务逻辑
  ---- repository/ 声明 Model 对应的数据层接口
  ---- model/      业务 Model、UI Model 定义和相互映射
---- data/    数据层
  ---- database/   数据库层
    ---- dao/          数据库查询的具体实现
    ---- entities/     数据库表、枚举、扩展类型的定义
    ---- converters/   一些数据类型转换操作的实现
  ---- repository/ 调用 DAO 获取 Model 对应的数据层接口
  ---- mapper/     Entity 和 Model 的相互映射

二、数据流拆解

从数据层到 UI 层,数据流动时会经过很多的节点做转换和映射。在整个流程中,我们使用最多的数据类型是 Model 和 UiModel 着两种。在数据层中,虽然我们定义了 Entity 类型,但它主要是作为数据库表的定义存在,并不会在数据层外直接使用 Entity 类型的数据。在 Dao 中查询出的 Entity 会由 RepositoryImpl 转换为 Model 数据,提供给上层,也就是业务层使用。

DB     --(query by)--> DAO --(return)->
Entity --(called by)-> RepositoryImpl --(return)->
Model  --(called by)-> Service/UseCase --(called by)-> ui

UiModel 是提供给 UI 层使用的数据类型,和 Model 是并存的关系,支持相互转换。一般来说,就是在 Model 的基础上增加了一些扩展字段:

KOTLIN
/**  
 * 待办事项领域模型  
 */  
data class TodoItem(  
    val id: String,  
    val planId: String,  
    val title: String,  
    val content: String,  
    val isCompleted: Boolean = false,  
    val triggerTime: LocalDateTime,  
    val completedAt: LocalDateTime? = null,  
    val createdAt: LocalDateTime  
)

data class TodoItemUiModel(  
    val id: String,  
    val planId: String,  
    val title: String,  
    val content: String,  
    val isCompleted: Boolean,  
    val triggerTime: LocalDateTime,  
    val completedAt: LocalDateTime?,  
    val createdAt: LocalDateTime,  
    val formattedTime: String = "",  
    val formattedDate: String = "",  
    val timeAgo: String = "", // 相对时间显示,如"2小时前"  
    val isOverdue: Boolean = false  
)

三、UI 层的状态管理

Compose 的状态管理和 Flutter 类似,Compose 本身默认是无状态的组件,输入静态数据并渲染 UI。有两种方法可以让 Compose 获得响应式状态,一种是直接在 Compose 内部创建 remember 变量,还有一种就是使用 ViewModel。

3.1 remember

KOTLIN
// remember 搭配 MutableState 或者 State 可以创建响应式的局部变量
// rememberSaveable 的区别是会自动保存,当 Activity 或者进程重建时会恢复状态
// @Parcelize 让 data class 能够被序列化


@Parcelize
data class City(val name: String, val country: String) : Parcelable

@Composable
fun CityScreen() {
    var selectedCity = rememberSaveable {
        mutableStateOf(City("Madrid", "Spain"))
    }
}

3.2 ViewModel

ViewModel 一般会和 Hilt 依赖注入一起使用,这样的好处是状态和 UI 的解耦更加彻底。一般 ViewModel 会包含 uiStateuiEvent 两部分,概念上像是 Vue 的 Data 和 Event。

在 Compose 当中,可以通过 LaunchedEffect 来注册 uiEvent 的监听器,使用 ViewModel.onEvent 来触发事件。在 ViewModel 当中,需要 onEvent 用于定义事件的监听器,也就是触发事件之后应该如何调整状态、调用 Service,并通过 uiEvent 来与 Componse 通信。

KOTLIN
@Composable  
fun HomeScreen(  
    viewModel: HomeViewModel = hiltViewModel()  
) {
	val uiState by viewModel.uiState.collectAsState()
	
	LaunchedEffect(viewModel.uiEvent) {  
	    viewModel.uiEvent.collect { event ->  
	        when (event) {  
	            is HomeUiEvent.ShowSnackbar -> {  
	                snackbarHostState.showSnackbar(event.message)  
	            }            
	            is HomeUiEvent.ShowError -> {  
	                snackbarHostState.showSnackbar("错误: ${event.error}")  
	            }            
	            else -> {}  
	        }    
	    }  
	}
}
KOTLIN
@HiltViewModel  
class HomeViewModel @Inject constructor() : ViewModel() {  
    private val _uiState = MutableStateFlow(HomeUiState())  
    val uiState: StateFlow<HomeUiState> = _uiState.asStateFlow()  
  
    private val _uiEvent = MutableSharedFlow<HomeUiEvent>()  
    val uiEvent: SharedFlow<HomeUiEvent> = _uiEvent.asSharedFlow()
    
    fun onEvent(event: HomeUiEvent) {  
	    when (event) {  
	        is HomeUiEvent.DateSelected -> {  
	            selectDate(event.date)  
	        }        
	        is HomeUiEvent.RefreshData -> {  
	            loadData()  
	        }        
	        is HomeUiEvent.WeekChanged -> {  
	            changeWeek(event.weekOffset)  
	        }
    }
}

所以 ViewModel 也可以称为多个不直接相关的 Compose 之间通信的桥梁,只要两个 Compose 都依赖同一个 ViewModel 即可。