Should I unsubscribe when using rxbinding? - android

There is how I use RxBinding with Kotlin:
override fun onViewCreated(view: View?, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
reset_password_text_view.clicks().subscribe { presenter.showConfirmSecretQuestionBeforeResetPassword() }
password_edit_text.textChanges().skip(1).subscribe { presenter.onPasswordChanged(it.toString()) }
password_edit_text.editorActionEvents().subscribe { presenter.done(password_edit_text.text.toString()) }
}
Observable.subscribe(action) returns Subscription. Should I keep it as reference and unsubscribe onPause() or onDestroy()?
Like this:
private lateinit var resetPasswordClicksSubs: Subscription
override fun onViewCreated(view: View?, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
resetPasswordClicksSubs = reset_password_text_view.clicks().subscribe { presenter.showConfirmSecretQuestionBeforeResetPassword() }
}
override fun onDestroy() {
super.onDestroy()
resetPasswordClicksSubs.unsubscribe()
}

I think that Jake Wharton (the creator of the library) gave the best answer:
Treat a subscribed RxView.clicks() (or any Observable from this
library for that matter) like you would the View reference itself. If
you pass it (or subscribe to it) somewhere outside the lifetime of the
View, you've just leaked your entire activity.
So if you're just subscribing inside your ViewHolder there's no need
to unsubscribe just like there'd be no need to unregister a click
listener were you doing it manually.

I've made a small test setup to find it out. It's not an Android app but it simulates the class relationships. Here's what it looks like:
class Context
class View(val context: Context) {
lateinit var listener: () -> Unit
fun onClick() = listener.invoke()
}
fun View.clicks() = Observable.fromEmitter<String>({ emitter ->
listener = { emitter.onNext("Click") }
}, Emitter.BackpressureMode.DROP)
var ref: PhantomReference<Context>? = null
fun main(args: Array<String>) {
var c: Context? = Context()
var view: View? = View(c!!)
view!!.clicks().subscribe(::println)
view.onClick()
view = null
val queue = ReferenceQueue<Context>()
ref = PhantomReference(c, queue)
c = null
val t = thread {
while (queue.remove(1000) == null) System.gc()
}
t.join()
println("Collected")
}
In this snippet I instantiate a View that holds a reference to a Context. the view has a callback for click events that I wrap in an Observable. I trigger the callback once, then I null out all references to the View and the Context and only keep a PhantomReference. Then, on a separate thread I wait until the Context instance is released. As you can see, I'm never unsubscribing from the Observable.
If you run the code, it will print
Click
Collected
and then terminate proving that the reference to the Context was indeed released.
What this means for you
As you can see, an Observable will not prevent referenced objects from being collected if the only references it has to it are circular. You can read more about circular references in this question.
However this isn't always the case. Depending on the operators that you use in the observable chain, the reference can get leaked, e.g. by a scheduler or if you merge it with an infinite observable, like interval(). Explictly unsubscribing from an observable is always a good idea and you can reduce the necessary boilerplate by using something like RxLifecycle.

Yes, you should unsubscribe when using RxBinding.
Here's one way... (in java, could be tweaked for kotlin?)
Collect
Within your Activity or Fragment, add disposables to a CompositeDisposable that you'll dispose at onDestroy().
CompositeDisposable mCompD; // collector
Disposable d = RxView.clicks(mButton).subscribe(new Consumer...);
addToDisposables(mCompD, d); // add to collector
public static void addToDisposables(CompositeDisposable compDisp, Disposable d) {
if (compDisp == null) {
compDisp = new CompositeDisposable();
}
compDisp.add(d);
}
Dispose
#Override
protected void onDestroy() {
mCompD.dispose();
super.onDestroy();
}

Yep, if you look in the doc, it explicitely says:
Warning: The created observable keeps a strong reference to view. Unsubscribe to free this reference.

Related

How to prevent data duplication caused by LiveData observation in Fragment?

