Compose代码架构

89 阅读4分钟

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高度可扩展,团队协作友好

核心原则

  1. 单向数据流:确保数据流向的可预测性
  2. 状态提升:将状态提升到合适的层级
  3. 关注点分离:UI、业务逻辑、数据持久化分离
  4. 可测试性:每个层都可以独立测试
  5. 可组合性:组件尽可能小而专注

这种架构能够确保应用的可维护性可测试性可扩展性,是 Compose 应用开发的最佳实践。