← Coding round practice

Image Upload with Progress, Retry and Background Completion

HardWorkManager · CoroutinesSurvives process death

The problem

Let the user pick an image and upload it with a visible progress bar.

Requirements:

  • Progress updates while uploading, and a cancel button.
  • The upload must finish even if the user leaves the screen or the app is backgrounded.
  • Automatic retry with backoff on a network failure.
  • Compress before upload; do not send a 12 MP original.
Stuck? Open one hint

Ask yourself who owns the upload. If it is the ViewModel, leaving the screen kills it — so it cannot be the ViewModel.

Show the worked solution

The trap is doing this in viewModelScope. That dies with the screen, so the requirement "finishes after the user leaves" fails.

The right shape:

  • WorkManager owns the upload, with a network constraint and exponential backoff. It is persisted, so it survives process death and reboot.
  • setProgress publishes progress; the UI observes the WorkInfo, so progress survives rotation and screen exit.
  • Compress first — decode with a sampled size, re-encode as JPEG/WebP around 70-80%, and keep the temp file in cache.
  • Cancel is cancelWorkById, and the worker must check isStopped.
  • Foreground info if the upload is long and user-visible, so the system does not kill it.

Saying "WorkManager, because the work must outlive the screen" in the first thirty seconds is most of the score.

KotlinSurvives process death
class UploadWorker(ctx: Context, params: WorkerParameters) : CoroutineWorker(ctx, params) {

    override suspend fun doWork(): Result {
        val uri = inputData.getString(KEY_URI)?.toUri() ?: return Result.failure()

        return try {
            val file = compress(uri)                       // sampled decode + re-encode
            api.upload(file) { sent, total ->
                setProgressAsync(workDataOf(KEY_PROGRESS to (sent * 100 / total).toInt()))
            }
            Result.success()
        } catch (e: IOException) {
            if (runAttemptCount < 3) Result.retry() else Result.failure()
        }
    }

    companion object { const val KEY_URI = "uri"; const val KEY_PROGRESS = "progress" }
}

// enqueue
val request = OneTimeWorkRequestBuilder<UploadWorker>()
    .setInputData(workDataOf(UploadWorker.KEY_URI to uri.toString()))
    .setConstraints(Constraints(requiredNetworkType = NetworkType.CONNECTED))
    .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 10, TimeUnit.SECONDS)
    .build()

WorkManager.getInstance(context).enqueueUniqueWork("upload-$id", ExistingWorkPolicy.KEEP, request)