首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >前端开发者的 Kotlin 之旅:Compose 自定义组件开发实战

前端开发者的 Kotlin 之旅:Compose 自定义组件开发实战

原创
作者头像
骑猪耍太极
修改2025-10-21 14:37:59
修改2025-10-21 14:37:59
11800
代码可运行
举报
运行总次数:0
代码可运行

作为一名前端开发工程师,在掌握了 Compose 的基础组件、布局系统、状态管理和 Material 3 组件库后,我开始探索如何设计和开发自定义组件。本文将从前端开发者的视角,深入解析 Compose 自定义组件的设计原则、实现模式和性能优化技术。

在前端开发中,我们经常需要封装可复用的组件。无论是 React 的自定义 Hooks、Vue 的 Composables,还是 Web Components,组件化都是现代前端开发的核心思想。Jetpack Compose 同样提供了强大的组件化能力,但它的设计理念和实现方式与前端框架有所不同。

通过本模块的学习,我将探索:

  • 如何设计参数化的组件
  • 如何使用 Slot API 实现组合模式
  • 如何优化组件性能
  • 如何提升用户体验

从前端视角理解自定义组件

组件设计思维对比

前端开发

Compose

说明

Props

参数 (Parameters)

组件配置和数据传递

Slots

@Composable 函数参数

内容插槽设计

React.memo

remember

缓存和优化

useMemo

derivedStateOf

派生状态计算

useEffect

LaunchedEffect

副作用处理

debounce

LaunchedEffect + delay

防抖实现

设计原则对比

React 组件设计

代码语言:jsx
复制
// React 自定义组件
function StatusBadge({ 
  text, 
  status = 'default',
  size = 'medium',
  icon,
  onClick 
}) {
  return (
    <div 
      className={`badge badge-${status} badge-${size}`}
      onClick={onClick}
    >
      {icon && <Icon name={icon} />}
      <span>{text}</span>
    </div>
  );
}

Compose 组件设计

代码语言:kotlin
复制
// Compose 自定义组件
@Composable
fun StatusBadge(
    text: String,
    status: BadgeStatus = BadgeStatus.Default,
    size: BadgeSize = BadgeSize.Medium,
    modifier: Modifier = Modifier,
    icon: ImageVector? = null,
    onClick: (() -> Unit)? = null
) {
    // 组件实现
}

核心差异

  • Compose 使用枚举类型提供类型安全
  • Compose 通过 Modifier 提供样式定制
  • Compose 使用 @Composable 注解标记可组合函数
  • Compose 的参数默认值更加明确和类型安全

快速体验自定义组件

在深入学习之前,让我们先运行示例项目,直观感受自定义组件的效果。

克隆项目

代码语言:bash
复制
git clone https://github.com/easonxie/learn-jetpack-compose.git
cd learn-jetpack-compose

运行 Lesson 05

Web 版本(推荐给前端开发者)

代码语言:bash
复制
# 启动 Web 开发服务器
./gradlew :lesson-05-custom-components:wasmJsBrowserDevelopmentRun

# 访问地址
http://localhost:8080
Lesson 05: 自定义组件开发
Lesson 05: 自定义组件开发

开发模式(自动重新加载)

代码语言:bash
复制
./gradlew :lesson-05-custom-components:wasmJsBrowserDevelopmentRun --continuous

一、参数化组件设计

1.1 什么是参数化设计?

参数化设计是指通过参数控制组件的外观和行为,使组件具有高度的可配置性和复用性。

前端对比

  • React:通过 Props 传递配置
  • Vue:通过 Props 和 Emits 定义接口
  • Compose:通过函数参数和枚举类型

1.2 StatusBadge 组件实现

让我们看一个完整的参数化组件示例:

代码语言:kotlin
复制
/**
 * 自定义状态徽章组件
 * 展示参数化设计原则
 */
