架构视角:声明式UI的范式转变

Jetpack Compose代表了Android UI开发的范式转变——从命令式的View系统转向声明式编程模型。这种转变不仅是API的变化,更是架构思维的革新:开发者描述UI应该是什么样子,而非如何一步步构建它。

声明式UI核心特征

  • 状态驱动:UI是状态的函数,状态变化自动触发UI更新
  • 可组合性:复杂UI由简单组件组合而成,支持高度复用
  • 单向数据流:数据从父组件流向子组件,事件反向传递
  • 智能重组:仅更新发生变化的部分,性能自动优化

命令式 vs 声明式对比

// 传统命令式UI(XML + findViewById)
class OldActivity : AppCompatActivity() {
    private lateinit var textView: TextView
    private lateinit var button: Button
    private var count = 0
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        
        textView = findViewById(R.id.textView)
        button = findViewById(R.id.button)
        
        button.setOnClickListener {
            count++
            textView.text = "Count: $count" // 手动更新UI
        }
    }
}

// Jetpack Compose声明式UI
@Composable
fun CounterScreen() {
    var count by remember { mutableStateOf(0) } // 状态声明
    
    Column(
        modifier = Modifier.fillMaxSize(),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center
    ) {
        Text(
            text = "Count: $count",
            style = MaterialTheme.typography.headlineMedium
        )
        Button(onClick = { count++ }) { // 状态变化自动触发重组
            Text("Increment")
        }
    }
}

核心概念:状态与重组

状态管理基础

状态是Compose的核心,理解状态的生命周期和作用域是掌握Compose的关键:

// 状态管理详解
@Composable
fun StateManagementDemo() {
    // remember:在重组时保持值
    var normalState by remember { mutableStateOf(0) }
    
    // rememberSaveable:配置变化(如旋转屏幕)时保持值
    var savedState by rememberSaveable { mutableStateOf("") }
    
    // derivedStateOf:派生状态,仅在依赖变化时重新计算
    val isValid by remember {
        derivedStateOf { savedState.length >= 3 }
    }
    
    // 状态提升:将状态移到调用方,实现单向数据流
    StatefulComponent(
        value = savedState,
        onValueChange = { savedState = it },
        isValid = isValid
    )
}

// 无状态组件:接收状态和回调作为参数
@Composable
fun StatelessComponent(
    value: String,
    onValueChange: (String) -> Unit,
    isValid: Boolean,
    modifier: Modifier = Modifier
) {
    Column(modifier = modifier) {
        OutlinedTextField(
            value = value,
            onValueChange = onValueChange,
            label = { Text("Input") },
            isError = !isValid
        )
        if (!isValid) {
            Text(
                text = "至少需要3个字符",
                color = MaterialTheme.colorScheme.error,
                style = MaterialTheme.typography.bodySmall
            )
        }
    }
}

// 有状态组件:封装内部状态
@Composable
fun StatefulComponent(
    value: String,
    onValueChange: (String) -> Unit,
    isValid: Boolean,
    modifier: Modifier = Modifier
) {
    // 内部UI状态
    var isFocused by remember { mutableStateOf(false) }
    
    Column(modifier = modifier) {
        OutlinedTextField(
            value = value,
            onValueChange = onValueChange,
            label = { Text("Input") },
            isError = !isValid && !isFocused,
            modifier = Modifier.onFocusChanged { isFocused = it.isFocused }
        )
    }
}

重组机制深度解析

// 重组优化示例
@Composable
fun RecompositionOptimization() {
    var count by remember { mutableStateOf(0) }
    var name by remember { mutableStateOf("Compose") }
    
    Column {
        // 每次count变化,整个Column都会重组
        Text("Count: $count")
        Button(onClick = { count++ }) {
            Text("Increment")
        }
        
        // 使用key优化列表重组
        LazyColumn {
            items(
                items = itemList,
                key = { it.id } // 指定key帮助Compose识别item
            ) { item ->
                ListItem(item)
            }
        }
        
        // 使用@Stable和@Immutable标记提高稳定性
        StableComponent(data = stableData)
    }
}

