Jetpack Compose初体验:声明式UI的革命性尝试
背景介绍
2020年,Google在Android Dev Summit上正式发布了Jetpack Compose的预览版,这标志着Android UI开发进入了一个全新的时代。在此之前,我们的项目已经完成了Kotlin迁移,代码质量得到了显著提升。但传统的XML布局+View的开发模式依然存在诸多痛点:
- 布局与逻辑分离:XML布局文件与Activity/Fragment代码分离,维护困难
- 样板代码过多:findViewById、ViewHolder、适配器等大量模板代码
- 动态UI复杂:运行时修改UI需要复杂的findViewById和状态管理
- 预览限制:XML布局预览功能有限,难以实时看到效果
- 类型安全不足:findViewById存在类型转换风险
经过技术调研,我们决定在新项目中尝试使用Jetpack Compose,探索声明式UI开发的新范式。
Jetpack Compose核心概念
1. 声明式UI vs 命令式UI
传统的命令式UI(XML + View):
// XML布局文件
// res/layout/activity_user.xml
/*
<TextView
android:id="@+id/tv_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<Button
android:id="@+id/btn_update"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="更新用户信息" />
*/
// Activity中的命令式操作
class UserActivity : AppCompatActivity() {
private lateinit var tvName: TextView
private lateinit var btnUpdate: Button
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_user)
// 命令式地查找和设置UI元素
tvName = findViewById(R.id.tv_name)
btnUpdate = findViewById(R.id.btn_update)
btnUpdate.setOnClickListener {
updateUser()
}
}
private fun updateUser() {
// 命令式地更新UI
tvName.text = "新的用户名"
tvName.visibility = View.VISIBLE
}
}
Jetpack Compose声明式UI:
@Composable
fun UserScreen(viewModel: UserViewModel = hiltViewModel()) {
val user by viewModel.user.collectAsState()
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp)
) {
Text(
text = user?.name ?: "用户未登录",
style = MaterialTheme.typography.h6,
modifier = Modifier.padding(bottom = 16.dp)
)
Button(
onClick = { viewModel.updateUser() },
modifier = Modifier.align(Alignment.CenterHorizontally)
) {
Text("更新用户信息")
}
}
}
// UI状态变化会自动触发重新绘制
@Composable
fun UserViewModel.updateUser() {
// 更新状态,UI会自动响应变化
_user.value = user.copy(name = "新的用户名")
}
2. Composable函数
基本的Composable函数:
@Composable
fun Greeting(name: String) {
Text(text = "Hello, $name!")
}
@Composable
fun UserProfile(user: User) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
// 头像
Image(
painter = rememberImagePainter(user.avatarUrl),
contentDescription = "User avatar",
modifier = Modifier
.size(64.dp)
.clip(CircleCropTransformation())
.border(2.dp, Color.Blue, CircleShape)
)
Spacer(modifier = Modifier.width(16.dp))
// 用户信息
Column {
Text(
text = user.name,
style = MaterialTheme.typography.h6,
fontWeight = FontWeight.Bold
)
Text(
text = user.email,
style = MaterialTheme.typography.body2,
color = Color.Gray
)
if (user.isOnline) {
Text(
text = "在线",
color = Color.Green,
style = MaterialTheme.typography.caption
)
}
}
}
}
3. 状态管理
Compose中的状态管理:
@Composable
fun CounterApp() {
// 使用mutableStateOf管理状态
val count = remember { mutableStateOf(0) }
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(
text = "当前计数: ${count.value}",
style = MaterialTheme.typography.h5
)
Row {
Button(
onClick = { count.value-- },
enabled = count.value > 0
) {
Text("-")
}
Spacer(modifier = Modifier.width(16.dp))
Button(onClick = { count.value++ }) {
Text("+")
}
}
}
}
// 使用ViewModel管理状态
@Composable
fun UserListScreen(viewModel: UserViewModel = hiltViewModel()) {
val users by viewModel.users.collectAsState()
val loading by viewModel.loading.collectAsState()
val error by viewModel.error.collectAsState()
when {
loading -> {
LoadingScreen()
}
error != null -> {
ErrorScreen(error) { viewModel.retry() }
}
else -> {
UserList(users = users) { user ->
viewModel.selectUser(user)
}
}
}
}
实际项目尝试
1. 简单页面重构
我们选择了一个相对简单的用户设置页面作为Compose的试点项目。
重构前(XML + Kotlin):
class SettingsActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_settings)
setupToolbar()
setupRecyclerView()
loadSettings()
}
private fun setupRecyclerView() {
val adapter = SettingsAdapter(settingsList)
recyclerView.adapter = adapter
recyclerView.layoutManager = LinearLayoutManager(this)
}
private fun loadSettings() {
// 加载设置项
settingsList.addAll(getDefaultSettings())
(recyclerView.adapter as SettingsAdapter).notifyDataSetChanged()
}
}
// 复杂的ViewHolder
class SettingsViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val title: TextView = itemView.findViewById(R.id.tv_title)
private val subtitle: TextView = itemView.findViewById(R.id.tv_subtitle)
private val switch: Switch = itemView.findViewById(R.id.switch_toggle)
fun bind(setting: Setting) {
title.text = setting.title
subtitle.text = setting.subtitle
switch.isChecked = setting.isEnabled
switch.setOnCheckedChangeListener { _, isChecked ->
setting.isEnabled = isChecked
// 更新设置
}
}
}
重构后(Jetpack Compose):
@Composable
fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel()) {
val settings by viewModel.settings.collectAsState()
val loading by viewModel.loading.collectAsState()
Scaffold(
topBar = {
TopAppBar(
title = { Text("设置") },
navigationIcon = {
IconButton(onClick = { onBackPressed() }) {
Icon(Icons.Default.ArrowBack, contentDescription = "返回")
}
}
)
}
) { padding ->
if (loading) {
LoadingScreen()
} else {
LazyColumn(
modifier = Modifier
.fillMaxSize()
.padding(padding)
) {
items(settings) { setting ->
SettingItem(
setting = setting,
onToggle = { enabled ->
viewModel.updateSetting(setting.id, enabled)
},
onClick = {
viewModel.navigateToSettingDetail(setting.id)
}
)
}
}
}
}
}
@Composable
fun SettingItem(
setting: Setting,
onToggle: (Boolean) -> Unit,
onClick: () -> Unit
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
.clickable(onClick = onClick),
verticalAlignment = Alignment.CenterVertically
) {
Column(
modifier = Modifier.weight(1f)
) {
Text(
text = setting.title,
style = MaterialTheme.typography.h6
)
Text(
text = setting.subtitle,
style = MaterialTheme.typography.body2,
color = Color.Gray
)
}
Switch(
checked = setting.isEnabled,
onCheckedChange = onToggle,
modifier = Modifier.padding(start = 16.dp)
)
}
Divider(
modifier = Modifier.padding(start = 72.dp)
)
}
2. 复杂列表页面
用户列表页面的Compose实现:
@Composable
fun UserListScreen(
viewModel: UserListViewModel = hiltViewModel()
) {
val users by viewModel.users.collectAsState()
val loading by viewModel.loading.collectAsState()
val refreshState by viewModel.refreshState.collectAsState()
val pullRefreshState = rememberPullRefreshState(
refreshing = refreshState,
onRefresh = { viewModel.refreshUsers() }
)
Box(modifier = Modifier.fillMaxSize()) {
// 下拉刷新的LazyColumn
PullRefreshBox(
state = pullRefreshState,
modifier = Modifier.fillMaxSize()
) {
LazyColumn(
modifier = Modifier.fillMaxSize()
) {
// 顶部横幅
item {
BannerCarousel(banners = viewModel.banners)
}
// 用户统计信息
item {
UserStatistics(stats = viewModel.userStats)
}
// 用户列表
items(
items = users,
key = { user -> user.id }
) { user ->
UserListItem(
user = user,
onClick = { selectedUser ->
viewModel.selectUser(selectedUser)
},
onLongClick = { user ->
viewModel.showUserMenu(user)
}
)
}
// 加载更多
if (viewModel.hasMore) {
item {
LoadingMoreItem {
viewModel.loadMoreUsers()
}
}
}
}
}
// 下拉刷新指示器
PullRefreshIndicator(
refreshing = refreshState,
state = pullRefreshState,
modifier = Modifier.align(Alignment.TopCenter)
)
// 空状态
if (users.isEmpty() && !loading) {
EmptyState(
icon = Icons.Default.Person,
message = "暂无用户数据",
onRetry = { viewModel.refreshUsers() }
)
}
}
}
@Composable
fun UserListItem(
user: User,
onClick: (User) -> Unit,
onLongClick: (User) -> Unit
) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 8.dp)
.clickable(onClick = { onClick(user) })
.combinedClickable(
onClick = { onClick(user) },
onLongClick = { onLongClick(user) }
),
elevation = 4.dp,
backgroundColor = MaterialTheme.colors.surface
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
// 用户头像
AsyncImage(
model = ImageRequest.Builder(LocalContext.current)
.data(user.avatarUrl)
.crossfade(true)
.build(),
contentDescription = "User avatar",
modifier = Modifier
.size(56.dp)
.clip(CircleShape)
.border(2.dp, MaterialTheme.colors.primary, CircleShape),
contentScale = ContentScale.Crop
)
Spacer(modifier = Modifier.width(16.dp))
// 用户信息
Column(
modifier = Modifier.weight(1f)
) {
Row(
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = user.name,
style = MaterialTheme.typography.h6,
fontWeight = FontWeight.Medium
)
if (user.isVip) {
Spacer(modifier = Modifier.width(8.dp))
Icon(
imageVector = Icons.Default.Star,
contentDescription = "VIP用户",
tint = Color.Yellow,
modifier = Modifier.size(16.dp)
)
}
if (user.isOnline) {
Spacer(modifier = Modifier.width(8.dp))
Box(
modifier = Modifier
.size(8.dp)
.background(Color.Green, CircleShape)
)
}
}
Text(
text = user.signature ?: "暂无签名",
style = MaterialTheme.typography.body2,
color = Color.Gray,
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
Row(
modifier = Modifier.padding(top = 4.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = Icons.Default.LocationOn,
contentDescription = "Location",
modifier = Modifier.size(16.dp),
tint = Color.Gray
)
Spacer(modifier = Modifier.width(4.dp))
Text(
text = user.location,
style = MaterialTheme.typography.caption,
color = Color.Gray
)
}
}
// 操作按钮
Column(
horizontalAlignment = Alignment.End
) {
Button(
onClick = { /* 发送消息 */ },
colors = ButtonDefaults.buttonColors(
backgroundColor = MaterialTheme.colors.primary
),
modifier = Modifier.height(32.dp)
) {
Text(
"发消息",
fontSize = 12.sp,
color = Color.White
)
}
Spacer(modifier = Modifier.height(8.dp))
if (user.isFriend) {
Text(
text = "好友",
style = MaterialTheme.typography.caption,
color = Color.Green
)
}
}
}
}
}
遇到的问题和解决方案
1. 性能问题
问题:Compose初期内存占用较大,复杂列表滑动不够流畅
解决方案:
// 使用remember优化重组
@Composable
fun ExpensiveComponent(data: List<User>) {
val expensiveValue = remember(data) {
// 只有当data改变时才重新计算
calculateExpensiveValue(data)
}
// 使用LazyColumn优化列表性能
LazyColumn {
items(
items = data,
key = { user -> user.id }, // 提供稳定的key
contentType = { "user" } // 提供内容类型
) { user ->
UserItem(user = user)
}
}
}
// 避免在Composable中创建对象
@Composable
fun UserProfile(user: User) {
// 错误做法:每次重组都创建新对象
// val padding = PaddingValues(16.dp)
// 正确做法:使用remember缓存
val padding = remember { PaddingValues(16.dp) }
val cornerRadius = remember { CornerSize(8.dp) }
Card(
modifier = Modifier.padding(padding),
shape = RoundedCornerShape(cornerRadius)
) {
// 内容
}
}
2. 学习曲线陡峭
问题:团队成员对声明式UI概念理解困难
解决方案:
// 创建学习文档和示例
object ComposeBestPractices {
// 1. Composable函数应该小巧单一
@Composable
fun SimpleText(text: String) {
Text(text = text)
}
// 2. 状态提升
@Composable
fun Counter(count: Int, onIncrement: () -> Unit, onDecrement: () -> Unit) {
Row {
Button(onClick = onDecrement) { Text("-") }
Text(text = "$count")
Button(onClick = onIncrement) { Text("+") }
}
}
// 3. 避免副作用
@Composable
fun UserProfile(userId: String) {
val user by remember(userId) {
// 使用LaunchedEffect处理副作用
flow { emit(fetchUser(userId)) }.asLiveData()
}.observeAsState()
user?.let { userData ->
UserCard(user = userData)
}
}
}
3. 与现有代码集成
问题:如何在现有项目中逐步引入Compose
解决方案:
// 在现有Activity中嵌入Compose
class SettingsActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 使用ComposeView嵌入Compose UI
setContentView(R.layout.activity_settings)
val composeView = findViewById<ComposeView>(R.id.compose_view)
composeView.setContent {
MyTheme {
SettingsScreen(viewModel = viewModel)
}
}
}
}
// 在Fragment中使用
class UserListFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return ComposeView(requireContext()).apply {
setContent {
MyTheme {
UserListScreen()
}
}
}
}
}
// 从Compose调用传统View
@Composable
fun WebViewPage(url: String) {
AndroidView(
factory = { context ->
WebView(context).apply {
webViewClient = WebViewClient()
settings.javaScriptEnabled = true
loadUrl(url)
}
},
modifier = Modifier.fillMaxSize()
)
}
开发体验对比
1. 代码量对比
| 功能 | XML + View | Jetpack Compose | 改进 |
|---|---|---|---|
| 简单页面 | 200行 | 80行 | ↓60% |
| 列表页面 | 400行 | 150行 | ↓62.5% |
| 复杂自定义View | 300行 | 120行 | ↓60% |
| 布局文件 | 2个文件 | 0个文件 | ↓100% |
2. 开发效率提升
// 1. 实时预览
@Preview(showBackground = true, widthDp = 360, heightDp = 640)
@Composable
fun PreviewUserProfile() {
MyTheme {
UserProfile(
user = User(
name = "张三",
email = "zhangsan@example.com",
isOnline = true
)
)
}
}
// 2. 主题系统
@Composable
fun MyTheme(content: @Composable () -> Unit) {
MaterialTheme(
colors = if (isSystemInDarkTheme()) {
DarkColorPalette
} else {
LightColorPalette
},
typography = Typography,
shapes = Shapes,
content = content
)
}
// 3. 动画简化
@Composable
fun AnimatedCounter(count: Int) {
val animatedCount = animateIntAsState(
targetValue = count,
animationSpec = tween(durationMillis = 500)
)
Text(
text = animatedCount.value.toString(),
style = MaterialTheme.typography.h2,
modifier = Modifier
.graphicsLayer {
// 3D旋转效果
rotationY = 180f * (animatedCount.value / 100f)
}
.animateContentSize()
)
}
3. 测试友好性
// Compose测试更加简单
@get:Rule
val composeTestRule = createComposeRule()
@Test
fun userProfile_displayCorrectUserInfo() {
val testUser = User(
name = "测试用户",
email = "test@example.com",
isOnline = true
)
composeTestRule.setContent {
UserProfile(user = testUser)
}
// 验证UI元素
composeTestRule.onNodeWithText("测试用户")
.assertExists()
.assertIsDisplayed()
composeTestRule.onNodeWithText("test@example.com")
.assertExists()
// 点击测试
composeTestRule.onNodeWithContentDescription("用户头像")
.performClick()
// 等待动画完成
composeTestRule.mainClock.advanceTimeBy(1000)
}
技术栈总结(2020年6月)
Android SDK 29 (10.0)
Jetpack Compose 1.0.0-alpha
Kotlin 1.4.30
Coroutines 1.4.2
Hilt 2.32
Navigation Compose 2.3.0
Accompanist (Compose Extensions)
经验总结
1. 收益评估
开发效率:
- 代码量减少50-60%
- 布局预览实时可见
- 主题切换即时生效
- 组件复用更加灵活
代码质量:
- 类型安全提升
- 状态管理更加清晰
- 副作用控制更好
- 测试覆盖更容易
维护成本:
- 单一数据源
- 组件化程度高
- 逻辑与UI紧密结合
- 重构更加安全
2. 挑战与不足
技术挑战:
- 学习曲线较陡峭
- 性能优化需要经验
- 调试工具不够完善
- 第三方库支持有限
团队适应:
- 需要转变UI开发思维
- 命令式到声明式的转换
- 状态管理理念更新
- 团队培训成本较高
3. 最佳实践
// 1. 组件设计原则
@Composable
fun StandardButton(
text: String,
onClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
loading: Boolean = false
) {
Button(
onClick = onClick,
enabled = enabled && !loading,
modifier = modifier,
colors = ButtonDefaults.buttonColors(
disabledBackgroundColor = Color.Gray
)
) {
if (loading) {
CircularProgressIndicator(
modifier = Modifier.size(20.dp),
strokeWidth = 2.dp,
color = Color.White
)
} else {
Text(text.uppercase())
}
}
}
// 2. 状态管理
@Composable
fun UserListScreen(viewModel: UserListViewModel) {
val users by viewModel.users.collectAsState()
val uiState by viewModel.uiState.collectAsState()
when (uiState) {
is Loading -> LoadingScreen()
is Error -> ErrorScreen(uiState.message) {
viewModel.retry()
}
is Success -> UserListContent(users)
}
}
// 3. 性能优化
@Composable
fun OptimizedUserList(users: List<User>) {
LazyColumn {
items(
items = users,
key = { user -> user.id }, // 稳定的key
contentType = { "user_item" } // 内容类型
) { user ->
// 使用remember避免重复计算
val userColors = remember(user.isOnline) {
if (user.isOnline) Color.Green else Color.Gray
}
UserListItem(
user = user,
onColor = userColors,
modifier = Modifier.animateItemPlacement() // 动画
)
}
}
}
后续规划
- 渐进式迁移:在新功能中优先使用Compose
- 性能优化:深入学习Compose性能调优技巧
- 团队培训:建立Compose开发规范和最佳实践
- 生态建设:关注Compose生态发展,适时引入第三方库
- 生产环境:在合适的项目中尝试全Compose开发
总结
Jetpack Compose的初体验是一次充满挑战但也收获颇丰的技术探索。虽然作为预览版还存在一些不完善的地方,但其声明式UI的理念确实为Android开发带来了革命性的变化。
技术收益:
- UI开发效率提升50%以上
- 代码可维护性显著改善
- 开发体验更加现代化
团队收益:
- 学习了新的UI开发范式
- 掌握了声明式编程思想
- 为未来技术升级做好准备
业务收益:
- 新功能开发速度加快
- UI质量更加稳定
- 用户体验得到提升
这次Compose尝试让我深刻认识到,技术的革新不仅仅是工具的更换,更是思维方式的转变。从命令式到声明式,从分离到统一,这种转变需要时间和实践来适应,但方向是正确的。
Compose初体验感悟:
- 新技术需要勇气尝试,但也要控制风险
- 声明式UI是未来趋势,值得投入学习
- 渐进式迁移比激进式改造更稳妥
- 团队学习和技术分享同样重要