You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Jetpack Compose中实现适配服务端数据的国家代码选择器

Jetpack Compose 国家代码选择器实现方案

效果参考

  • 默认收起状态:
    选择器默认状态示意图
  • 点击展开状态:
    选择器展开状态示意图

实现步骤

1. 定义数据结构与数据源

首先定义国家代码对应的数据类,存储接口返回的字段:

data class CountryCodeItem(
    val id: String,
    val countryName: String,
    val countryCode: String, // 国家区号,如+86
    val flagUrl: String? = null // 可选国旗资源地址
)

封装自有服务端的请求逻辑,示例采用协程挂起函数实现:

class CountryCodeRepository {
    // 替换为实际的服务端接口请求逻辑
    suspend fun getCountryCodeList(): Result<List<CountryCodeItem>> {
        return try {
            // 此处插入Retrofit/OkHttp等请求自有接口的代码
            val response = yourApiService.fetchCountryCodeList()
            Result.success(response)
        } catch (e: Exception) {
            Result.failure(e)
        }
    }
}

2. 核心选择器组件实现

组件内部维护展开状态、选中项、列表加载状态,完整实现代码如下:

@Composable
fun CountryCodePicker(
    modifier: Modifier = Modifier,
    defaultSelected: CountryCodeItem? = null,
    onSelected: (CountryCodeItem) -> Unit
) {
    // 控制下拉展开/收起状态
    var expanded by rememberSaveable { mutableStateOf(false) }
    // 当前选中的国家代码项
    var selectedItem by rememberSaveable { mutableStateOf<CountryCodeItem?>(defaultSelected) }
    // 服务端返回的国家代码列表
    var countryList by remember { mutableStateOf<List<CountryCodeItem>>(emptyList()) }
    // 加载状态标记
    var isLoading by remember { mutableStateOf(false) }
    // 错误提示内容
    var errorMsg by remember { mutableStateOf<String?>(null) }
    val coroutineScope = rememberCoroutineScope()
    val repository = remember { CountryCodeRepository() }

    // 组件首次加载时拉取服务端列表
    LaunchedEffect(Unit) {
        isLoading = true
        val result = repository.getCountryCodeList()
        result.onSuccess {
            countryList = it
            // 未设置默认选中项时,可默认匹配当前系统区域的国家代码,可选逻辑
            if (selectedItem == null && it.isNotEmpty()) {
                val defaultItem = it.firstOrNull { item ->
                    item.countryCode == Locale.getDefault().country
                } ?: it.first()
                selectedItem = defaultItem
                onSelected(defaultItem)
            }
            errorMsg = null
        }.onFailure {
            errorMsg = "列表加载失败,点击重试"
        }
        isLoading = false
    }

    Column(modifier = modifier) {
        // 头部点击区域:展示当前选中状态
        Row(
            modifier = Modifier
                .fillMaxWidth()
                .clickable {
                    if (countryList.isNotEmpty()) {
                        expanded = !expanded
                    } else if (errorMsg != null) {
                        // 加载失败时点击重试
                        coroutineScope.launch {
                            isLoading = true
                            repository.getCountryCodeList().onSuccess { list ->
                                countryList = list
                                errorMsg = null
                            }.onFailure {
                                errorMsg = "列表加载失败,点击重试"
                            }
                            isLoading = false
                        }
                    }
                }
                .padding(horizontal = 16.dp, vertical = 12.dp),
            verticalAlignment = Alignment.CenterVertically,
            horizontalArrangement = Arrangement.SpaceBetween
        ) {
            when {
                isLoading -> Text("加载中...")
                errorMsg != null -> Text(errorMsg ?: "", color = Color.Red)
                selectedItem != null -> {
                    Row(verticalAlignment = Alignment.CenterVertically) {
                        // 可选:展示国旗图标
                        selectedItem?.flagUrl?.let { url ->
                            AsyncImage(
                                model = url,
                                contentDescription = null,
                                modifier = Modifier.size(24.dp),
                                placeholder = painterResource(R.drawable.default_flag)
                            )
                            Spacer(modifier = Modifier.width(8.dp))
                        }
                        Text(selectedItem?.countryCode ?: "")
                        Spacer(modifier = Modifier.width(4.dp))
                        Text(selectedItem?.countryName ?: "", color = Color.Gray)
                    }
                    // 下拉箭头
                    Icon(
                        imageVector = if (expanded) Icons.Default.ArrowDropUp else Icons.Default.ArrowDropDown,
                        contentDescription = null
                    )
                }
            }
        }

        // 展开的下拉列表
        if (expanded && countryList.isNotEmpty()) {
            Column(
                modifier = Modifier
                    .fillMaxWidth()
                    .heightIn(max = 300.dp)
                    .verticalScroll(rememberScrollState())
                    .border(1.dp, Color.LightGray, RoundedCornerShape(4.dp))
            ) {
                countryList.forEach { item ->
                    Row(
                        modifier = Modifier
                            .fillMaxWidth()
                            .clickable {
                                selectedItem = item
                                expanded = false
                                onSelected(item)
                            }
                            .padding(horizontal = 16.dp, vertical = 12.dp),
                        verticalAlignment = Alignment.CenterVertically
                    ) {
                        // 可选:展示国旗图标
                        item.flagUrl?.let { url ->
                            AsyncImage(
                                model = url,
                                contentDescription = null,
                                modifier = Modifier.size(24.dp),
                                placeholder = painterResource(R.drawable.default_flag)
                            )
                            Spacer(modifier = Modifier.width(8.dp))
                        }
                        Text(item.countryCode)
                        Spacer(modifier = Modifier.width(8.dp))
                        Text(item.countryName, color = Color.Gray)
                        // 选中状态标记
                        if (item.id == selectedItem?.id) {
                            Spacer(modifier = Modifier.weight(1f))
                            Icon(
                                imageVector = Icons.Default.Check,
                                contentDescription = null,
                                tint = Color.Blue
                            )
                        }
                    }
                }
            }
        }
    }
}

提示:代码中AsyncImage使用Coil图片加载库实现,如需使用需在build.gradle中引入Coil的Compose依赖。

3. 组件调用示例

@Composable
fun RegisterScreen() {
    Column(modifier = Modifier.padding(16.dp)) {
        Text("请选择国家代码")
        Spacer(modifier = Modifier.height(8.dp))
        CountryCodePicker(
            modifier = Modifier.fillMaxWidth(),
            onSelected = { selectedCountry ->
                // 处理选中结果
                Log.d("CountryCode", "选中:${selectedCountry.countryCode} ${selectedCountry.countryName}")
            }
        )
    }
}

可选优化点

  • 可添加搜索框,支持按国家名/区号快速筛选
  • 实现点击下拉列表外部区域自动收起,可通过Clickable Modifier配合全局触摸事件监听实现
  • 列表可添加分组,比如常用国家置顶
  • 支持深色模式适配

内容的提问来源于stack exchange,提问作者Saeed Noshadi

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.28 08:06:03