// @Immutable:标记类不可变,属性不会变化
@Immutable
data class UserProfile(
    val id: String,
    val name: String,
    val avatar: String
)

// @Stable:标记类稳定,属性变化会通知Compose
@Stable
class UserViewModel {
    var userName by mutableStateOf("")
        private set
    
    fun updateName(newName: String) {
        userName = newName
    }
}

// 副作用处理:LaunchedEffect、DisposableEffect等
@Composable
fun SideEffectsDemo(userId: String) {
    // LaunchedEffect:在协程中执行副作用,key变化时取消并重新启动
    LaunchedEffect(userId) {
        // 加载用户数据
        val user = repository.getUser(userId)
        // 更新UI状态
    }
    
    // DisposableEffect:需要清理的副作用
    DisposableEffect(Unit) {
        val listener = object : SomeListener {
            override fun onEvent() { /* ... */ }
        }
        someManager.addListener(listener)
        
        onDispose {
            // 清理资源
            someManager.removeListener(listener)
        }
    }
    
    // SideEffect:每次重组成功时执行
    SideEffect {
        // 同步外部状态,如Analytics上报
        analytics.trackScreenView("Profile")
    }
    
    // rememberCoroutineScope:获取与组件生命周期绑定的协程作用域
    val scope = rememberCoroutineScope()
    
    Button(onClick = {
        scope.launch {
            // 执行异步操作
        }
    }) {
        Text("Load Data")
    }
    
    // rememberUpdatedState:在副作用中访问最新状态
    val currentOnTimeout by rememberUpdatedState(onTimeout)
    
    LaunchedEffect(Unit) {
        delay(3000)
        currentOnTimeout() // 始终调用最新的回调
    }
}

重组优化最佳实践

  • 使用@Stable和@Immutable:帮助Compose识别哪些对象可以跳过比较
  • 将状态读取推迟到最低层级:避免高层级组件频繁重组
  • 使用Lambda记忆化:remember(key) { { ... } }避免创建新lambda
  • 合理使用key:列表项使用稳定key帮助Compose识别变化
  • 避免在Composable中创建对象:使用remember缓存复杂对象

布局系统:从Modifier到自定义布局

Modifier链式调用

// Modifier详解
@Composable
fun ModifierDemo() {
    Box(
        modifier = Modifier
            // 尺寸
            .fillMaxWidth()
            .height(100.dp)
            .sizeIn(minWidth = 50.dp, maxWidth = 200.dp)
            
            // 边距与内边距
            .padding(16.dp)
            .padding(horizontal = 8.dp, vertical = 4.dp)
            
            // 背景与形状
            .background(Color.Blue, RoundedCornerShape(8.dp))
            .clip(CircleShape)
            
            // 边框
            .border(2.dp, Color.Red, RoundedCornerShape(8.dp))
            
            // 点击与手势
            .clickable { /* 点击处理 */ }
            .pointerInput(Unit) {
                detectTapGestures(
                    onTap = { /* 轻触 */ },
                    onDoubleTap = { /* 双击 */ },
                    onLongPress = { /* 长按 */ }
                )
            }
            
            // 滚动
            .verticalScroll(rememberScrollState())
            .horizontalScroll(rememberScrollState())
            
            // 布局位置
            .offset(x = 10.dp, y = 20.dp)
            .absoluteOffset(x = 10.dp, y = 20.dp)
            
            // 权重(仅在Row/Column中有效)
            .weight(1f)
            
            // 对齐
            .align(Alignment.Center)
            
            // 绘制
            .drawBehind {
                // 自定义绘制
                drawRect(Color.Gray)
            }
            .drawWithContent {
                // 在内容绘制前后执行
                drawContent()
                drawRect(Color.Black.copy(alpha = 0.3f))
            }
    ) {
        Text("Modifier Chain")
    }
}

// 自定义Modifier
fun Modifier.customBorder(
    width: Dp,
    color: Color,
    shape: Shape
): Modifier = this.then(
    Modifier.border(width, color, shape)
        .padding(width)
)

