Jetpack Compose 从入门到精通(六):架构与工程化

121 阅读3分钟

深入理解 Compose 在大型项目中的架构设计,掌握 Navigation、Hilt、性能优化等企业级开发技能。


前言

在前五篇中,我们掌握了 Compose 的核心开发技能。但在实际项目中,我们还需要考虑:

  • 如何组织大型项目的代码结构?
  • 如何实现页面导航?
  • 如何管理依赖注入?
  • 如何优化应用性能?

本篇文章将深入讲解 Compose 在企业级项目中的架构设计与工程化实践。


一、项目架构设计

1.1 推荐的项目结构

app/
├── src/main/java/com/example/app/
│   ├── App.kt                    # Application 类
│   ├── di/                       # 依赖注入模块
│   │   ├── AppModule.kt
│   │   └── NetworkModule.kt
│   ├── data/                     # 数据层
│   │   ├── local/                # 本地数据源
│   │   │   ├── dao/
│   │   │   ├── entity/
│   │   │   └── AppDatabase.kt
│   │   ├── remote/               # 远程数据源
│   │   │   ├── api/
│   │   │   └── dto/
│   │   ├── repository/           # 仓库层
│   │   └── model/                # 数据模型
│   ├── domain/                   # 领域层(可选)
│   │   ├── usecase/
│   │   └── model/
│   ├── ui/                       # UI 层
│   │   ├── theme/                # 主题配置
│   │   ├── component/            # 通用组件
│   │   ├── screen/               # 页面
│   │   │   ├── home/
│   │   │   │   ├── HomeScreen.kt
│   │   │   │   ├── HomeViewModel.kt
│   │   │   │   └── HomeUiState.kt
│   │   │   └── profile/
│   │   └── navigation/           # 导航配置
│   └── util/                     # 工具类
└── src/main/res/                 # 资源文件

1.2 分层架构原则

┌─────────────────────────────────────────────────────────────────┐
│                      分层架构图                                 │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  ┌─────────────────────────────────────────────────────────┐   │
│  │  UI Layer (Presentation)                                │   │
│  │  - Composable 函数                                       │   │
│  │  - ViewModel                                             │   │
│  │  - UI State                                              │   │
│  │  职责:展示数据、处理用户交互                             │   │
│  └─────────────────────────────────────────────────────────┘   │
│                              ↓                                  │
│  ┌─────────────────────────────────────────────────────────┐   │
│  │  Domain Layer (Optional)                                │   │
│  │  - UseCase                                               │   │
│  │  - Domain Model                                          │   │
│  │  职责:业务逻辑、数据转换                                 │   │
│  └─────────────────────────────────────────────────────────┘   │
│                              ↓                                  │
│  ┌─────────────────────────────────────────────────────────┐   │
│  │  Data Layer                                             │   │
│  │  - Repository                                            │   │
│  │  - DataSource (Local/Remote)                             │   │
│  │  职责:数据获取、缓存策略                                 │   │
│  └─────────────────────────────────────────────────────────┘   │
│                                                                 │
│  依赖方向:UI → Domain → Data                                  │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

二、Navigation 组件

2.1 为什么需要 Navigation?

传统 Fragment 导航的问题:

  • 事务管理复杂
  • 返回栈难以控制
  • 深度链接实现困难
  • 参数传递类型不安全

Compose Navigation 的优势

  • 声明式导航
  • 类型安全的参数传递
  • 自动处理返回栈
  • 支持深层链接

2.2 Navigation 基础

2.2.1 设置导航

// 1. 添加依赖
// implementation("androidx.navigation:navigation-compose:2.7.0")

// 2. 定义路由
sealed class Screen(val route: String) {
    data object Home : Screen("home")
    data object Profile : Screen("profile/{userId}") {
        fun createRoute(userId: String) = "profile/$userId"
    }
    data object Settings : Screen("settings")
}

// 3. 创建 NavHost
@Composable
fun AppNavigation(
    navController: NavHostController = rememberNavController()
) {
    NavHost(
        navController = navController,
        startDestination = Screen.Home.route
    ) {
        composable(Screen.Home.route) {
            HomeScreen(
                onNavigateToProfile = { userId ->
                    navController.navigate(Screen.Profile.createRoute(userId))
                }
            )
        }
        
        composable(
            route = Screen.Profile.route,
            arguments = listOf(
                navArgument("userId") { type = NavType.StringType }
            )
        ) { backStackEntry ->
            val userId = backStackEntry.arguments?.getString("userId") ?: ""
            ProfileScreen(
                userId = userId,
                onBack = { navController.popBackStack() }
            )
        }
        
        composable(Screen.Settings.route) {
            SettingsScreen()
        }
    }
}