I'm subscribed to an observable in my Fragment, the observable listens for some user input from three different sources.
The main issue is that once I navigate to another Fragment and return to the one with the subscription, the data is duplicated as the observable is handled twice.
What is the correct way to handle a situation like this?
I've migrated my application to a Single-Activity and before it, the subscription was made in the activity without any problem.
Here is my Fragment code:
#AndroidEntryPoint
class ProductsFragment : Fragment() {
#Inject
lateinit var sharedPreferences: SharedPreferences
private var _binding: FragmentProductsBinding? = null
private val binding get() = _binding!!
private val viewModel: ProductsViewModel by viewModels()
private val scanner: CodeReaderViewModel by activityViewModels()
private fun observeBarcode() {
scanner.barcode.observe(viewLifecycleOwner) { barcode ->
if (barcode.isNotEmpty()) {
if (binding.searchView.isIconified) {
addProduct(barcode) // here if the fragment is resumed from a backstack the data is duplicated.
}
if (!binding.searchView.isIconified) {
binding.searchView.setQuery(barcode, true)
}
}
}
}
private fun addProduct(barcode: String) {
if (barcode.isEmpty()) {
return
}
viewModel.insert(barcode)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.start(args.documentId)
if (args.documentType == "Etichette") {
binding.cvLabels.visibility = View.VISIBLE
}
initUI()
observe()
}
private fun observe() {
observeBarcode()
observeProducts()
observeLoading()
observeLast()
}
}
Unfortunately, LiveData is a terribly bad idea (the way it was designed), Google insisted till they kinda phased it out (but not really since it's still there) that "it's just a value holder"...
Anyway... not to rant too much, the solution you have to use can be:
Use The "SingleLiveEvent" (method is officially "deprecated now" but... you can read more about it here).
Follow the "official guidelines" and use a Flow instead, as described in the official guideline for handling UI Events.
Update: Using StateFlow
The way to collect the flow is, for e.g. in a Fragment:
viewLifecycleOwner.lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) { // or RESUMED
viewModel.yourFlow.collectLatest { ... } // or collect { ... }
}
}
For that in your ViewModel you'd expose something like:
Warning: Pseudo-Code
// Imagine your state is represented in this sealed class
sealed class State {
object Idle: State
object Loading: State
data class Success(val name: String): State
data class Failure(val reason: String): State
}
// You need an initial state
private val _yourFlow = MutableStateFlow(State.Idle)
val yourFlow: StateFlow<State> = _yourFlow
Then you can emit using
_yourFlow.emit(State.Loading)
Every time you call
scanner.barcode.observe(viewLifecycleOwner){
}
You are creating a new anonymous observer. So every new call to observe will add another observer that will get onChanged callbacks. You could move this observer out to be a property. With this solution observe won't register new observers.
Try
class property
val observer = Observer<String> { onChanged() }
inside your method
scanner.barcode.observe(viewLifecycleOwner, observer)
Alternatively you could keep your observe code as is but move it to a Fragment's callback that only gets called once fex. onCreate(). onCreate gets called only once per fragment instance whereas onViewCreated gets called every time the fragment's view is created.

How to pass lambda as intent to start an activity in android using kotlin

I am writing a library that allow user to start my activity with pass in params and callbacks.
The architecture I been following is the MVVM. I dont have any issue with the MVVM pattern however, I am running into an occasional issue with the callback pass in from my caller.
class mainActivity :Activity {
// standard lifecycle
override fun onCreate(savedInstanceState: Bundle?) {
...
}
override fun onResume() {
// here I am accessing the static callback from companion object.
// but sometimes I am getting lateinit property callback has not been initialized
//exception.
callback.invoke()
}
companion object {
lateinit var callback: () -> Unit
//user will call my library from this method and provide me the callback
fun play(
callback: () -> Unit
){
callback = callback // here I assigned the callback so that my activity can use it later.
}
}
}
inside onResume I am accessing the callback. sometimes I am getting
lateinit property callback has not been initialized
however, I am not available to reproduce it 100% at all. What can be an issue? can you please shed some lights with me please?

Properly initialize heavy objects

