RxJava 2 + Retrofit 2 in combination

Keywords: Android Retrofit network Java

Instead of rxjava and retrofit, I'm going to go straight to 2, because 2 encapsulates better and more useful.

1. Observer model

For example, a common button click event is that a button is an observer, a listener is an observer, and a setOnClickListener process is a subscription. With a subscription relationship, when a button is clicked, the listener listener can respond to the event.

The button.setOnClickListener(listener) here seems to mean that the observer subscribes to the observer (magazine subscribes to the reader), which logically does not conform to everyday habits. In fact, this is the habit of design patterns, not entanglement, used to this mode is conducive to understanding the observer pattern.

2. Observer pattern in RxJava

  • Observa ble: Observed.
  • Observer: Observer.
  • subscribe: Subscription

First create Observable and Observer, and then observable.subscribe(observer), so that events emitted by Observable will be responded by Observer. Usually we don't create Observable manually, but Retrofit returns to us. After we get Observable, we just need to care about how to operate the data in Observer.
However, for a shallow-to-deep demonstration, the Observable is created manually.

2.1 Create Observable

Several common ways, not often written, because I think this module is not the focus.

  • var observable=Observable.create(ObservableOnSubscribe<String> {...})
  • var observable=Observable.just(...)
  • var observable = Observable.fromIterable(mutableListOf(...))

2.1.1 create()

var observable2=Observable.create(object :ObservableOnSubscribe<String>{
            override fun subscribe(emitter: ObservableEmitter<String>) {
                emitter.onNext("Hello ")
                emitter.onNext("RxJava ")
                emitter.onNext("GoodBye ")
                emitter.onComplete()            }

        })

Observable On Subscribe and Observable Emitter are strangers. When it comes to source code analysis in detail, there are many things (mainly unfamiliar to me), so it can be understood that Observable On Subscribe is used to help create Observable and Observable Emitter is used to emit events (these events are in view). Response processing is available in Observer.
emitter fires three events at a time, and then calls onComplete(), which is also mentioned in the following section on Observer, the observer, and explained together.

2.1.2 just

var observable=Observable.just("Hello","RxJava","GoodBye")

The effect of this sentence is the same as that of creating observable above, which calls onNext three times and then calls onComplete again.

2.1.3 fromIterable

var observable = Observable.fromIterable(mutableListOf("Hello","RxJava","GoodBye"))

The effect of this sentence is the same as that of creating observable above, which calls onNext three times and then calls onComplete again.

2.2 Create Observer

val observer = object : Observer<String> {
            override fun onComplete() {
                Log.e("abc", "-----onComplete-----")
            }

            override fun onSubscribe(d: Disposable) {
                Log.e("abc", "-----onSubscribe-----")
            }

            override fun onNext(t: String) {
                Log.e("abc", "-----onNext-----$t")
            }

            override fun onError(e: Throwable) {
                Log.e("abc", "-----onError-----$e")
            }
        }
//Subscribe
observable.subscribe(observer)

log printing:

-----onSubscribe-----
-----onNext-----Hello
-----onNext-----RxJava
-----onNext-----GoodBye
-----onComplete-----

As you can see, first you build a subscription relationship, then print onNext according to the order of the previous observable. The parameters are passed in through onNext(t: String), and finally onComplete is called. In the case of just and fromIterable, Emitter is not called manually, but onNext is still called first, and finally onNext is called. Call onComplete

2.3 Consumer and Action

These two words mean consumers (which can be interpreted as events emitted by the observer) and behavior (which can be interpreted as responding to the observer's behavior). For the four callback methods in Observer, we may not all be able to use them. If only a part of them is needed, Consumer and Action will be needed.

The onSubscribe, onNext, onError with parameters are replaced by Consumer, and the onComplete without parameters is replaced by Action:

2.3.1 subscribe(Consumer<? super T> onNext)

observable.subscribe(object :Consumer<String>{
            override fun accept(t: String?) {
                Log.e("abc", "-----onNext-----$t")
            }
        })
//Printing
-----onNext-----Hello
-----onNext-----RxJava
-----onNext-----GoodBye

To illustrate, if we only pass an object parameter in subscribe, it can only be subscribe (Consumer <? Super T > onNext) (onNext method), not Action or Consumer <? Super Throwable > onError, Consumer <? Super Disposable > onSubscribe.

== Note that the callback method name in Consumer is accept, which is different from onNext

2.3.2 subscribe(Consumer<? super T> onNext, Consumer<? super Throwable> onError)

With two Consumer parameters, responsible for onNext and onError callbacks, respectively.

observable.subscribe(object : Consumer<String> {
            override fun accept(t: String?) {
                Log.e("abc", "-----onNext-----$t")
            }
        }, object : Consumer<Throwable> {
            override fun accept(t: Throwable?) {
                Log.e("abc", "-----onError-----$e")
            }
        })

If you want a pair with two Consumers but not this combination (e.g. subscribe (Consumer <? Super T > onNext, Consumer <? Super Disposable > onSubscribe)), is that OK? The answer is: No.

2.3.3 subscribe(Consumer<? super T> onNext, Consumer<? super Throwable> onError,Action onComplete)

With three parameters, each responsible for onNext, onError and onComplete callbacks.

observable.subscribe(object : Consumer<String> {
            override fun accept(t: String?) {
                Log.e("abc", "-----onNext-----$t")
            }
        }, object : Consumer<Throwable> {
            override fun accept(t: Throwable?) {
                Log.e("abc", "-----onError-----$e")
            }
        }, object : Action {
            override fun run() {
                Log.e("abc", "-----onComplete-----")
            }
        })

Similarly, three parameters can only have this collocation

== Note that the callback method name in Action is run, which is different from the onComplete above.

2.3.4 subscribe(Consumer<? super T> onNext, Consumer<? super Throwable> onError,Action onComplete, Consumer<? super Disposable> onSubscribe)

This is the same as the Observer that comes out of direct new.

observable2.subscribe(object : Consumer<String> {
            override fun accept(t: String?) {
                Log.e("abc", "-----onNext-----$t")
            }
        }, object : Consumer<Throwable> {
            override fun accept(t: Throwable?) {
                Log.e("abc", "-----onError-----$e")
            }
        }, object : Action {
            override fun run() {
                Log.e("abc", "-----onComplete-----")
            }
        },object : Consumer<Disposable>{
            override fun accept(t: Disposable?) {
                Log.e("abc", "-----onSubscribe-----")            }
        })

3. transformation

In the above example, Observable sends String-type data, so it receives String in Observer. There are many kinds of data in real development, and sometimes the data provided by Observable is not our ideal situation. In this case, we need to use the conversion operator.
Similarly, we will only talk about the commonly used:

3.1 map

For example, we want to convert upstream Int-type data into String, which can be done as follows:

Observable.fromIterable(mutableListOf<Int>(1, 3, 5, 7, 8))
                .map(object : Function<Int, String> {
                    override fun apply(t: Int): String {
                        return "zb$t"
                    }
                })
                .subscribe(object : Consumer<String> {
                    override fun accept(t: String?) {
                        Log.e("abc","-- $t --")
                    }
                })
//Log log
-- zb1 --
-- zb3 --
-- zb5 --
-- zb7 --
-- zb8 --

Through the map operator, Int type data has become String in Consumer (here you can use Consumer for the sake of simply looking at the data without using Observer). Function is used here. Its first generic type is the data type emitted in Observable. The second generic type is the data type after we want to change. The data transformation is done manually in the application method of Function.
Schema: map turns a circle into a square.

3.2 flatMap

Similar to map, flatMap returns an Observable, that is to say, the second generic type of Function is fixed, which is Observable. This is not easy to understand. Let's see an example:
If there are more than one student, each student has more than one subject, and each subject has been tested many times, all the scores should be printed now. map alone can't be done directly. Try it.

class Course(var name: String, var scores: MutableList<Int>)
class Student(var name: String, var courses: MutableList<Course>)

var stu1Course1 = Course("Sports",mutableListOf(80, 81, 82))
var stu1Course2 = Course("Fine Arts",mutableListOf(63, 62, 60))
var stu1 = Student("StuA", mutableListOf(stu1Course1, stu1Course2))
var stu2Course1 = Course("Music",mutableListOf(74, 77, 79))
var stu2Course2 = Course("Greek language",mutableListOf(90, 90, 91))
var stu2 = Student("StuB", mutableListOf(stu2Course1, stu2Course2))

Observable.just(stu1,stu2)
                .map(object :Function<Student,MutableList<Course>>{
                    override fun apply(t: Student): MutableList<Course> {
                        return t.courses
                    }
                })
                .subscribe(object :Consumer<MutableList<Course>>{
                    override fun accept(t: MutableList<Course>?) {
                        for (item in t!!){
                            for (i in item.scores){
                                Log.e("abc","--->$i")
                            }
                        }
                    }
                })

It's also impossible to print through a two-tier for loop, because only Course collections are available in the map. This is the case with flatMap:

Observable.just(stu1, stu2)
                .flatMap(object : Function<Student, ObservableSource<Course>> {
                    override fun apply(t: Student): ObservableSource<Course> {
                        return Observable.fromIterable(t.courses)
                    }
                })
                .flatMap(object : Function<Course, ObservableSource<Int>> {
                    override fun apply(t: Course): ObservableSource<Int> {
                        return Observable.fromIterable(t.scores)
                    }

                })
                .subscribe(object : Consumer<Int> {
                    override fun accept(t: Int?) {
                        Log.e("abc", "---> $t")
                    }
                })
// log printing
    ---> 80
    ---> 81
    ---> 82
    ---> 63
    ---> 62
    ---> 60
    ---> 74
    ---> 77
    ---> 79
    ---> 90
    ---> 90
    ---> 91

With two flatMaps, chain calls are clearer than indentation calls. The flatMap return value type here is ObservableSource, which is not the Observable we mentioned earlier. Looking at the Observable source code, you can see that it inherits ObservableSource, so this polymorphic usage is possible.
In addition, the Observable.fromIterable(t.courses) returned in apply is not the way we create Observable?
Simply put, map is to transform the data emitted by Observable into a type. flatMap is to send every element in the data set/array through Observable again.
Sketch: faltMap transforms a series of circles into a series of squares through a series of Observable s.

Although the picture is ugly, I think the meaning is quite clear.

3.3 filter

Filter means filtering. It decides whether to launch events by judging whether it conforms to the logic we want. Only events that return true are emitted, and others are discarded. For example, if we only want to see the score above 80, we can filter it as follows:

Observable.just(stu1, stu2)
                .flatMap(object : Function<Student, ObservableSource<Course>> {
                    override fun apply(t: Student): ObservableSource<Course> {
                        return Observable.fromIterable(t.courses)
                    }
                })
                .flatMap(object : Function<Course, ObservableSource<Int>> {
                    override fun apply(t: Course): ObservableSource<Int> {
                        return Observable.fromIterable(t.scores)
                    }

                })
                .filter(object :Predicate<Int>{
                    override fun test(t: Int): Boolean {
                        return t > 80
                    }

                })
                .subscribe(object : Consumer<Int> {
                    override fun accept(t: Int?) {
                        Log.e("abc", "---> $t")
                    }
                })
// log printing
    ---> 81
    ---> 82
    ---> 90
    ---> 90
    ---> 91

Note that Function is not used in filter, but Predicate. This word means "based on...", based on T > 80, that is, choosing a score greater than 80.

4. Use in conjunction with Retrofit

The first three sections cover a lot to clarify the whole workflow of RxJava, and have not yet touched on thread switching. More often in real development, Observable is returned to us via Retrofit. Retrofit is a network request framework based on OkHttp3, which makes better encapsulation. It can greatly improve the development efficiency when used in RxJava. Again, we just look at how to use it, not source code interpretation.

4.1 Retrofit makes a simple Get request

implementation 'com.squareup.retrofit2:retrofit:2.6.2'
implementation 'com.squareup.retrofit2:converter-gson:2.6.2'

First we introduce dependencies, and then we request news data from a well-known daily newspaper (click to see the data: https://news-at.zhihu.com/api...:

// ZhEntity
class ZhEntity {
    var date: String? = null
    var stories: MutableList<StoriesBean>? = null
    var top_stories: MutableList<TopStoriesBean>? = null

    class StoriesBean {
        var image_hue: String? = null
        var title: String? = null
        var url: String? = null
        var hint: String? = null
        var ga_prefix: String? = null
        var type: Int = 0
        var id: Int = 0
        var images: MutableList<String>? = null
    }

    class TopStoriesBean {
        var image_hue: String? = null
        var hint: String? = null
        var url: String? = null
        var image: String? = null
        var title: String? = null
        var ga_prefix: String? = null
        var type: Int = 0
        var id: Int = 0
    }
}
// ApiService
import retrofit2.Call
import retrofit2.http.GET
import retrofit2.http.Url
interface ApiService {
    @GET("news/latest")
    fun getLatestNews(): Call<ZhEntity>
}
// call
val retrofit = Retrofit.Builder()
                .addConverterFactory(GsonConverterFactory.create())
                .baseUrl("https://news-at.zhihu.com/api/4/")
                .build()
        val service: ApiService = retrofit.create(ApiService::class.java)
        val call: Call<ZhEntity> = service.getLatestNews()
        call.enqueue(object : Callback<ZhEntity> {
            override fun onFailure(call: Call<ZhEntity>?, t: Throwable?) {
                Log.e("abc", "--> $t")
            }

            override fun onResponse(call: Call<ZhEntity>?, response: Response<ZhEntity>?) {
                Log.e("abc", "-->${Gson().toJson(response?.body())}")
            }
        })

The code is a little bit more. Explain separately that ZhEntity is an entity class and ApiService is an interface. It defines a method getLatestNews by annotation and @GET represents Get request. So you can imagine that there must be @POST,@GET has parameters in it. This is a subfolder behind the request address BaseUrl.

The return type of the getLatestNews function is Call, which is defined by Retrofit to request the network.
The third piece of code creates a Retrofit object. The addConverterFactory(GsonConverterFactory.create()) converts the json-type data returned by the interface into the type of entity class, which was introduced in the implementation of'com. squareup. retrofit2: converter-gson: 2.6.2'.

Then there is a series of Call s calling qnqueue operations and so on. It can be seen that network requests can be completed without Rxjava, and the code is not complicated. Okay, this article is over.

Okay, I'm talking nonsense. Go on, some people say that they don't like URLs being truncated into two sections. They can modify URLs in this way, and the effect is exactly the same.

// ApiService
import retrofit2.Call
import retrofit2.http.GET
import retrofit2.http.Url
interface ApiService {
    @GET
    fun getLatestNews(@Url url:String): Call<ZhEntity>
}
// call
val retrofit = Retrofit.Builder()
                .addConverterFactory(GsonConverterFactory.create())
                .baseUrl("https://www.baidu.com")
                .build()
        val service: ApiService = retrofit.create(ApiService::class.java)
        val call: Call<ZhEntity> = service.getLatestNews("https://news-at.zhihu.com/api/4/news/latest")
        call.enqueue(object : Callback<ZhEntity> {
            override fun onFailure(call: Call<ZhEntity>?, t: Throwable?) {
                Log.e("abc", "--> $t")
            }

            override fun onResponse(call: Call<ZhEntity>?, response: Response<ZhEntity>?) {
                Log.e("abc", "-->${Gson().toJson(response?.body())}")
            }
        })

baseUrl is still needed, but it doesn't matter if you set it to other values, because it won't be requested.

4.2 Retrofit with RxJava

It took so much time to get to this point. I'm sorry for my limited ability to explain complex problems in simple language.
First, add one more sentence to support RxJava when introducing dependencies:

implementation 'com.squareup.retrofit2:retrofit:2.6.2'
implementation 'com.squareup.retrofit2:converter-gson:2.6.2'
implementation 'com.squareup.retrofit2:adapter-rxjava2:2.6.2'

Then, our getLatestNews can go straight back to an Observable!

import io.reactivex.Observable
import retrofit2.http.GET

interface ApiService {
    @GET("news/latest")
    fun getLatestNews(): Observable<ZhEntity>
}

Don't worry about writing, you won't make mistakes. With Observable, it's easy to do.

val retrofit = Retrofit.Builder()
                .addConverterFactory(GsonConverterFactory.create())
                .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
                .baseUrl("https://news-at.zhihu.com/api/4/")
                .build()
        val service: ApiService = retrofit.create(ApiService::class.java)
        val observable = service.getLatestNews()
        observable.subscribeOn(Schedulers.newThread())
                .subscribe(object : Observer<ZhEntity> {
            override fun onComplete() {
            }

            override fun onSubscribe(d: Disposable) {
            }

            override fun onNext(t: ZhEntity) {
                Log.e("abc","-->${Gson().toJson(t)}")
            }

            override fun onError(e: Throwable) {
                Log.e("abc","-->$e")
            }
        })

Except for the change in Observable sources, there is no difference from RxJava, which was first described in this article. Not to mention the difference, there is one more sentence subscribe On (Schedulers. newThread ()). Now I will talk about this.

4.3 Thread Switching

  • subscribeOn: Defines the thread in which Observable emits events
  • observeOn: Defines the threads (map, flatMap, Observer, etc.) where the conversion/response events are located, and can be switched multiple times

Thread switching is common, such as sub-threads requesting the main thread of network data to update the UI, which threads can subscribeOn and observeOn choose? How do they work? Let's start with an example:

Thread(object : Runnable {
            override fun run() {
                Log.e("abc","Thread Current thread: ${Thread.currentThread().name}")
                observable.subscribeOn(Schedulers.newThread())
                        .doOnNext(object :Consumer<ZhEntity>{
                            override fun accept(t: ZhEntity?) {
                                Log.e("abc","doOnNext Current thread: ${Thread.currentThread().name}")
                            }
                        })
                        .observeOn(Schedulers.io())
                        .flatMap(object :Function<ZhEntity,ObservableSource<ZhEntity.StoriesBean>>{
                            override fun apply(t: ZhEntity): ObservableSource<ZhEntity.StoriesBean> {
                                Log.e("abc","flatMap Current thread: ${Thread.currentThread().name}")
                                return Observable.fromIterable(t.stories)
                            }
                        })
                        .observeOn(AndroidSchedulers.mainThread())
                        .subscribe(object : Observer<ZhEntity.StoriesBean> {
                            override fun onComplete() {
                            }

                            override fun onSubscribe(d: Disposable) {
                                Log.e("abc","onSubscribe Current thread: ${Thread.currentThread().name}")
                            }

                            override fun onNext(t: ZhEntity.StoriesBean) {
                                Log.e("abc","Observer Current thread: ${Thread.currentThread().name}")
                                Log.e("abc", "-->${Gson().toJson(t)}")
                            }

                            override fun onError(e: Throwable) {
                                Log.e("abc", "-->$e")
                            }
                        })
            }
        }).start()
// log printing
Thread Current thread: Thread-4
onSubscribe Current thread: Thread-4
doOnNext Current thread: RxNewThreadScheduler-1
flatMap Current thread: RxCachedThreadScheduler-1
Observer Current thread: main
Observer Current thread: main
Observer Current thread: main

Only doOnNext hasn't talked about this. Now let's say that this method is called back before sending onNext(), so the subscribe of doOnNext and Observable is in the same thread.
From this example, we can see that:

  1. Observable and Observer establish a subscription relationship in the current thread (Thread-4)
  2. subscribeOn determines the thread in which Observable emits events (that is, the thread in which Retrofit requests the network)
  3. The first observeOn determines the thread of flatMap (RxCachedThreadScheduler-1)
  4. Again, observeOn determines the thread of the Observer (Android main thread)

So every time observeOn is called, the thread is switched, and the next transformation / response thread is determined. To add another word, subscribeOn has been set up many times, only for the first time.

Thread Options:

Thread name Explain
Schedulers.immediate() The default Scheduler, which runs directly on the current thread, is equivalent to not specifying a thread
Schedulers.newThread() Enable new threads and perform operations on new threads
Schedulers.io() Scheduler used in I/O operations (reading and writing files, reading and writing databases, network information exchange, etc.). The behavior pattern is similar to that of newThread(), but the difference is that the internal implementation of io() uses an infinite number of upper bound thread pools to reuse idle threads, so in most cases io() is more efficient than newThread(). Don't put computing in io(), you can avoid creating unnecessary threads
Schedulers.computation() Scheduler used for calculation. This calculation refers to CPU-intensive computing, i.e. operations that are not restricted by operations such as I/O, such as graphics computing. This Scheduler uses a fixed thread pool of CPU core size. Do not put I/O operations in computation(), otherwise the waiting time of I/O operations will waste CPU.
AndroidSchedulers.mainThread() Android main thread

Posted by tha_mink on Mon, 30 Sep 2019 19:02:05 -0700