How I made a custom Okhttp breaker through Kotlin's coroutines

Let's start with the statement of the problem.



  1. It is necessary to send token and user id in each request in header's
  2. From each answer it is necessary to pull out a new token and user id from headers
  3. Received data must be saved.


The library for server interaction is Retrofit. Coroutines are responsible for multithreading.

The task is not difficult, you just need to add the Okhttp client breaker to each request. Half an hour and everything is ready, everything works, everyone is happy. But I was wondering, is it possible to make a breaker without an Okhttp client?



Let's start solving problems in order. If there is no problem with adding header (you just need to add @HeaderMap in the request), then how to get the headers that come in the response? Very simple, we need to wrap our response in the Response class, which has a headers () method.



Here was the query interface:



@FormUrlEncoded @POST("someurl/") suspend fun request1(@Field("idLast") idLastFeed: Long, @Field("autoview") autoView: Boolean, @HeaderMap headers: Map<String, String?>): Answer1 @FormUrlEncoded @POST("someurl/") suspend fun request2(@Field("ransom") ransom: Long, @HeaderMap headers: Map<String, String?>): Answer2
      
      





But this has become:



 @FormUrlEncoded @POST("someurl") suspend fun request1(@Field("idLast") idLastFeed: Long, @Field("autoview") autoView: Boolean, @HeaderMap headers: Map<String, String?>?): Response<Answer1> @FormUrlEncoded @POST("someurl") suspend fun request2(@Field("ransom") ransom: Long, @HeaderMap headers: Map<String, String?>?): Response<Answer2>
      
      





Now for each request you need to add the headersMap parameter. Let's create a separate RestClient class for the query shell so that the token and id are not pulled out of sharedPreferences constantly in the presenter. This is how it turns out:



 class RestClient(private val api: Api, private val prefs: SharedPreferences) { suspend fun request1(last: Long, autoView: Boolean): Answer1 { return api.request1(last, autoView, headers()) } suspend fun request2(id: Long): Answer2 { return api.request2(id, headers()) } private val TOKEN_KEY = "Token" private val ID_KEY = "ID" fun headers(): Map<String, String> { return mapOf( TOKEN_KEY to prefs.getString(Constants.Preferences.SP_TOKEN_KEY, ""), ID_KEY to prefs.getLong(Constants.Preferences.SP_ID, -1).toString() ) } }
      
      





It can be seen that we are doing the same thing:



  1. We get some parameters for the request.
  2. Add headers to the request.
  3. Call the method.
  4. We get new values ​​from headers.
  5. We return the result.


Why don't we make one function for all requests? To do this, modify the queries. Instead of individual variables of type @Field, we will now use @FieldMap. This will be the first parameter for our function - the percher. The second parameter we will have is the request itself. Here I used Kotlin DSL (I really wanted to). I created the Request class in which I made the send function to call the request.



This is what the query interface looks like:



 @FormUrlEncoded @POST("someurl/") suspend fun feedListMap(@FieldMap map: HashMap<String, out Any>?, @HeaderMap headers: Map<String, String?>?): Response<Answer1> @FormUrlEncoded @POST("someurl/") suspend fun feedListMap(@FieldMap map: HashMap<String, out Any>?, @HeaderMap headers: Map<String, String?>?): Response<Answer2>
      
      





And here is the Request class:



 class Request<T>( var fieldHashMap: java.util.HashMap<String, out Any> = hashMapOf(), var headersHashMap: Map<String, String?>? = mapOf(), var req: suspend (HashMap<String, out Any>?, Map<String, String?>?) -> Response<T>? = { _,_ -> null} ){ fun send(): Response<T>? { return runBlocking { try { req.invoke(fieldHashMap, headersHashMap) } catch (e: Exception) { throw Exception(e.message ?: " ") } catch (t: Throwable) { throw Exception(t.message ?: " ") } } } }
      
      





Now the RestClient class looks like this:



 class RestClient(private val api: Api, private val prefs: SharedPreferences) { private val TOKEN_KEY = "Token" private val ID_KEY = "ID" fun headers(): Map<String, String> { return mapOf( TOKEN_KEY to prefs.getString(Constants.Preferences.SP_TOKEN_KEY, ""), ID_KEY to prefs.getLong(Constants.Preferences.SP_ID, -1).toString() ) } fun <T> buildRequest(request: Request<T>.() -> Unit): T? { val req = Request<T>() request(req) val res = req.send() val newToken = res?.headers()?.get(TOKEN_KEY) val newID = res?.headers()?.get(ID_KEY)?.toLong() if (newToken.notNull() && newID.notNull()) { prefs.edit() .putString(TOKEN_KEY, newToken) .putLong(ID_KEY, newID) .apply() } return res?.body() } fun fiedsMapForRequest1(last: Long, autoView: Boolean) = hashMapOf("idLast" to last, "autoview" to autoView) fun fiedsMapForRequest2(ransom: Long, autoView: Boolean) = hashMapOf("ransom" to ransom) }
      
      





And finally, this is how we call our requests in the presenter:



 try { val answer1 = restClient.buildRequest<Answer1> { fieldHashMap = restClient.fiedsMapForRequest1(1, false) headersHashMap = restClient.headers() req = api::request1 } val answer2 = restClient.buildRequest<Answer2> { fieldHashMap = restClient.fiedsMapForRequest2(1234) headersHashMap = restClient.headers() req = api::request2 } // do something with answer } catch (e: Exception) { viewState.showError(e.message.toString()) } finally { viewState.hideProgress() }
      
      





Here I made a custom breaker with the help of Kotlin.



PS The solution to this problem was very exciting, but, unfortunately, the project uses an Okhttp breaker.



All Articles