架构视角:声明式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)
}
}
// 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与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开发的标准选择。