I have been working on a file upload feature for an app given specific requirements. The upload endpoint is hit with a series of multipart requests and results in a single "submission". Consider it a batch of X files. A progress indicator for each file is very much desired. Files are allowed to be large and must upload on slow connections, due to this they are being uploaded in succession.
Assuming the app stays active in the foreground it behaves as intended. My trouble has been handling the series of file uploads when the app is placed in the background. As of now, based on the logging, the uploads are pausing until the app is resumed. Something about the process also causes a retry that in some cases uploads the file multiple times based on how many times the app is suspended and resumed.
Whats the best way of dealing with this?
Service.kt
@Multipart
@POST("upload/files")
suspend fun uploadFilesWithName(@Header("finalFile") finalFile: Boolean,
@Part file: MultipartBody.Part) : Response<FileUploadResponse>
Repository.kt
fun uploadFilesWithComplete(files: MutableList<MultipartBody.Part>): LiveData<ApiState<FileUploadResponse>?> = liveData {
val counter = AtomicInteger()
files.forEachIndexed { index, file ->
submitFilesWithName(finalFile = (files.size - 1 == index), file = file)
.onStart { emit(ApiState.loading()) }
.collect { response ->
counter.incrementAndGet()
when(response) {
is ApiSuccessResponse<FileUploadResponse> -> {
emit(ApiState.success(data = response.data, position = index))
}
is ApiErrorResponse<FileUploadResponse> -> {
if (response.error != null) {
emit(
ApiState.error(
error = response.error,
msg = response.error.validation.summary,
position = index
)
)
} else {
emit(
ApiState.error(
error = null,
msg = "Error uploading file.",
position = index
)
)
}
}
else -> {}
}
}
}
}
suspend fun submitFilesWithName(finalFile: Boolean = false,
file: MultipartBody.Part
) : Flow<ApiResponse<FileUploadResponse>?> {
return flow {
emit (
ApiResponse.make(
requestFunc = { service.uploadFilesWithName(
finalFile = finalFile,
file = file
)},
maxRetries = SHORT_RETRY_ATTEMPTS
)
)
}.flowOn(Dispatchers.IO)
}
FileUploadDialog.kt
val files: MutableList<MultipartBody.Part>
... // Add files to the multipart body list
repository?.uploadFilesWithComplete(files = files)?.observe(viewLifecycleOwner) { response ->
if (response != null) {
when (response.status) {
Status.LOADING -> { /* kick off a loading animation */ }
Status.SUCCESS -> {
response.position?.let { position ->
selectedFiles.value?.let { selected ->
selected[position].apply {
inError = false
errorMessage = ""
isComplete = true
}
binding.fileList.adapter?.notifyItemChanged(position)
}
}
}
Status.ERROR -> {
response.position?.let { position ->
selectedFiles.value?.let { selected ->
selected[position].apply {
inError = true
errorMessage = "Upload Failed"
isComplete = true
}
binding.fileList.adapter?.notifyItemChanged(position)
}
}
}
}
}
}
Google recommends using Work Manager
for such things. From what you said it could be considered as Long-running work
.
https://developer.android.com/guide/background
Another alternative is to use Services
but this shouldn't be your first option.
Android 12 restricts launching foreground services from the background. For most cases, you should use setForeground() from WorkManager rather than handle foreground services yourself. This allows WorkManager to manage the lifecycle of the foregound service, ensuring efficiency.
You still should use foreground services to perform tasks that are long running and need to notify the user that they are ongoing. If you use foreground services directly, ensure you shut down the service correctly to preserve resource efficiency.
Also there are multiple libraries that help with that stuff but as any 3th party libraries, the development is uncertain and not always up-to-date.