1, the preface

People ask me all the time, what are the advantages of RxHttp over Retrofit? Here, I want to talk about my ideas in terms of stability, functionality, and ease of use.

First of all, RxHttp is not to kill anyone, but to give you a choice, a different choice.

The stability of

I always think that Retrofit is the player with the highest overall score at present, and RxHttp is also excellent, but the score is so low than Retrofit. Then what is the difference? After all, Retrofit is a well-known project in the world, github 37K + star, stability is certainly no need to say, in contrast to RxHttp, only 2.6K + star, only a small domestic reputation.

RxHttp is less stable than Retrofit, but that doesn’t mean RxHttp is unstable. As of 2020-12-27, RxHttp has been submitted more than 1000 times on Github, with over 200 issues closed and over 40 versions published. Although these data can’t directly indicate the stability of a project, But also as a reference, personally, for a project that has only been open source for 1.5 years, this is very good, can say that RxHttp is very stable, I will actively fix problems.

functional

In fact, all the functions are realized, but the way of implementation is different, there is nothing to say, once saw a certain domestic network framework, Retrofit said nothing, said Retrofit this function does not have, that function does not have (in fact), and then their own said high and high, I don’t know whether I didn’t know About Retrofit or did it on purpose, but in my opinion, it’s outrageous to put someone else down and put yourself up.

Ease of use

In ease of use, the individual thinks, RxHttp is generally the existence of god, whether you are encrypted request, upload, download, progress monitoring, failure retry, dynamic Baseurl, custom parser, and so on any request scenario, all follow request trilogy, just remember to request a trilogy, it captured the essence RxHttp, write request code will, Especially for new people, very friendly, can quickly get started.

In contrast, Retrofit, a lot of scenarios, we all need to encapsulate again to better use, for example, file upload/download/progress monitoring and so on, as many as 20 several annotations and Retrofit, the couple is really not too friendly, for veteran, sometimes forget a annotation is why use, then there is more annotations illegal when used together, It gives us clear errors only during build time, which was one of my first headaches with Retrofit.

From what has been discussed above

RxHttp is much easier to use than Retrofit, but not as stable as Retrofit, and can be implemented in both features, so it’s a tie.

RxHttp&RxLife exchange group (group number: 378530627, there will often be technical exchanges, welcome to the group)

This article only describes the use of RxHttp + coroutines, see more features

RxHttp is an impressive Http request framework

RxHttp is perfect for Android 10/11 upload/download/progress monitoring

RxHttp Optimal solution for network Http cache

Gradle rely on

1, will be selected

Add the JitPack to your project’s build.gradle file as follows:

allprojects {
    repositories {
        maven { url "https://jitpack.io"}}}Copy the code

Note: RxHttp has been fully migrated from JCenter to JITPack since version 2.6.0

// Must be used when kapt relies on rxhttp-compiler
apply plugin: 'kotlin-kapt'

android {
    // Must, Java 8 or higher
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
}

dependencies {
    implementation 'com. Making. Liujingxing. RXHTTP: RXHTTP: 2.6.5'
    implementation 'com. Squareup. Okhttp3: okhttp: 4.9.1' // From RXHTTP v2.2.2, you need to manually rely on okhttp
    kapt 'com. Making. Liujingxing. RXHTTP: RXHTTP - compiler: 2.6.5' // Generate RxHttp class, pure Java project, please use annotationProcessor instead of kapt
 }
Copy the code

2, optional

android {
    defaultConfig {
        javaCompileOptions {
            annotationProcessorOptions {
                arguments = [
                    rxhttp_package: 'rxhttp'.// Optional, specify the RxHttp class package name
                    // Pass the version of RxJava that you depend on. You can pass rxJavA2, rxJavA3, but you must if you depend on RxJava
                    rxhttp_rxjava: 'rxjava3'

                ]
            }
        }
    }
}
dependencies {
    implementation 'com. Making. Liujingxing. Rxlife: rxlife - coroutine: 2.1.0' // Manage the coroutine life cycle, page destruction, close requests
    
    //rxjava2 (rxjava2 /Rxjava3)
    implementation 'the IO. Reactivex. Rxjava2: rxjava: 2.2.8'
    implementation 'the IO. Reactivex. Rxjava2: rxandroid: 2.1.1'
    implementation 'com. Making. Liujingxing. Rxlife: rxlife - rxjava2:2.1.0' // Manage the RxJava2 lifecycle, page destruction, close requests

    //rxjava3
    implementation 'the IO. Reactivex. Rxjava3: rxjava: 3.0.6'
    implementation 'the IO. Reactivex. Rxjava3: rxandroid: 3.0.0'
    implementation 'com. Making. Liujingxing. Rxlife: rxlife - rxjava3:2.1.0' // Manage the RxJava3 lifecycle, page destruction, close requests

    RxHttp has GsonConverter built in by default
    implementation 'com. Making. Liujingxing. RXHTTP: converter - fastjson: 2.6.5'
    implementation 'com. Making. Liujingxing. RXHTTP: converter - Jackson: 2.6.5'
    implementation 'com. Making. Liujingxing. RXHTTP: converter - moshi: 2.6.5'
    implementation 'com. Making. Liujingxing. RXHTTP: converter - protobuf: 2.6.5'
    implementation 'com. Making. Liujingxing. RXHTTP: converter - simplexml: 2.6.5'
}
Copy the code

When you have a problem, click here, click here, and 99 percent of the problems will solve themselves