2.2.2 导航操作

// 导航到目标页面
navController.navigate("profile/123")

// 返回上一页
navController.popBackStack()

// 返回指定页面
navController.popBackStack("home", inclusive = false)

// 清空返回栈并导航
navController.navigate("home") {
    popUpTo("home") { inclusive = true }
}

// 启动单例页面(防止重复)
navController.navigate("detail") {
    launchSingleTop = true
}

2.3 类型安全的导航(Navigation 2.8.0+)

// 1. 定义序列化路由类
@Serializable
object Home

@Serializable
data class Profile(val userId: String)

@Serializable
object Settings

// 2. 创建 NavHost
@Composable
fun AppNavigation(
    navController: NavHostController = rememberNavController()
) {
    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)
        }
        
        composable<Settings> {
            SettingsScreen()
        }
    }
}

2.4 底部导航栏

@Composable
fun BottomNavApp() {
    val navController = rememberNavController()
    
    val items = listOf(
        BottomNavItem.Home,
        BottomNavItem.Search,
        BottomNavItem.Profile
    )
    
    Scaffold(
        bottomBar = {
            NavigationBar {
                val navBackStackEntry by navController.currentBackStackEntryAsState()
                val currentDestination = navBackStackEntry?.destination
                
                items.forEach { item ->
                    NavigationBarItem(
                        icon = { Icon(item.icon, contentDescription = item.label) },
                        label = { Text(item.label) },
                        selected = currentDestination?.hierarchy?.any {
                            it.route == item.route
                        } == true,
                        onClick = {
                            navController.navigate(item.route) {
                                popUpTo(navController.graph.findStartDestination().id) {
                                    saveState = true
                                }
                                launchSingleTop = true
                                restoreState = true
                            }
                        }
                    )
                }
            }
        }
    ) { innerPadding ->
        NavHost(
            navController = navController,
            startDestination = BottomNavItem.Home.route,
            modifier = Modifier.padding(innerPadding)
        ) {
            composable(BottomNavItem.Home.route) { HomeScreen() }
            composable(BottomNavItem.Search.route) { SearchScreen() }
            composable(BottomNavItem.Profile.route) { ProfileScreen() }
        }
    }
}

sealed class BottomNavItem(
    val route: String,
    val icon: ImageVector,
    val label: String
) {
    data object Home : BottomNavItem("home", Icons.Default.Home, "首页")
    data object Search : BottomNavItem("search", Icons.Default.Search, "搜索")
    data object Profile : BottomNavItem("profile", Icons.Default.Person, "我的")
}

2.5 深层链接

// AndroidManifest.xml 中配置
<activity
    android:name=".MainActivity"
    android:exported="true">
    <intent-filter>
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />
        <data android:scheme="https" android:host="example.com" />
    </intent-filter>
</activity>

// Navigation 中配置深层链接
composable(
    route = "article/{articleId}",
    deepLinks = listOf(
        navDeepLink {
            uriPattern = "https://example.com/article/{articleId}"
        }
    ),
    arguments = listOf(
        navArgument("articleId") { type = NavType.StringType }
    )
) { backStackEntry ->
    val articleId = backStackEntry.arguments?.getString("articleId")
    ArticleScreen(articleId = articleId)
}

三、Hilt 依赖注入

3.1 为什么需要依赖注入?

传统方式的痛点

// ❌ 紧耦合,难以测试
class UserRepository {
    private val api = RetrofitClient.api  // 硬编码依赖
    private val dao = AppDatabase.dao     // 硬编码依赖
}

依赖注入的优势

  • 解耦组件
  • 便于单元测试
  • 生命周期自动管理
  • 单例控制

3.2 Hilt 基础配置

// 1. 添加依赖
/*
plugins {
    id("kotlin-kapt")
    id("dagger.hilt.android.plugin")
}

dependencies {
    implementation("com.google.dagger:hilt-android:2.48")
    kapt("com.google.dagger:hilt-compiler:2.48")
    implementation("androidx.hilt:hilt-navigation-compose:1.1.0")
}
*/

