Upload một lần một file 200 MB qua 4G sẽ thất bại khi mạng rớt ở 90% và phải làm lại từ đầu. Thiết kế cần trả lời được: mất mạng thì sao, app bị đóng thì sao, retry có tạo bản ghi trùng không.
1. Giảm dung lượng trước khi gửi: resize/nén ảnh, nén video trên máy — thường giảm nhiều lần so với file gốc.
2. Upload có thể resume (chunked): chia file thành các chunk, server nhớ đã nhận tới byte nào. Mạng rớt thì hỏi server offset hiện tại rồi gửi tiếp, không gửi lại từ đầu. Có thể dùng giao thức có sẵn: resumable upload của Google Cloud Storage (session URI, chunk là bội số 256 KiB), S3 multipart, hoặc tus. Thường app xin presigned URL rồi upload thẳng lên object storage, không đi qua API server.
3. Tách upload khỏi vòng đời màn hình: giao cho WorkManager để sống qua việc người dùng rời màn hình hoặc tắt app:
val request = OneTimeWorkRequestBuilder<UploadWorker>()
.setInputData(workDataOf("fileUri" to uri.toString()))
.setConstraints(
Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build()
)
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 30, TimeUnit.SECONDS)
.build()
// Unique work: tapping "upload" twice does not start two uploads of the same file
WorkManager.getInstance(context)
.enqueueUniqueWork("upload-$fileId", ExistingWorkPolicy.KEEP, request)Trong UploadWorker, lỗi mạng trả Result.retry() và lưu offset đã gửi; upload lớn người dùng đang chờ thì chạy dạng foreground (có notification tiến độ).
4. Idempotency: bước "tạo bài viết gắn file" gửi kèm idempotency key (ID sinh ở client), retry không tạo bài trùng.
Lưu ý: hiển thị trạng thái rõ ràng (đang chờ mạng, đang gửi x%, thất bại — thử lại) và cho phép chỉ upload qua Wi-Fi với file lớn (NetworkType.UNMETERED).