Реалізація імпорту даних (CSV, Excel) у мобільне додаток
Імпорт — складніший за експорт. Коли експортуєш, контролюєш формат сам. При імпорті отримуєш файл, який користувач відкрив у Excel, переименував колонки, додав строку-заголовок «Мої дані за 2024», зберіг у Windows-1251 та прислав через Telegram. І все це потрібно розпізнати та завантажити без крашів.
Разбір вхідного файлу — найнепередбачуваніше
Визначення кодування. CSV приходить у UTF-8, UTF-8 з BOM, Windows-1251, CP866. Універсальне рішення — бібліотека визначення кодування: на Android juniversalchardet, на iOS — власний аналіз BOM + fallback на String.Encoding.windowsCP1251. Якщо кодування не визначена правильно — Клиент замість «Клієнт».
Визначення розділювача. Парсер повинен пробовувати ,, ;, \t та вибирати той, що дає найбільше однорідних колонок. Або дати користувачу вибрати вручну — чесніше та надійніше.
Пусті рядки, дублі заголовків, змішані типи. Реальні користувальницькі файли містять пусті рядки між блоками, об'єднані ячейки у Excel, числа у колонці «дата». Кожен із цих випадків потрібно обробляти явно, а не падати з ArrayIndexOutOfBoundsException.
Архітектура імпорту
Крок 1: Вибір файлу
// Android — DocumentPicker
val launcher = registerForActivityResult(
ActivityResultContracts.GetContent()
) { uri -> uri?.let { viewModel.importFile(it) } }
launcher.launch("text/csv,application/vnd.ms-excel,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
// iOS — UIDocumentPickerViewController
let picker = UIDocumentPickerViewController(forOpeningContentTypes: [.commaSeparatedText, .spreadsheet])
picker.delegate = self
present(picker, animated: true)
Крок 2: Парсинг
На Android для CSV — opencsv або univocity-parsers (останній швидше на великих файлах та коректно обробляє екранування кавичок). Для .xlsx — Apache POI XSSFWorkbook.
На iOS для CSV — власний парсер через Scanner або CSVParser з SwiftCSV. Для .xlsx — CoreXLSX (Swift Package Manager).
// Android, univocity-parsers
val settings = CsvParserSettings().apply {
isHeaderExtractionEnabled = true
format.delimiter = ';'
}
val parser = CsvParser(settings)
val rows: List<Record> = parser.parseAllRecords(inputStream.reader(Charsets.UTF_8))
Крок 3: Валідація та маппінг
Перед записом у базу — валідація кожного рядка. Не «упасти на першій помилці», а зібрати всі невалідні рядки та показати сводку: «Імпортовано 847 з 900 рядків. 53 рядки пропущені — некоректний формат дати у колонці D». Користувач повинен зрозуміти, що пішло не так, та виправити файл.
data class ImportResult(
val imported: Int,
val skipped: List<SkippedRow>
)
data class SkippedRow(val line: Int, val reason: String)
Крок 4: Запис у базу даних
Транзакція цілком — або все, або нічого. На Room:
@Transaction
suspend fun importRows(rows: List<TransactionEntity>) {
database.clearAll()
database.insertAll(rows)
}
Для інкрементального імпорту (додати нові, оновити існуючі) — INSERT OR REPLACE з унікальним полем-ідентифікатором.
UI-паттерни
Прогрес-бар з поточним рядком (Обработано 3 412 з 10 000), скасування через Job.cancel() на Android / Task cancellation на iOS. Після імпорту — екран з итогами: скільки додано, скільки оновлено, скільки пропущено з причинами.
Предпросмотр перших 5–10 рядків перед підтвердженням імпорту — хороша UX-практика, яка знижує кількість «я не то завантажив» випадків.
Типічні помилки
- Парсинг на main thread — ANR/freeze гарантований на файлах від 1 МБ
- Не обработані кавичка у CSV:
"Іванов, Іван"без екранування ломає розбивку по комам - Дата
01.03.2024vs2024-03-01vs3/1/24— три різних формати, всі зустрічаються у реальних файлах
Обсяг роботи
- Вибір файлу через DocumentPicker (CSV, XLS, XLSX)
- Автовизначення кодування та розділювача
- Валідація з звітом про помилки по рядках
- Трансакційна запис у локальну БД
- UI прогресу та предпросмотру
- Підтримка скасування імпорту
Строки
Базовий CSV-імпорт з фіксованою структурою: 1–1,5 дня. З автовизначенням формату, валідацією, предпросмотром та звітом про помилки: 3–4 дні. Вартість залежит від складності маппінгу полів.