@Composable
fun StatusBadge(
    text: String,                                    // 必需参数:显示文本
    status: BadgeStatus = BadgeStatus.Default,       // 可选参数:状态类型
    size: BadgeSize = BadgeSize.Medium,              // 可选参数:尺寸大小
    modifier: Modifier = Modifier,                   // 可选参数:样式修饰符
    icon: ImageVector? = null,                       // 可选参数:图标
    onClick: (() -> Unit)? = null                    // 可选参数:点击回调
) {
    val colors = getBadgeColors(status)
    val dimensions = getBadgeDimensions(size)
    
    // 动画效果
    val animatedColor by animateColorAsState(
        targetValue = colors.backgroundColor,
        animationSpec = tween(300)
    )
    
    Box(
        modifier = modifier
            .background(
                color = animatedColor,
                shape = RoundedCornerShape(dimensions.cornerRadius)
            )
            .border(
                width = dimensions.borderWidth,
                color = colors.borderColor,
                shape = RoundedCornerShape(dimensions.cornerRadius)
            )
            .then(
                if (onClick != null) {
                    Modifier.clickable { onClick() }
                } else Modifier
            )
            .padding(
                horizontal = dimensions.horizontalPadding,
                vertical = dimensions.verticalPadding
            )
    ) {
        Row(
            horizontalArrangement = Arrangement.spacedBy(dimensions.iconSpacing),
            verticalAlignment = Alignment.CenterVertically
        ) {
            // 可选图标
            if (icon != null) {
                Icon(
                    imageVector = icon,
                    contentDescription = null,
                    tint = colors.contentColor,
                    modifier = Modifier.size(dimensions.iconSize)
                )
            }
            
            // 文本内容
            ChineseText(
                text = text,
                color = colors.contentColor,
                fontSize = dimensions.fontSize,
                fontWeight = FontWeight.Medium
            )
        }
    }
}

1.3 类型安全的参数设计

使用枚举类型

代码语言:kotlin
复制
/**
 * 徽章状态枚举
 */
enum class BadgeStatus {
    Default, Success, Warning, Error, Info
}

/**
 * 徽章尺寸枚举
 */
enum class BadgeSize {
    Small, Medium, Large
}

前端对比

代码语言:typescript
复制
// TypeScript 类型定义
type BadgeStatus = 'default' | 'success' | 'warning' | 'error' | 'info';
type BadgeSize = 'small' | 'medium' | 'large';

interface BadgeProps {
  text: string;
  status?: BadgeStatus;
  size?: BadgeSize;
  icon?: string;
  onClick?: () => void;
}

优势

  • ✅ Kotlin 枚举提供编译时类型检查
  • ✅ IDE 自动补全和提示
  • ✅ 避免字符串拼写错误
  • ✅ 更好的代码可维护性

1.4 配置数据类

颜色配置

代码语言:kotlin
复制
/**
 * 徽章颜色配置
 */
data class BadgeColors(
    val backgroundColor: Color,
    val contentColor: Color,
    val borderColor: Color
)

@Composable
private fun getBadgeColors(status: BadgeStatus): BadgeColors {
    return when (status) {
        BadgeStatus.Success -> BadgeColors(
            backgroundColor = Color(0xFFE8F5E8),
            contentColor = Color(0xFF2E7D32),
            borderColor = Color(0xFF4CAF50)
        )
        BadgeStatus.Warning -> BadgeColors(
            backgroundColor = Color(0xFFFFF3E0),
            contentColor = Color(0xFFEF6C00),
            borderColor = Color(0xFFFF9800)
        )
        // ... 其他状态
    }
}

尺寸配置

代码语言:kotlin
复制
/**
 * 徽章尺寸配置
 */
data class BadgeDimensions(
    val fontSize: TextUnit,
    val horizontalPadding: Dp,
    val verticalPadding: Dp,
    val cornerRadius: Dp,
    val borderWidth: Dp,
    val iconSize: Dp,
    val iconSpacing: Dp
)

private fun getBadgeDimensions(size: BadgeSize): BadgeDimensions {
    return when (size) {
        BadgeSize.Small -> BadgeDimensions(
            fontSize = 12.sp,
            horizontalPadding = 8.dp,
            verticalPadding = 4.dp,
            cornerRadius = 8.dp,
            borderWidth = 1.dp,
            iconSize = 12.dp,
            iconSpacing = 4.dp
        )
        // ... 其他尺寸
    }
}

1.5 使用示例

代码语言:kotlin
复制
// 基础用法
StatusBadge(
    text = "成功",
    status = BadgeStatus.Success
)

// 带图标
StatusBadge(
    text = "警告",
    status = BadgeStatus.Warning,
    icon = Icons.Default.Warning
)

// 不同尺寸
StatusBadge(
    text = "大尺寸",
    status = BadgeStatus.Info,
    size = BadgeSize.Large,
    icon = Icons.Default.Star
)

// 可点击
var clickCount by remember { mutableIntStateOf(0) }
StatusBadge(
    text = "点击次数: $clickCount",
    status = BadgeStatus.Success,
    icon = Icons.Default.TouchApp,
    onClick = { clickCount++ }
)

二、组合模式设计

2.1 什么是 Slot API?

Slot API 是 Compose 中实现组合模式的核心机制,允许调用者提供自定义内容。

前端对比

前端框架

插槽机制

说明

React

children / render props

通过 props 传递组件

Vue

<slot>

具名插槽和作用域插槽

Compose

@Composable 函数参数

类型安全的插槽设计

React 示例

代码语言:jsx
复制
// React 组件插槽
function Card({ header, children, footer }) {
  return (
    <div className="card">
      <div className="header">{header}</div>
      <div className="content">{children}</div>
      <div className="footer">{footer}</div>
    </div>
  );
}

// 使用
<Card
  header={<h2>标题</h2>}
  footer={<button>确定</button>}
>
  <p>内容</p>
</Card>

Vue 示例

代码语言:txt
复制
<!-- Vue 组件插槽 -->
<template>
  <div class="card">
    <div class="header">
      <slot name="header"></slot>
    </div>
    <div class="content">
      <slot></slot>
    </div>
    <div class="footer">
      <slot name="footer"></slot>
    </div>
  </div>
</template>

<!-- 使用 -->
<Card>
  <template #header>
    <h2>标题</h2>
  </template>
  <p>内容</p>
  <template #footer>
    <button>确定</button>
  </template>
</Card>

2.2 ExpandableCard 组件实现

Compose Slot API 设计

代码语言:kotlin
复制
/**
 * 可扩展卡片组件
 * 展示组合模式和 Slot API 设计
 */
@Composable
fun ExpandableCard(
    title: String,
    modifier: Modifier = Modifier,
    subtitle: String? = null,
    icon: ImageVector? = null,
    expandedInitially: Boolean = false,
    // Slot API:操作区域插槽
    actions: @Composable RowScope.() -> Unit = {},
    // Slot API:内容区域插槽
    content: @Composable ColumnScope.() -> Unit
) {
    var isExpanded by remember { mutableStateOf(expandedInitially) }
    
    Card(
        modifier = modifier.fillMaxWidth(),
        elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
    ) {
        Column {
            // 头部区域
            Row(
                modifier = Modifier
                    .fillMaxWidth()
                    .clickable { isExpanded = !isExpanded }
                    .padding(16.dp),
                horizontalArrangement = Arrangement.SpaceBetween,
                verticalAlignment = Alignment.CenterVertically
            ) {
                Row(
                    modifier = Modifier.weight(1f),
                    horizontalArrangement = Arrangement.spacedBy(12.dp),
                    verticalAlignment = Alignment.CenterVertically
                ) {
                    // 可选图标
                    if (icon != null) {
                        Icon(
                            imageVector = icon,
                            contentDescription = null,
                            tint = MaterialTheme.colorScheme.primary,
                            modifier = Modifier.size(24.dp)
                        )
                    }
                    
                    // 标题和副标题
                    Column {
                        ChineseText(
                            text = title,
                            style = MaterialTheme.typography.titleMedium,
                            fontWeight = FontWeight.Bold
                        )
                        if (subtitle != null) {
                            ChineseText(
                                text = subtitle,
                                style = MaterialTheme.typography.bodySmall,
                                color = MaterialTheme.colorScheme.onSurfaceVariant
                            )
                        }
                    }
                }
                
                Row(
                    horizontalArrangement = Arrangement.spacedBy(8.dp),
                    verticalAlignment = Alignment.CenterVertically
                ) {
                    // 自定义操作区域(Slot API)
                    actions()
                    
                    // 展开/收起图标
                    Icon(
                        imageVector = if (isExpanded) Icons.Default.ExpandLess 
                                     else Icons.Default.ExpandMore,
                        contentDescription = if (isExpanded) "收起" else "展开"
                    )
                }
            }
            
            // 可展开内容区域
            AnimatedVisibility(
                visible = isExpanded,
                enter = fadeIn() + expandVertically(),
                exit = fadeOut() + shrinkVertically()
            ) {
                Column(
                    modifier = Modifier.padding(
                        start = 16.dp,
                        end = 16.dp,
                        bottom = 16.dp
                    )
                ) {
                    HorizontalDivider()
                    Spacer(modifier = Modifier.height(16.dp))
                    
                    // 内容区域(Slot API)
                    content()
                }
            }
        }
    }
}

