1. Preface
Hello, you little friends, have met again. Looking back, RxHttp We are about to have our anniversary birthday (launched in April 19). This year, we have come over with sincerity.... Sincerity is not easy. Code maintenance, writing articles, writing documents, etc. are often done after zero o'clock. It is also the first time that I spend most of my spare time to maintain an open source project. Maintain it all by myself. You know, the Network Request Library is different from other open source projects. My colleagues are very interested in this.Class projects are very demanding, and there is a mountain in front of Retrofit, how can I get out of the siege in this case?That's only the dead details, so that people have no me, and people have my essence.
Fortunately, RxHttp did, and by the time this article was published, on Github, it had 1600+star In the RxHttp$RxLife Exchange Group (Group number: 378530627, there are often technical exchanges, welcome to the group) there are also 300+ people, this time, RxHttp Updated to version 2.x, to bring you different experience of the process, why is it different?You will have an answer after reading this article
gradle dependency
dependencies { //Must implementation 'com.ljx.rxhttp:rxhttp:2.2.0' annotationProcessor 'com.ljx.rxhttp:rxhttp-compiler:2.2.0' //Generate RxHttp class //All of the following are not required implementation 'com.ljx.rxlife:rxlife-coroutine:2.0.0' //Manage the contract life cycle, destroy pages, close requests implementation 'com.ljx.rxlife2:rxlife-rxjava:2.0.0' //Manage the RxJava2 life cycle, destroy pages, close requests implementation 'com.ljx.rxlife3:rxlife-rxjava:3.0.0' //Manage the RxJava3 life cycle, destroy pages, close requests //Converter chooses RxHttp to suit its needs. GsonConverter is built in by default implementation 'com.ljx.rxhttp:converter-jackson:2.2.0' implementation 'com.ljx.rxhttp:converter-fastjson:2.2.0' implementation 'com.ljx.rxhttp:converter-protobuf:2.2.0' implementation 'com.ljx.rxhttp:converter-simplexml:2.2.0' }
Note: For pure Java projects, use annotation Processor instead of kapt; when you are done, remember rebuild to generate RxHttp classes
Since version 2.2.0 of RxHttp2, RxJava has been completely eliminated, replaced by plug-in methods, supporting RxJava2, RxJava3, see details Beginner with RxHttp
Encountered problems, click here, click here, 99% of the problems can be solved by themselves
This article only describes the RxHttp coprocess-related sections, and if you have not known RxHttp before, it is recommended that you read it first RxHttp Brightens Your Eyes on the Http Request Framework One Article
It doesn't matter if you know a little about the collaboration right now, that's because you haven't found a working scenario yet, and network requests are a good entry scenario. This article will show you how to start the collaboration elegantly and safely, and multitask with the collaboration, then you'll be able to use it.
2. RxHttp collaboration use
Students who have used RxHttp know that any request sent by RxHttp follows the request trilogy as follows:
Code representation
RxHttp.get("/service/...") //The first step is to determine how the request will be made, choosing postForm, postJson, and so on .asString() //Step 2, determine the return type using the asXXX series method .subscribe(s -> { //Step 3, Subscribe to Observers //Successful callback }, throwable -> { //Failure Callback });
This makes it very easy for beginners to start, master the Request Trilogy, master the essence of RxHttp, and the protocol also follows the Request Trilogy, as follows:
Code representation
val str = RxHttp.get("/service/...") //The first step is to determine how the request will be made, choosing postForm, postJson, and so on .toStr() //Step 2, confirm the return type, which here represents the return String type .await() //Step 2, use the await method to get the return value
Note: await() is a suspend hang method and needs to be called in another suspend method or coprogramming environment
Next, if we want to get a Student object or any data type such as List<Student>Collection object, we also use the await() method, as follows:
//Student object val student = RxHttp.get("/service/...") .toClass<Student>() .await() //List<Student>Object val students = RxHttp.get("/service/...") .toClass<List<Student>>() .await()
The toClass() method is omnipotent, it can get any data type. Let's look at the full signature of the toClass() method
inline fun <reified T : Any> IRxHttp.toClass() : IAwait<T>
You can see that it has no parameters, just declares a generic T and uses it as a return type, so you can get any data type with this method.
That's RxHttp's most common operation in the partnership, and next, get real dry goods
2.1. Unified Judgment of Business code
I think most people's interface returns in this format
class Response<T> { var code = 0 var msg : String? = null var data : T }
The first step to get this object is to make a judgment on code, if code!= 200 (assuming the 200 code data is correct), you will get msg field to give the user some error hints, if equal to 200, you will get data field to update UI, the general operation is like this
val response = RxHttp.get("/service/...") .toClass<Response<Student>>() .await() if (response.code == 200) { //Get the data field (Student) to refresh the UI } else { //Get the msg field and give an error message }
Just imagine a project with 30+ interfaces. If each interface reads this judgment, it is not elegant enough, or it is a disaster, and no one can believe it.And for the UI, all it takes is the data field, and I don't care what the error prompts are.
Is there any way to get the data field directly and make a unified judgment about the code?Yes, code directly
val student = RxHttp.get("/service/...") .toResponse<Student>() //Call this method to get the data field directly, which is the Student object .await() //Start updating UI directly
You can see that if you call the toResponse() method here, you get the data field, which is the Student object.
At this point, I believe many people will have questions.
-
Where is the business code judged?
-
How do I get the msg field when the business code is not 200?
To do this, first answer the first question, where does the business code tell you?
In fact, the toResponse() method is not provided internally by RxHttp, but by a user-defined parser, labeled with @Parser annotation, and finally automatically generated by the annotation processor rxhttp-compiler. Do you understand it?That's okay, just look at the code
@Parser(name = "Response") open class ResponseParser<T> : AbstractParser<T> { //The following two construction methods are required protected constructor() : super() constructor(type: Class<T>) : super(type) @Throws(IOException::class) override fun onParse(response: okhttp3.Response): T { val type: Type = ParameterizedTypeImpl[Response::class.java, mType] //Getting generic types val data: Response<T> = convert(response, type) //Get Response Object val t = data.data //Get the data field if (data.code != 200 || t == null) { //code is not equal to 200, indicating the data is incorrect, throwing an exception throw ParseException(data.code.toString(), data.msg, response) } return t } }
The above code only needs to focus on two points.
First, we used the @Parser annotation at the beginning of the class and named the parser Response, so we have the toResponse() method (named after the name set in the to + Parser annotation);
Second, we make a judgment about code in the if statement. When not 200 or data is empty, we throw an exception with the code and msg fields, so we get them where the exception callback is.
Then answer the second question, how do I get the msg field when code is not 200?Go directly to the code and see a complete case of sending requests using a collaboration
//The current environment is in Fragment fun getStudent() { //rxLifeScope in the rxLife-coroutine library needs to be dependent on independently rxLifeScope.launch({ //Start a protocol by launch ing val student = RxHttp.get("/service/...") .toResponse<Student>() .await() }, { //Exception callback, where it is of type Throwable val code = it.code val msg = it.msg }) }
Note: RxLifeScope is RxLife-Coroutine Classes in libraries, described in more detail later in this article
The code above gives you the code and MSG fields in the exception callback. It is important to note that it.code and it.msg are two properties that I extended for the Throwable class. The code is as follows:
val Throwable.code: Int get() { val errorCode = when (this) { is HttpStatusCodeException -> this.statusCode //Http status code exception is ParseException -> this.errorCode //Business code exception else -> "-1" } return try { errorCode.toInt() } catch (e: Exception) { -1 } } val Throwable.msg: String get() { return if (this is UnknownHostException) { //Network exception "There is currently no network, please check your network settings" } else if ( this is SocketTimeoutException //okhttp global settings timeout || this is TimeoutException //Timeout method timeout in rxjava || this is TimeoutCancellationException //Coprocess timeout ) { "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) { //Request Failure Exception "Http Status Code Exception" } else if (this is JsonSyntaxException) { //The request succeeded, but the Json syntax exception caused the parsing to fail "Data parsing failed,Please check that the data is correct" } else if (this is ParseException) { // ParseException exception indicates the request succeeded, but the data is incorrect this.message ?: errorCode //msg is empty, showing code } else { "Request failed, please try again later" } }
At this point, the unified judgment of business code is introduced. Most people can simply modify the above code and use it directly on their own projects, such as ResponseParser parser, simply change the judgment condition of if statement.
2.2, retry failed retry
OkHttp provides us with a global failure retry mechanism, but it is far from meeting our needs, for example, I need failed retries for some interfaces, not global ones; I need to judge if a retry is required based on certain criteria; or I need to retry periodically, i.e., after a few seconds interval, etc.
How does the RxHttp protocol solve these problems?RxHttp provides a retry() method to solve these challenges, so take a look at the complete method signature
/** * Failed retry, this method is only valid when using a collaboration * @param times Number of retries, default Int.MAX_VALUE means keep retrying * @param period Retry cycle, default 0, in milliseconds * @param test Retry condition, empty by default, unconditional retry */ fun retry( times: Int = Int.MAX_VALUE, period: Long = 0, test: ((Throwable) -> Boolean)? = null )
The retry() method has three parameters, retry number, retry cycle and retry condition, all with default values. The three parameters can be matched at will, such as:
retry() //Unconditional, uninterrupted, keep retrying retry(2) //Unconditional, uninterrupted, retry twice retry(2, 1000) //Unconditional interval 1s retry 2This retry { it is ConnectException } //Conditional, uninterrupted, keep retrying retry(2) { it is ConnectException } //Conditional, uninterrupted, retry twice retry(2, 1000) { it is ConnectException } //Conditional, 1s interval, 2 retries retry(period = 1000) { it is ConnectException } //Conditional, 1s off, keep retrying
The first two parameters are believed to be clear at a glance. Let's add an extra word to the third parameter here. With the third parameter, we can get the Throwable exception object. We can make a judgment on the exception. If we need to try again, we can return true or false. Let's see the code below.
val student = RxHttp.postForm("/service/...") .toResponse<Student>() .retry(2, 1000) { //Retry twice, 1 s interval each it is ConnectException //Retry if network exception occurs } .await()
2.3, timeout timeout
OkHttp provides global read, write, and connection timeouts. Sometimes we also need to set different timeouts for a request. We can use the timeout(Long) method of RxHttp as follows:
val student = RxHttp.postForm("/service/...") .toResponse<Student>() .timeout(3000) //Timeout length is 3s .await()
2.4, async asynchronous operator
If we need to have two requests in parallel, we can use this operator as follows:
//Get two student information at the same time suspend void initData() { val asyncStudent1 = RxHttp.postForm("/service/...") .toResponse<Student>() .async() //Deferred<Student>is returned here val asyncStudent2 = RxHttp.postForm("/service/...") .toResponse<Student>() .async() //Deferred<Student>is returned here //The await method is then called to get the object val student1 = asyncStudent1.await() val student2 = asyncStudent2.await() }
2.5, delay, startDelay delay
The delay operator is to delay returning after the request ends, while the startDelay operator is to delay sending the request after a period of time, as follows:
val student = RxHttp.postForm("/service/...") .toResponse<Student>() .delay(1000) //Delay return by 1 s after request returns .await() val student = RxHttp.postForm("/service/...") .toResponse<Student>() .startDelay(1000) //Delay 1 s before sending request .await()
2.6, onErrorReturn, onErrorReturnItem exception defaults
In some cases, we don't want to go directly to the exception callback when an exception occurs, so we can give the default value by two operators, as follows:
//Give default values based on exceptions val student = RxHttp.postForm("/service/...") .toResponse<Student>() .timeout(100) //Timeout length is 100 milliseconds .onErrorReturn { //If a time-out exception occurs, a default value is given; otherwise, the original exception is thrown return@onErrorReturn if (it is TimeoutCancellationException) Student() else throw it } .await() //Return default whenever an exception occurs val student = RxHttp.postForm("/service/...") .toResponse<Student>() .timeout(100) //Timeout length is 100 milliseconds .onErrorReturnItem(Student()) .await()
2.7, tryAwait exception returns null
If you don't want to return the default value when an exception occurs and you don't want the exception to affect the execution of the program, tryAwait is useful. It returns null when an exception occurs, as follows:
val student = RxHttp.postForm("/service/...") .toResponse<Student>() .timeout(100) //Timeout length is 100 milliseconds .tryAwait() //Student? Object is returned here, which may be empty
2.8 map to convert symbols
The map operator is well understood. RxJava, the Flow of a protocol, has this operator, and all functions are the same for converting objects, as follows:
val student = RxHttp.postForm("/service/...") .toStr() .map { it.length } //String to Int .tryAwait() //Student? Object is returned here, which may be empty
2.9. Operators above match freely
The above operators can be used together at will, but the effect is different depending on the calling order. Let me tell you first that the above operators will only affect the upstream code.
Such as timeout and retry:
val student = RxHttp.postForm("/service/...") .toResponse<Student>() .timeout(50) .retry(2, 1000) { it is TimeoutCancellationException } .await()
The above code, whenever a timeout occurs, will retry, and at most twice.
However, if timeout and retry exchange the lower locations, they will be different, as follows:
val student = RxHttp.postForm("/service/...") .toResponse<Student>() .retry(2, 1000) { it is TimeoutCancellationException } .timeout(50) .await()
At this point, if the request is not completed within 50 milliseconds, a timeout exception will be triggered, and the exception callback will go straight without retrying.Why is that?The reason is simple: the timeout and retry operators only work on the upstream code.As with the retry operator, downstream exceptions are not caught, which is why timeout under retry does not trigger a retry mechanism when it times out.
Look at timeout and startDelay operators
val student = RxHttp.postForm("/service/...") .toResponse<Student>() .startDelay(2000) .timeout(1000) .await()
The above code is bound to trigger the timeout exception, because startDelay, which is delayed by 2000 milliseconds, and the timeout is only 1000 milliseconds, must trigger the timeout.
But the lower position is different, as follows:
val student = RxHttp.postForm("/service/...") .toResponse<Student>() .timeout(1000) .startDelay(2000) .await()
The above code can get the return value correctly under normal circumstances, why?The reason is simple. As mentioned above, operators only affect the upstream, and the downstream startDelay delay is neither.
3. Upload/Download
RxHttp's elegant manipulation of files is inherent, and in a collaborative environment it still does, and there's nothing more persuasive than code, directly coding
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()
You just need to add File objects through the addFile family method, which is so simple and rude that you want to monitor the upload progress?Simple, add an upload operator as follows:
val result = RxHttp.postForm("/service/...") .addFile("file", File("xxx/1.png")) .addFile("fileList", ArrayList<File>()) .upload(this) { //This this is a CoroutineScope object, the current collaboration object //it is a Progress object val process = it.progress //Uploaded progress 0-100 val currentSize = it.currentSize //Uploaded size in byte val totalSize = it.totalSize //Total size unit to upload: byte } .toResponse<String>() .await()
Let's look at the full signature of the upload method as follows:
/** * Call this method to monitor upload progress * @param coroutine CoroutineScope Object that opens a protocol and calls back progress. The thread on which the progress callback is based depends on the thread on which the protocol is located * @param progress Progress Callback * Note: This method only works in a collaborative environment */ fun RxHttpFormParam.upload( coroutine: CoroutineScope? = null, progress: (Progress) -> Unit ):RxHttpFormParam
3.2. File Download
Next, take a look at the download and paste the code directly
val localPath = "sdcard//android/data/..../1.apk" val student = RxHttp.postForm("/service/...") .toDownload(localPath) //Download requires incoming local file path .await()
Download calls the toDownload(String) method and passes in the local file path. Do you want to monitor the download progress?Simple as follows:
val localPath = "sdcard//android/data/..../1.apk" val student = RxHttp.postForm("/service/...") .toDownload(localPath, this) { //this is a CoroutineScope object //it is a Progress object val process = it.progress //Downloaded progress 0-100 val currentSize = it.currentSize //Downloaded size in byte val totalSize = it.totalSize //Total size unit to download: byte } .await()
Look at the toDownload method full signature
/** * @param destPath Local Storage Path * @param coroutine CoroutineScope Object that opens a protocol and calls back progress. The thread on which the progress callback is based depends on the thread on which the protocol is located * @param progress Progress Callback */ fun IRxHttp.toDownload( destPath: String, coroutine: CoroutineScope? = null, progress: (Progress) -> Unit ): IAwait<String>
If you need a breakpoint to download, that's okay. One line of code does the following:
val localPath = "sdcard//android/data/..../1.apk" val student = RxHttp.postForm("/service/...") .setRangeHeader(1000, 300000) //Breakpoint Download, Set Download Start/End Location .toDownload(localPath, this) { //this is a CoroutineScope object //it is a Progress object val process = it.progress //Downloaded progress 0-100 val currentSize = it.currentSize //Lower size in byte val totalSize = it.totalSize //Total size unit to be dropped: byte } .await()
Old rule, look at the setRangeHeader full signature
/** * Set breakpoint download start/end location * @param startIndex Breakpoint Download Start Location * @param endIndex Breakpoint download end location, default is -1, that is, the default end location is at the end of the file * @param connectLastProgress Whether to follow the last download progress, this parameter will only take effect when downloading with progress breakpoint */ fun setRangeHeader ( startIndex: Long, endIndex: Long = 0L, connectLastProgress: Boolean = false )
At this point, the basic Api of the RxHttp protocol has been basically introduced. So the problem is that the API described above all depends on the environment of the protocol. So how can I start the protocol?Or, I don't know about protocol. You just need to tell me how to use it if it's safe. ok, how to safely open a protocol below to achieve automatic exception capture and automatically close the protocol and request when the page is destroyed
4. Cooperative Opening and Closing
This is the time to introduce another library of my own open source RxLife-Coroutine , which opens/closes the protocol and automatically catches exceptions, depending on the following:
implementation 'com.ljx.rxlife:rxlife-coroutine:2.0.0'
When introducing business code unification in this paper, we use the rxLifeScope attribute to start a protocol. 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
As you can see, we have extended a property called rxLifeScope for both ViewModel and Lifecycle Owner, the type of which is RxLifeScope. ViewModel is sure everyone knows that, just to briefly talk about the Lifecycle Owner interface, our Fragments and FragmentActivities both implement the Lifecycle Owner interface, and our Activities generally inherit from AppCompatActivity, while AppCompatActivityInherited from FragmentActivity, we can start a protocol directly using rxLifeScope in the FragmentActivity/Fragment/ViewModel environment as follows:
rxLifeScope.lanuch({ //Code block, running on UI thread }, { //Exception callbacks, any exceptions to the codeblock of the protocol, go straight here })
A protocol opened this way will automatically close the protocol when the page is destroyed. Of course, if you have RxHttp request code in the protocol code block, the protocol will also close the request at the same time, so in this case, you just need to know how to start the protocol, and everything else will be gone.
Now let's look at the full signature of the rxLifeScope.lanuch method
/** * @param block Code block, running on UI thread * @param onError Exception callback, running on UI thread * @param onStart The console starts a callback and runs on the UI thread * @param onFinally A callback to end a collaboration, whether successful or unsuccessful, runs on the UI thread */ fun launch( block: suspend CoroutineScope.() -> Unit, onError: ((Throwable) -> Unit)? = null, onStart: (() -> Unit)? = null, onFinally: (() -> Unit)? = null ): Job
You can see that there are not only failed callbacks, but also start and end callbacks, which are really convenient for us to make requests, as follows:
rxLifeScope.launch({ //COOPERATION CODE BLOCK val students = RxHttp.postJson("/service/...") .toResponse<List<Student>>() .await() //UI can be updated directly }, { //Exception callback, where you can get the Throwable object }, { //Start callback to open waiting window }, { //End callback to destroy waiting window })
The above code runs in the UI thread and updates the UI directly when the request returns
Perhaps you still have questions about how I can start a project and close it in a non-FragmentActivity/Fragment/ViewModel environment, which is as simple as the following:
val job = RxLifeScope().launch({ val students = RxHttp.postJson("/service/...") .toResponse<List<Student>>() .await() }, { //Exception callback, where you can get the Throwable object }, { //Start callback to open waiting window }, { //End callback to destroy waiting window }) job.cancel() //Close protocol
There are two things to note about the above code. First, we need to manually create the RxLifeScope() object and then start the protocol. Second, after opening the protocol, we can get the Job object and we need to manually close the protocol through the object.Nothing else is different.
5. Multi-task handling of projects
We know that the greatest advantage of collaboration is the ability to write asynchronous logic in seemingly synchronous code, which allows us to implement multitask scenarios with great grace, such as parallel/serial multiple requests
5.1, Protocol Serial Multiple Requests
Suppose we have a scenario where we first get a Student object, then a list of the family members of the students through studentId, which depends on the former, which is a typical serial scenario
See how you can solve this problem through a collaboration as follows:
class MainActivity : AppCompatActivity() { //Start a protocol and send a request fun sendRequest() { rxLifeScope.launch({ //Currently running in a console and in the main thread val student = getStudent() val personList = getFamilyPersons(student.id) //Query family member information through Student Id //Once you get the information, you can update the UI directly, such as: tvName.text = student.name }, { //When an exception occurs, you get here, where it is of type Throwable it.show("fail in send,please try again later!") //The show method is an extension of the Demo method }) } //Hang up method to get student information suspend fun getStudent(): Student { return RxHttp.get("/service/...") .add("key", "value") .addHeader("headKey", "headValue") .toClass<Student>() .await() } //Hang up method to get family member information suspend fun getFamilyPersons(studentId: Int): List<Person> { return RxHttp.get("/service/...") .add("studentId", "studentId") .toClass<List<Person>>() .await() } }
Let's focus on the code block for the collaboration. First we get the Student object through the first request, then the studentId, and then we send the second request to get the list of learning family members. Once we get it, we can update the UI directly. How about if it looks like synchronous code and write out asynchronous logic?
In a serial request, whenever one of the requests has an exception, the protocol closes (and also closes the request), stops executing the remaining code, and then goes through the exception callback
5.2, Concurrent multiple requests
Requests are concurrent, and in real-world development, they are common chores. In an Activity, we often need to get a variety of data to display to users, and these data are sent from different interfaces.
If we have a page with a Banner bar scrolling horizontally at the top and a Learning List displayed below the Banner bar, then there are two interfaces, one to get the Banner bar list and the other to get the Learning List, which are independent of each other and can be executed in parallel as follows:
class MainActivity : AppCompatActivity() { //Start a protocol and send a request fun sendRequest() { rxLifeScope.launch({ //Currently running in a console and in the main thread val asyncBanner = getBanners() //Deferred<List<Banner> object returned here val asyncPersons = getStudents() //Deferred<List<Student> object returned here val banners = asyncBanner.await() //List<Banner>object returned here val students = asyncPersons.await() //List<Student>object returned here //Start updating UI }, { //When an exception occurs, you get here, where it is of type Throwable it.show("fail in send,please try again later!") //The show method is an extension of the Demo method }) } //Hang up method to get student information suspend fun getBanners(): Deferred<List<Banner>> { return RxHttp.get("/service/...") .add("key", "value") .addHeader("headKey", "headValue") .toClass<List<Banner>>() .async() //Note that async asynchronous operator is used here } //Hang up method to get family member information suspend fun getStudents(): Deferred<List<Student>> { return RxHttp.get("/service/...") .add("key", "value") .toClass<List<Student>>() .async() //Note that async asynchronous operator is used here } }
The async asynchronous operator is used in both of the hang methods in the code above, where the two requests send the request in parallel, get the Deferred<T>object, call its await() method, get the Banner and Student lists, and then update the UI directly.
Focus
Parallel is like serial, if one of the requests has an exception, the protocol automatically closes (closes the request at the same time), stops executing the remaining code, and then goes through the exception callback.If you want multiple requests to be independent of each other, you can use the onErrorReturn, onErrorReturnItem operators described above to give a default object when an exception occurs, or use the tryAwait operator to get a return value and return null when an exception occurs, so that other requests will not be affected.
6. Summary
After reading this article, I believe you have learned that RxHttp is elegant and simple, that business code is handled uniformly, that retries fail, timeouts, file uploads/downloads, and progress monitoring are failing, that everything is elegant until the opening/closing/exception handling/multitask handling of the rxLifeScope protocol follows.
In fact, RxHttp is much more than that. This article just explains what RxHttp is about about collaboration and more elegant functions, such as processing of multiple/dynamic baseUrl s, adding of public parameters/request headers, requesting encryption and decryption, caching, etc. Please see
RxHttp Brightens Your Eyes on the Http Request Framework
RxHttp Network-wide Http Cache Optimal Solution
Finally, open source is not easy, writing articles is not easy, and you need to bother everyone to give this article a compliment, if you can, give it another star Not enough,_