// 使用composed创建带状态的Modifier
fun Modifier.customClickable(
    onClick: () -> Unit
): Modifier = composed {
    var isPressed by remember { mutableStateOf(false) }
    
    this.then(
        Modifier
            .pointerInput(Unit) {
                detectTapGestures(
                    onPress = {
                        isPressed = true
                        tryAwaitRelease()
                        isPressed = false
                    },
                    onTap = { onClick() }
                )
            }
            .graphicsLayer {
                alpha = if (isPressed) 0.7f else 1f
                scaleX = if (isPressed) 0.95f else 1f
                scaleY = if (isPressed) 0.95f else 1f
            }
    )
}

自定义布局

// 自定义Layout
@Composable
fun CustomLayout(
    modifier: Modifier = Modifier,
    content: @Composable () -> Unit
) {
    Layout(
        content = content,
        modifier = modifier
    ) { measurables, constraints ->
        // 测量子元素
        val placeables = measurables.map { measurable ->
            measurable.measure(constraints)
        }
        
        // 计算布局尺寸
        val width = placeables.maxOf { it.width }
        val height = placeables.sumOf { it.height }
        
        // 放置子元素
        layout(width, height) {
            var yPosition = 0
            placeables.forEach { placeable ->
                placeable.placeRelative(x = 0, y = yPosition)
                yPosition += placeable.height
            }
        }
    }
}

// 瀑布流布局实现
@Composable
fun StaggeredGrid(
    columns: Int,
    modifier: Modifier = Modifier,
    content: @Composable () -> Unit
) {
    Layout(
        content = content,
        modifier = modifier
    ) { measurables, constraints ->
        val columnWidth = constraints.maxWidth / columns
        val itemConstraints = constraints.copy(maxWidth = columnWidth)
        
        // 记录每列的高度
        val columnHeights = IntArray(columns) { 0 }
        
        val placeables = measurables.map { measurable ->
            val placeable = measurable.measure(itemConstraints)
            
            // 找到最短的列
            val minColumn = columnHeights.indices.minByOrNull { columnHeights[it] } ?: 0
            columnHeights[minColumn] += placeable.height
            
            placeable to minColumn
        }
        
        val height = columnHeights.maxOrNull()?.coerceIn(
            constraints.minHeight, constraints.maxHeight
        ) ?: constraints.minHeight
        
        layout(width = constraints.maxWidth, height = height) {
            val currentHeights = IntArray(columns) { 0 }
            
            placeables.forEach { (placeable, column) ->
                placeable.placeRelative(
                    x = column * columnWidth,
                    y = currentHeights[column]
                )
                currentHeights[column] += placeable.height
            }
        }
    }
}

// 使用自定义布局
@Composable
fun StaggeredGridDemo() {
    StaggeredGrid(columns = 3, modifier = Modifier.fillMaxWidth()) {
        for (i in 1..20) {
            Box(
                modifier = Modifier
                    .padding(4.dp)
                    .height((50 + (i * 10) % 100).dp)
                    .background(Color.Blue),
                contentAlignment = Alignment.Center
            ) {
                Text("$i", color = Color.White)
            }
        }
    }
}

Navigation Compose

