Jetpack Compose 的架构主要基于 单向数据流 (Unidirectional Data Flow) 和 响应式编程 理念。下面详细解析各种架构模式及其实现。
1. Compose 架构核心原则
1.1 单向数据流 (UDF)
用户事件 → 处理逻辑 → 状态更新 → UI 重组
1.2 状态提升 (State Hoisting)
将状态提升到使用该状态的多个组件的共同祖先中。
2. 常见架构模式
2.1 MVVM + Repository 模式(推荐)
这是 Android 官方推荐的架构模式,特别适合中大型项目。
项目结构
app/
├── data/
│ ├── local/ # 本地数据源 (Room)
│ ├── remote/ # 远程数据源 (Retrofit)
│ └── repository/ # 数据仓库
├── domain/ # 业务逻辑层
│ ├── model/ # 领域模型
│ └── usecase/ # 用例
└── ui/
├── screen/ # 页面组件
├── component/ # 可复用组件
└── viewmodel/ # ViewModel
完整示例:Todo 应用
1. 数据模型
// domain/model/Todo.kt
data class Todo(
val id: Long,
val title: String,
val description: String,
val isCompleted: Boolean,
val createdAt: Long
)
2. 数据层
// data/local/TodoDao.kt
@Dao
interface TodoDao {
@Query("SELECT * FROM todo ORDER BY createdAt DESC")
fun getTodos(): Flow<List<TodoEntity>>
@Insert
suspend fun insertTodo(todo: TodoEntity)
@Update
suspend fun updateTodo(todo: TodoEntity)
@Delete
suspend fun deleteTodo(todo: TodoEntity)
}
// data/repository/TodoRepositoryImpl.kt
class TodoRepositoryImpl(
private val todoDao: TodoDao,
private val dispatcher: CoroutineDispatcher = Dispatchers.IO
) : TodoRepository {
override fun getTodos(): Flow<List<Todo>> = todoDao.getTodos()
.map { entities -> entities.map { it.toTodo() } }
.flowOn(dispatcher)
override suspend fun addTodo(todo: Todo) {
withContext(dispatcher) {
todoDao.insertTodo(todo.toEntity())
}
}
override suspend fun updateTodo(todo: Todo) {
withContext(dispatcher) {
todoDao.updateTodo(todo.toEntity())
}
}
}
3. ViewModel
// ui/viewmodel/TodoViewModel.kt
class TodoViewModel(
private val todoRepository: TodoRepository
) : ViewModel() {
// UI 状态
data class TodoUiState(
val todos: List<Todo> = emptyList(),
val isLoading: Boolean = false,
val errorMessage: String? = null
)
private val _uiState = MutableStateFlow(TodoUiState())
val uiState: StateFlow<TodoUiState> = _uiState.asStateFlow()
private val _navigationEvent = MutableSharedFlow<String>()
val navigationEvent: SharedFlow<String> = _navigationEvent.asStateFlow()
init {
loadTodos()
}
private fun loadTodos() {
viewModelScope.launch {
todoRepository.getTodos()
.onStart { _uiState.update { it.copy(isLoading = true) } }
.catch { error ->
_uiState.update {
it.copy(errorMessage = error.message, isLoading = false)
}
}
.collect { todos ->
_uiState.update {
it.copy(todos = todos, isLoading = false)
}
}
}
}
fun addTodo(title: String, description: String) {
viewModelScope.launch {
try {
val todo = Todo(
id = 0, // ID 由数据库自动生成
title = title,
description = description,
isCompleted = false,
createdAt = System.currentTimeMillis()
)
todoRepository.addTodo(todo)
} catch (e: Exception) {
_uiState.update { it.copy(errorMessage = "添加失败: ${e.message}") }
}
}
}
fun toggleTodoCompletion(todo: Todo) {
viewModelScope.launch {
try {
val updatedTodo = todo.copy(isCompleted = !todo.isCompleted)
todoRepository.updateTodo(updatedTodo)
} catch (e: Exception) {
_uiState.update { it.copy(errorMessage = "更新失败: ${e.message}") }
}
}
}
}
4. UI 层
// ui/screen/TodoScreen.kt
@Composable
fun TodoScreen(
viewModel: TodoViewModel = hiltViewModel()
) {
val uiState by viewModel.uiState.collectAsState()
// 处理副作用(如一次性的导航事件)
LaunchedEffect(Unit) {
viewModel.navigationEvent.collect { route ->
// 处理导航
}
}
TodoScreenContent(
uiState = uiState,
onAddTodo = { title, desc -> viewModel.addTodo(title, desc) },
onToggleTodo = { todo -> viewModel.toggleTodoCompletion(todo) }
)
}
@Composable
fun TodoScreenContent(
uiState: TodoViewModel.TodoUiState,
onAddTodo: (String, String) -> Unit,
onToggleTodo: (Todo) -> Unit
) {
var showAddDialog by remember { mutableStateOf(false) }
Scaffold(
topBar = {
TopAppBar(title = { Text("待办事项") })
},
floatingActionButton = {
FloatingActionButton(onClick = { showAddDialog = true }) {
Icon(Icons.Default.Add, contentDescription = "添加")
}
}
) { padding ->
when {
uiState.isLoading -> {
CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
}
uiState.errorMessage != null -> {
ErrorMessage(
message = uiState.errorMessage,
modifier = Modifier.padding(padding)
)
}
else -> {
TodoList(
todos = uiState.todos,
onToggleTodo = onToggleTodo,
modifier = Modifier.padding(padding)
)
}
}
if (showAddDialog) {
AddTodoDialog(
onConfirm = { title, desc ->
onAddTodo(title, desc)
showAddDialog = false
},
onDismiss = { showAddDialog = false }
)
}
}
}
@Composable
fun TodoList(
todos: List<Todo>,
onToggleTodo: (Todo) -> Unit,
modifier: Modifier = Modifier
) {
LazyColumn(modifier = modifier) {
items(todos, key = { it.id }) { todo ->
TodoItem(
todo = todo,
onCheckedChange = { onToggleTodo(todo) }
)
}
}
}
@Composable
fun TodoItem(
todo: Todo,
onCheckedChange: (Boolean) -> Unit
) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
) {
Row(
modifier = Modifier.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Checkbox(
checked = todo.isCompleted,
onCheckedChange = onCheckedChange
)
Spacer(modifier = Modifier.width(16.dp))
Column(modifier = Modifier.weight(1f)) {
Text(
text = todo.title,
style = MaterialTheme.typography.h6,
textDecoration = if (todo.isCompleted) {
TextDecoration.LineThrough
} else {
TextDecoration.None
}
)
Text(
text = todo.description,
style = MaterialTheme.typography.body2,
maxLines = 2
)
}
}
}
}
2.2 MVI 模式 (Model-View-Intent)
MVI 强调单向数据流和不可变性,适合复杂交互场景。
实现示例
// 状态
data class SearchState(
val query: String = "",
val results: List<SearchResult> = emptyList(),
val isLoading: Boolean = false,
val error: String? = null
)
// 意图(用户动作)
sealed class SearchIntent {
data class QueryChanged(val query: String) : SearchIntent()
object Search : SearchIntent()
object ClearError : SearchIntent()
}
// ViewModel
class SearchViewModel : ViewModel() {
private val _state = MutableStateFlow(SearchState())
val state: StateFlow<SearchState> = _state.asStateFlow()
fun processIntent(intent: SearchIntent) {
when (intent) {
is SearchIntent.QueryChanged -> {
_state.update { it.copy(query = intent.query) }
}
SearchIntent.Search -> {
search()
}
SearchIntent.ClearError -> {
_state.update { it.copy(error = null) }
}
}
}
private fun search() {
viewModelScope.launch {
_state.update { it.copy(isLoading = true, error = null) }
try {
val results = searchRepository.search(_state.value.query)
_state.update { it.copy(results = results, isLoading = false) }
} catch (e: Exception) {
_state.update {
it.copy(error = e.message, isLoading = false)
}
}
}
}
}
// UI
@Composable
fun SearchScreen(viewModel: SearchViewModel = viewModel()) {
val state by viewModel.state.collectAsState()
SearchScreenContent(
state = state,
onIntent = viewModel::processIntent
)
}
@Composable
fun SearchScreenContent(
state: SearchState,
onIntent: (SearchIntent) -> Unit
) {
Column {
SearchBar(
query = state.query,
onQueryChange = { onIntent(SearchIntent.QueryChanged(it)) },
onSearch = { onIntent(SearchIntent.Search) }
)
when {
state.isLoading -> LoadingIndicator()
state.error != null -> ErrorMessage(
message = state.error,
onDismiss = { onIntent(SearchIntent.ClearError) }
)
else -> SearchResults(results = state.results)
}
}
}
2.3 分层架构 + 依赖注入
使用 Hilt 进行依赖注入
// DependencyModule.kt
@Module
@InstallIn(SingletonComponent::class)
object AppModule {
@Provides
@Singleton
fun provideTodoDatabase(@ApplicationContext context: Context): TodoDatabase {
return Room.databaseBuilder(
context,
TodoDatabase::class.java,
"todo.db"
).build()
}
@Provides
fun provideTodoRepository(database: TodoDatabase): TodoRepository {
return TodoRepositoryImpl(database.todoDao())
}
}
@Module
@InstallIn(ViewModelComponent::class)
object ViewModelModule {
@Provides
@HiltViewModel
fun provideTodoViewModel(repository: TodoRepository): TodoViewModel {
return TodoViewModel(repository)
}
}
3. 状态管理最佳实践
3.1 状态分类
class ProductDetailViewModel : ViewModel() {
// UI 状态(必须提升)
data class UiState(
val product: Product? = null,
val isLoading: Boolean = false,
val error: String? = null
)
// 界面状态(可本地管理)
var isExpanded by mutableStateOf(false)
private set
// 事件状态(一次性)
private val _navigationEvent = Channel<String>()
val navigationEvent = _navigationEvent.receiveAsFlow()
fun expandCard() {
isExpanded = true
}
}
3.2 状态测试
@Test
fun `when loading products, should show loading state`() = runTest {
val viewModel = ProductListViewModel(fakeRepository)
viewModel.loadProducts()
assertEquals(true, viewModel.uiState.value.isLoading)
}
@Test
fun `when products loaded successfully, should update state`() = runTest {
val viewModel = ProductListViewModel(fakeRepository)
viewModel.loadProducts()
testScheduler.advanceUntilIdle()
assertEquals(false, viewModel.uiState.value.isLoading)
assertEquals(3, viewModel.uiState.value.products.size)
}
4. 导航架构
4.1 类型安全的导航
// 定义路由
sealed class Screen(val route: String) {
object Home : Screen("home")
object Detail : Screen("detail/{productId}") {
fun createRoute(productId: Long) = "detail/$productId"
}
}
// 导航图
@Composable
fun AppNavigation() {
val navController = rememberNavController()
NavHost(navController, startDestination = Screen.Home.route) {
composable(Screen.Home.route) {
HomeScreen(onProductClick = { productId ->
navController.navigate(Screen.Detail.createRoute(productId))
})
}
composable(Screen.Detail.route) { backStackEntry ->
val productId = backStackEntry.arguments?.getString("productId")?.toLongOrNull()
DetailScreen(productId = productId)
}
}
}
5. 性能优化架构
5.1 记住计算昂贵的操作
@Composable
fun ProductList(products: List<Product>) {
val expensiveData = remember(products) {
products.map { it.toUiModel() } // 昂贵转换
}
LazyColumn {
items(expensiveData) { product ->
ProductItem(product)
}
}
}
5.2 使用 derivedStateOf 优化重组
@Composable
fun SearchableList(items: List<String>) {
var searchQuery by remember { mutableStateOf("") }
val filteredItems = remember(items, searchQuery) {
derivedStateOf {
items.filter { it.contains(searchQuery, ignoreCase = true) }
}
}
Column {
SearchBar(query = searchQuery, onQueryChange = { searchQuery = it })
ListContent(items = filteredItems.value)
}
}
总结
架构选择指南
| 项目规模 | 推荐架构 | 特点 |
|---|---|---|
| 小型项目 | 简单 MVVM | 快速开发,结构简单 |
| 中型项目 | MVVM + Repository | 良好的可测试性和维护性 |
| 大型项目 | 分层架构 + MVI | 高度可扩展,团队协作友好 |
核心原则
- 单向数据流:确保数据流向的可预测性
- 状态提升:将状态提升到合适的层级
- 关注点分离:UI、业务逻辑、数据持久化分离
- 可测试性:每个层都可以独立测试
- 可组合性:组件尽可能小而专注
这种架构能够确保应用的可维护性、可测试性和可扩展性,是 Compose 应用开发的最佳实践。