2.3 使用示例

基础用法

代码语言:kotlin
复制
ExpandableCard(
    title = "基础可扩展卡片",
    subtitle = "这是一个简单的可扩展卡片示例",
    icon = Icons.Default.Info,
    expandedInitially = true
) {
    ChineseText(
        text = "这里是卡片的详细内容。可扩展卡片允许用户查看摘要信息,并根据需要展开查看更多详细信息。",
        style = MaterialTheme.typography.bodyMedium
    )
}

带操作按钮的用法

代码语言:kotlin
复制
ExpandableCard(
    title = "带操作的可扩展卡片",
    subtitle = "展示 Slot API 的强大功能",
    icon = Icons.Default.Settings,
    // 使用 actions 插槽添加自定义操作
    actions = {
        IconButton(onClick = { /* 编辑操作 */ }) {
            Icon(
                imageVector = Icons.Default.Edit,
                contentDescription = "编辑"
            )
        }
        IconButton(onClick = { /* 分享操作 */ }) {
            Icon(
                imageVector = Icons.Default.Share,
                contentDescription = "分享"
            )
        }
    }
) {
    // 使用 content 插槽添加自定义内容
    Column(
        verticalArrangement = Arrangement.spacedBy(12.dp)
    ) {
        ChineseText(
            text = "通过 Slot API,我们可以在不修改组件内部实现的情况下,插入自定义的操作按钮和内容。",
            style = MaterialTheme.typography.bodyMedium
        )
        
        // 嵌套使用其他组件
        TagGroup(
            tags = listOf("Slot API", "组合模式", "可扩展性")
        )
    }
}

2.4 多插槽组件设计

InfoDisplayCard 组件

代码语言:kotlin
复制
/**
 * 信息展示卡片组件
 * 展示更复杂的组合模式
 */
@Composable
fun InfoDisplayCard(
    modifier: Modifier = Modifier,
    // 头部插槽
    header: @Composable () -> Unit = {},
    // 主要内容插槽
    content: @Composable ColumnScope.() -> Unit,
    // 底部操作插槽
    footer: @Composable RowScope.() -> Unit = {}
) {
    Card(
        modifier = modifier.fillMaxWidth(),
        elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
    ) {
        Column(
            modifier = Modifier.padding(16.dp)
        ) {
            // 头部插槽
            header()
            
            // 主要内容插槽
            content()
            
            // 底部操作插槽
            Column {
                Spacer(modifier = Modifier.height(16.dp))
                HorizontalDivider()
                Spacer(modifier = Modifier.height(16.dp))
                
                Row(
                    modifier = Modifier.fillMaxWidth(),
                    horizontalArrangement = Arrangement.End
                ) {
                    footer()
                }
            }
        }
    }
}

使用示例

代码语言:kotlin
复制
InfoDisplayCard(
    header = {
        Row(
            modifier = Modifier.fillMaxWidth(),
            horizontalArrangement = Arrangement.SpaceBetween,
            verticalAlignment = Alignment.CenterVertically
        ) {
            Column {
                ChineseText(
                    text = "信息展示卡片",
                    style = MaterialTheme.typography.titleMedium,
                    fontWeight = FontWeight.Bold
                )
                ChineseText(
                    text = "多插槽组合设计",
                    style = MaterialTheme.typography.bodySmall
                )
            }
            
            StatusBadge(
                text = "新功能",
                status = BadgeStatus.Info,
                size = BadgeSize.Small
            )
        }
    },
    content = {
        ChineseText(
            text = "这个组件展示了如何使用多个插槽(slot)来创建灵活的布局结构。",
            style = MaterialTheme.typography.bodyMedium
        )
    },
    footer = {
        TextButton(onClick = { /* 了解更多 */ }) {
            ChineseText(text = "了解更多")
        }
        
        Button(onClick = { /* 立即使用 */ }) {
            ChineseText(text = "立即使用")
        }
    }
)