This article only covers RxHttp + coroutines. If you want to learn how to use RxHttp + RxJava, read RxHttp’s exciting Http request framework article

If you still don’t know a thing or two about coroutines, that’s ok, because you haven’t found a scenario yet, and network requests are a good place to start. This article will show you how to open coroutines gracefully and safely, and how to multitask with coroutines.

2, RxHttp coroutine use

2.1. Request trilogy

For those of you who have used RxHttp, any request sent by RxHttp follows the following three steps:The code says

/ / Kotlin coroutines
val str = RxHttp.get("/service/...") // The first step is to determine the request mode. You can select postForm, postJson, and other methods
    .toStr()    // The second step is to confirm the return type, which represents a mandatory String
    .await()    // Third, use the await method to get the return value
    
//RxJava
RxHttp.get("/service/...") // The first step is to determine the request mode. You can select postForm, postJson, and other methods
    .asString()            // Second, use the asXXX series of methods to determine the return type
    .subscribe(s -> {      // The third step is to subscribe to the observer
        // Successful callback
    }, throwable -> {
        // Fail callback
    });
Copy the code

Note:await()Is a suspend hang up method that needs to be called in another suspend method or coroutine environment

Coroutine request trilogy in detail

  • First, select get, postForm, postJson and other methods to determine the request mode, and then add parameters, files, requests and other information through add, addFile, addHeader and other methods

  • The second step is to call the toXxx series of methods to determine the return type. Commonly used are toStr, toClass, toList, More than 30 operators, including asFlow, retry, timeout, flowOn, filter, DISTINCT, and sort, can then be invoked to perform different business logic, as described in this article

  • Third, finally, we need to call any of the three operators of await, tryAwait, or awaitResult to get the return value. This step needs to be called in the coroutine environment

Next, if we want to get an object of any data type such as a Student object or a List

collection object, we will also get await(), as follows:

/ / Student object
val student = RxHttp.get("/service/...")
    .toClass<Student>()
    .await()
    
/ / the List < Student > object
val students = RxHttp.get("/service/...")
    .toClass<List<Student>>()
    .await()
Copy the code

Note:toClass()Method is versatile, you can pass any data type

This is the most common operation of RxHttp in the coroutine, master the request trilogy, you have mastered the essence of RxHttp

2.2 BaseUrl processing

RxHttp uses @defaultDomain and @domain annotations to configure the default and non-default Domain names as follows:

public class Url {

    @DefaultDomain // Use this annotation to set the default domain name
    public static String BASE_URL = "https://www.wanandroid.com";
    
    / / the name parameter in this will generate setDomainToGoogleIfAbsent method, can be arbitrarily specified name
    // The className argument generates the RxGoogleHttp class, optionally named
    @Domain(name = "Google", className = "Google")
    public static String GOOGLE = "https://www.google.com";
}
Copy the code

Set www.wanandroid.com as the default domain name and www.google.com as the non-default domain name

More BaseUrl processing

// Send the request using the default domain name
RxHttp.get("/service/...")
    .toSrt().await()
   
// Use the Google domain name method 1: The incoming URL directly carries the Google domain name
RxHttp.get("https://wwww.google.com/service/...")
    .toSrt().await()
    
/ / by using Google domain way 2: call setDomainToGoogleIfAbsent method
RxHttp.get("/service/...")
    .setDomainToGoogleIfAbsent()
    .toSrt().await()
 
// Use the Google domain name method 3: directly use the RxGoogleHttp class to send the request
RxGoogleHttp.get("/service/...")
    .toSrt().await()
Copy the code

Note: The domain name passed in manually has the highest priority, followed by a call to the setDomainToXxx method, before the default domain name is used

Dynamic domain name processing

// Re-assign the url value directly, and the change takes effect immediately
Url.BASE_URL = "https://www.baidu.com";
RxHttp.get("/service/...")
    .toSrt().await()
/ / request url for https://www.baidu.com/service/ at this time...
Copy the code

2.3 Unified judgment of service code

I think most people’s interface return format is like this

class Response<T> {
    var code = 0
    var msg : String? = null
    var data : T 
}
Copy the code

The first step in getting this object is to make a judgment on code. If code! = 200(assuming that 200 is correct), it will get the MSG field and give the user some error. If it is equal to 200, it will get the DATA field and update the UI

val response = RxHttp.get("/service/...")
    .toClass<Response<Student>>()
    .await()
if (response.code == 200) {
    // Get the data field (Student) and refresh the UI
} else {
    // Get the MSG field and give an error message
} 
Copy the code

If you think of a project with at least 30+ such interfaces, it would be inelegant and disastrous to read such a judgment on each interface, and no one would do it. And for the UI, all you need is the data field. I don’t care about errors.

Is there any way to directly get the data field and make a uniform judgment on code? Yes, go straight to the code

val student = RxHttp.get("/service/...")
    .toResponse<Student>() // Call this method to get the data field, which is the Student object
    .await()  
// Start updating the UI directly
Copy the code

As you can see, when you call the toResponse() method, you get the data field directly, which is the Student object.

At this point, I’m sure a lot of people are wondering,

  • Where is the business code judged?

  • When the service code is not 200, how to get the MSG field?

To this end, let’s answer the first question, where to judge the business code?

The toResponse() method is not provided internally by RxHttp, but is automatically generated by the annotation handler rxHTTP-compiler. It doesn’t matter. Just look at the code

@Parser(name = "Response")
open class ResponseParser<T> : AbstractParser<T> {
    
