Implementing Data Import (CSV, Excel) into Mobile App
Import — harder than export. When exporting, you control format. On import you get file user opened in Excel, renamed columns, added row "My data for 2024", saved in Windows-1251 and sent via Telegram. And all this needs to be recognized and loaded without crashes.
Parsing Incoming File — Most Unpredictable
Encoding detection. CSV comes in UTF-8, UTF-8 with BOM, Windows-1251, CP866. Universal solution — encoding detection library: on Android juniversalchardet, on iOS — own BOM analysis + fallback to String.Encoding.windowsCP1251. If encoding detected wrong — Клиент instead of "Client".
Delimiter detection. Parser should try ,, ;, \t and pick one giving most uniform columns. Or let user choose manually — more honest and reliable.
Empty rows, duplicate headers, mixed types. Real user files contain empty rows between blocks, merged cells in Excel, numbers in "date" column. Each needs explicit handling, not crash with ArrayIndexOutOfBoundsException.
Import Architecture
Step 1: File Selection
// 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)
Step 2: Parsing
On Android for CSV — opencsv or univocity-parsers (latter faster on big files, handles quote escaping correctly). For .xlsx — Apache POI XSSFWorkbook.
On iOS for CSV — own parser via Scanner or CSVParser from SwiftCSV. For .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))
Step 3: Validation and Mapping
Before writing to database — validate each row. Not "crash on first error", but collect all invalid rows and show summary: "Imported 847 of 900 rows. 53 rows skipped — incorrect date format in column D". User should understand what went wrong and fix file.
data class ImportResult(
val imported: Int,
val skipped: List<SkippedRow>
)
data class SkippedRow(val line: Int, val reason: String)
Step 4: Database Write
Transaction as whole — either all or nothing. On Room:
@Transaction
suspend fun importRows(rows: List<TransactionEntity>) {
database.clearAll()
database.insertAll(rows)
}
For incremental import (add new, update existing) — INSERT OR REPLACE with unique identifier field.
UI Patterns
Progress bar with current row (Processed 3,412 of 10,000), cancellation via Job.cancel() on Android / Task cancellation on iOS. After import — summary screen: how many added, updated, skipped with reasons.
Preview first 5–10 rows before import confirmation — good UX practice reducing "I uploaded the wrong thing" cases.
Typical Mistakes
- Parsing on main thread — ANR/freeze guaranteed on 1+ MB files
- Unhandled quotes in CSV:
"Ivanov, Ivan"without escaping breaks comma split - Date
01.03.2024vs2024-03-01vs3/1/24— three formats, all appear in real files
Work Scope
- File selection via DocumentPicker (CSV, XLS, XLSX)
- Auto-detect encoding and delimiter
- Validation with error report by rows
- Transactional database write
- UI progress and preview
- Import cancellation support
Timeline
Basic CSV import with fixed structure: 1–1.5 days. With auto format detection, validation, preview and error report: 3–4 days. Cost depends on field mapping complexity.







