28、联系人导出导入:CSV格式导出、Excel格式导出、批量导入联系人、导入进度条与错误处理

说到联系人数据的迁移,我猜你肯定遇到过这种场景:用户换手机了,或者想把几百个同事的电话批量录入系统。手动一个个敲?那不得疯掉。所以导出导入功能,几乎是每个联系人应用的刚需。

今天我们就来聊聊,怎么把联系人数据玩转起来。从CSV到Excel,从导出到批量导入,再到进度条和错误处理,一条龙讲清楚。

核心知识点一览

  • CSV格式导出:轻量、通用、兼容性好
  • Excel格式导出:适合办公场景,支持多Sheet
  • 批量导入:解析文件、校验数据、写入数据库
  • 导入进度条:让用户知道“还要等多久”
  • 错误处理:哪行错了?错在哪?给用户明确反馈
联系人导出导入知识体系 联系人数据迁移 CSV格式导出 Excel格式导出 批量导入 文件解析与校验 进度条反馈 错误处理与回滚 导出是“读数据→格式化→写文件”,导入是“读文件→校验→写数据库”

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 配合 LiveDataStateFlow 来更新 UI。

你想想看,如果导入 1000 个联系人,每处理 10 个更新一次进度,用户就能看到“正在导入第 120/1000 个”,心里就有底了。

实现要点:

  • 在后台线程执行导入操作(比如使用 Kotlin 协程的 Dispatchers.IO
  • 每处理一批数据,通过 ChannelMutableStateFlow 发送进度事件
  • 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,从导出到导入,再到进度条和错误处理,每一步都有细节需要注意。希望这些经验能帮你少走弯路。

公众号:蓝海资料掘金营,微信deep3321