    // The following two constructors are required
    protected constructor() : super(a)constructor(type: Type) : super(type)

    @Throws(IOException::class)
    override fun onParse(response: okhttp3.Response): T {
        val type: Type = ParameterizedTypeImpl[Response::class.java, mType] // Get the generic type
        val data: Response<T> = convert(response, type)   // Get the Response object
        val t = data.data                             // Get the data field
        if (data.code ! =200 || t == null) { // If code is not equal to 200, the data is incorrect and an exception is thrown
            throw ParseException(data.code.toString(), data.msg, response)
        }
        return t
    }
}
Copy the code

The above code only needs to focus on two things,

First, we start the class with the @parser annotation and name the Parser Response, so we have the toResponse() method (named as: to + the name set in the Parser annotation).

Second, in the if statement, we made a judgment that if code is not 200 or data is null, we will throw an exception with the code and MSG fields, so we will get those two fields in the exception callback

Then to answer the second question, how do I get the MSG field when code is not 200? Go straight to the code and see a complete example of sending a request using a coroutine

// The current environment is in the Fragment
fun getStudent(a) {   
    //rxLifeScope is in the rxLife-Coroutine library and needs to be relied on separately
    rxLifeScope.launch({    // Start a coroutine with the launch method
        val student = RxHttp.get("/service/...") 
            .toResponse<Student>()
            .await()                 
    }, {                                               
        // Exception callback, where it is Throwable
        val code = it.code                            
        val msg = it.msg                              
    })                                                
}                                                     
Copy the code

Note: RxLifeScope isRxLife-CoroutineLibrary classes, described in detail later in this article

It. Code and it. MSG are the two properties I extend for Throwable class.

val Throwable.code: Int
    get() {
        val errorCode = when (this) {
            is HttpStatusCodeException -> this.statusCode // The Http status code is abnormal
            is ParseException -> this.errorCode     // The service code is abnormal
            else -> "1"
        }
        return try {
            errorCode.toInt()
        } catch (e: Exception) {
            -1}}val Throwable.msg: String
    get() {
        return if (this is UnknownHostException) { // The network is abnormal
            "Currently no network, please check your network Settings."
        } else if (
            this is SocketTimeoutException  // Okhttp global Settings timeout
            || this is TimeoutException     // The timeout method in rxJava times out
            || this is TimeoutCancellationException  // The coroutine times out
        ) {
            "Connection timed out, please try again later"
        } else if (this is ConnectException) {
            "The network is not working, please try again later!"
        } else if (this is HttpStatusCodeException) {               // The request failed
            "Http status code is abnormal"
        } else if (this is JsonSyntaxException) {  // The request succeeds, but the Json syntax is abnormal, causing parsing failure
            "Data parsing failed. Please check if the data is correct."
        } else if (this is ParseException) {       // ParseException Indicates that the request was successful, but the data is incorrect
            this.message ? : errorCode// MSG is empty, and code is displayed
        } else {
            "Request failed. Please try again later."}}Copy the code

In the ResponseParser parser, you need to change the if statement to determine the condition

2.4. Operation Description

AwaitResult return kotlin. The Result

AwaitResult is the most common character that handles request success/failure callbacks like this:

val result: Result<Student> = RxHttp
    .postForm("/service/...")
    .toClass<Student>()
    .awaitResult {  
        // Obtain the Student object from it
    }.onFailure { 
        // Request an exception and get the Throwable through IT
    }                                                            
Copy the code

After you get the kotlin.result object, the subsequent success/failure processing of the associated logic

TryAwait exception returns null

TryAwait will return null if an exception occurs, as follows:

val student = RxHttp.postForm("/service/...")
    .toResponse<Student>()
    .timeout(100)      // The timeout period is 100 milliseconds
    .tryAwait()     // Return Student? Object, which is null if an exception occurs
Copy the code

OnErrorReturn, onErrorReturnItem Exception Default value

In cases where we don’t want an exception to be called, we can use the default values with both operators, as follows:

// Give default values based on exceptions
val student = RxHttp.postForm("/service/...")
    .toResponse<Student>()
    .timeout(100)      // The timeout period is 100 milliseconds
    .onErrorReturn {
        // If there is a timeout exception, the default value is given, otherwise, the original exception is thrown
        return@onErrorReturn if (it is TimeoutCancellationException)
            Student()                                              
        else                                                        
            throw it                                                
    }
    .await()
    
// Return the default value whenever an exception occurs
val student = RxHttp.postForm("/service/...")
    .toResponse<Student>()
    .timeout(100)      // The timeout period is 100 milliseconds
    .onErrorReturnItem(Student())
    .await()
Copy the code

Repeat rotation request

The repeat operator takes three parameters, as follows:

/ * * *@paramTimes Indicates the number of rotations. The default value is long. MAX_VALUE@paramPeriod Rotation period. The default value is 0 *@paramStop Indicates the termination condition of the rotation. The default value is false, that is, the unconditional rotation times */
fun <T> IAwait<T>.repeat(
    times: Long = Long.MAX_VALUE,
    period: Long = 0,
    stop: suspend (T) - >Boolean = { false})
Copy the code

The above three parameters can be used together at will. The usage is as follows:

val student = RxHttp.postForm("/service/...")
    .toResponse<Student>()
    .repeat(10.1000) {            // Rotate for 10 times, each interval is 1s
        return it.id = 8888       // If student ID is 8888, the rotation will stop
    }                                             
    .await()                     
Copy the code

Retry failed retry

