Compose 代码组织与架构实战技巧

2024-12-23 · 40-45 min · 架构设计

随着 Compose 项目规模的增长,如何组织代码、设计合理的架构以及确保代码的可维护性成为了核心问题。本文总结了在实际大型项目中使用 Compose 的架构设计和代码组织模式,涵盖从组件拆分到整体架构设计的全方位技巧。

📚 官方参考

目录

I. 模块化与包结构

合理的模块化和包结构是大型项目的基础。

1.1 按功能模块化 (Feature-based Modularization)

推荐按功能划分模块,而非按组件类型划分。


        // ✅ 推荐的包结构
com.example.myapp
├── common              // 公共模块
│   ├── ui              // 通用组件
│   ├── theme           // 主题
│   └── util            // 工具类
├── data                // 数据层
│   ├── repository      // 仓库
│   ├── source          // 数据源
│   └── model           // 数据模型
├── domain              // 领域层
│   ├── usecase         // 用例
│   └── model           // 领域模型
└── features            // 功能模块
    ├── home            // 首页功能
    │   ├── ui          // UI 组件
    │   ├── viewmodel   // ViewModel
    │   └── model       // UI 模型
    └── profile         // 个人中心功能
        

为什么重要?

1.2 内部/外部 API 分离

在模块内区分公开的 API 和内部实现的私有类。


        // features/home/ui/HomeContent.kt
@Composable
fun HomeContent(...) { ... }  // 公开组件

// features/home/ui/internal/Header.kt
@Composable
internal fun HomeHeader(...) { ... }  // 内部组件,不暴露给外部
        

II. UI 组件化与复用

Compose 的声明式特性让组件化变得非常容易,但过度拆分或不合理的拆分也会带来问题。

2.1 原子化组件设计

参考原子设计理论 (Atomic Design) 拆分组件。


        // atoms/MyButton.kt
@Composable
fun MyButton(onClick: () -> Unit, text: String) { ... }

// molecules/SearchBar.kt
@Composable
fun SearchBar(query: String, onQueryChange: (String) -> Unit) {
    Row {
        MyIcon(Icons.Default.Search)
        MyTextField(query, onQueryChange)
    }
}
        

2.2 提取通用组件库

将跨功能的 UI 组件提取到独立的 common:ui 模块中。


        // common-ui/src/main/java/com/example/common/ui/LoadingView.kt
@Composable
fun LoadingView(modifier: Modifier = Modifier) {
    Box(modifier = modifier.fillMaxSize()) {
        CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
    }
}
        

2.3 使用 CompositionLocal 传递全局配置


        // common-ui/src/main/java/com/example/common/ui/LocalAppConfig.kt
val LocalAppConfig = compositionLocalOf { AppConfig() }

@Composable
fun MyAppTheme(content: @Composable () -> Unit) {
    val config = remember { AppConfig() }
    CompositionLocalProvider(LocalAppConfig provides config) {
        content()
    }
}
        

III. ViewModel 与状态管理架构

ViewModel 是连接 UI 和业务逻辑的核心。

3.1 统一 UI State 模型

使用 Sealed Class 或 Data Class 定义完整的 UI 状态。


        // ✅ 推荐的 UI State 定义
sealed class HomeUiState {
    object Loading : HomeUiState()
    data class Success(val products: List<Product>, val banner: List<Banner>) : HomeUiState()
    data class Error(val message: String) : HomeUiState()
}

class HomeViewModel : ViewModel() {
    private val _uiState = MutableStateFlow<HomeUiState>(HomeUiState.Loading)
    val uiState: StateFlow<HomeUiState> = _uiState.asStateFlow()
    
    init {
        loadData()
    }
    
    private fun loadData() {
        // 加载数据并更新 _uiState
    }
}
        

3.2 状态提升 (State Hoisting) 模式


        // ✅ 状态提升模式:Stateless Composable
@Composable
fun ProductList(
    products: List<Product>,
    onProductClick: (Product) -> Unit
) {
    LazyColumn {
        items(products) { product ->
            ProductRow(product, onClick = { onProductClick(product) })
        }
    }
}

// Stateful Composable:负责状态管理
@Composable
fun ProductScreen(viewModel: ProductViewModel = hiltViewModel()) {
    val uiState by viewModel.uiState.collectAsState()
    
    ProductList(
        products = (uiState as? ProductUiState.Success)?.products ?: emptyList(),
        onProductClick = { viewModel.onProductSelected(it) }
    )
}
        

IV. 依赖注入 (Hilt) 实战