三、性能优化技术

3.1 remember 和 derivedStateOf

前端对比

前端框架

优化技术

Compose 对应

React

useMemo

remember

React

useMemo (派生)

derivedStateOf

Vue

computed

derivedStateOf

React 示例

代码语言:jsx
复制
function SearchComponent() {
  const [searchText, setSearchText] = useState('');
  
  // useMemo 缓存计算结果
  const trimmedText = useMemo(() => {
    return searchText.trim();
  }, [searchText]);
  
  // useMemo 计算派生状态
  const searchResults = useMemo(() => {
    if (!trimmedText) return [];
    return data.filter(item => 
      item.name.includes(trimmedText)
    );
  }, [trimmedText, data]);
  
  return (
    <div>
      <input 
        value={searchText}
        onChange={e => setSearchText(e.target.value)}
      />
      <div>找到 {searchResults.length} 个结果</div>
    </div>
  );
}

Compose 实现

代码语言:kotlin
复制
@Composable
fun OptimizedSearchComponent(
    modifier: Modifier = Modifier,
    onSearch: (String) -> Unit = {}
) {
    var searchText by remember { mutableStateOf("") }
    var isSearching by remember { mutableStateOf(false) }
    
    // 使用 derivedStateOf 避免不必要的重组
    val trimmedSearchText by remember {
        derivedStateOf { searchText.trim() }
    }
    
    // 模拟搜索结果(派生状态)
    val searchResults by remember {
        derivedStateOf {
            if (trimmedSearchText.isEmpty()) {
                emptyList()
            } else {
                // 模拟搜索结果
                listOf(
                    "搜索结果:$trimmedSearchText 相关项目1",
                    "搜索结果:$trimmedSearchText 相关项目2",
                    "搜索结果:$trimmedSearchText 相关项目3"
                ).filter { it.contains(trimmedSearchText, ignoreCase = true) }
            }
        }
    }
    
    Column(
        modifier = modifier.fillMaxWidth(),
        verticalArrangement = Arrangement.spacedBy(12.dp)
    ) {
        // 搜索输入框
        OutlinedTextField(
            value = searchText,
            onValueChange = { searchText = it },
            modifier = Modifier.fillMaxWidth(),
            label = { ChineseText("搜索内容") }
        )
        
        // 搜索统计信息
        if (trimmedSearchText.isNotEmpty()) {
            ChineseText(
                text = "找到 ${searchResults.size} 个结果",
                style = MaterialTheme.typography.bodySmall
            )
        }
    }
}

核心差异

  • derivedStateOf 只在依赖变化时重新计算
  • ✅ 避免不必要的重组和性能开销
  • ✅ 类型安全的依赖追踪

3.2 LaunchedEffect 和防抖

前端对比

代码语言:javascript
代码运行次数:0
运行
复制
// JavaScript 防抖实现
function debounce(func, delay) {
  let timeoutId;
  return function(...args) {
    clearTimeout(timeoutId);
    timeoutId = setTimeout(() => {
      func.apply(this, args);
    }, delay);
  };
}

// React 使用防抖
function SearchComponent() {
  const [searchText, setSearchText] = useState('');
  
  useEffect(() => {
    const handler = setTimeout(() => {
      if (searchText.trim()) {
        performSearch(searchText);
      }
    }, 500);
    
    return () => clearTimeout(handler);
  }, [searchText]);
  
  return (
    <input 
      value={searchText}
      onChange={e => setSearchText(e.target.value)}
    />
  );
}

Compose 实现

