Fragment and viewLifecycleOwner deviation - android

I'm collecting a flow on the viewLifecycleOwner. It flows on Dispatchers.Default, but the collection itself takes place on Dispatchers.Main.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
viewLifecycleOwner.lifecycleScope.launch {
flow.flowOn(Default).collect {
requireContext()
}
}
}
In one occation I found the IllegalStateException stating, that the fragment is not attached.
IllegalStateException: Fragment Test not attached to a context.
I assumed that the collection of the flow would be canceled before the fragment is detached.
How is it possible for the coroutine to resume on a detached fragment?

First of all, it is worth noting that flowOn changes the upstream context, in case you have operator functions such as map, filter, etc, preceding to the flowOn. So, it doesn't affect the context of the terminal functions like collect. It is stated on the kotlin docs. So, if you want to change the context of the collect terminal, you should change it from the outer block, I mean the launch builder function.
Next, to avoid IllegalStateException use context in a safe manner, instead of requireContext() to be sure that the fragment is attached. There is no doubt that all of the coroutines launched in the viewLifecycleOwner.lifecycleScope should be terminated when the fragment is getting destroyed, but in some cases, there might exist a race condition in threads which causes this problem.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewLifecycleOwner.lifecycleScope.launch(Main/Default/WhateverContextYouWant) {
flow.collect {
context?.let { }
}
}
}

Related

Android Kotlin Room database. Query fails when called from ViewModel. Passes from App Inspection window

I have an app that use fragments, navigation and viewModels. I cannot call a query "getCustomerById" from my view model (CustomerViewModel.kt).
More details:
I am selecting customer from CustomerFragment which opens CustomerDetailFragment and display details about customer. I am using a common ViewModel for both fragments. Most of the things work, but I cannot get my query (getCustmoerbyID) to work from CustomerDetailsFragment. I have a compiler error.
The code looks like this.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val customer = customerViewModel.geCustomerById("11")
Log.i(Constants.LOG_TAG, "Customer details fragment. ${customer.first().firstName}. Done ** onViewCreated. **")
}
The error is in the log statement, for method first(). "Suspend function 'first' should be called only from a coroutine or another suspend function"
Same issue with "getCustomerCount". I have spent hours or days on it, because of my lack of knowledge of Coroutines. I cannot put code from original app, so I have put a simplified app on github. The link is as follows.
https://github.com/msyusuf. The ViewModel is CustomerViewModel. The query is
fun geCustomerById(cust_id: String) : Flow<Customer> { return repository.geCustomerById(cust_id) }
Other classes are CustomerDao.kt, CustomerRepository.kt, CustomerFragment, CustomerDetailsFragment.kt.
You need to launch a coroutine to call suspend function Flow.first(). In Activity or Fragment you can use lifecycleScope, in ViewModel - viewModelScope to launch a coroutine. Example:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
lifecycleScope.launch {
val customer = customerViewModel.geCustomerById("11")
val name = customer.first()?.firstName;
// ... use name
}
}

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"
}
}

Use viewLifecycleOwner as the LifecycleOwner

