Last active
May 9, 2020 07:33
-
-
Save roschlau/246e9ae7f2f8e4abb0576e4d8415c858 to your computer and use it in GitHub Desktop.
Kotlin Coroutines implementation of "More Granular Retry" from https://www.techyourchance.com/concurrency-frameworks-overrated-android/
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import kotlinx.coroutines.Dispatchers | |
import kotlinx.coroutines.async | |
import kotlinx.coroutines.suspendCancellableCoroutine | |
import kotlinx.coroutines.withContext | |
import java.io.File | |
class UploadFilesUseCase : BaseBusyObservable<UploadFilesUseCase.Listener>() { | |
interface Listener { | |
fun onFilesUploaded() | |
fun onFilesUploadFailed() | |
} | |
companion object { | |
private const val MAX_RETRIES = 3 | |
} | |
suspend fun uploadFiles() { | |
if (!isFreeAndBecomeBusy()) { | |
// log concurrent invocation attempt | |
return | |
} | |
uploadFilesSync() | |
} | |
private suspend fun uploadFilesSync() = withContext(Dispatchers.IO) { | |
val aJob = async { processAndMergeFilesOfTypeAWithRetry() } | |
val bJob = async { processAndMergeFilesOfTypeBWithRetry() } | |
val mergedA = aJob.await() | |
val mergedB = bJob.await() | |
if (mergedA == null || mergedB == null) { | |
flowFailed() | |
return@withContext | |
} | |
val archive: File = try { | |
compressMergedFiles(mergedA, mergedB) | |
} catch (e: OperationFailedException) { | |
flowFailed() | |
throw e | |
} | |
if (uploadFileToServerWithRetry(archive)) { | |
deleteTempDir() | |
notifySuccess() | |
} else { | |
flowFailed() | |
} | |
} | |
private suspend fun uploadFileToServerWithRetry(archive: File): Boolean { | |
for (i in 0 until MAX_RETRIES) { | |
val responseCode = uploadFileToServer(archive) | |
if (responseCode / 100 == 2) { | |
return true | |
} | |
} | |
return false | |
} | |
private suspend fun uploadFileToServer(archive: File): Int { | |
return suspendCancellableCoroutine { continuation -> | |
HttpManager.getInstance.uploadFiles( | |
archive, | |
object : HttpRequestListener() { | |
fun onDone(code: Int, body: ByteArray?) { | |
continuation.resumeWith(Result.success(code)) | |
} | |
fun onFailure() { | |
continuation.resumeWith(Result.success(0)) | |
} | |
} | |
) | |
} | |
} | |
private suspend fun flowFailed() { | |
deleteTempDir() | |
notifyFailure() | |
} | |
private fun processAndMergeFilesOfTypeAWithRetry(): File? { | |
for (i in 0 until MAX_RETRIES) { | |
try { | |
// ... | |
} catch (e: OperationFailedException) { | |
// log the exception | |
} | |
} | |
return null | |
} | |
private fun processAndMergeFilesOfTypeBWithRetry(): File? { | |
for (i in 0 until MAX_RETRIES) { | |
try { | |
// ... | |
} catch (e: OperationFailedException) { | |
// log the exception | |
} | |
} | |
return null | |
} | |
@Throws(OperationFailedException::class) | |
private fun compressMergedFiles(fileA: File?, fileB: File?): File { TODO() } | |
private fun deleteTempDir() { TODO() } | |
private suspend fun notifySuccess() = withContext(Dispatchers.Main) { | |
for (listener in listeners) { | |
listener.onFilesUploaded() | |
} | |
becomeNotBusy() | |
} | |
private suspend fun notifyFailure() = withContext(Dispatchers.Main) { | |
for (listener in listeners) { | |
listener.onFilesUploadFailed() | |
} | |
becomeNotBusy() | |
} | |
} |
How to get rid of BaseBusyObservable's uploadFilesSync and becomeNotBusy?
I don't know the implementation details and requirements of the base class, so I just left these as they are. Would probably need to use a Mutex under the hood to play nice with coroutines.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
@techyourchance It's been quite some time since I actually worked on Android and have thus never worked with ViewModel, so I have no idea how that would integrate specifically 😅 So yeah, points taken, Making the coroutines an implementation detail certainly makes sense in this kind of scenario. I'm used to working in a backend service that's coroutines through and through, so I didn't really think about this.
About awaitAll vs individual awaits: Mostly because I wanted to leave the function signatures mostly intact, and having the async jobs in a list would have required to change the
compressMergedFiles
call, or work around it awkwardly. So, I just left it that way. But yeah, in a real scenario I would probably use that as well.