Because the background returns a unified data structure, such as code, data, message; Students who have used Retrofit must have defined a category like BaseResponse, but the processing logic of BaseResponse is similar. It's really annoying to write every time. Is there any good way to solve this pain point? This paper introduces an elegant way to solve this problem.
background
When we open the official document of Retrofit, the official example is as follows:
public interface GitHubService { @GET("users/{user}/repos") Call<List<Repo>> listRepos(@Path("user") String user); }
When it comes to our own projects, most of them do:
public interface UserService { @GET("/users") Call<BaseResponse<List<User>>> getAllUsers(); }
Different companies have different data structures, but they are all similar. For example, code, data, message or status, data, message, what code == 0 should be written every time. It's boring. There's no technical content at all...
Wouldn't it be nice if we could get rid of BaseResponse? As defined below:
public interface UserService { @GET("/users") Call<List<User>> getAllUsers(); }
If it's Kotlin, it's even better. Go straight to the suspend method.
interface UserService { @GET("/users") suspend fun getAllUsers() : List<User> }
1.Convert. Ignored parameters in factory
public interface Converter<F, T> { abstract class Factory { // The parameter annotations is the annotation on the Method public @Nullable Converter<ResponseBody, ?> responseBodyConverter( Type type, Annotation[] annotations, Retrofit retrofit) { return null; } } } public final class GsonConverterFactory extends Converter.Factory { // Retrofit official converter Factory does not use the annotations parameter @Override public Converter<ResponseBody, ?> responseBodyConverter( Type type, Annotation[] annotations, Retrofit retrofit) { TypeAdapter<?> adapter = gson.getAdapter(TypeToken.get(type)); return new GsonResponseBodyConverter<>(gson, adapter); } }
From the above code, it is not difficult to see that in the implementation of convert In factory, the official implementation of Retrofit does not use the parameter annotations, which is the key to removing BaseResponse.
2. Implementation mode
In fact, the implementation scheme is very simple, that is, add a custom annotation to the Method, and then implement a converter Factory, and then this converter In the factory class, judge whether there is a custom annotation for the Method. If so, convert the data and finally submit it to Gson for parsing.
Project address: Convex
https://github.com/ParadiseHe...
Implementation details
Data conversion interface:
interface ConvexTransformer { // Convert the original InputStream into an InputStream of specific business data // It is equivalent to obtaining the data stream of code, data and message and converting it into a data stream only @Throws(IOException::class) fun transform(original: InputStream): InputStream }
Custom annotation:
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION) @Retention(AnnotationRetention.RUNTIME) annotation class Transformer(val value: KClass<out ConvexTransformer>) // With this custom annotation, we can add this annotation to the Method interface UserService { @GET("xxx") @Transformer(XXXTransformer::class) fun xxxMethod() : Any }
Convert.Factory implementation class:
class ConvexConverterFactory : Converter.Factory() { override fun responseBodyConverter( type: Type, annotations: Array<Annotation>, retrofit: Retrofit ): Converter<ResponseBody, *>? { // Get Transformer annotation on Method // To determine which specific ConvexTransformer class to use return annotations .filterIsInstance<Transformer>() .firstOrNull()?.value ?.let { transformerClazz -> // Gets the Converter that can handle the current return value // Convertconverterfactory only does data flow and serializes specific data // Give it to a similar GsonConverterFactory retrofit.nextResponseBodyConverter<Any>( this, type, annotations )?.let { converter -> // If there is a Converter that can handle the return value, create a proxy convertor ConvexConverter<Any>(transformerClazz, converter) } } } } class ConvexConverter<T> constructor( private val transformerClazz: KClass<out ConvexTransformer>, private val candidateConverter: Converter<ResponseBody, *> ) : Converter<ResponseBody, T?> { @Suppress("UNCHECKED_CAST") @Throws(IOException::class) override fun convert(value: ResponseBody): T? { // Get the ConvexTransformer from Convex // Convex is a service registry that stores ConvexTransformer // If there is no converttransformer, the corresponding converttransformer will be created through reflection // And save it in Convex for next use return Convex.getConvexTransformer(transformerClazz.java) .let { transformer -> // Convert data stream, here is to convert code, data and message data stream into data stream transformer.transform(value.byteStream()).let { responseStream -> ResponseBody.create(value.contentType(), responseStream.readBytes()) } }?.let { responseBody -> // Use the concrete Converter to convert the data stream into concrete data candidateConverter.convert(responseBody) as T? } } }
3. Use of converse
Add dependency
dependencies { implementation "org.paradisehell.convex:convex:1.0.0" }
Implement converttransformer
private class TestConvexTransformer : ConvexTransformer { @Throws(IOException::class) override fun transform(original: InputStream): InputStream { TODO("Return the business data InputStream.") } }
Add converterfactory to Retrofit
Retrofit.Builder() .baseUrl("https://test.com/") // Convertconverterfactory must be at the top .addConverterFactory(ConvexConverterFactory()) .addConverterFactory(GsonConverterFactory.create()) .build()
Define Service interface
interface XXXService { @GET("/users") // Use the Transformer annotation to add a specific ConvexTransformer implementation class @Transformer(TestConvexTransformer::class) suspend fun getAllUsers() : List<User> }
4. Examples
First of all, thank you very much for the free API provided by WanAndroid.
https://www.wanandroid.com/bl...
// 1. Define BaseResponse // It is used to process the data returned from the background, deserialize and get the final data data class BaseResponse<T>( @SerializedName("errorCode") val errorCode: Int = 0, @SerializedName("errorMsg") val errorMsg: String? = null, @SerializedName("data") val data: T? = null ) // 2. Implement converttransformer // The user transfers the data returned from the background into specific data class WanAndroidConvexTransformer : ConvexTransformer { private val gson = Gson() @Throws(IOException::class) override fun transform(original: InputStream): InputStream { // Deserialize to BaseResponse first val response = gson.fromJson<BaseResponse<JsonElement>>( original.reader(), object : TypeToken<BaseResponse<JsonElement>>() { }.type ) // Judge whether the Response is successful // If successful, the data will be converted into InputStream, and finally processed by the specific Converter if (response.errorCode == 0 && response.data != null) { return response.data.toString().byteInputStream() } throw IOException( "errorCode : " + response.errorCode + " ; errorMsg : " + response.errorMsg ) } } // 3. Define model data data class Article( @SerializedName("id") val id: Int = 0, @SerializedName("link") val link: String? = null, @SerializedName("author") val author: String? = null, @SerializedName("superChapterName") val superChapterName: String? = null ) // 4. Define Service interface WanAndroidService { @GET("/article/top/json") // Specify ConvexTransformer for the method, so that BaseResponse can be converted to data @Transformer(WanAndroidConvexTransformer::class) suspend fun getTopArticles(): List<Article> } // 5. Define Retrofit private val retrofit by lazy { Retrofit.Builder() .baseUrl("https://wanandroid.com/") // Be sure to put convertconverterfactory in all converters Front of factory .addConverterFactory(ConvexConverterFactory()) .addConverterFactory(GsonConverterFactory.create()) .build() } private val wanAndroidService by lazy { retrofit.create(WanAndroidService::class.java) } // 6. Execution method lifecycleScope.launch(Main) { val result = withContext(IO) { // A try catch operation is required because an exception will be thrown if the request fails runCatching { wanAndroidService.getTopArticles() }.onSuccess { return@withContext it }.onFailure { it.printStackTrace() return@withContext it } } // print data // Successfully print data list, failed to print Exeception printlin(result) }
Summary
Perfect. There is no need to write BaseResponse for defining Method in the future. BaseResponse can finally be handled uniformly.
Moreover, this scheme also supports a variety of different Data types, because different methods can specify different ConvexTransformer, and it doesn't matter how BaseResponse is processed for specific business processing, because specific business codes get specific Data.
I have to admire the design of Retrofit, converter The factory has set aside the parameter annotations, which is extremely scalable. Pay tribute to square and salute~~
Advanced notes of Android advanced development system, latest interview review notes PDF, My GitHub
end of document
Your favorite collection is my greatest encouragement!
Welcome to follow me, share Android dry goods and exchange Android technology.
If you have any opinions on the article or any technical problems, please leave a message in the comment area for discussion!