I am trying to convert some Java code to Kotlin. I have a "heavy" object that I cannot reason about how to initialize properly in the app. The object can take some time to create and I don't want to block except for when the functionality is actually required. I wrote some code that meets my requirements, but it doesn't seem like a good pattern and I was hoping someone tell me what the proper pattern here (will list what I don't like about it after the code):
package com.example.myapplication
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import kotlinx.coroutines.*
import javax.inject.Provider
import kotlinx.coroutines.channels.Channel
class MainActivity : AppCompatActivity() {
lateinit var heavyInitObject: Provider<HeavyInitObject>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
heavyInitObject = initHeavyObject()
}
fun useTheHeavyInitObject() {
// Need it to block here, before work is done.
val hio = heavyInitObject.get()
}
}
fun initHeavyObject(): Provider<HeavyInitObject> {
val cnl = Channel<HeavyInitObject>(Channel.BUFFERED)
//purposely block to ensure to ensure it is initialized
val provider = Provider { runBlocking { cnl.receive().also { cnl.close() }}}
HeavyInitObject.get(object : HeavyInitObject.HeavyInitObjectListener{
override fun onReady(heavyInitObject: HeavyInitObject) = runBlocking {
cnl.send(heavyInitObject)
}
})
return provider
}
// Mocked library I am using (i.e. I don't have control over the implementation)
class HeavyInitObject {
companion object {
fun get(listener: HeavyInitObjectListener) {
val heavyInitObject = HeavyInitObject()
listener.onReady(heavyInitObject)
}
}
interface HeavyInitObjectListener {
fun onReady(heavyInitObject: HeavyInitObject)
}
}
What I don't like
Should be a val
It naturally really be a val, because the value should never change once initialized.
class MainActivity : AppCompatActivity() {
val heavyInitObject: Provider<HeavyInitObject> = initHeavyObject()
// OR...
val heavyInitObject: HeavyInitObject by lazy {
initHeavyObject().get()
}
The first option seems like it could do too much too fast. Depending on how someone would add MainActivity to the object graph it could really affect startup performance.
The second one is too slow. If we haven't requested the heavy object to be created before it is needed, there will be definite jank in the application when the heavy object is queried the first time.
Is there a good way to have the object be a val while requesting the object to be created in onCreate (understanding that I don't have control over implementation of the underlying library)?
Is channel the right data structure here?
Maybe this is bareable, but I wanted to see if there is a better option. A RENDEZVOUS channel makes more sense, but send suspends until receive is called and I don't want to block anything on thread initializing the object (i.e. since i can't convert the implementation to a suspend function). Switching to a bufferend channel won't block since I only send one element through, but that seems like a hack. What is the best data structure for this task?
Edit:
Thanks to some help in the comments I have improved the second condition (eliminate akward use of channel). I have a couple ideas for how to improve the first condition...
Code for getting rid of channel
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
class MainActivity : AppCompatActivity() {
lateinit var heavyInitObject: Provider<HeavyInitObject>
override suspend fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
heavyInitObject = lifecycleScope.async {deferredHeavyObject()}
}
fun useTheHeavyInitObject() {
// Need it to block here, before work is done.
val hio = heavyInitObject.await()
}
}
suspend fun initHeavyObject(): HeavyInitObject = suspendCancellableCoroutine { continuation ->
HeavyInitObject.get(object : HeavyInitObject.HeavyInitObjectListener {
override fun onReady(heavyInitObject: HeavyInitObject) {
continuation.resume(heavyInitObject)
}
})
}
Code to finalize heavyInitObject
class MainActivity : AppCompatActivity() {
val heavyInitObject by lazy { heavyInitObjectBackingField }
private lateinit var heavyInitObjectBackingField: Deferred<HeavyInitObject>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
heavyInitObjectBackingField = lifecycleScope.async { deferredHeavyObject()}
}}
I basically get a lateinit val that way... and can be confident I won't get an error for it not being initialized. Ideally, it makes me realize this is overcomplicated, because I can't get under the hood and easily seperate the object initialization from the call back initialization.. Unless anyone else has a better idea?
Channel is kind of weird for returning a single item. I would load it in a Deferred privately and publicly expose a suspend getter that awaits the Deferred result. Once the object is ready, it won’t have to be waited for. And since it’s a suspend function, you can access it via a coroutine without unlocking your main thread.
object HeavyObjectCreator {
private val heavyObject: Deferred<HeavyObject> = GlobalScope.async {
// Long running actions to generate the object…
HeavyObject(params)
}
suspend fun getInstance(): HeavyObject =
heavyObject.await()
}
In your activity you can use lifecycleScope.launch to start a coroutine when you need to do a task that uses the object and it can call the above function to get it in a suspending way. If you want to preload the heavy object, you can put the statement HeavyObjectCreator in onCreate of your Application class or your Activity so the creator object will be instantiated and start the coroutine to load the heavy object.
This is just one example of a way to do it for an object that you’ll be reusing on multiple screens. If you intend to load a new heavy object only on screens that need it, I’d consider putting the contents of the class above directly in a ViewModel and use viewModelScope instead of GlobalScope.

AutoClearedValue accessed from another thread after View is Destroyed

I am using AutoClearedValue class from this link and when view is destroyed, backing field becomes null and that is good but i have a thread(actually a kotlin coroutine) that after it is done, it accesses the value(which uses autoCleared) but if before it's Job is done i navigate to another fragment(view of this fragment is destroyed), then it tries to access the value, but since it is null i get an exception and therefore a crash.
what can i do about this?
also for which variables this autoCleared needs to be used? i use it for viewBinding and recyclerview adapters.
You have 2 option:
1- Cancelling all the running job(s) that may access to view after its destruction. override onDestroyView() to do it.
Also, you can launch the coroutines viewLifecycleOwner.lifecycleScope to canceling it self when view destroy.
viewLifecycleOwner.lifecycleScope.launch {
// do sth with view
}
2- (Preferred solution) Use Lifecycle aware components (e.g LiveData) between coroutines and view:
coroutines push the state or data in the live-data and you must observe it with viewLifeCycleOwner scope to update the view.
private val stateLiveData = MutableLiveData<String>()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
stateLiveData.observe(viewLifecycleOwner) { value ->
binding.textView.text = value
}
}
private fun fetchSomething() {
lifecycleScope.launch {
delay(10_000)
stateLiveData.value = "Hello"
}
}

