Part of this article is overdue and up to dateRxHttp + coroutinesDocumentation, please checkRxHttp, a more elegant coroutine experience than Retrofit

1, the preface

RxHttp has been updated to V2.3.3 and has passed 2200+ Star on Github. For a project that has been open source for a little over a year, It can be said that we have achieved a good result. Here, thanks for your support and affirmation. RxHttp will be better and better.

RxHttp already has a number of useful operators, such as timeout, retry, map, and so on. For example, flowOn, asFlow, filter, DISTINCT, sort and a series of operators are frequently used and support customization, which will be described next.

This article is too long, you can like the collection, like, hope you can give a star support, open source is not easy, grateful.

RxHttp&RxLife exchange group: 378530627

Gradle rely on

1, will be selected

// 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. LJX. RXHTTP: RXHTTP: 2.5.2'
    implementation 'com. Squareup. Okhttp3: okhttp: 4.9.0' // From RXHTTP v2.2.2, you need to manually rely on okhttp
    kapt 'com. LJX. RXHTTP: RXHTTP - compiler: 2.5.2' // 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. LJX. Rxlife: rxlife - coroutine: 2.0.1' // 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. LJX. Rxlife2: rxlife - rxjava: 2.0.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. LJX. Rxlife3: rxlife - rxjava: 3.0.0' // Manage the RxJava3 lifecycle, page destruction, close requests

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

Note: After dependency, remember rebuild (must), RxHttp class will be generated

This article covers only the coroutine-related parts of RxHttp. If you want to learn more about RxJava, read RxHttp’s stunning Http request framework

2, RxHttp coroutine use

2.1. Request trilogy

Any request, any return value, follows the request trilogy. To master the request trilogy, you master the essence of RxHttp, as follows:

Note: RxHttp2.2.0 version, has completely eliminated RxJava, using the plug-in method to replace, support RxJava2, RxJava3, details view RxHttp start

The coroutine requests three code representations

// coroutine to get the data returned by the interface and return it as String
val str = RxHttp.get("/service/...")  //1, determine the request mode, optionally get, postXxx and other methods
    .toStr()                          //2. Use the toXxx series of methods to determine the return type
    .await()                          //3, use the await method to get the return value
Copy the code

Coroutine request trilogy in detail

  • The first step is to select get, postForm, postJson and other methods to determine the request mode. Then you can add parameters, files, request headers using the add, addFile, addHeader/ operators. Of course, we can call more operators to meet the business needs. Such as setCacheXxx, upload and so on to set the cache policy and listen to upload progress and so on

  • 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

For example, we want to send a post form request, add one parameter/file/request header, and deserialize the returned data into a Student object with a timeout of 5 seconds. Failed to retry 2 times, each interval 1 second, the code is as follows:

val student = RxHttp
    .postForm("/service/...")   //1. Determine the request mode. You can select get, postXxx and other methods
    .add("key"."value")
    .addFile("file", File("... /1.png"))
    .addHeader("headerKey"."headerValue")
    .toClass<Student>()         //2. Use the toXxx series of methods to determine the return type
    .timeout(5000)
    .retry(2.1000)
    .await()                    //3, use the await method to get the return value
Copy the code

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 to 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 about a project with at least a dozen of these interfaces, it would be inelegant and disastrous if each interface read this judgment, 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

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

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

Retry failed retry

OkHttp provides us with a global failure retry mechanism, however, this is far from sufficient for our needs. For example, I have partial interfaces that require failure retries rather than global ones. I need to decide if I need to retry based on certain conditions; Or MAYBE I need to try again periodically, after a few seconds, etc

If 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 retry() method has three parameters, namely retry times, retry period, and retry condition, which have default values. The three parameters can be used 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

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

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

AwaitResult return kotlin. The Result

I’ll just look at the code for this

val result: Result<Student> = RxHttp
    .postForm("/service/...")
    .toClass<Student>()
    .awaitResult()
if (result.isSuccess) { 
    // The request succeeds and the Student object is obtained
    val student = result.getOrThrow()            
} else { 
    // An exception occurred in the request and the Throwable object was retrieved
    val throwable = result.exceptionOrNull()     
}                                                
Copy the code

Once we have the kotlin.result object, we need to determine whether the request was successful or not and then execute 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

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, this) {   This is a CoroutineScope object
        //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 *@paramCoroutine CoroutineScope object used to start a coroutine and to call back progress, depending on the thread of the coroutine *@paramProgress Progress callback */
fun IRxHttp.toDownload(
    destPath: String,
    coroutine: CoroutineScope? = null,
    progress: (Progress) - >Unit
): IAwait<String>
Copy the code

If you need a breakpoint download, that’s ok, as a one-line code thing, as follows:

val localPath = "sdcard//android/data/.... /1.apk"                        
val student = RxHttp.get("/service/...")                            
    .setRangeHeader(1000.300000)   // Breakpoint download, set the start/end of the download location
    .toDownload(localPath, this) { This is a CoroutineScope object
        //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

Old rule, look at setRangeHeader full signature

/** * Set breakpoint download start/end position *@paramStartIndex Start point of the breakpoint download *@paramEndIndex Indicates the end of the download. The default value is -1, that is, the default end value is * at the end of the file@paramConnectLastProgress Indicates whether to connect to the last download progress. This parameter is valid only for downloads with progress breakpoints */                                                                                      
fun setRangeHeader (
    startIndex: Long, 
    endIndex: Long = 0L, 
    connectLastProgress: Boolean = false
)
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

Rxlife-coroutine, another open source library, will be introduced to enable/disable coroutines and automatically catch exceptions, depending on the following:

implementation 'com. LJX. Rxlife: rxlife - coroutine: 2.0.0'
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"."studentId")
            .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, summary

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: 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

Finally, open source is not easy, write less, still need to bother everybody to give this Wen Dian praise, if you can, give a star again, I would be grateful, πŸ™ πŸ™ πŸ™ πŸ™ πŸ™ πŸ™ πŸ™ πŸ™ πŸ™ πŸ™ πŸ™ πŸ™