Jetpack Compose初体验:声明式UI的革命性尝试

记录团队首次尝试Jetpack Compose的完整过程,从传统XML布局到声明式UI的思维转变

-- 次阅读

Jetpack Compose初体验:声明式UI的革命性尝试

背景介绍

2020年,Google在Android Dev Summit上正式发布了Jetpack Compose的预览版,这标志着Android UI开发进入了一个全新的时代。在此之前,我们的项目已经完成了Kotlin迁移,代码质量得到了显著提升。但传统的XML布局+View的开发模式依然存在诸多痛点:

  1. 布局与逻辑分离:XML布局文件与Activity/Fragment代码分离,维护困难
  2. 样板代码过多:findViewById、ViewHolder、适配器等大量模板代码
  3. 动态UI复杂:运行时修改UI需要复杂的findViewById和状态管理
  4. 预览限制:XML布局预览功能有限,难以实时看到效果
  5. 类型安全不足: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 + ViewJetpack Compose改进
简单页面200行80行↓60%
列表页面400行150行↓62.5%
复杂自定义View300行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()  // 动画
            )
        }
    }
}

后续规划

  1. 渐进式迁移:在新功能中优先使用Compose
  2. 性能优化:深入学习Compose性能调优技巧
  3. 团队培训:建立Compose开发规范和最佳实践
  4. 生态建设:关注Compose生态发展,适时引入第三方库
  5. 生产环境:在合适的项目中尝试全Compose开发

总结

Jetpack Compose的初体验是一次充满挑战但也收获颇丰的技术探索。虽然作为预览版还存在一些不完善的地方,但其声明式UI的理念确实为Android开发带来了革命性的变化。

技术收益

  • UI开发效率提升50%以上
  • 代码可维护性显著改善
  • 开发体验更加现代化

团队收益

  • 学习了新的UI开发范式
  • 掌握了声明式编程思想
  • 为未来技术升级做好准备

业务收益

  • 新功能开发速度加快
  • UI质量更加稳定
  • 用户体验得到提升

这次Compose尝试让我深刻认识到,技术的革新不仅仅是工具的更换,更是思维方式的转变。从命令式到声明式,从分离到统一,这种转变需要时间和实践来适应,但方向是正确的。

Compose初体验感悟

  • 新技术需要勇气尝试,但也要控制风险
  • 声明式UI是未来趋势,值得投入学习
  • 渐进式迁移比激进式改造更稳妥
  • 团队学习和技术分享同样重要
-- 次访问
Powered by Hugo & Stack Theme
使用 Hugo 构建
主题 StackJimmy 设计