Should I include LifecycleOwner in ViewModel?

LifecycleOwner is currently needed in order for me to create an observer.
I have code which creates an Observer in the ViewModel so I attach the LifecycleOwner when retrieving the ViewModel in my Fragment.
According to Google's documentation.
Caution: A ViewModel must never reference a view, Lifecycle, or any class that may hold a reference to the activity context.
Did I break that warning and If I did, what way do you recommend me to move my creation of an observer for data return?
I only made an observer so I'm wondering if it's still valid. Since also in Google's documentation it also said.
ViewModel objects can contain LifecycleObservers, such as LiveData objects.
MainFragment
private lateinit var model: MainViewModel
/**
* Observer for our ViewModel IpAddress LiveData value.
* #see Observer.onChanged
* */
private val ipObserver = Observer<String> {
textIp.text = it
hideProgressBar()
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
model = ViewModelProviders.of(this).get(MainViewModel::class.java)
model.attach(this)
}
override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?, savedInstanceState: Bundle?): View? =
inflater?.inflate(R.layout.fragment_main, container, false)
override fun onViewCreated(view: View?, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
buttonRetrieveIp.setOnClickListener {
showProgressBar()
model.fetchMyIp().observe(this, ipObserver) //Here we attach our ipObserver
}
}
override fun showProgressBar() {
textIp.visibility = View.GONE
progressBar.visibility = View.VISIBLE
}
override fun hideProgressBar() {
progressBar.visibility = View.GONE
textIp.visibility = View.VISIBLE
}
MainViewModel
private var ipAddress = MutableLiveData<String>()
private lateinit var owner: LifecycleOwner
fun attach(fragment: MainFragment) {
owner = fragment
}
/**
* For more information regarding Fuel Request using Fuel Routing and Live Data Response.
* #see Fuel Routing Support
* #see Fuel LiveData Support
* */
fun fetchMyIp(): LiveData<String> {
Fuel.request(IpAddressApi.MyIp())
.liveDataResponse()
.observe(owner, Observer {
if (it?.first?.statusCode == 200) {//If you want you can add a status code checker here.
it.second.success {
ipAddress.value = Ip.toIp(String(it))?.ip
}
}
})
return ipAddress
}
Update 1: Improved ViewModel thanks to #pskink suggestion for using Transformations.
private lateinit var ipAddress:LiveData<String>
/**
* Improved ViewModel since January 23, 2018 credits to pskink <a href="
*
* For more information regarding Fuel Request using Fuel Routing and Live Data Response.
* #see Fuel Routing Support
* #see Fuel LiveData Support
* */
fun fetchMyIp(): LiveData<String> {
ipAddress = Transformations.map(Fuel.request(IpAddressApi.MyIp()).liveDataResponse(), {
var ip:String? = ""
it.second.success {
ip = Ip.toIp(String(it))?.ip
}
ip
})
return ipAddress
}
No. If you wish to observe changes of some LiveData inside your ViewModel you can use observeForever() which doesn't require LifecycleOwner.
Remember to remove this observer on ViewModel's onCleared() event:
val observer = new Observer() {
override public void onChanged(Integer integer) {
//Do something with "integer"
}
}
...
liveData.observeForever(observer);
...
override fun onCleared() {
liveData.removeObserver(observer)
super.onCleared()
}
Very good reference with examples of observe LiveData.
Assumptions:
Fuel refers to your ViewModel
Fuel.request(IpAddressApi.MyIp()) is a method in your ViewModel
IpAddressApi.MyIp() does not have a reference to your LifecycleOwner,
If all are true,then you are not violating it. So long as you are not passing a LifecycleOwner reference to the ViewModel you are safe!
LifecycleOwner - relates to an Activity or Fragment as it owns the various Android Lifecycles e.g onCreate, onPause, onDestroy etc
in Kotlin this can be something like:
val mObserver = Observer<List<QueueTabData>> { myString->
// do something with myString
}
Should I include LifecycleOwner in ViewModel?
Ans: No
The purpose of viewmodel is to hold UI data, so that it survives across configuration changes.
And the reason for the following
Caution: A ViewModel must never reference a view, Lifecycle, or any class that may hold a reference to the activity context.
Is because the viewmodel survives configuration changes whereas activities don't. They are destroyed and re-created on configuration change. So, if you have any activity context references in viewmodel they would refer to previous activity that got destroyed.
So this leads to memory leak. And hence it is not recommended.
Furthermore,
If you have repositories that act as your data source we should avoid using LiveData for such purposes as mentioned here in the paragraph just above the code block.
This is because LiveData are handled on MainThread that may lead to UI freeze.
We should use kotlin flows for such purposes.

Categories

Resources