Hilt 是 Android 官方推荐的 DI 框架,在 Compose 中应用广泛。

4.1 注入 ViewModel


        @HiltViewModel
class UserViewModel @Inject constructor(
    private val userRepository: UserRepository
) : ViewModel() {
    // ...
}

@Composable
fun UserScreen(viewModel: UserViewModel = hiltViewModel()) {
    // 使用注入的 ViewModel
}
        

4.2 注入 Repository 和 UseCase


        @Module
@InstallIn(SingletonComponent::class)
object AppModule {
    @Provides
    @Singleton
    fun provideUserRepository(api: ApiService): UserRepository {
        return UserRepositoryImpl(api)
    }
}
        

V. 导航与路由架构

Compose Navigation 提供了类型安全的导航方案。

5.1 类型安全路由 (Type-safe Navigation)

在 Compose 2.8.0+ 中推荐使用类型安全的路由。


        // 定义路由模型
@Serializable
object Home

@Serializable
data class Profile(val userId: String)

// 配置导航
@Composable
fun AppNavigation(navController: NavHostController) {
    NavHost(navController = navController, startDestination = Home) {
        composable<Home> {
            HomeScreen(onNavigateToProfile = { userId ->
                navController.navigate(Profile(userId))
            })
        }
        composable<Profile> { backStackEntry ->
            val profile: Profile = backStackEntry.toRoute()
            ProfileScreen(userId = profile.userId)
        }
    }
}
        

5.2 抽离导航逻辑

将导航路径定义在独立的模块或文件中。


        // navigation/Screen.kt
sealed class Screen(val route: String) {
    object Home : Screen("home")
    object Profile : Screen("profile/{userId}") {
        fun createRoute(userId: String) = "profile/$userId"
    }
}
        

VI. 业务逻辑与 UI 分离

保持 UI 层纯粹,业务逻辑下沉。

6.1 使用 UseCase (Interactor) 模式


        class GetProductListUseCase @Inject constructor(
    private val repository: ProductRepository
) {
    operator fun invoke(): Flow<List<Product>> = repository.getProducts()
}

@HiltViewModel
class ProductViewModel @Inject constructor(
    private val getProductListUseCase: GetProductListUseCase
) : ViewModel() {
    // 调用 UseCase 而非直接调用 Repository
}
        

6.2 区分 UI Model 和 Data Model


        // Data Model (Repository 层)
data class ProductEntity(val id: Int, val name: String, val price: Double)

// UI Model (UI 层)
data class ProductUiModel(val id: Int, val title: String, val formattedPrice: String)

// 转换逻辑
fun ProductEntity.toUiModel() = ProductUiModel(
    id = id,
    title = name,
    formattedPrice = "$${price}"
)
        

VII. 主题与样式管理

集中管理主题和样式。

7.1 自定义 Design System


        // theme/MyTheme.kt
object MyTheme {
    val colors: MyColors
        @Composable
        get() = LocalMyColors.current
        
    val typography: MyTypography
        @Composable
        get() = LocalMyTypography.current
}

@Composable
fun MyTheme(
    colors: MyColors = MyDefaultColors,
    content: @Composable () -> Unit
) {
    CompositionLocalProvider(
        LocalMyColors provides colors,
        // ...
    ) {
        content()
    }
}
        

7.2 样式扩展


        // UI 组件中使用自定义主题
@Composable
fun StyledButton(text: String) {
    Button(
        colors = ButtonDefaults.buttonColors(
            backgroundColor = MyTheme.colors.primary
        )
    ) {
        Text(text, style = MyTheme.typography.button)
    }
}
        

VIII. 测试与可维护性

架构设计的最终目标是可测试和可维护。

8.1 UI 组件的可测试性


        @Test
fun testProductRow() {
    composeTestRule.setContent {
        ProductRow(
            product = ProductUiModel(1, "Test", "$10"),
            onClick = { }
        )
    }
    
    composeTestRule.onNodeWithText("Test").assertIsDisplayed()
}
        

8.2 ViewModel 的单元测试


        @Test
fun testViewModelLoadData() = runTest {
    val viewModel = ProductViewModel(fakeUseCase)
    viewModel.loadData()
    
    val state = viewModel.uiState.value
    assert(state is ProductUiState.Success)
}
        

总结

Compose 代码组织与架构设计的核心原则:

掌握这些技巧,可以构建出结构清晰、易于扩展和维护的大型 Compose 应用。

---

推荐阅读

📚 官方参考
Guide to app architecture - Android Developers
Architecture in Jetpack Compose - Android Developers
Modern Android Development (MAD) skills - Architecture