The retry operator has three parameters, namely retry times, retry period, and retry conditions, as follows:

/** * Retry on failure. This method is only valid when coroutines are used *@paramTimes Indicates the number of retries. By default, Int.MAX_VALUE indicates repeated retries@paramPeriod Retry period. The default value is 0. The unit is milliseconds *@paramTest Retry condition, which defaults to true. Retry */ whenever an exception occurs
fun retry(
    times: Int = Int.MAX_VALUE,
    period: Long = 0,
    test: suspend (Throwable) - >Boolean = { true})
Copy the code

The three parameters can be used together at will. We need to retry 2 times with an interval of 1 second when the network is abnormal. The code is as follows:

val student = RxHttp.postForm("/service/...")
    .toResponse<Student>()
    .retry(2.1000) {            // Retry twice with 1s interval
        it is ConnectException   // If the network is abnormal, try again
    }                                             
    .await()                     
Copy the code

The timeout timeout

OkHttp provides global read, write, and connection timeouts. Sometimes we need to set a different timeout period for a request. In this case, we can use the timeout(Long) method of RxHttp.

val student = RxHttp.postForm("/service/...")
    .toResponse<Student>()
    .timeout(3000)      // The timeout duration is 3s
    .await()                       
Copy the code

Map conversion symbol

The Map operator is easy to understand. Both RxJava and coroutine Flow have the same function. It is used to transform objects.

val student = RxHttp.postForm("/service/...")
    .toStr()
    .map { it.length }  / / String Int
    .tryAwait()     // Return Student? Object, that is, it may be empty
Copy the code

Filter Filter operation

If the server returns list data, we can filter the list as follows:

val students = RxHttp.postForm("/service/...")
    .toList<Student>()
    .filter{ it.age > 20 }   // Filter students over 20 years old
    .await()                     
Copy the code

You can also use the filterTo operator to add the filtered data to the specified list, as shown below:

val list = mutableListOf<Student>()
val students = RxHttp.postForm("/service/...")
    .toList<Student>()
    .filterTo(list){ it.age > 20 }   // Filter students over 20 years old
    .await()  // The list object returned is the list object we passed in
Copy the code

Distinct to heavy

This operator can redo the list returned by the server, as follows:

// Deduplicate according to the hashCode of the Student object
val students = RxHttp.postForm("/service/...")
    .toList<Student>()
    .distinct()   
    .await()    

// Deduplicate the Student object based on its ID
val students = RxHttp.postForm("/service/...")
    .toList<Student>()
    .distinctBy { it.id }
    .await()   
    
// Add the deduplicated data to the specified list, and judge the data in the specified list when deduplicated
val list = mutableListOf<Student>()
val students = RxHttp.postForm("/service/...")
    .toList<Student>()
    .distinctTo(list) { it.id }
    .await()   
Copy the code

The sort order

Sorting sortXxx, sortedXxx two types of operators, the difference is that sortXxx within the list sorting, sorting out, return to itself, and sortedXxx list sorting, sorting out, return to the new list, here only to sortXxx introduction, as follows:

// Sort by ID order
val students = RxHttp.postForm("/service/...")
    .toList<Student>()
    .sortBy { it.id }   
    .await() 
    
// Sort by id and age, id first, age second
val students = RxHttp.postForm("/service/...")
    .toList<Student>()
    .sortBy({ it.id }, { it.age })  
    .await() 

// Return two collation objects and implement the collation rules yourself
val students = RxHttp.postForm("/service/...")
    .toList<Student>()
    .sortWith { student1, student2 ->             
        student1.id.compareTo(student2.id)        
    }                                             
    .await() 
Copy the code

FlowOn Specifies the upstream thread

This operator, like the flowOn operator in Flow, specifies the upstream thread, as follows:

val students = RxHttp.postForm("/service/...")
    .toList<Student>()
    .sortBy { it.id }        // The IO thread executes
    .flowOn(Dispatchers.IO)
    .distinctBy { it.id }    // The Default thread executes
    .flowOn(Dispatchers.Default)
    .filter{ it.age > 20 }   // The IO thread executes
    .flowOn(Dispatchers.IO)
    .flowOn(Dispatchers.Default)
    .await() 
Copy the code

AsFlow To Flow object

If you like Kotlin’s flow, then asFlow comes in handy, as follows:

RxHttp.postForm("/service/...")
    .toList<Student>()
    .asFlow()
    .collect {       
        // Get the List
      
        object
      
    }                
Copy the code

Note: After using the asFlow operator, you need to use collect instead of await operator

SubList, take interception list

SubList is used to intercept a certain list. If the interception range is out of bounds, an out-of-bounds exception is thrown. Take is used to take n data from 0, and return all data when n is less than, as follows:

val students = RxHttp.postForm("/service/...")
    .toList<Student>()
    .subList(1.10)  // Capture 9 data
    .take(5)         // Take the first five out of nine
    .await()               
Copy the code

Async Asynchronous operations

We can use this operator if we want two requests to be in parallel, as follows:

// Obtain information about two students at the same time
suspend void initData() {
  val asyncStudent1 = RxHttp.postForm("/service/...")
    .toResponse<Student>()
    .async(this)   // This is a CoroutineScope object. Deferred
      
    
  val asyncStudent2 = RxHttp.postForm("/service/...")
    .toResponse<Student>()
    .async(this)   // This is a CoroutineScope object. Deferred
      

  // Then call the await method to get the object
  val student1 = asyncStudent1.await()
  val student2 = asyncStudent2.await()
} 
Copy the code