// 2. Application 类
@HiltAndroidApp
class MyApplication : Application()

// 3. Activity
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MyApp()
        }
    }
}

3.3 定义和注入依赖

// 1. 创建 Module
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
    
    @Provides
    @Singleton
    fun provideRetrofit(): Retrofit {
        return Retrofit.Builder()
            .baseUrl("https://api.example.com/")
            .addConverterFactory(GsonConverterFactory.create())
            .build()
    }
    
    @Provides
    @Singleton
    fun provideApiService(retrofit: Retrofit): ApiService {
        return retrofit.create(ApiService::class.java)
    }
}

@Module
@InstallIn(SingletonComponent::class)
object DatabaseModule {
    
    @Provides
    @Singleton
    fun provideDatabase(@ApplicationContext context: Context): AppDatabase {
        return Room.databaseBuilder(
            context,
            AppDatabase::class.java,
            "app_database"
        ).build()
    }
    
    @Provides
    fun provideUserDao(database: AppDatabase): UserDao {
        return database.userDao()
    }
}

// 2. Repository 注入依赖
class UserRepository @Inject constructor(
    private val apiService: ApiService,
    private val userDao: UserDao
) {
    suspend fun getUsers(): List<User> {
        return try {
            val users = apiService.getUsers()
            userDao.insertAll(users)
            users
        } catch (e: Exception) {
            userDao.getAll()
        }
    }
}

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

// 4. Composable 获取 ViewModel
@Composable
fun UserScreen(
    viewModel: UserViewModel = hiltViewModel()
) {
    // ...
}

3.4 限定符(Qualifiers)

// 定义限定符
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class AuthInterceptorOkHttpClient

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class OtherInterceptorOkHttpClient

// 提供不同实例
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
    
    @AuthInterceptorOkHttpClient
    @Provides
    fun provideAuthOkHttpClient(authInterceptor: AuthInterceptor): OkHttpClient {
        return OkHttpClient.Builder()
            .addInterceptor(authInterceptor)
            .build()
    }
    
    @OtherInterceptorOkHttpClient
    @Provides
    fun provideOtherOkHttpClient(otherInterceptor: OtherInterceptor): OkHttpClient {
        return OkHttpClient.Builder()
            .addInterceptor(otherInterceptor)
            .build()
    }
}

// 注入指定实例
class ApiService @Inject constructor(
    @AuthInterceptorOkHttpClient private val client: OkHttpClient
)

四、性能优化

4.1 重组优化

4.1.1 避免不必要的重组

// ❌ 错误:整个列表都会重组
@Composable
fun UserList(users: List<User>) {
    Column {
        users.forEach { user ->
            UserItem(user = user)  // 所有 item 都会重组
        }
    }
}

// ✅ 正确:使用 LazyColumn,只重组可见项
@Composable
fun UserList(users: List<User>) {
    LazyColumn {
        items(users, key = { it.id }) { user ->
            UserItem(user = user)
        }
    }
}

4.1.2 使用 @Stable 和 @Immutable

// ❌ 不稳定类,任何变化都会触发重组
class User {
    var name: String = ""
}

// ✅ 稳定类
@Stable
class User {
    var name: String = ""
}

// ✅ 不可变类,实例替换时才重组
@Immutable
data class User(
    val name: String
)

4.1.3 使用 remember 缓存计算

@Composable
fun ExpensiveComponent(data: List<Int>) {
    // ❌ 每次重组都计算
    val sum = data.sum()
    
    // ✅ 只在 data 变化时计算
    val sum = remember(data) { data.sum() }
    
    Text("Sum: $sum")
}

4.2 布局检查器使用

┌─────────────────────────────────────────────────────────────────┐
│                  使用布局检查器优化性能                         │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  1. 打开 Layout Inspector                                       │
│     Tools → Layout Inspector                                    │
│                                                                 │
│  2. 查看重组次数                                                  │
│     - 右侧面板显示每个 Composable 的重组次数                    │
│     - 高重组次数的组件需要优化                                  │
│                                                                 │
│  3. 常见优化方案                                                  │
│     - 使用 key 优化列表                                          │
│     - 提取不依赖状态的部分                                       │
│     - 使用 derivedStateOf 减少重组                              │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

