This post is the part of the New Era of Rx -> Learning Rx. In this post, we will learn about how to survive our RxStreams when the Android device rotates.
Mostly, as Android Developers, we start our Android App without taking care of the landscape mode. Instead, we mostly do portrait mode hardcoded in our apps. Due to this, mostly, our architectures ignore landscape mode. As we know, requirements are changing very fast, and the day when Project Manager comes and asks the team we need to support landscape mode due to the new UX/UI requirement is the judgment day for devs.
Prerequisite:
- Subjects in Rx ( Confusion between Subject and Observable + Observer Part8 )
- Android ViewModels
- Proxy Pattern ( Optional )
Introduction:
In this post, I am going to use a straightforward Rx example, but hopefully, that example will convey the main concept, and I am sure anyone can use this concept in any real-world scenario. By the way, at the end of this post, I will add some links in which you can see the use of this technique in real-world scenarios.
Rx Example Project:
As you can see in the above gif. When a device rotates, we lost our seconds state, and seconds restarts again from zero value.
The code is available until here on rx-stream-break-on-device-rotate also has shown below:
class MainActivity : AppCompatActivity() {
private val disposable = CompositeDisposable()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
Observable.interval(0, 1, TimeUnit.SECONDS)
.map(Long::toString)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(textView::setText)
.let(disposable::add)
}
override fun onDestroy() {
disposable.clear()
super.onDestroy()
}
}
Now, as we know in Android, we can save the state in a bundle by overriding OnConfiguration related API’s, but I am going with a more easy technique. We will use Android ViewModel. This API gives us a guarantee when a Configuration change occurs that will stay alive, and we will get the same instance of the ViewModel once device rotated and activity recreated.
Refactoring of the current code:
Now, we need to move business logic to ViewModel, and UI logic will stay in the Activity.
So in our case, we will move our interval Observable to ViewModel and will expose a uiState method. So any can subscribe to get seconds from viewModel.
class MainViewModel : ViewModel() { fun uiState(): Observable<String> = Observable.interval(0, 1, TimeUnit.SECONDS) .map(Long::toString) .observeOn(AndroidSchedulers.mainThread()) }
class MainActivity : AppCompatActivity() { private val disposable = CompositeDisposable() private lateinit var viewModel: MainViewModel override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) viewModel = ViewModelProviders.of(this).get(MainViewModel::class.java) viewModel.uiState() .subscribe(textView::setText) .let(disposable::add) } override fun onDestroy() { disposable.clear() super.onDestroy() } }
It’s time to run our app again.
Ah, still, we have the same problem as shown in the above image. Is our ViewModel working as expected? For that, we can debug our app by adding one testing method, as shown below:
class MainViewModel : ViewModel() { fun test(){ println("Is Same Instance after rotation: $this") } ... }
class MainActivity : AppCompatActivity() { ... override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) ... viewModel.test() } ... }
After a restart, we got the below output:
com.uwantolearn.surviverxstream.MainViewModel@8575276
com.uwantolearn.surviverxstream.MainViewModel@8575276
Its mean viewModel is working correctly. So what is the issue? The issue is in our MainActivity code.
class MainActivity : AppCompatActivity() { private val disposable = CompositeDisposable() private lateinit var viewModel: MainViewModel override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) viewModel = ViewModelProviders.of(this).get(MainViewModel::class.java) viewModel.uiState() .subscribe(textView::setText) .let(disposable::add) } override fun onDestroy() { disposable.clear() super.onDestroy() } }
As we know, when Activity recreated that always calls its onDestroy method first and after that onCreate method call again. In our case, we are subscribing to our stream in onCreate() and disposing of in our onDestroy() method. It means code is working as expected.
Solution:
After doing a focus on our problem and the code, I find out the issue. We want to run our business logic at ViewModel Life Cycle scope and not as Activity Life Cycle scope.
In this case, we can use more than one technique, but I am going with Proxy Pattern if you guys are not aware of the Proxy Pattern. That is Ok. Only focus on our next implementation, and everything will be clear.
Now, as ViewModel constructed, we will create a one Publish Subject , and we will use this Publish Subject as a proxy.
class MainViewModel : ViewModel() { private val proxySubject = PublishSubject.create<String>() }
Next, our Proxy Subject will subscribe to intervalObservable in the constructor, as shown below :
class MainViewModel : ViewModel() { private val proxySubject = PublishSubject.create<String>() init { Observable.interval(0, 1, TimeUnit.SECONDS) .map(Long::toString) .subscribe(proxySubject::onNext) } }
In this code, we are saying, when ViewModel initialized our Proxy Subject will subscribe to interval Observable. As we know, Rx Subjects work as an Observable + Observer. If you are confused, I will recommend you to read my other post on Subjects Confusion between Subject and Observable + Observer Part8. Back to our topic, now our second’s stream will start as ViewModel constructed not when uiState() method called in MainActivity. Now, we need to change the uiState() method implementation because currently, uiState() returning an interval Observable but after Proxy Subject we will return Proxy Subject as Observable as shown below:
// Before fun uiState(): Observable<String> = Observable.interval(0, 1, TimeUnit.SECONDS) .map(Long::toString) .observeOn(AndroidSchedulers.mainThread())
// After fun uiState(): Observable<String> = proxySubject.hide() .observeOn(AndroidSchedulers.mainThread())
Next, we need to complete our Proxy Subject when viewModel is ready to destroy. In this case, we will override the onCleared() method of viewModel, as shown below:
class MainViewModel : ViewModel() { ... override fun onCleared() { proxySubject.onComplete() super.onCleared() } }
Now, all cases implemented, its time review MainViewModel code, as shown below:
class MainViewModel : ViewModel() {
private val proxySubject = PublishSubject.create<String>()
init {
Observable.interval(0, 1, TimeUnit.SECONDS)
.map(Long::toString)
.subscribe(proxySubject::onNext)
}
fun uiState(): Observable<String> = proxySubject.hide()
.observeOn(AndroidSchedulers.mainThread())
override fun onCleared() {
proxySubject.onComplete()
super.onCleared()
}
}
I hope until now; everything is clear to everyone.
Its time to run our app and check is seconds state are working as per our expectation or not.
HURRAY, everything is working as per our expectations. Now our Rx Stream is alive when Android device rotates.
Bonus Proxy Pattern:
There is one fantastic thing happen, we did changes in ViewModel, but we did no change on MainActivity code. It’s mean we correctly implemented a Proxy Pattern. Instead, as we know, Proxy Pattern is a new layer between the client and the business logic object with the same interface, and due to this same interface, we did no changes in MainActivity code.
Conclusion:
In this post, we learn about how to survive Rx Stream in between rotations by using Android ViewModel and Proxy Pattern.
The complete code is available on the GitHub branch rx-stream-alive-on-device-rotate.
Real-World Scenarios:
- if you are using MVI Architecture with Rx in Android, then you can use this technique. ( Step by step Mvi Android Design Pattern Slides )
- In our New Era of Rx Android Watch, we will use this technique in our version 3, where we need to save our timer stream as the user will rotate the device. For now, this version 3 is in the draft, but you guys can read the first two parts meanwhile, and as I published version 3, I will update links here. v1: (V1-Android StopWatch ( New Era of Rx )) v2: (V2-Android StopWatch ( New Era of Rx ))