Delay, startDelay Delay

The delay operator is a delay of some time after the request has finished. The startDelay operator, on the other hand, delays sending the request for a period of time, as follows:

val student = RxHttp.postForm("/service/...")
    .toResponse<Student>()
    .delay(1000)      // After the request returns, delay the return for 1s
    .await()       
    
val student = RxHttp.postForm("/service/...")
    .toResponse<Student>()
    .startDelay(1000)     // Delay the request for 1s before sending it
    .await()     
Copy the code

Custom operators

RxHttp has a number of powerful and useful operators built in, but it is certainly not sufficient for all business scenarios, so it is time to consider custom operators

Custom takeLast operator

If we have such a requirement, the custom needs to fetch n pieces of data at the end of the list

Earlier we introduced the take operator, which starts at 0, takes n pieces of data, and returns them all if there are fewer than n pieces. Let’s look at the source code

fun <T> IAwait<out Iterable<T>>.take(
    count: Int
): IAwait<List<T>> = newAwait {
    await().take(count)
}
Copy the code

Code interpretation,

1. IAwait is an interface, as follows:

interface IAwait<T> {

    suspend fun await(a): T
}
Copy the code

This interface has only an await() method, which returns the declared T

The newAwait operator simply creates an implementation of the IAwait interface as follows:

inline fun <T, R> IAwait<T>.newAwait(
    crossinline block: suspend IAwait<T>. () - >R
): IAwait<R> = object : IAwait<R> {

    override suspend fun await(a): R {
        return this@newAwait.block()
    }
}
Copy the code

3. Since we are extending the take method for an IAwait

> object, internally we call the await() method, which returns the Iterable

object, Finally implement Iterable < T > object extension methods take (Int) to get open yes from 0 n data, take (Int) is the system to provide the method, the source code is as follows:

public fun <T>可迭代<T>.take(n: Int): List<T> {
    require(n >= 0) { "Requested element count $n is less than zero." }
    if (n == 0) return emptyList()
    if (this is Collection<T>) {
        if (n >= size) return toList()
        if (n == 1) return listOf(first())
    }
    var count = 0
    val list = ArrayList<T>(n)
    for (item in this) {
        list.add(item)
        if (++count == n)
            break
    }
    return list.optimizeReadOnlyList()
}
Copy the code

Ok, back to the previous topic, how to customize an operation to achieve a list at the end of n pieces of data, less than n, return all

Look at the above take(int) source code, we can easily write the following code:

fun <T> IAwait<out List<T>>.takeLast(
    count: Int
): IAwait<List<T>> = newAwait {
    await().takeLast(count)
}
Copy the code

First, we extend the takeLast(Int) method on IAwait

>, then call newAwait to create an instance object of the IAwait interface, and then call await() to return the List

object. Finally, call the takeLast(Int) method that the system extends for List

Once defined, we can use it directly, as follows:

val students = RxHttp.postForm("/service/...")
    .toList<Student>()
    .takeLast(5)         // Return all 5 pieces of data at the end of the list
    .await()               
Copy the code

The above operators are randomly combined

The above operators can be used in combination with each other at will, but they have different effects depending on the order in which they are called. Let me tell you that the above operators only affect upstream code.

Such as timeout and retry:

val student = RxHttp.postForm("/service/...")
    .toResponse<Student>()
    .timeout(50)
    .retry(2.1000) { it is TimeoutCancellationException }                                  
    .await()                       
Copy the code

The above code, whenever a timeout occurs, will retry, at most twice.

If (timeout) and (retry) are interchanged, the difference is as follows:

val student = RxHttp.postForm("/service/...")
    .toResponse<Student>()
    .retry(2.1000) { it is TimeoutCancellationException }       
    .timeout(50)                                  
    .await()                       
Copy the code

At this point, if the request is not completed within 50 milliseconds, a timeout exception will be raised and an exception callback will be performed without a retry. Why is that? The reason for this is simple: the timeout and retry operators only work for upstream code. Such as the retry operator, downstream exceptions are not caught, which is why timeout in a retry case does not trigger the retry mechanism.

Look at the timeout and startDelay operators

val student = RxHttp.postForm("/service/...")
    .toResponse<Student>()
    .startDelay(2000)       
    .timeout(1000)                                  
    .await()                       
Copy the code

The above code is bound to trigger a timeout exception because startDelay is 2000 milliseconds late and the timeout is only 1000 milliseconds. But when you switch places, it’s different, as follows:

val student = RxHttp.postForm("/service/...")
    .toResponse<Student>()
    .timeout(1000)    
    .startDelay(2000)       
    .await()                       
Copy the code

The above code normally gets the return value correctly, why? The reason for this is simple. As mentioned above, the operator only affects the upstream. It does not care about the downstream startDelay.

3. Upload/download

RxHttp’s elegant manipulation of files is inherent, and still is in the context of coroutines. Nothing is more convincing than code

3.1 File upload

 val result = RxHttp.postForm("/service/...")  
     .addFile("file", File("xxx/1.png"))        // Add a single file
     .addFile("fileList", ArrayList<File>())    // Add multiple files
     .toResponse<String>()
     .await()                   
Copy the code

You just need to add the File object through the addFile series method. That’s as simple as that. Want to listen to the upload progress? Add the upload operator as follows:

