28、联系人导出导入:CSV格式导出、Excel格式导出、批量导入联系人、导入进度条与错误处理
说到联系人数据的迁移,我猜你肯定遇到过这种场景:用户换手机了,或者想把几百个同事的电话批量录入系统。手动一个个敲?那不得疯掉。所以导出导入功能,几乎是每个联系人应用的刚需。
今天我们就来聊聊,怎么把联系人数据玩转起来。从CSV到Excel,从导出到批量导入,再到进度条和错误处理,一条龙讲清楚。
核心知识点一览
- CSV格式导出:轻量、通用、兼容性好
- Excel格式导出:适合办公场景,支持多Sheet
- 批量导入:解析文件、校验数据、写入数据库
- 导入进度条:让用户知道“还要等多久”
- 错误处理:哪行错了?错在哪?给用户明确反馈
CSV格式导出:简单但别踩坑
CSV 说白了就是逗号分隔的文本文件。轻量、通用,任何文本编辑器都能打开。我个人习惯用 CSV 做快速数据备份,因为它的读写速度比 Excel 快不少。
但有个坑——中文乱码。Android 默认的 CSV 写入用的是 UTF-8,但 Windows 上的 Excel 打开 UTF-8 的 CSV 会乱码。怎么办?我建议在 CSV 文件头部写入 BOM(Byte Order Mark),也就是 0xEF 0xBB 0xBF 这三个字节。这样 Excel 就能正确识别编码了。
小技巧:导出 CSV 时,如果某个字段里包含逗号、换行符或双引号,记得用双引号包裹整个字段,并且把字段内的双引号转义成两个双引号。这是 CSV 标准,很多新手会忽略。
// CSV 导出核心代码片段
fun exportContactsToCsv(contacts: List<Contact>, file: File) {
file.bufferedWriter().use { writer ->
// 写入 BOM,防止中文乱码
writer.write("\uFEFF")
// 写入表头
writer.write("姓名,电话,邮箱,备注\n")
// 写入数据行
contacts.forEach { contact ->
val name = escapeCsvField(contact.name)
val phone = escapeCsvField(contact.phone)
val email = escapeCsvField(contact.email)
val note = escapeCsvField(contact.note)
writer.write("$name,$phone,$email,$note\n")
}
}
}
fun escapeCsvField(field: String): String {
return if (field.contains(",") || field.contains("\"") || field.contains("\n")) {
"\"${field.replace("\"", "\"\"")}\""
} else {
field
}
}
Excel格式导出:更专业的办公选择
Excel 导出我一般用 Apache POI 库。虽然库体积大了点,但胜在功能全。你可以创建多个 Sheet,设置单元格样式,甚至加个筛选按钮。
我记得有一次,产品经理要求导出的 Excel 里,电话号码列必须显示为文本格式,不然长数字会被科学计数法显示。嗯,这里要注意:在 POI 里设置单元格格式为 HSSFDataFormat 的文本格式,或者直接设置单元格类型为 CellType.STRING。
// Excel 导出核心代码片段(使用 Apache POI)
fun exportContactsToExcel(contacts: List<Contact>, file: File) {
val workbook = XSSFWorkbook()
val sheet = workbook.createSheet("联系人")
// 创建表头行
val headerRow = sheet.createRow(0)
val headers = arrayOf("姓名", "电话", "邮箱", "备注")
headers.forEachIndexed { index, header ->
headerRow.createCell(index).setCellValue(header)
}
// 填充数据
contacts.forEachIndexed { index, contact ->
val row = sheet.createRow(index + 1)
row.createCell(0).setCellValue(contact.name)
// 电话列强制设为文本格式
val phoneCell = row.createCell(1)
phoneCell.setCellType(CellType.STRING)
phoneCell.setCellValue(contact.phone)
row.createCell(2).setCellValue(contact.email)
row.createCell(3).setCellValue(contact.note)
}
// 自动调整列宽
for (i in 0..3) {
sheet.autoSizeColumn(i)
}
file.outputStream().use { workbook.write(it) }
workbook.close()
}
注意:POI 的 XSSFWorkbook 对应 .xlsx 格式,HSSFWorkbook 对应 .xls 格式。.xlsx 支持更多行数(1048576行),建议优先使用。如果你的联系人超过 65535 行,千万别用 .xls 格式,否则数据会被截断。
批量导入联系人:解析、校验、写入三步走
批量导入看起来简单,其实最容易出问题。我曾经在一个项目中,用户导入一个 5000 行的 CSV,结果因为某一行电话号码格式不对,整个导入失败,用户气得跳脚。
所以我的做法是:先解析,再校验,最后批量写入。解析阶段只读文件,不写数据库。校验阶段逐行检查,把错误行记录下来。写入阶段用事务批量提交,要么全成功,要么全回滚。
// 批量导入核心逻辑(伪代码)
fun importContacts(file: File, onProgress: (Int, Int) -> Unit, onError: (List<ImportError>) -> Unit) {
val lines = file.readLines()
val total = lines.size - 1 // 减去表头
val errors = mutableListOf<ImportError>()
val validContacts = mutableListOf<Contact>()
// 第一步:解析与校验
lines.drop(1).forEachIndexed { index, line ->
val fields = parseCsvLine(line)
if (fields.size < 2) {
errors.add(ImportError(index + 2, "字段数量不足,至少需要姓名和电话"))
return@forEachIndexed
}
val name = fields[0].trim()
val phone = fields[1].trim()
if (name.isEmpty()) {
errors.add(ImportError(index + 2, "姓名为空"))
return@forEachIndexed
}
if (!phone.matches(Regex("^1[3-9]\\d{9}$"))) {
errors.add(ImportError(index + 2, "电话号码格式不正确"))
return@forEachIndexed
}
validContacts.add(Contact(name = name, phone = phone, email = fields.getOrElse(2) { "" }))
onProgress(index + 1, total)
}
// 如果有错误,直接返回,不写入数据库
if (errors.isNotEmpty()) {
onError(errors)
return
}
// 第二步:批量写入数据库(事务)
database.transaction {
validContacts.forEach { contact ->
insertContact(contact)
}
}
onProgress(total, total)
}
导入进度条:别让用户干等
用户导入几百个联系人时,如果界面卡住不动,用户会以为 App 死了。所以进度条是必须的。我个人习惯用 ProgressBar 配合 LiveData 或 StateFlow 来更新 UI。
你想想看,如果导入 1000 个联系人,每处理 10 个更新一次进度,用户就能看到“正在导入第 120/1000 个”,心里就有底了。
实现要点:
- 在后台线程执行导入操作(比如使用 Kotlin 协程的
Dispatchers.IO) - 每处理一批数据,通过
Channel或MutableStateFlow发送进度事件 - UI 层收集事件,更新
ProgressBar的进度和文本 - 导入完成后,显示成功提示,如果有错误则展示错误列表
// 使用 StateFlow 更新进度
class ImportViewModel : ViewModel() {
private val _progress = MutableStateFlow(0 to 0) // (当前, 总数)
val progress: StateFlow<Pair<Int, Int>> = _progress.asStateFlow()
fun startImport(file: File) {
viewModelScope.launch(Dispatchers.IO) {
importContacts(file,
onProgress = { current, total ->
_progress.value = current to total
},
onError = { errors ->
// 处理错误
}
)
}
}
}
错误处理:给用户一个交代
导入过程中,错误是难免的。但你不能只弹一个“导入失败”就完事。用户需要知道:哪一行错了?错在哪里?
我建议把错误信息组织成一个列表,每一行包含:行号、原始数据、错误原因。这样用户可以快速定位问题,修改后重新导入。
| 行号 | 原始数据 | 错误原因 |
|---|---|---|
| 3 | 张三,13800138000,zhang@test.com | 电话号码格式不正确(缺少一位) |
| 7 | ,13912345678, | 姓名为空 |
| 12 | 李四,abc123,li@test.com | 电话号码包含非数字字符 |
避坑指南:我曾经遇到过一个情况,用户导入的 CSV 文件里,某一行末尾多了一个逗号,导致解析出 5 个字段,而表头只有 4 列。所以解析 CSV 时,建议忽略空字段,或者严格校验字段数量。另外,文件编码也容易出问题,最好在导入时让用户选择编码格式,或者自动检测 BOM。
好了,关于联系人导出导入的核心内容就这些。从 CSV 到 Excel,从导出到导入,再到进度条和错误处理,每一步都有细节需要注意。希望这些经验能帮你少走弯路。