4.3 图片加载优化

// 使用 Coil 加载图片
AsyncImage(
    model = ImageRequest.Builder(LocalContext.current)
        .data(imageUrl)
        .crossfade(true)
        .placeholder(R.drawable.placeholder)
        .error(R.drawable.error)
        .memoryCachePolicy(CachePolicy.ENABLED)
        .diskCachePolicy(CachePolicy.ENABLED)
        .build(),
    contentDescription = null,
    contentScale = ContentScale.Crop,
    modifier = Modifier.fillMaxWidth()
)

4.4 列表性能优化

@Composable
fun OptimizedList(items: List<Item>) {
    LazyColumn {
        items(
            items = items,
            key = { it.id },                    // ✅ 使用 key
            contentType = { it.type }           // ✅ 使用 contentType
        ) { item ->
            when (item.type) {
                Type.HEADER -> HeaderItem(item)
                Type.CONTENT -> ContentItem(item)
            }
        }
    }
}

@Composable
fun ContentItem(item: Item) {
    // ✅ 缓存计算结果
    val formattedDate = remember(item.date) {
        formatDate(item.date)
    }
    
    // ✅ 避免创建新的 lambda
    val onClick = remember(item.id) {
        { viewModel.onItemClick(item.id) }
    }
    
    Card(onClick = onClick) {
        Text(formattedDate)
    }
}

五、测试

5.1 Compose UI 测试

// 1. 添加依赖
// androidTestImplementation("androidx.compose.ui:ui-test-junit4")

// 2. 编写测试
@RunWith(AndroidJUnit4::class)
class CounterTest {
    @get:Rule
    val composeTestRule = createComposeRule()
    
    @Test
    fun counterIncrements_whenClicked() {
        composeTestRule.setContent {
            Counter()
        }
        
        // 查找并点击按钮
        composeTestRule.onNodeWithText("Count: 0").assertExists()
        composeTestRule.onNodeWithText("Increment").performClick()
        
        // 验证结果
        composeTestRule.onNodeWithText("Count: 1").assertExists()
    }
}

5.2 ViewModel 测试

@ExperimentalCoroutinesApi
class UserViewModelTest {
    @get:Rule
    val instantExecutorRule = InstantTaskExecutorRule()
    
    @get:Rule
    val mainDispatcherRule = MainDispatcherRule()
    
    private lateinit var viewModel: UserViewModel
    private lateinit var repository: FakeUserRepository
    
    @Before
    fun setup() {
        repository = FakeUserRepository()
        viewModel = UserViewModel(repository)
    }
    
    @Test
    fun `load users updates ui state`() = runTest {
        // Given
        val users = listOf(User("1", "Alice"), User("2", "Bob"))
        repository.setUsers(users)
        
        // When
        viewModel.loadUsers()
        
        // Then
        assertEquals(UiState.Success(users), viewModel.uiState.value)
    }
}

六、本篇小结

今天我们深入探讨了 Compose 的架构与工程化:

项目架构

  • 理解了分层架构的设计原则
  • 掌握了推荐的项目结构

Navigation

  • 掌握了 Navigation 的基础用法
  • 学会了类型安全的导航
  • 理解了深层链接的实现

Hilt

  • 理解了依赖注入的优势
  • 掌握了 Hilt 的配置和使用
  • 学会了限定符的使用

性能优化

  • 掌握了重组优化的方法
  • 学会了使用布局检查器
  • 理解了列表性能优化

测试

  • 掌握了 Compose UI 测试
  • 学会了 ViewModel 测试

下篇预告

第七篇:高级特性与实战 将深入讲解:

  • 自定义绘制与 Canvas
  • 图形变换与 Shader 效果
  • 窗口 Insets 与边缘到边缘适配
  • 完整实战项目

敬请期待!


📌 系列文章导航

  • 第一篇:初识 Compose ✅
  • 第二篇:核心基石 ✅
  • 第三篇:状态管理 ✅
  • 第四篇:Material 组件与主题 ✅
  • 第五篇:动画与交互 ✅
  • 第六篇:架构与工程化(当前)✅
  • 第七篇:高级特性与实战

如果这篇文章对你有帮助,欢迎 点赞收藏关注!有任何问题可以在评论区留言。