1. 为什么需要提取Compose按钮组件?
在Jetpack Compose的日常开发中,我们经常会遇到这样的场景:同一个按钮样式在应用的多个页面重复出现,每次都要重新编写相似的Modifier和点击逻辑。更糟糕的是,当设计团队要求调整按钮圆角或颜色时,开发者不得不逐个页面修改,这种重复劳动不仅低效,还容易遗漏某些角落里的按钮。
我在最近的一个电商App项目中就深有体会:商品详情页的"立即购买"、购物车的"去结算"、订单确认页的"提交订单"按钮,本质上都是同一个红色圆角按钮的变体。最初直接硬编码的结果是,当产品经理要求将主色调从#FF5722改为#E53935时,我花了整整两小时进行全局搜索替换。
2. 基础按钮封装方案
2.1 创建可配置的Button组件
我们先从最基础的封装开始,创建一个可接受主要参数的Composable函数:
kotlin复制@Composable
fun PrimaryButton(
text: String,
onClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
backgroundColor: Color = Color(0xFFE53935),
contentColor: Color = Color.White,
cornerRadius: Dp = 8.dp
) {
Button(
onClick = onClick,
modifier = modifier,
enabled = enabled,
colors = ButtonDefaults.buttonColors(
backgroundColor = backgroundColor,
contentColor = contentColor
),
shape = RoundedCornerShape(cornerRadius)
) {
Text(text = text)
}
}
这个基础版本已经解决了80%的复用场景。使用时只需要:
kotlin复制PrimaryButton(
text = "确认支付",
onClick = { /* 处理点击 */ }
)
2.2 添加图标支持
实际项目中按钮常需要搭配图标。我们扩展组件支持前/后置图标:
kotlin复制@Composable
fun PrimaryButton(
text: String,
onClick: () -> Unit,
modifier: Modifier = Modifier,
leadingIcon: @Composable (() -> Unit)? = null,
trailingIcon: @Composable (() -> Unit)? = null,
// 其他参数...
) {
Button(
// ...其他参数
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp)
) {
leadingIcon?.invoke()
Text(text = text)
trailingIcon?.invoke()
}
}
}
使用示例:
kotlin复制PrimaryButton(
text = "微信支付",
leadingIcon = {
Icon(
imageVector = Icons.Filled.Wechat,
contentDescription = null
)
},
onClick = { /* 微信支付逻辑 */ }
)
3. 高级封装技巧
3.1 状态管理封装
对于网络请求等异步操作,按钮通常需要显示加载状态。我们可以封装一个带状态管理的版本:
kotlin复制@Composable
fun LoadingButton(
text: String,
isLoading: Boolean,
onClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true
) {
Button(
onClick = onClick,
modifier = modifier,
enabled = enabled && !isLoading
) {
if (isLoading) {
CircularProgressIndicator(
color = MaterialTheme.colors.onPrimary,
strokeWidth = 2.dp,
modifier = Modifier.size(20.dp)
)
} else {
Text(text = text)
}
}
}
3.2 主题集成方案
为了保持设计系统一致性,建议将按钮样式与MaterialTheme集成:
kotlin复制@Composable
fun ThemedButton(
text: String,
onClick: () -> Unit,
modifier: Modifier = Modifier,
buttonType: ButtonType = ButtonType.Primary
) {
val colors = when (buttonType) {
ButtonType.Primary -> ButtonDefaults.buttonColors(
backgroundColor = MaterialTheme.colors.primary,
contentColor = MaterialTheme.colors.onPrimary
)
ButtonType.Secondary -> ButtonDefaults.buttonColors(
backgroundColor = MaterialTheme.colors.secondary,
contentColor = MaterialTheme.colors.onSecondary
)
ButtonType.Error -> ButtonDefaults.buttonColors(
backgroundColor = MaterialTheme.colors.error,
contentColor = MaterialTheme.colors.onError
)
}
Button(
onClick = onClick,
modifier = modifier,
colors = colors,
shape = MaterialTheme.shapes.medium
) {
Text(text = text)
}
}
enum class ButtonType {
Primary, Secondary, Error
}
4. 实战中的优化策略
4.1 性能优化要点
- 避免不必要的重组:确保所有参数都是稳定的(使用@Stable注解或不可变类型)
- 默认参数谨慎使用复杂表达式:默认值在每次重组时都会重新计算
- 内容lambda的处理:对于复杂的按钮内容,考虑使用remember缓存
优化后的代码示例:
kotlin复制@Composable
fun OptimizedButton(
text: String,
onClick: () -> Unit,
modifier: Modifier = Modifier,
icon: @Composable (() -> Unit)? = null
) {
val buttonColors = remember {
ButtonDefaults.buttonColors(
backgroundColor = Color.Red,
contentColor = Color.White
)
}
Button(
onClick = onClick,
modifier = modifier,
colors = buttonColors
) {
if (icon != null) {
Row {
icon()
Spacer(modifier = Modifier.width(4.dp))
Text(text = text)
}
} else {
Text(text = text)
}
}
}
4.2 测试方案设计
为封装好的按钮组件编写测试:
kotlin复制class ButtonTest {
@get:Rule
val composeTestRule = createComposeRule()
@Test
fun primaryButton_click() {
var clicked = false
composeTestRule.setContent {
PrimaryButton(
text = "Test",
onClick = { clicked = true }
)
}
composeTestRule.onNodeWithText("Test").performClick()
assertTrue(clicked)
}
@Test
fun loadingButton_disabledWhenLoading() {
composeTestRule.setContent {
LoadingButton(
text = "Test",
isLoading = true,
onClick = {}
)
}
composeTestRule.onNodeWithText("Test").assertIsNotEnabled()
}
}
5. 复杂场景解决方案
5.1 组合式按钮组
对于常见的"取消/确认"按钮组,可以进一步封装:
kotlin复制@Composable
fun DialogButtonRow(
onCancel: () -> Unit,
onConfirm: () -> Unit,
confirmText: String = "确认",
cancelText: String = "取消",
modifier: Modifier = Modifier
) {
Row(
modifier = modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.End,
verticalAlignment = Alignment.CenterVertically
) {
TextButton(onClick = onCancel) {
Text(text = cancelText)
}
Spacer(modifier = Modifier.width(16.dp))
Button(onClick = onConfirm) {
Text(text = confirmText)
}
}
}
5.2 动态样式按钮
需要根据状态动态改变样式的按钮:
kotlin复制@Composable
fun StatefulButton(
text: String,
state: ButtonState,
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
val (backgroundColor, contentColor) = when (state) {
ButtonState.Normal -> Color.Blue to Color.White
ButtonState.Highlighted -> Color.Red to Color.White
ButtonState.Disabled -> Color.Gray to Color.LightGray
}
Button(
onClick = onClick,
modifier = modifier,
enabled = state != ButtonState.Disabled,
colors = ButtonDefaults.buttonColors(
backgroundColor = backgroundColor,
contentColor = contentColor
)
) {
Text(text = text)
}
}
enum class ButtonState {
Normal, Highlighted, Disabled
}
6. 设计系统集成实践
6.1 与Figma设计稿同步
建议建立设计Token与Compose的映射关系:
kotlin复制// 在theme/Color.kt中
val Purple80 = Color(0xFFD0BCFF)
val PurpleGrey80 = Color(0xFFCCC2DC)
// 在components/Buttons.kt中
@Composable
fun DesignSystemButton(
text: String,
onClick: () -> Unit,
variant: ButtonVariant = ButtonVariant.Primary
) {
val (backgroundColor, contentColor) = when (variant) {
ButtonVariant.Primary -> MaterialTheme.colors.primary to MaterialTheme.colors.onPrimary
ButtonVariant.Secondary -> Purple80 to Color.Black
ButtonVariant.Tertiary -> PurpleGrey80 to Color.Black
}
Button(
onClick = onClick,
colors = ButtonDefaults.buttonColors(
backgroundColor = backgroundColor,
contentColor = contentColor
),
shape = RoundedCornerShape(4.dp)
) {
Text(
text = text,
style = MaterialTheme.typography.button
)
}
}
6.2 动效与交互增强
为按钮添加优雅的交互反馈:
kotlin复制@Composable
fun AnimatedButton(
text: String,
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
var pressed by remember { mutableStateOf(false) }
val elevation by animateDpAsState(
targetValue = if (pressed) 2.dp else 8.dp,
animationSpec = tween(durationMillis = 100)
)
Button(
onClick = onClick,
modifier = modifier
.shadow(elevation = elevation, shape = CircleShape)
.pointerInteropFilter {
when (it.action) {
MotionEvent.ACTION_DOWN -> {
pressed = true
true
}
MotionEvent.ACTION_UP -> {
pressed = false
false
}
else -> false
}
},
shape = CircleShape
) {
Text(text = text)
}
}
7. 企业级应用建议
7.1 组件文档规范
为团队创建组件使用文档模板:
markdown复制# PrimaryButton
## 功能描述
主操作按钮,用于关键流程的关键操作点
## 基本用法
```kotlin
PrimaryButton(
text = "提交",
onClick = { /* 处理点击 */ }
)
```
## 参数说明
| 参数名 | 类型 | 默认值 | 说明 |
|--------|------|--------|------|
| text | String | 无 | 按钮文字(必填) |
| onClick | () -> Unit | 无 | 点击回调(必填) |
| modifier | Modifier | Modifier | 布局修饰符 |
| enabled | Boolean | true | 是否启用按钮 |
| backgroundColor | Color | 主题色 | 背景颜色 |
| contentColor | Color | Color.White | 文字颜色 |
## 设计规范
- 圆角: 8dp
- 最小宽度: 120dp
- 高度: 48dp
- 字体: Button(MaterialTheme)
7.2 版本兼容方案
处理多版本兼容的推荐做法:
kotlin复制@Composable
fun CompatButton(
text: String,
onClick: () -> Unit,
modifier: Modifier = Modifier,
// 其他参数...
) {
if (LocalConfiguration.current.screenWidthDp < 600) {
// 移动端样式
Button(
onClick = onClick,
modifier = modifier.height(48.dp),
// ...
) {
Text(text = text)
}
} else {
// 平板/桌面样式
Button(
onClick = onClick,
modifier = modifier.height(56.dp),
// ...
) {
Text(text = text, style = MaterialTheme.typography.h6)
}
}
}
在大型项目中,我通常会建立一个buttons模块专门管理所有按钮组件,每个按钮都有对应的测试用例和演示Preview。当设计系统更新时,只需要修改这个模块中的基础配置,所有使用这些按钮的界面都会自动同步更新,这种架构显著提升了我们的开发效率和视觉一致性。