// Navigation Compose架构
@Composable
fun AppNavigation() {
    val navController = rememberNavController()
    
    NavHost(
        navController = navController,
        startDestination = "home"
    ) {
        // 简单路由
        composable("home") {
            HomeScreen(
                onNavigateToDetail = { itemId ->
                    navController.navigate("detail/$itemId")
                }
            )
        }
        
        // 带参数路由
        composable(
            route = "detail/{itemId}",
            arguments = listOf(
                navArgument("itemId") { type = NavType.StringType }
            )
        ) { backStackEntry ->
            val itemId = backStackEntry.arguments?.getString("itemId")
            DetailScreen(
                itemId = itemId,
                onBack = { navController.popBackStack() }
            )
        }
        
        // 可选参数
        composable(
            route = "search?query={query}",
            arguments = listOf(
                navArgument("query") {
                    type = NavType.StringType
                    defaultValue = ""
                    nullable = true
                }
            )
        ) { backStackEntry ->
            val query = backStackEntry.arguments?.getString("query")
            SearchScreen(query = query)
        }
        
        // 深层链接
        composable(
            route = "profile/{userId}",
            deepLinks = listOf(
                navDeepLink { uriPattern = "https://example.com/profile/{userId}" }
            )
        ) { backStackEntry ->
            ProfileScreen(userId = backStackEntry.arguments?.getString("userId"))
        }
        
        // 嵌套导航
        navigation(startDestination = "list", route = "items") {
            composable("list") { ItemListScreen() }
            composable("edit/{itemId}") { ItemEditScreen() }
        }
    }
}

// 类型安全导航(Navigation 2.8.0+)
@Serializable
data class DetailRoute(val itemId: String)

@Serializable
object HomeRoute

@Composable
fun TypeSafeNavigation() {
    val navController = rememberNavController()
    
    NavHost(navController = navController, startDestination = HomeRoute) {
        composable {
            HomeScreen(
                onNavigateToDetail = { itemId ->
                    navController.navigate(DetailRoute(itemId))
                }
            )
        }
        composable { backStackEntry ->
            val route = backStackEntry.toRoute()
            DetailScreen(itemId = route.itemId)
        }
    }
}

ViewModel集成

// ViewModel与Compose集成
class TodoViewModel(
    private val repository: TodoRepository
) : ViewModel() {
    
    // UI状态封装
    data class UiState(
        val todos: List = emptyList(),
        val isLoading: Boolean = false,
        val error: String? = null,
        val filter: FilterType = FilterType.ALL
    )
    
    // 使用StateFlow管理状态
    private val _uiState = MutableStateFlow(UiState())
    val uiState: StateFlow = _uiState.asStateFlow()
    
    init {
        loadTodos()
    }
    
    fun loadTodos() {
        viewModelScope.launch {
            _uiState.update { it.copy(isLoading = true, error = null) }
            
            try {
                repository.getTodos().collect { todos ->
                    _uiState.update { it.copy(todos = todos, isLoading = false) }
                }
            } catch (e: Exception) {
                _uiState.update { it.copy(error = e.message, isLoading = false) }
            }
        }
    }
    
    fun addTodo(title: String) {
        viewModelScope.launch {
            repository.addTodo(title)
        }
    }
    
    fun toggleTodo(id: String) {
        viewModelScope.launch {
            repository.toggleTodo(id)
        }
    }
    
    fun setFilter(filter: FilterType) {
        _uiState.update { it.copy(filter = filter) }
    }
}

// Compose中使用ViewModel
@Composable
fun TodoScreen(
    viewModel: TodoViewModel = viewModel()
) {
    // 收集状态
    val uiState by viewModel.uiState.collectAsStateWithLifecycle()
    
    TodoContent(
        todos = uiState.todos,
        isLoading = uiState.isLoading,
        error = uiState.error,
        filter = uiState.filter,
        onAddTodo = viewModel::addTodo,
        onToggleTodo = viewModel::toggleTodo,
        onFilterChange = viewModel::setFilter,
        onRetry = viewModel::loadTodos
    )
}

// 纯UI组件
@Composable
fun TodoContent(
    todos: List,
    isLoading: Boolean,
    error: String?,
    filter: FilterType,
    onAddTodo: (String) -> Unit,
    onToggleTodo: (String) -> Unit,
    onFilterChange: (FilterType) -> Unit,
    onRetry: () -> Unit,
    modifier: Modifier = Modifier
) {
    Column(modifier = modifier.fillMaxSize()) {
        // 过滤选项
        FilterBar(
            selectedFilter = filter,
            onFilterChange = onFilterChange
        )
        
        // 内容区域
        when {
            isLoading -> LoadingIndicator()
            error != null -> ErrorMessage(error, onRetry)
            todos.isEmpty() -> EmptyMessage()
            else -> TodoList(
                todos = todos,
                onToggle = onToggleTodo
            )
        }
        
        // 输入框
        TodoInput(onAdd = onAddTodo)
    }
}

