Created
November 15, 2020 14:49
-
-
Save alexandrepiveteau/d6a0d04bd2b0475fb62a1d5e4f5587d7 to your computer and use it in GitHub Desktop.
A better coroutine-based HTTP requests manager
This file contains hidden or 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
| package ch.heigvd.iict.sym.labo2.betterComm | |
| import ch.heigvd.iict.sym.labo2.betterComm.RequestManager.Backoff | |
| import ch.heigvd.iict.sym.labo2.betterComm.RequestManager.ResponseOrCancelled.Cancelled | |
| import ch.heigvd.iict.sym.labo2.betterComm.RequestManager.ResponseOrCancelled.Success | |
| import kotlinx.coroutines.Dispatchers | |
| import kotlinx.coroutines.delay | |
| import kotlinx.coroutines.withContext | |
| /** | |
| * A class that can handle requests in a suspending fashion, with support for custom [Backoff] | |
| * strategies and automatic request re-scheduling. | |
| * | |
| * Using a suspending function (rather than a callback-based API) allows for easier composition of | |
| * multiple requests together, since coroutine builders (such as [kotlinx.coroutines.async] and | |
| * [kotlinx.coroutines.launch]) can be used to compose parallel requests. | |
| * | |
| * Additionally, error handling can be treated as if the code was sequential. | |
| */ | |
| class RequestManager { | |
| /** | |
| * A sealed class representing a successful response for the request, or an indication that the | |
| * request was cancelled by the [Backoff] strategy. | |
| * | |
| * We may also want to use [kotlin.Result] when the ABI for inline classes is stable. | |
| */ | |
| sealed class ResponseOrCancelled { | |
| data class Success(val response: Response) : ResponseOrCancelled() | |
| object Cancelled : ResponseOrCancelled() | |
| } | |
| /** | |
| * An interface defining a [Backoff] strategy. A [Backoff] is responsible for the scheduling of | |
| * a certain call to the API. | |
| * | |
| * When scheduled, a [Backoff] should specify a [Result] after executing. This result indicates | |
| * whether the response was a success, if the [Backoff] decided to cancel the request or if the | |
| * request should be rescheduled. If rescheduled, the [Backoff.schedule] method will be called | |
| * again. | |
| * | |
| * A benefit of using a suspending [Backoff] API is that [Backoff]s can await certain conditions | |
| * to be true before resuming. In particular, we may be interested in listening for some | |
| * system services (such as the [android.net.ConnectivityManager] before triggering a request. | |
| */ | |
| interface Backoff { | |
| /** | |
| * A sealed class representing the result of the [Backoff.schedule] call. | |
| */ | |
| sealed class Result { | |
| data class Success(val response: Response) : Result() | |
| object Cancel : Result() | |
| object Reschedule : Result() | |
| } | |
| /** | |
| * Schedules the execution of a certain [call], for a provided [request]. The scheduling | |
| * might be dependent on the kind of [Request] we're making. | |
| * | |
| * @param request the [Request] that should be scheduled and executed. | |
| * @param call the suspending function to use to perform the [Request]. | |
| * | |
| * @return a [Result] indicating how the execution of the request went, and if retry is | |
| * needed. | |
| */ | |
| suspend fun schedule( | |
| request: Request, | |
| call: suspend (Request) -> Response?, | |
| ): Result | |
| // For extension functions builders. | |
| companion object | |
| } | |
| /** | |
| * Performs a new [Request], with a specified [Backoff] policy. | |
| * | |
| * @param request the request to perform. | |
| * @param backoff the [Backoff] policy to apply. | |
| * | |
| * @return a [ResponseOrCancelled] that indicates how the response went. We could use a | |
| * [kotlin.Result] once the inline classes ABI is stable. | |
| */ | |
| suspend fun request( | |
| request: Request, | |
| backoff: Backoff = Backoff.Once, | |
| ): ResponseOrCancelled { | |
| val perform: suspend (Request) -> Response? = { | |
| withContext(Dispatchers.IO) { | |
| TODO("This is where our UrlConnection implementation would go.") | |
| } | |
| } | |
| while (true) { | |
| when (val result = backoff.schedule(request, perform)) { | |
| is Backoff.Result.Success -> { | |
| return Success(result.response) | |
| } | |
| Backoff.Result.Cancel -> { | |
| return Cancelled | |
| } | |
| Backoff.Result.Reschedule -> { | |
| /* Ignored. */ | |
| } | |
| } | |
| } | |
| } | |
| /** | |
| * A data class representing what we want our requests to do. | |
| */ | |
| data class Request( | |
| val resource: String, | |
| val headers: Map<String, List<String>> = emptyMap(), | |
| val content: String? = null, | |
| // ... | |
| ) | |
| /** | |
| * A data class representing what we want our responses to look like. | |
| */ | |
| data class Response( | |
| val status: Int, | |
| val headers: Map<String, List<String>>, | |
| val content: String, | |
| // ... | |
| ) | |
| } | |
| /** | |
| * Creates a new [Backoff] that does not reschedule a failed [RequestManager.Request]. | |
| */ | |
| val Backoff.Companion.Once: Backoff | |
| get() = object : Backoff { | |
| override suspend fun schedule( | |
| request: RequestManager.Request, | |
| call: suspend (RequestManager.Request) -> RequestManager.Response?, | |
| ): Backoff.Result { | |
| return when (val response = call(request)) { | |
| null -> Backoff.Result.Cancel | |
| else -> Backoff.Result.Success(response) | |
| } | |
| } | |
| } | |
| /** | |
| * Creates a new [Backoff] that will always be re-scheduled for the same amount of time. | |
| */ | |
| fun Backoff.Companion.repeat(every: Long): Backoff = object : Backoff { | |
| var first = true | |
| override suspend fun schedule( | |
| request: RequestManager.Request, | |
| call: suspend (RequestManager.Request) -> RequestManager.Response? | |
| ): Backoff.Result { | |
| val delay = if (first) 0 else every | |
| delay(delay) | |
| first = false | |
| return when (val response = call(request)) { | |
| null -> Backoff.Result.Reschedule | |
| else -> Backoff.Result.Success(response) | |
| } | |
| } | |
| } |
This file contains hidden or 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
| package ch.heigvd.iict.sym.labo2.betterComm | |
| import android.os.Bundle | |
| import android.widget.TextView | |
| import androidx.appcompat.app.AppCompatActivity | |
| import androidx.lifecycle.lifecycleScope | |
| import ch.heigvd.iict.sym.labo2.betterComm.RequestManager.ResponseOrCancelled.Cancelled | |
| import ch.heigvd.iict.sym.labo2.betterComm.RequestManager.ResponseOrCancelled.Success | |
| import com.example.labo2.R | |
| import kotlinx.coroutines.launch | |
| /** | |
| * An example usage of the "better" communication API. | |
| */ | |
| class UsageSample : AppCompatActivity() { | |
| private val manager = RequestManager() | |
| private val request = RequestManager.Request(resource = "https://www.google.com") | |
| override fun onCreate(savedInstanceState: Bundle?) { | |
| super.onCreate(savedInstanceState) | |
| setContentView(R.layout.activity) | |
| val textView = findViewById<TextView>(android.R.id.text1) | |
| // Requests are tied to the lifecycle of our Activity. If it gets destroyed, our coroutines | |
| // will be cancelled. | |
| lifecycleScope.launch { | |
| // Make a request with a synchronous-looking API. | |
| val r = manager.request( | |
| request = request, | |
| backoff = RequestManager.Backoff.Once, | |
| ) | |
| // Handle errors with a dedicated type. | |
| when (r) { | |
| is Success -> textView.text = r.response.content | |
| Cancelled -> textView.text = "Cancelled." | |
| } | |
| } | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment