Write a modern Android project -Part1 – from scratch using Kotlin
Write a modern Android project -Part2 – from scratch using Kotlin
Begin the text!
What is RxJava?
A broad concept about RxJava is that RxJava is a Java implementation of an API for asynchronous programming with an observable flow and a responsive API. In fact, it is a combination of these three concepts: the observer pattern, the iterator pattern, and functional programming. There are also libraries for other programming languages, such as RxSwift, RxJs, RxNet, etc.
I had a hard time getting started with RxJava, and at times, it can be really confusing and can cause you some problems if not implemented properly. Still, it is worth our time to learn it. I’ll try to explain RxJava in simple steps.
First, let’s answer some simple questions that you might ask yourself as you start reading about RxJava:
Do we really need it?
The answer is no, RxJava is just another library that can be used in Android development. It’s not necessary if you’re using Kotlin, but I hope you know what I’m saying, it’s just a library that will help you, just like all the other libraries you use.
Do I have to learn RxJava1 to learn RxJava2?
You can start with RxJava2 directly, but as an Android developer, it’s good for you to know both, since you may be involved in maintaining someone else’s RxJava1 code.
I see RxAndroid, should I use RxAndroid or RxJava?
RxJava can be used on any Java development platform, not just Android. For example, RxJava can be used with frameworks like Spring for back-end development. RxAndroid is a library that contains the libraries needed to use RxJava on Android. Therefore, if you want to use RxJava in Android development, you must add RxAndroid. Later, I’ll explain what RxAndroid adds based on RxJava.
We developed using Kotlin, why not RxKotin?
There is no need to add another Rx library, as Kotlin is fully compatible with Java, and there is indeed an RxKotin library: github.com/ReactiveX/R… , but the library is written on top of RxJava. It simply adds Kotlin functionality to RxJava. You can use RxJava with Kotlin without using the RxKotlin library. For simplicity, I will not use RxKotlin in this section.
How do I add Rxjava2 to my project?
To use RxJava, you need to add the following code to build.gradle:
dependencies {
...
implementation "IO. Reactivex. Rxjava2: rxjava: 2.1.8"
implementation "IO. Reactivex. Rxjava2: rxandroid: 2.0.1." ". }Copy the code
Then, click sync to download the Rxjava library.
What does RxJava contain?
I’d like to divide RxJava into the following three parts:
- Classes for observer schema and data flow:
Observables
和Observers
- 2, Schedulers,
- 3. Data flow operators
Observables
和 Observers
We have explained the pattern. You can think of an Observable as the source of the data (observed) and an Observer as the source of the data received (Observer).
There are many ways to create Observables. The simplest way is to use Observable.just () to get an item and create an Observable to launch the item.
Let’s go to the GitRepoRemoteDataSource class and change the getRepositories method to return Observable:
class GitRepoRemoteDataSource {
fun getRepositories(a) : Observable<ArrayList<Repository>> {
var arrayList = ArrayList<Repository>()
arrayList.add(Repository("First from remote"."Owner 1".100.false))
arrayList.add(Repository("Second from remote"."Owner 2".30.true))
arrayList.add(Repository("Third from remote"."Owner 3".430.false))
return Observable.just(arrayList).delay(2,TimeUnit.SECONDS)
}
}
Copy the code
Observable
> represents an ArrayList of objects that an Observable emits from a Repository. To create an Observable that emits a Repository object, use Observable.from (arrayList).
. Delay (2, timeunit. SECONDS) Indicates that data is transmitted after 2s.
But wait! We don’t have a high number of Observables that emit data. Observables usually start sending out data after a few Observer subscriptions.
Please note that we no longer need the following interfaces
interface OnRepoRemoteReadyCallback {
fun onRemoteDataReady(data: ArrayList<Repository>)
}
Copy the code
Make the same change in the GitRepoLocalDataSource: class
class GitRepoLocalDataSource {
fun getRepositories(a) : Observable<ArrayList<Repository>> {
var arrayList = ArrayList<Repository>()
arrayList.add(Repository("First From Local"."Owner 1".100.false))
arrayList.add(Repository("Second From Local"."Owner 2".30.true))
arrayList.add(Repository("Third From Local"."Owner 3".430.false))
return Observable.just(arrayList).delay(2, TimeUnit.SECONDS)
}
fun saveRepositories(arrayList: ArrayList<Repository>) {
//todo save repositories in DB}}Copy the code
Also, this interface is not needed:
interface OnRepoLocalReadyCallback {
fun onLocalDataReady(data: ArrayList<Repository>)
}
Copy the code
Now we need to return Observable in Repository
class GitRepoRepository(private val netManager: NetManager) {
private val localDataSource = GitRepoLocalDataSource()
private val remoteDataSource = GitRepoRemoteDataSource()
fun getRepositories(a): Observable<ArrayList<Repository>> { netManager.isConnectedToInternet? .let {if (it) {
//todo save those data to local data store
return remoteDataSource.getRepositories()
}
}
return localDataSource.getRepositories()
}
}
Copy the code
If the network is connected, we returned from the remote data source observables, otherwise, the observables, returning from a trip to the local data source in the same way, we no longer need OnRepositoryReadyCallback interface.
As you might expect, we need to change the way we get data in the MainViewModel. Now we should get the Observable from gitRepoRepository and subscribe to it. Once we subscribe to the Observable to an Observer, the Observable will start sending out data:
class MainViewModel(application: Application) : AndroidViewModel(application) {
...
fun loadRepositories(a) {
isLoading.set(true)
gitRepoRepository.getRepositories().subscribe(object: Observer<ArrayList<Repository>>{
override fun onSubscribe(d: Disposable) {
//todo
}
override fun onError(e: Throwable) {
//todo
}
override fun onNext(data: ArrayList<Repository>) {
repositories.value = data
}
override fun onComplete(a) {
isLoading.set(false)}})}}Copy the code
Once an Observer subscribes to an Observable, the onSubscribe method is called, the main onSubscribe parameter (Disposable), which we’ll talk about later.
The onNext () method is called every time an Observable emits data. OnComplete () is called once when Observable completes sending S data. After that, the Observable terminates.
If something goes wrong, the onError () method is called back and the Observable terminates. This means that the Observable will no longer emit data, so onNext () will not be called and onComplete () will not be called.
Also, please note. If you try to subscribe to a terminated Observable, you receive an IllegalStateException.
So how does RxJava help us?
- First, we get rid of these interfaces, which are boilerplate interfaces for all Repository and data sources.
- If we use interfaces and some exceptions occur in the data layer, our application may crash. Errors using RxJava will occur in
OnError ()
Method, so we can display the appropriate error message to the user. - Because we always use RxJava for the data layer, it’s cleaner.
- I didn’t tell you before: the old approach could cause a memory leak.
How do I prevent memory leaks when using RxJava2 and ViewModel
Let’s look at the ViewModel’s life cycle diagram again
Once the Activity is destroyed, the ViewModel’s onCleared method is called. In the onCleared method, we need to cancel all subscriptions
class MainViewModel(application: Application) : AndroidViewModel(application) {
...
lateinit var disposable: Disposable
fun loadRepositories(a) {
isLoading.set(true)
gitRepoRepository.getRepositories().subscribe(object: Observer<ArrayList<Repository>>{
override fun onSubscribe(d: Disposable) {
disposable = d
}
override fun onError(e: Throwable) {
//if some error happens in our data layer our app will not crash, we will
// get error here
}
override fun onNext(data: ArrayList<Repository>) {
repositories.value = data
}
override fun onComplete(a) {
isLoading.set(false)}}}override fun onCleared(a) {
super.onCleared()
if(! disposable.isDisposed){ disposable.dispose() } } }Copy the code
We can optimize the above code:
First, replace the Observer with DisposableObserver, which implements Disposable and has the Dispose () method, we no longer need the onSubscribe() method, Because we can call Dispose () directly on the DisposableObserver instance.
Second, replace the.subscribe() method that returns Void with the.subscribe with () method, which returns the specified Observer
class MainViewModel(application: Application) : AndroidViewModel(application) {
...
lateinit var disposable: Disposable
fun loadRepositories(a) {
isLoading.set(true)
disposable = gitRepoRepository.getRepositories().subscribeWith(object: DisposableObserver<ArrayList<Repository>>() {
override fun onError(e: Throwable) {
// todo
}
override fun onNext(data: ArrayList<Repository>) {
repositories.value = data
}
override fun onComplete(a) {
isLoading.set(false)}}}override fun onCleared(a) {
super.onCleared()
if(! disposable.isDisposed){ disposable.dispose() } } }Copy the code
The above code can be further optimized:
We saved a Disposable instance so we can call Dispose () in the onCleared() callback, but wait! Do we need to do this for every call? If there are 10 callbacks, then we have to save 10 instances and unsubscribe 10 times in onCleared()? Obviously not, there is a better way, we should save them all in a bucket and process them all at once when the onCleared () method is called. We can use CompositeDisposable.
CompositeDisposable: A container that holds more than one Disposable
Therefore, each time a Disposable is created, it needs to be added to the CompositeDisposable:
class MainViewModel(application: Application) : AndroidViewModel(application) {
...
private val compositeDisposable = CompositeDisposable()
fun loadRepositories(a) {
isLoading.set(true)
compositeDisposable.add(gitRepoRepository.getRepositories().subscribeWith(object: DisposableObserver<ArrayList<Repository>>() {
override fun onError(e: Throwable) {
//if some error happens in our data layer our app will not crash, we will
// get error here
}
override fun onNext(data: ArrayList<Repository>) {
repositories.value = data
}
override fun onComplete(a) {
isLoading.set(false)}}}))override fun onCleared(a) {
super.onCleared()
if(! compositeDisposable.isDisposed){ compositeDisposable.dispose() } } }Copy the code
Thanks to Kotlin’s extension function, we can go one step further:
Like C# and Gosu, Kotlin provides the ability to extend a class with new functionality without having to inherit the class, called extension functions.
Let’s create a new package called extensions and add a new file rxextensions.kt
import io.reactivex.disposables.CompositeDisposable
import io.reactivex.disposables.Disposable
operator fun CompositeDisposable.plusAssign(disposable: Disposable) {
add(disposable)
}
Copy the code
Now we can add the Disposable object to the CompositeDisposable instance using the + = symbol:
class MainViewModel(application: Application) : AndroidViewModel(application) {
...
private val compositeDisposable = CompositeDisposable()
fun loadRepositories(a) {
isLoading.set(true)
compositeDisposable += gitRepoRepository.getRepositories().subscribeWith(object : DisposableObserver<ArrayList<Repository>>() {
override fun onError(e: Throwable) {
//if some error happens in our data layer our app will not crash, we will
// get error here
}
override fun onNext(data: ArrayList<Repository>) {
repositories.value = data
}
override fun onComplete(a) {
isLoading.set(false)}}}override fun onCleared(a) {
super.onCleared()
if(! compositeDisposable.isDisposed) { compositeDisposable.dispose() } } }Copy the code
Now, we run the program, and when you click the Load Data button, 2s later, the program crashes, and then, if you look at the log, you’ll see that an error occurred inside the onNext method, and the reason for the exception is:
java.lang.IllegalStateException: Cannot invoke setValue on a background thread
Why does this exception occur?
Schedulers
RxJava comes with Schedulers that allow us to choose which thread code to execute on. More precisely, we can choose which thread to execute on using the subscribeOn () side, and the observeOn () method can observe which thread observer. In general, all of our data layer code should be executed in background threads. For example, if we use schedulers.newthread (), the Scheduler assigns us a newThread every time we call it. There are other methods in Scheduler for simplicity, I won’t cover it in this blog post.
You probably already know that all the UI code is done on the Android main thread. RxJava is a Java library that doesn’t understand the Android main thread, which is why we use RxAndroid. RxAndroid allows us to select the Android Main thread as the thread to execute the code. Obviously, our Observer should run on the Android Main thread.
Let’s change the code:
.fun loadRepositories(a) {
isLoading.set(true)
compositeDisposable += gitRepoRepository
.getRepositories()
.subscribeOn(Schedulers.newThread())
.observeOn(AndroidSchedulers.mainThread())
.subscribeWith(object: DisposableObserver<ArrayList<Repository>>() { ... })}...Copy the code
Then I run the code again and everything is fine, nice~
Other observables types
There are a few other Observable types
- Single: The observed emits only one data, or an exception
- Maybe: The observed emits no data, or only one data, or an exception
- Launch Completable:
onSuccess()
Events or exceptions - Flowable:
Observable<T>
Likewise, no data is emitted, or n data is emitted, or an exception is emitted, which is not supported by an ObservableBack pressureFlowable does.
What is backpressure?
To remember concepts, I like to compare them to real life examples
Think of it like a channel. If you stuff the channel with as many goods as the bottleneck can handle, it’s going to be bad. Here too, sometimes your observer can’t handle the number of events it receives and needs to slow down.
You can check out RxJava’s documentation on back pressure: github.com/ReactiveX/R…
The operator
The best thing about RxJava is its operators, which can solve problems that normally take 10 or more lines in RxJava with a single line of code. Here’s what the operator can do for us:
- Merge observables
- filter
- Do the operation according to the condition
- Convert observables to another type
Let me give you an example. Let’s save the data to a GitRepoLocalDataSource. Because we’re saving the data, we need Completable to simulate it. Suppose we also want to simulate a delay of 1 second. The naive approach is:
fun saveRepositories(arrayList: ArrayList<Repository>): Completable {
return Completable.complete().delay(1,TimeUnit.SECONDS)
}
Copy the code
Why naive?
Completable.complete () returns an instance of Completable, which is completed immediately after the subscription.
Once the Completable completes, it terminates. Therefore, no operator (delay being one of the operators) will be executed thereafter. In this case, our Completable doesn’t have any delay. Let’s find a solution:
fun saveRepositories(arrayList: ArrayList<Repository>): Completable {
return Single.just(1).delay(1,TimeUnit.SECONDS).toCompletable()
}
Copy the code
Why this way?
Single.just(1) creates a Single instance and emits only the digit 1. Because we used delay(1, timeunit.seconds), the launch operation is delayed for 1s.
ToCompletable () returns a Completable that discards the result of Single and calls onComplete when onSuccess is called by this Single.
Therefore, the code above returns Completable and calls onComplete() 1s later.
Now we should change our GitRepoRepository. Let’s review the logic. We check the Internet connection. If there is an Internet connection, we fetch the data from the remote data source, save it in the local data source and return the data. Otherwise, we just get the data from the local data source. Take a look at:
fun getRepositories(a): Observable<ArrayList<Repository>> { netManager.isConnectedToInternet? .let {if (it) {
return remoteDataSource.getRepositories().flatMap {
return@flatMap localDataSource.saveRepositories(it)
.toSingleDefault(it)
.toObservable()
}
}
}
return localDataSource.getRepositories()
}
Copy the code
Use flatMap. Once remoteDataSource. GetRepositories () emission data, the project will be mapped to a new observables of the same project. The same item emitted by the new Observable we created from The Completable is saved in the local data store and converted into a Single that emits the same emission. Since we need to return an Observable, we must convert Single to An Observable.
Crazy, huh? Imagine what else RxJava could do for us!
RxJava is a very useful tool, use it, explore it, I believe you will love it!
That’s all for this article. The next article will be the last in this series. Stay tuned!
This series has been updated:
Write a modern Android project -Part1 – from scratch using Kotlin
Write a modern Android project -Part2 – from scratch using Kotlin
Write a modern Android project -Part3 – from scratch using Kotlin
Write a modern Android project -Part4 – from scratch using Kotlin
The article was first published on the public account: “Technology TOP”, there are dry articles updated every day, you can search wechat “technology TOP” first time to read, reply [mind map] [interview] [resume] yes, I prepare some Android advanced route, interview guidance and resume template for you