val result = RxHttp.postForm("/service/...")                               
    .addFile("file", File("xxx/1.png"))                                     
    .addFile("fileList", ArrayList<File>())                                 
    .upload(this) {    // This is the CoroutineScope object, i.e. the current coroutine object
        //it is the Progress object
        val process = it.progress         // Progress 0-100 has been uploaded
        val currentSize = it.currentSize  // Uploaded size, unit: byte
        val totalSize = it.totalSize      // Total size to be uploaded. The unit is byte
    }                                                               
    .toResponse<String>()        
    .await()                                                                                                                       
Copy the code

Let’s look at the complete signature of the upload method as follows:

/** * Call this method to listen for upload progress *@paramCoroutine CoroutineScope object used to start a coroutine and to call back progress, depending on the thread of the coroutine *@paramNote: This method only works in coroutine environments */
fun RxHttpFormParam.upload(
    coroutine: CoroutineScope? = null, 
    progress: (Progress) - >Unit
):RxHttpFormParam
Copy the code

3.2. File download

Let’s take a look at downloads, directly pasting code

val localPath = "sdcard//android/data/.... /1.apk" 
val student = RxHttp.get("/service/...")     
    .toDownload(localPath)  // Download requires passing in the local file path
    .await()
Copy the code

Call toDownload(String), pass in the local file path, want to listen to the download progress? Also simple, as follows:

val localPath = "sdcard//android/data/.... /1.apk"  
val student = RxHttp.get("/service/...")      
    .toDownload(localPath, Dispatchers.Main) {       
        //it is the Progress object
        val process = it.progress        // Progress 0-100 has been downloaded
        val currentSize = it.currentSize // Downloaded size, unit: byte
        val totalSize = it.totalSize     // The total size to be downloaded is in byte
    }     
    .await()
Copy the code

Take a look at the toDownload method’s full signature

/ * * *@paramDestPath Local storage path *@paramContext CoroutineContext Specifies the progress/success/failure callback thread *@paramProgress Progress callback */
fun IRxHttp.toDownload(
    destPath: String,
    context: CoroutineContext? = null,
    progress: (suspend (ProgressT<String- > >)Unit)? = null
): IAwait<String>
Copy the code

If you need a breakpoint download, replace the toDownload method with the toAppendDownload method as follows:

val localPath = "sdcard//android/data/.... /1.apk"                        
val student = RxHttp.get("/service/...")             
    .toAppendDownload(localPath, Dispatchers.Main) { 
        //it is the Progress object
        val process = it.progress        // Progress 0-100 has been downloaded
        val currentSize = it.currentSize // Size, unit: byte
        val totalSize = it.totalSize     // The unit of the total size is byte
    }      
    .await()
Copy the code

This is the end of the basic Api for RxHttp coroutines, so the question is, all the apis that I’ve introduced are dependent on the coroutine environment, so how do I open the coroutine? In other words, I don’t know much about coroutines, you just need to ensure the premise of safety, tell how to use it, OK, then how to safely open a coroutine, do automatic exception catching, and page destruction, automatically close the coroutine and request

4. Coprogram opening and closing

For opening coroutines, Jetpack library provides two frameworks, that is, lifecycle run-time KTX and Lifecycle Viewmodel-kTX, which can be very convenient to open coroutines and automatically close the coroutine when the page is destroyed. Students who want to know about coroutines can find relevant materials by themselves.

Rxlife-coroutine, which is used to turn coroutines on/off and automatically catch exceptions, relies on the following:

implementation 'com. LJX. Rxlife: rxlife - coroutine: 2.0.1'
Copy the code

In this article, we use the rxLifeScope property to open coroutines when we introduce uniform processing of business codes. What type is this? Look at the code

val ViewModel.rxLifeScope: RxLifeScope
    get() {
        val scope: RxLifeScope? = this.getTag(JOB_KEY)
        if(scope ! =null) {
            return scope
        }
        return setTagIfAbsent(JOB_KEY, RxLifeScope())
    }

val LifecycleOwner.rxLifeScope: RxLifeScope
    get() = lifecycle.rxLifeScope
Copy the code

As you can see, we’ve extended a property called rxLifeScope for both the ViewModel and the LifecycleOwner, of type rxLifeScope. I’m sure you all know about the ViewModel, so let’s just briefly talk about the LifecycleOwner interface. Our fragments and FragmentActivities both implement the LifecycleOwner interface, and our activities generally inherit from AppCompatActivity, AppCompatActivity inheritance in FragmentActivity, so we in FragmentActivity/fragments/ViewModel environment, can be used directly rxLifeScope open coroutines, as follows:

rxLifeScope.lanuch({
    // Coroutine code block, run in the UI thread
}, {
    // The exception callback. Any exception in the coroutine block will go directly here
})
Copy the code

Coroutines opened in this way will automatically close the coroutine when the page is destroyed, of course, if your coroutine code block also has RxHttp request code, when the coroutine is closed, it also closes the request, so in this case, all you need to know is how to open the coroutine, and nothing else.

Now, let’s look at the full signature of the rxlifescope.lanuch method

/ * * *@paramBlock Coroutine block of code that runs on the UI thread *@paramOnError exception callback, run on the UI thread *@paramThe onStart coroutine starts the callback, running on the UI thread *@paramThe onFinally coroutine end callback, which is called regardless of success/failure, runs on the UI thread */                                                         
fun launch(                                                 
    block: suspend CoroutineScope. () - >Unit,               
    onError: ((Throwable) - >Unit)? = null,                 
    onStart: (() -> Unit)? = null,                          
    onFinally: (() -> Unit)? = null                         
): Job                                                    
Copy the code