动画与手势

// 动画系统
@Composable
fun AnimationDemo() {
    var expanded by remember { mutableStateOf(false) }
    
    // 简单动画:animate*AsState
    val size by animateDpAsState(
        targetValue = if (expanded) 200.dp else 100.dp,
        animationSpec = spring(
            dampingRatio = Spring.DampingRatioMediumBouncy,
            stiffness = Spring.StiffnessLow
        ),
        label = "size"
    )
    
    // 多属性动画:AnimatedVisibility
    AnimatedVisibility(
        visible = expanded,
        enter = fadeIn() + expandVertically(),
        exit = fadeOut() + shrinkVertically()
    ) {
        Text("Expanded Content")
    }
    
    // 内容切换动画:Crossfade
    Crossfade(targetState = expanded, label = "crossfade") { isExpanded ->
        if (isExpanded) {
            ExpandedContent()
        } else {
            CollapsedContent()
        }
    }
    
    // 自定义动画:updateTransition
    val transition = updateTransition(expanded, label = "transition")
    val scale by transition.animateFloat(label = "scale") { state ->
        if (state) 1.2f else 1f
    }
    val alpha by transition.animateFloat(label = "alpha") { state ->
        if (state) 1f else 0.5f
    }
    
    // 无限动画:rememberInfiniteTransition
    val infiniteTransition = rememberInfiniteTransition(label = "infinite")
    val angle by infiniteTransition.animateFloat(
        initialValue = 0f,
        targetValue = 360f,
        animationSpec = infiniteRepeatable(
            animation = tween(2000, easing = LinearEasing),
            repeatMode = RepeatMode.Restart
        ),
        label = "angle"
    )
    
    // 手势动画
    val offsetX = remember { Animatable(0f) }
    val offsetY = remember { Animatable(0f) }
    
    Box(
        modifier = Modifier
            .offset { IntOffset(offsetX.value.toInt(), offsetY.value.toInt()) }
            .pointerInput(Unit) {
                detectDragGestures(
                    onDragEnd = {
                        // 弹性回弹
                        offsetX.animateTo(
                            targetValue = 0f,
                            animationSpec = spring()
                        )
                        offsetY.animateTo(
                            targetValue = 0f,
                            animationSpec = spring()
                        )
                    }
                ) { change, dragAmount ->
                    change.consume()
                    coroutineScope.launch {
                        offsetX.snapTo(offsetX.value + dragAmount.x)
                        offsetY.snapTo(offsetY.value + dragAmount.y)
                    }
                }
            }
    )
}

架构决策总结

场景 推荐方案 说明
状态管理 ViewModel + StateFlow 屏幕级状态,生命周期感知
UI组件状态 remember + mutableStateOf 组件内部状态
配置变化保持 rememberSaveable 进程重建时恢复
依赖注入 Hilt 与Compose无缝集成
导航 Navigation Compose 声明式导航,支持深层链接
图片加载 Coil Kotlin优先,Compose支持好
主题适配 Material3 动态颜色,Material You

Compose常见陷阱

  • 在Composable中创建ViewModel:使用viewModel()或hiltViewModel()
  • 直接修改List/Map状态:创建新集合触发重组
  • 在remember中传入不稳定对象:可能导致意外重组
  • 滥用derivedStateOf:简单计算不需要
  • 使用Immutable集合:kotlinx.collections.immutable
  • 延迟状态读取:将状态读取移到叶子节点

总结

Jetpack Compose代表了Android UI开发的未来。通过声明式编程模型、强大的状态管理和智能的重组机制,Compose让UI开发变得更加直观和高效。

掌握Compose需要转变思维方式:从"如何更新UI"到"UI如何响应状态"。理解状态流、重组机制和性能优化技巧,是构建高质量Compose应用的关键。随着生态系统的成熟,Compose正在成为Android开发的标准选择。