代码语言:kotlin
复制
@Composable
fun OptimizedSearchComponent(
    modifier: Modifier = Modifier,
    onSearch: (String) -> Unit = {}
) {
    var searchText by remember { mutableStateOf("") }
    var isSearching by remember { mutableStateOf(false) }
    
    val trimmedSearchText by remember {
        derivedStateOf { searchText.trim() }
    }
    
    // 防抖搜索效果
    LaunchedEffect(trimmedSearchText) {
        if (trimmedSearchText.isNotEmpty()) {
            isSearching = true
            delay(500) // 防抖延迟
            onSearch(trimmedSearchText)
            isSearching = false
        }
    }
    
    OutlinedTextField(
        value = searchText,
        onValueChange = { searchText = it },
        modifier = Modifier.fillMaxWidth(),
        label = { ChineseText("搜索内容") },
        trailingIcon = {
            if (isSearching) {
                CircularProgressIndicator(modifier = Modifier.size(20.dp))
            }
        }
    )
}

优势

  • LaunchedEffect 自动管理协程生命周期
  • ✅ 当 key 变化时,旧协程自动取消
  • ✅ 组件销毁时自动清理资源
  • ✅ 更简洁的异步处理代码

3.3 计数器组件优化

完整示例

代码语言:kotlin
复制
@Composable
fun OptimizedCounterComponent(
    modifier: Modifier = Modifier
) {
    var count by remember { mutableIntStateOf(0) }
    
    // 使用 derivedStateOf 计算派生状态
    val isEven by remember {
        derivedStateOf { count % 2 == 0 }
    }
    
    val countDescription by remember {
        derivedStateOf {
            when {
                count == 0 -> "初始状态"
                count < 10 -> "个位数"
                count < 100 -> "两位数"
                else -> "三位数或更多"
            }
        }
    }
    
    Column(
        modifier = modifier.fillMaxWidth(),
        verticalArrangement = Arrangement.spacedBy(16.dp),
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        // 计数显示卡片
        Card(
            modifier = Modifier.fillMaxWidth(),
            elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
        ) {
            Column(
                modifier = Modifier.padding(24.dp),
                horizontalAlignment = Alignment.CenterHorizontally,
                verticalArrangement = Arrangement.spacedBy(12.dp)
            ) {
                ChineseText(
                    text = count.toString(),
                    style = MaterialTheme.typography.headlineLarge,
                    color = if (isEven) MaterialTheme.colorScheme.primary 
                           else MaterialTheme.colorScheme.secondary
                )
                
                StatusBadge(
                    text = if (isEven) "偶数" else "奇数",
                    status = if (isEven) BadgeStatus.Success else BadgeStatus.Info,
                    size = BadgeSize.Small
                )
                
                ChineseText(
                    text = countDescription,
                    style = MaterialTheme.typography.bodySmall
                )
            }
        }
        
        // 操作按钮
        Row(
            horizontalArrangement = Arrangement.spacedBy(12.dp)
        ) {
            Button(
                onClick = { count-- },
                enabled = count > 0
            ) {
                Icon(Icons.Default.Remove, contentDescription = "减少")
                Spacer(modifier = Modifier.width(4.dp))
                ChineseText("减少")
            }
            
            Button(onClick = { count = 0 }) {
                Icon(Icons.Default.Refresh, contentDescription = "重置")
                Spacer(modifier = Modifier.width(4.dp))
                ChineseText("重置")
            }
            
            Button(onClick = { count++ }) {
                Icon(Icons.Default.Add, contentDescription = "增加")
                Spacer(modifier = Modifier.width(4.dp))
                ChineseText("增加")
            }
        }
    }
}

3.4 实时数据组件

LaunchedEffect 管理副作用