As you can see, there are not only failure callbacks, but also start and end callbacks, which is really convenient for us to send requests, as follows:

rxLifeScope.launch({                                      
    // Coroutine code block
    val students = RxHttp.postJson("/service/...")
        .toResponse<List<Student>>()
        .await()   
    // You can update the UI directly
}, {                                                      
    // The exception callback is used to retrieve the Throwable object
}, {                                                     
    // Start the callback, which can open the wait popover
}, {                                                     
    // End the callback that can destroy the waiting popover
})                                                       
Copy the code

The above code runs in the UI thread, and when the request comes back, you can update the UI directly

Maybe you have doubt, I am not FragmentActivity/fragments/ViewModel environment, how to open coroutines, and how to shut down, actually is also very simple, as follows:

val job = RxLifeScope().launch({                                        
    val students = RxHttp.postJson("/service/...")
        .toResponse<List<Student>>()
        .await()            
}, {                                                
    // The exception callback is used to retrieve the Throwable object
}, {                                               
    // Start the callback, which can open the wait popover
}, {                                               
    // End the callback that can destroy the waiting popover
})                                                 
job.cancel()  // Close the coroutine
Copy the code

First, we need to manually create the RxLifeScope() object and then open the coroutine. Second, after opening the coroutine, we can get the Job object, which we need to use to manually close the coroutine. Nothing else is different.

Coroutine multitasking

As we know, the biggest advantage of coroutines is the ability to write asynchronous logic in seemingly synchronous code, which makes it very elegant to implement multi-tasking scenarios such as parallel/serial requests

5.1 coroutine serial multiple requests

Suppose we have a scenario where we first get the Student object and then get the Student’s list of family members through studentId, which depends on the former, which is a typical serial scenario

Let’s see how coroutines solve this problem, as follows:

class MainActivity : AppCompatActivity() {
    // Start the coroutine and send the request
    fun sendRequest(a) {
        rxLifeScope.launch({
            // Currently running in the coroutine, and running in the main thread
            val student = getStudent()
            val personList = getFamilyPersons(student.id) // Query family member information by student Id
            // Update the UI directly after receiving the relevant information, such as:
            tvName.text = student.name
        }, {
                // Throwable Throwable Throwable Throwable Throwable Throwable Throwable Throwable
            it.show("Sending failed, please try again later!") // The show method is an extended method in Demo})}// Hang up method to get student information
    suspend fun getStudent(a): Student {
        return RxHttp.get("/service/...")
            .add("key"."value")
            .addHeader("headKey"."headValue")
            .toClass<Student>()
            .await()
    }

    // Hang up method to get the information of family members
    suspend fun getFamilyPersons(studentId: Int): List<Person> {
        return RxHttp.get("/service/...")
            .add("studentId"."10000")
            .toClass<List<Person>>()
            .await()
    }
}
Copy the code

Let’s focus on the coroutine code block, so we get the Student object from the first request, and then we get the studentId, and we send the second request to get the list of learning family members, and when we get that, we can directly update the UI, ok, so if it looks synchronous, we write asynchronous logic.

In a serial request, whenever an exception occurs on one of the requests, the coroutine closes (along with the request), stops executing the rest of the code, and then goes through the exception callback

5.2 coroutine parallel multiple requests

Request parallelization, in real development, is also common, in an Activity, we often need to get a variety of data to show to the user, and these data, are sent by different interfaces.

If we have such a page, the top is a horizontally scrolling Banner bar, and the learning list is displayed below the Banner bar, then there are two interfaces, one is to get the list of Banner bars, the other is to get the list of learning bars. They are independent of each other and can be executed in parallel, as follows:

class MainActivity : AppCompatActivity() {
    // Start the coroutine and send the request
    fun sendRequest(a) {
        rxLifeScope.launch({
            // Currently running in the coroutine, and running in the main thread
            val asyncBanner = getBanners(this) // The Deferred
      
       > object is returned
      
            val asyncPersons = getStudents(this) // Return the Deferred
      
       > object
      
            val banners = asyncBanner.await()           // Return the List
      
        object
      
            val students = asyncPersons.await()         // Return the List
      
        object
      
            // Start updating the UI

        }, {
                // Throwable Throwable Throwable Throwable Throwable Throwable Throwable Throwable
            it.show("Sending failed, please try again later!") // The show method is an extended method in Demo})}// Hang up method to get student information
    suspend fun getBanners(scope: CoroutineScope): Deferred<List<Banner>> {
        return RxHttp.get("/service/...")
            .add("key"."value")
            .addHeader("headKey"."headValue")
            .toClass<List<Banner>>()
            .async(scope)  // Note the use of the async operator
    }

    // Hang up method to get the information of family members
    suspend fun getStudents(scope: CoroutineScope): Deferred<List<Student>> {
        return RxHttp.get("/service/...")
            .add("key"."value")
            .toClass<List<Student>>()
            .async(scope) // Note the use of the async operator}}Copy the code

The async asynchronous operator is used in both of the hang up methods in the code above, so the two requests are sent in parallel, then we get the Deferred

object, we call the await() method, we get the list of banners and the list of students, and we can update the UI directly.

To highlight

In parallel, as in serial, if an exception occurs on one of the requests, the coroutine closes (along with the request), stops executing the rest of the code, and then goes through the exception callback. You can use the onErrorReturn and onErrorReturnItem operators above to give a default object when an exception occurs, or use the tryAwait operator to return a value and return null when an exception occurs. This does not affect the execution of other requests.

6. Principle analysis

RxHttp uses the popular Annotation Processing Tool (APT), Libraries such as Eventbus, ButterKnife, Dagger2, Glide, and Jetpack are very useful Room database frameworks that use APT to retrieve annotation information at compile time. Generate Java classes, methods, and other related code through the Javapoet framework (to generate Kotlin-related code, use KotlinPoet) and thus achieve zero performance cost at run time.

So, what advantages does APT bring to RxHttp? How does RxHttp use APT? Keep reading

When we talk about APT, the first thing that comes to mind is probably decoupling. Yes, decoupling is one of its advantages. In fact, it has an even greater advantage, that is, generating different code logic based on configuration. In RxHttp, for example, the default is not dependent on RxJava, but if you need to use RxHttp + RxJava way to send a request, can be in annotationProcessorOptions label rxhttp_rxjava parameters to configure RxJava version, Can be passed RxJava2 or RxJava3, internal according to the passed RxJava version, generate different code, so that you can do a set of code simultaneously and through RxJava2 and RxJava3, if the subsequent out of the RxJava4, RxJava5 and other new versions, the same can be compatible, and very simple.

For this purpose, RxHttp ADAPTS any version of OkHttp v3.12.0 to V4.9.0 (the latest version as of 2020/12/27) (except v4.3.0). There is a bug in this version that makes it uncompatible), so with RxHttp, you don’t have to worry about okHTTP version conflicts at all.

The first advantage APT brings to RxHttp is that it is compatible with different versions of RxJava and OkHttp.

How does RxHttp use APT? In RxHttp, a total of six annotations are defined, as follows:

  • @defaultDomain: Use this to specify the default baseUrl, which can only be used once

  • @domain: Specifies a non-default baseUrl, which can be used multiple times

  • @parser: Specifies a custom Parser that can be used multiple times. This is very powerful. You can write your own data parsing logic in the Parser and return any type of data, which perfectly solves the problem of irregular data returned by the server

  • @param: Specifies a custom Param that can be used multiple times when sending unified encryption requests

  • OkClient: Configure different OkHttpClient objects for different requests, which can be used multiple times

  • @Converter: Configure different Converter objects for different requests and can be used multiple times

Note: Please see how the above six notes are usedRxHttp annotations are used

The RxHttp annotation processor is called the RXHTTP-Compiler. Its primary task is to generate the RxHttp class. The second task is to retrieve the above six annotations and generate the corresponding classes and methods. You can directly use the @param annotation generated method, as follows:

// Send an encrypted post form request with a method name optionally specified via the @param annotation
val student = RxHttp.postEncryptForm("/service/...")
    .add("key"."value")
    .toClass<Student>()
    .await()
Copy the code

The advantages of the other five annotations will not be explained in detail, but the other big advantage is decoupling so that any request follows the request trilogy

RxHttp workflow

Next, looking at the RxHttp workflow, there are five important roles, which are:

  • RxHttp: This is the most important role, so the only entry point to the Request, internally holds a Param object, which is responsible for processing Request parameters/headers /BaseUrl, scheduling Request threads, providing annotation generation methods, etc., and ultimately building an okHttp3.request object through Param. Then build an OkHttp3.Call object and throw the Call object to an Observable or IAwait that actually executes the request

  • Param: Its job is to handle the Request parameters/Request headers/URLS and everything else needed to build the okHttp3.request object. Its final job is to build the okHttp3.request object, which is held by the RxHttp class. RxHttp builds the okHttp3.request object. It’s all up to Param

  • IAwiat: The object that actually executes the network request when the request is sent in combination with the coroutine. The specific implementation class is AwaitImpl, which holds the Parser object inside. After the request is returned, okHttp3. Response is thrown to the Parser for parsing and the parsed object is returned

  • Observables. When RxJava sends a request, the object that actually executes the network request is implemented by ObservableCallExecute, ObservableCallEnqueue, ObservableParser, It is used to parse data from the OkHttp3. Response object for synchronous request, asynchronous request, and return request, respectively. The ObservableParser holds the Parser object internally, and the detailed parsing work is given to the Parser

  • Parser: Parses the data to the desired data type, which is an interface object with only onParse(Response: response) inside: The method T has four specific implementation classes: SimpleParser, StreamParser, SuspendStreamParser and BitmapParser. The first is a universal parser, and the internal asClass/toClss method is implemented through it. The second and third is to download the file when using the parser, the difference between the former is combined with RxJava download, the latter is combined with coroutine download; The last one is used to resolve Bitmap objects, such as asBitmap/toBitmap

Work flow chart is as follows:

7,

After reading this article, I believe you have understood the elegance and simplicity of RxHttp, unified processing of business code, retry failure, timeout, file upload/download and progress monitoring, to the rxLifeScope coroutine open/close/exception handling/multitasking, everything is so elegant.

In fact, RxHttp is much more than that, this article only explains the coroutine related things of RxHttp, more elegant features, such as: multi/dynamic baseUrl processing, public parameters/request header addition, request encryption and decryption, caching, and so on, please check out

RxHttp is an impressive Http request framework

RxHttp Optimal solution for network Http cache

Writing articles and finally, open source is not easy, more is not easy, you also need to bother everybody to give this Wen Dian praise, if you can, give a star again, I would be grateful, πŸ™ πŸ™ πŸ™ πŸ™ πŸ™ πŸ™ πŸ™ πŸ™ πŸ™ πŸ™ πŸ™ πŸ™