I have a fragment:
class MyFragment : BaseFragment() {
// my StudentsViewModel instance
lateinit var viewModel: StudentsViewModel
override fun onCreateView(...){
...
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel = ViewModelProviders.of(this).get(StudentsViewModel::class.java)
updateStudentList()
}
fun updateStudentList() {
// Compiler error on 'this': Use viewLifecycleOwner as the LifecycleOwner
viewModel.students.observe(this, Observer {
//TODO: populate recycler view
})
}
}
In my fragment, I have a instance of StudentsViewModel which is initiated in onViewCreated(...).
In, StudentsViewModel, students is a LiveData:
class StudentsViewModel : ViewModel() {
val students = liveData(Dispatchers.IO) {
...
}
}
Back to MyFragment, in function updateStudentList() I get compiler error complaining the this parameter I passed in to .observe(this, Observer{...}) that Use viewLifecycleOwner as the LifecycleOwner
Why I get this error? How to get rid of it?
Why I get this error?
Lint is recommending that you use the lifecycle of the fragment's views (viewLifecycleOwner) rather than the lifecycle of the fragment itself (this). Ian Lake and Jeremy Woods of Google go over the difference as part of this Android Developer Summit presentation, and Ibrahim Yilmaz covers the differences in this Medium post In a nutshell:
viewLifecycleOwner is tied to when the fragment has (and loses) its UI (onCreateView(), onDestroyView())
this is tied to the fragment's overall lifecycle (onCreate(), onDestroy()), which may be substantially longer
How to get rid of it?
Replace:
viewModel.students.observe(this, Observer {
//TODO: populate recycler view
})
with:
viewModel.students.observe(viewLifecycleOwner, Observer {
//TODO: populate recycler view
})
In your current code, if onDestroyView() is called, but onDestroy() is not, you will continue observing the LiveData, perhaps crashing when you try populating a non-existent RecyclerView. By using viewLifecycleOwner, you avoid that risk.
viewLifeCycleOwner is LifecycleOwner that represents the Fragment's View lifecycle. In most cases, this mirrors the lifecycle of the Fragment itself, but in cases of detached Fragments, the lifecycle of the Fragment can be considerably longer than the lifecycle of the View itself.
Fragment views get destroyed when a user navigates away from a fragment, even though the fragment itself is not destroyed. This essentially creates two lifecycles, the lifecycle of the fragment, and the lifecycle of the fragment's view. Referring to the fragment's lifecycle instead of the fragment view's lifecycle can cause subtle bugs when updating the fragment's view.
Instead of this use viewLifecycleOwner to observe LiveData
viewModel.students.observe(viewLifecycleOwner, Observer {
//TODO: populate recycler view
})
Captain obvious here, also useful could be this:
viewModel.searchConfiguration.observe(requireParentFragment().viewLifecycleOwner, Observer {}

Multiple LiveData Observers After Popping Fragment

Issue
Summary: Multiple LiveData Observers are being triggered in a Fragment after navigating to a new Fragment, popping the new Fragment, and returning to the original Fragment.
Details: The architecture consists of MainActivity that hosts a HomeFragment as the start destination in the MainActivity's navigation graph. Within HomeFragment is a programmatically inflated PriceGraphFragment. The HomeFragment is using the navigation component to launch a new child Fragment ProfileFragment. On back press the ProfileFragment is popped and the app returns to the HomeFragment hosting the PriceGraphFragment. The PriceGraphFragment is where the Observer is being called multiple times.
I'm logging the hashcode of the HashMap being emitted by the Observer and it is showing 2 unique hashcodes when I go to the profile Fragment, pop the profile Fragment, and return to the price Fragment. This is opposed to the one hashcode seen from the HashMap when I rotate the screen without launching the profile Fragment.
Implementation
Navigation component to launch new ProfileFragment within HomeFragment.
view.setOnClickListener(Navigation.createNavigateOnClickListener(
R.id.action_homeFragment_to_profileFragment, null))
ViewModel creation in Fragment (PriceGraphFragment). The ViewModel has been logged and the data that has multiple Observers only has data initialized in the ViewModel once.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
priceViewModel = ViewModelProviders.of(this).get(PriceDataViewModel::class.java)
}
Listen for data from ViewModel in original Fragment (PriceGraphFragment). This is being called multiple times, however it is only expected to have one Observer when the Fragment is loaded.
priceViewModel.graphLiveData.observe(
this, Observer { priceGraphDataMap: HashMap<Exchange, PriceGraphLiveData>? ->
// This is being called multiple times.
})
Attempted Solutions
Creating the Fragment's ViewModel in the onCreate() method.
priceViewModel = ViewModelProviders.of(this).get(PriceDataViewModel::class.java)
Creating the ViewModel using the Fragment's activity and the child Fragment's parent Fragment.
priceViewModel = ViewModelProviders.of(activity!!).get(PriceDataViewModel::class.java)
priceViewModel = ViewModelProviders.of(parentFragment!!).get(PriceDataViewModel::class.java)
Moving methods that create the Observers to the Fragment's onCreate() and onActivityCreated() methods.
Using viewLifecycleOwner instead of this for the LifecycleOwner in the method observe(#NonNull LifecycleOwner owner, #NonNull Observer<? super T> observer).
Storing the HashMap data that is showing as duplicates in the ViewModel opposed to the Fragment.
Launching the child Fragment using the ChildFragmentManager and the SupportFragmentManager (on the Activity level).
Similar Issues and Proposed Solutions
https://github.com/googlesamples/android-architecture-components/issues/47
https://medium.com/#BladeCoder/architecture-components-pitfalls-part-1-9300dd969808
https://plus.google.com/109072532559844610756/posts/Mn9SpcA5cHz
Next Steps
Perhaps the issue is related to creating the nested ChildFragment (PriceGraphFragment) in the ParentFragment's (HomeFragment) onViewCreated()?
ParentFragment
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
user = viewModel.getCurrentUser()
if (savedInstanceState == null) {
fragmentManager
?.beginTransaction()
?.replace(binding.priceDataContainer.id,
PriceGraphFragment.newInstance())
?.commit()
}
Test replacing the LiveData objects with RxJava observables.
This is basically a bug in the architecture. You can read more about it here. You can solve it by using getViewLifecycleOwner instead of this in the observer.
Like this:
mViewModel.methodToObserve().observe(getViewLifecycleOwner(), new Observer<Type>() {
#Override
public void onChanged(#Nullable Type variable) {
And put this code in onActivityCreated() as the use of getViewLifecycleOwner requires a view.
First off, thank you to everyone who posted here. It was a combination of your advice and pointers that helped me solve this bug over the past 5 days as there were multiple issues involved.
Issues Resolved
Creating nested Fragments properly in parent Fragment (HomeFragment).
Before:
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
if (savedInstanceState == null) {
fragmentManager
?.beginTransaction()
?.add(binding.priceDataContainer.id, PriceGraphFragment.newInstance())
?.commit()
fragmentManager
?.beginTransaction()
?.add(binding.contentFeedContainer.id, ContentFeedFragment.newInstance())
?.commit()
}
...
}
After:
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
if (savedInstanceState == null
&& childFragmentManager.findFragmentByTag(PRICEGRAPH_FRAGMENT_TAG) == null
&& childFragmentManager.findFragmentByTag(CONTENTFEED_FRAGMENT_TAG) == null) {
childFragmentManager.beginTransaction()
.replace(priceDataContainer.id, PriceGraphFragment.newInstance(),
PRICEGRAPH_FRAGMENT_TAG)
.commit()
childFragmentManager.beginTransaction()
.replace(contentFeedContainer.id, ContentFeedFragment.newInstance(),
CONTENTFEED_FRAGMENT_TAG)
.commit()
}
...
}
Creating ViewModels in onCreate() as opposed to onCreateView() for both the parent and child Fragments.
Initializing request for data (Firebase Firestore query) data of child Fragment (PriceFragment) in onCreate() rather than onViewCreated() but still doing so only when saveInstanceState is null.
Non Factors
A couple items were suggested but turned out to not have an impact in solving this bug.
Creating Observers in onActivityCreated(). I'm keeping mine in onViewCreated() of the child Fragment (PriceFragment).
Using viewLifecycleOwner in the Observer creation. I was using the child Fragment (PriceFragment)'s this before. Even though viewLifecycleOwner does not impact this bug it seems to be best practice overall so I'm keeping this new implementation.
It's better to initialize the view model and observe live data objects in onCreate.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel = ViewModelProvider(this).get(MyFragmentViewModel::class.java)
// 'viewLifecycleOwner' is not available here, so use 'this'
viewModel.myLiveData.observe(this) {
// Do something
}
}
However, no matter where you initialize the view model, whether in onCreate or onViewCreated, it will still give you the same view model object as it's created only once for the lifecycle of the Fragment.
The important part is observing the live data in onCreate. Because onCreate is called only on fragment creation, you're calling observe only once.
onViewCreated is called both when the fragment is created and when it is brought back from the back stack (after popping the fragment on top of it). If you observe live data in onViewCreated it will get the existing data that your live data is holding from the previous call immediately on returning from the back stack.
Instead, use onViewCreated only to fetch data from the view model. So whenever the fragment appears, either on the creation or returning from the back stack, it will always fetch the latest data.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.fetchData()
...
}

Should I unsubscribe when using rxbinding?

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.

Categories

Resources