代码语言:kotlin
复制
@Composable
fun RealTimeDataComponent(
    modifier: Modifier = Modifier
) {
    var data by remember { mutableStateOf(generateRandomData()) }
    var isAutoRefresh by remember { mutableStateOf(false) }
    var lastUpdateTime by remember { mutableStateOf(Clock.System.now()) }
    
    // 自动刷新效果
    LaunchedEffect(isAutoRefresh) {
        while (isAutoRefresh) {
            delay(2000) // 每2秒更新一次
            data = generateRandomData()
            lastUpdateTime = Clock.System.now()
        }
    }
    
    Column(
        modifier = modifier.fillMaxWidth(),
        verticalArrangement = Arrangement.spacedBy(16.dp)
    ) {
        // 控制面板
        Card {
            Row(
                modifier = Modifier
                    .fillMaxWidth()
                    .padding(16.dp),
                horizontalArrangement = Arrangement.SpaceBetween,
                verticalAlignment = Alignment.CenterVertically
            ) {
                Column {
                    ChineseText(
                        text = "自动刷新",
                        style = MaterialTheme.typography.bodyMedium
                    )
                    ChineseText(
                        text = if (isAutoRefresh) "每2秒更新" else "已暂停",
                        style = MaterialTheme.typography.bodySmall
                    )
                }
                
                Row(
                    horizontalArrangement = Arrangement.spacedBy(8.dp)
                ) {
                    Switch(
                        checked = isAutoRefresh,
                        onCheckedChange = { isAutoRefresh = it }
                    )
                    
                    IconButton(
                        onClick = {
                            data = generateRandomData()
                            lastUpdateTime = Clock.System.now()
                        }
                    ) {
                        Icon(
                            imageVector = Icons.Default.Refresh,
                            contentDescription = "手动刷新"
                        )
                    }
                }
            }
        }
        
        // 数据展示
        data.forEach { item ->
            DataItemCard(item = item)
        }
        
        // 更新时间显示
        ChineseText(
            text = "最后更新:${formatTimestamp(lastUpdateTime)}",
            style = MaterialTheme.typography.bodySmall
        )
    }
}

四、自定义组件设计原则

4.1 单一职责原则

每个组件应该专注于特定功能:

代码语言:kotlin
复制
// ✅ 正确:单一职责
@Composable
fun StatusBadge(
    text: String,
    status: BadgeStatus = BadgeStatus.Default
) {
    // 只负责显示状态徽章
}

// ❌ 错误:职责过多
@Composable
fun ComplexComponent(
    // 太多不相关的参数
    title: String,
    subtitle: String,
    badge: String,
    buttons: List<String>,
    data: List<Item>
) {
    // 组件做了太多事情
}

4.2 参数化和默认值

提供合理的默认值,简化使用:

代码语言:kotlin
复制
// ✅ 正确:提供默认值
@Composable
fun StatusBadge(
    text: String,                                    // 必需参数
    status: BadgeStatus = BadgeStatus.Default,       // 有默认值
    size: BadgeSize = BadgeSize.Medium,              // 有默认值
    modifier: Modifier = Modifier,                   // 有默认值
    icon: ImageVector? = null,                       // 可选参数
    onClick: (() -> Unit)? = null                    // 可选参数
) {
    // 实现
}

// 简单使用
StatusBadge(text = "成功")

// 完整配置
StatusBadge(
    text = "成功",
    status = BadgeStatus.Success,
    size = BadgeSize.Large,
    icon = Icons.Default.CheckCircle,
    onClick = { /* 处理点击 */ }
)

4.3 Modifier 优先

始终接受 Modifier 参数,让调用者控制样式:

代码语言:kotlin
复制
// ✅ 正确:接受 Modifier
@Composable
fun StatusBadge(
    text: String,
    modifier: Modifier = Modifier  // 接受 Modifier
) {
    Box(
        modifier = modifier  // 应用 Modifier
            .background(...)
            .padding(...)
    ) {
        // 内容
    }
}

// 使用时可以自定义样式
StatusBadge(
    text = "成功",
    modifier = Modifier
        .fillMaxWidth()
        .padding(16.dp)
)

4.4 组合优于继承

使用组合模式而非继承:

代码语言:kotlin
复制
// ✅ 正确:使用组合
@Composable
fun ExpandableCard(
    title: String,
    content: @Composable ColumnScope.() -> Unit  // 组合内容
) {
    Card {
        Column {
            Text(title)
            content()  // 调用者提供内容
        }
    }
}

// ❌ 错误:不要使用继承
// Compose 不支持组件继承

五、实际应用场景

5.1 表单构建

组合多个自定义组件构建复杂表单:

代码语言:kotlin
复制
@Composable
fun UserProfileForm() {
    var name by remember { mutableStateOf("") }
    var email by remember { mutableStateOf("") }
    var status by remember { mutableStateOf(BadgeStatus.Default) }
    
    ExpandableCard(
        title = "用户信息",
        subtitle = "编辑个人资料",
        icon = Icons.Default.Person,
        expandedInitially = true
    ) {
        Column(
            verticalArrangement = Arrangement.spacedBy(16.dp)
        ) {
            OutlinedTextField(
                value = name,
                onValueChange = { name = it },
                label = { ChineseText("姓名") },
                modifier = Modifier.fillMaxWidth()
            )
            
            OutlinedTextField(
                value = email,
                onValueChange = { email = it },
                label = { ChineseText("邮箱") },
                modifier = Modifier.fillMaxWidth()
            )
            
            Row(
                horizontalArrangement = Arrangement.spacedBy(8.dp)
            ) {
                ChineseText("账户状态:")
                StatusBadge(
                    text = "活跃",
                    status = BadgeStatus.Success,
                    size = BadgeSize.Small
                )
            }
        }
    }
}

5.2 数据展示卡片

代码语言:kotlin
复制
@Composable
fun ProductCard(product: Product) {
    InfoDisplayCard(
        header = {
            Row(
                modifier = Modifier.fillMaxWidth(),
                horizontalArrangement = Arrangement.SpaceBetween
            ) {
                ChineseText(
                    text = product.name,
                    style = MaterialTheme.typography.titleMedium,
                    fontWeight = FontWeight.Bold
                )
                StatusBadge(
                    text = product.status,
                    status = BadgeStatus.Success,
                    size = BadgeSize.Small
                )
            }
        },
        content = {
            Column(
                verticalArrangement = Arrangement.spacedBy(8.dp)
            ) {
                ChineseText(
                    text = product.description,
                    style = MaterialTheme.typography.bodyMedium
                )
                
                TagGroup(
                    tags = product.tags
                )
            }
        },
        footer = {
            TextButton(onClick = { /* 查看详情 */ }) {
                ChineseText("查看详情")
            }
            Button(onClick = { /* 立即购买 */ }) {
                ChineseText("立即购买")
            }
        }
    )
}

总结

通过本模块的学习,我对 Compose 自定义组件开发有了深入的理解。从参数化设计到组合模式,从性能优化到设计原则,Compose 提供了一套完整而优雅的组件开发体系。

相比前端框架,Compose 的类型安全设计(枚举类型、数据类)让组件 API 更加清晰可靠。Slot API 的设计理念与 React 的 render props 和 Vue 的插槽异曲同工,但通过 @Composable 函数参数提供了更强的类型约束。性能优化方面,rememberderivedStateOfLaunchedEffect 的组合使用,让我们能够轻松构建高性能的响应式组件。

对于前端开发者来说,掌握自定义组件开发是构建复杂应用的关键一步。下一步,我将继续探索应用架构与导航系统,学习如何将这些组件组织成完整的应用程序。


项目仓库https://github.com/easonxie/learn-jetpack-compose

后续会继续更新基于这个项目的更多内容,敬请期待!

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 从前端视角理解自定义组件
    • 组件设计思维对比
    • 设计原则对比
  • 快速体验自定义组件
    • 克隆项目
    • 运行 Lesson 05
  • 一、参数化组件设计
    • 1.1 什么是参数化设计?
    • 1.2 StatusBadge 组件实现
    • 1.3 类型安全的参数设计
    • 1.4 配置数据类
    • 1.5 使用示例
  • 二、组合模式设计
    • 2.1 什么是 Slot API?
    • 2.2 ExpandableCard 组件实现
    • 2.3 使用示例
    • 2.4 多插槽组件设计
  • 三、性能优化技术
    • 3.1 remember 和 derivedStateOf
    • 3.2 LaunchedEffect 和防抖
    • 3.3 计数器组件优化
    • 3.4 实时数据组件
  • 四、自定义组件设计原则
    • 4.1 单一职责原则
    • 4.2 参数化和默认值
    • 4.3 Modifier 优先
    • 4.4 组合优于继承
  • 五、实际应用场景
    • 5.1 表单构建
    • 5.2 数据展示卡片
  • 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档