I've migrated to Mosby 3.0.3 and now investigating abilities of PresenterManager.
For me is not completely clear the lifetime of presenter regarding to Activity, Fragment and ViewGroup.
I've looked through ViewGroupMvpDelegateImpl, FragmentMvpDelegateImpl and ActivityMvpDelegateImpl and couldn't 100% be sure when presenter is terminated.
As far as I understand by default keepPresenterInstance and keepPresenterOnBackstack are always true and we have such situation:
Fragment's presenter exists until it's fragment is removed from backstack or the whole process is terminated. In this case I rely on method from FragmentMvpDelegateImpl:
protected boolean retainPresenterInstance() {
Activity activity = getActivity();
if (activity.isChangingConfigurations()) {
return keepPresenterInstanceDuringScreenOrientationChanges;
}
if (activity.isFinishing()) {
return false;
}
if (keepPresenterOnBackstack && BackstackAccessor.isFragmentOnBackStack(fragment)) {
return true;
}
return !fragment.isRemoving();
}
But what about Fragments in childFragmentManagers or about Fragments from FragmentPagerAdapter (also inside another fragment)?
Activity's and ViewGroup's presenters exist until theirs's activity isn't finished or the whole process is terminated. In this case I rely on method from ActivityMvpDelegateImpl:
static boolean retainPresenterInstance(boolean keepPresenterInstance, Activity activity)
{
return keepPresenterInstance && (activity.isChangingConfigurations()
|| !activity.isFinishing());
}
Is this true for all ViewGroups, wherever they are resided?
The reason of asking such question is that in our app we have activities, fragments, fragments inside fragments, custom viewgroups and all of them have presenters.
The idea is to release huge resources in presenter in detachView(final boolean retainInstance) but keep in memory something lightweight - like inMemory cache, to reuse it when view will be restored from backstack.
Likewise we use Dagger2 and it's important to understand when to release appropriate subcomponent.
Activity: Presenter gets destroyed (removed from Presentation Manager and therefore can be garbage collected) when Activity gets finished (or on Activity Process death). So activity keeps presenter during screen orientation changes and also on the back stack (if you have multiple activities on the back stack). Works pretty the same as in Mosby 2.x
Fragments: In Mosby 2.x Presenter were only kept across screen orientation changes if you set Fragment.setRetainInstanceState(true). This is not required anymore in Mosby 3.x because Mosby 3.x uses PresenterManager to keep presenter across orientation changes. Also, since Mosby 3.x Presenter will be kept if fragment is on the back stack. That means, even if the Fragment has no View because Fragment is on the back stack so UI widgets has been destroyed (Fragment.onDestroyView() has been called), Presenter instance will be kept and if the user pops the Fragment back stack so that you come back the presenter is reused. However, while Fragment is on the back stack, View is detached from Presenter (presenter.detachView(true) will be called) and gets reattached after Fragment is the one that is on top of fragment back stack. This is the default behavior and works for any kind of Fragment incl. ViewPager and Nested ChildFragments on (child)back stack. You can configure this as constructor parameter of FragmentMvpDelegateImpl(keepPresenterInstance, keepPresenterOnBackstack) to don't keep presenter at all during screen orientation change or to don't keep presenter while fragment on the back stack.
ViewGroups: In mosby 2.x they didn't survive screen orientation changes at all. In Mosby 3.x they do, because they use PresenterManager. However, you have to give each ViewGroup a id (#id/something) otherwise Android Framework dont know how to map views (portrait view to landscape view). As you have already observed correctly, ViewGroups are kept until "host activity" dies or ViewGroup gets removed from parent ViewGroup programmatically like parentViewGroup.removeChild(indexOfViewGroup). Doesn't matter where it resides. Please note that it may or may not work well if you put ViewGroup in a Fragment. I haven't thought / tested that edge case yet. Still it will take "host activity" into account (and not the Fragment as parent), however, it might be the case that if a Fragment will be put on the back stack (and push a new Fragment) that the View of the Fragment containing your ViewGroup will be removed which is similar to programmatically calling parentViewGroup.removeChild(indexOfViewGroup) (maybe FragmentManager internally calls this) so that the presenter of your ViewGroup will be destroyed but it might shouldn't (because Fragment user can pop backstack so that fragment). In that case it is likely that ViewGroup creates a new Presenter instance. As already said, I haven't tested nor thought about this edge case yet, but there is no way (as far as I know) to get a reference to the "hosting Fragment" since Fragment is not known to ViewGroup. I recommend to either use Mosby Fragments or Mosby ViewGroups but not Mosby ViewGroup in a Fragment (prefer a child fragment then if really necessary).
You can configure all that behavior by setting boolean flag in corresponding Delegate constructor.
The easiest way to detect if that works as expected is to add logs to createPresenter() methods and persenter.detachView() method. Also Mosby provides some logging for debugging. You can enable that in each delegate i.e. FragmentMvpDelegateImpl.DEBUG = true (for example call this in Application.onCreate()). Please note you have to do that for each delegate (Activity, Fragment, ViewGroup and also for each variant Mvp, Mvp+ViewState, Mvi). You can also enable logging for PresenterManager in the same way if you want to.
Related
So i am using Navigation in my main activity of my app and i have a fragment which is my start navigation fragment.
In this fragment, after it is created, in my presenter i post a thread to fetch data from the network.
After data have been fetched, i am using the main thread to show the data to my screen.
The first time that the app runs, this is working fine.
However if user opens the drawer and selects again THIS fragment and not another one, fragment is recreated again meaning that it gets destroyed and created from scratch as Navigation Component is designed to do.
However this time, when my presenter posts thread fetching-data-thread and this gets finished and sends the results to the UI, fragment's isAdded() method returns false as well as getActivity is null.
Having that, means that i can't use the Activity Context (getActivity() is null or requireActivity() throws an illegal state exception) and consequently i cannot load images etc since i don't have the context available.
I highlight that this happens when user opens the drawer while this fragment is visible and selects again to navigate to this fragment from the drawer. In case that user navigates to another fragment and then presses the back button everything is ok.
Any idea how to handle this problem?
Fragments are meant to be destroyed, as well as activities.
You can never rely on android framework component lifecycle state, and because of it android architecture components were made. ViewModel, for example, can outlive it's host fragment.
But - viewmodel/presenter/controller is not a right place to perform network request and handle app logic, just because it's not their job (SOLID's S-single responsibility).
There is official guide to app architecture. Simply speaking, you have a layer for android-related code, where you update UI, layer for handling app logic (which is java/kotlin and android framework independent) and layer for requesting/holding data.
So, during creation of your ui class you obtain viewmodel, which has reference to class that handle logic and exposes result to render in ui. Inner layers are persisted - view is not.
So, after testing and searching i found out the origin of the problem i described above.
I am nullifying my presenter's view in onDestroy/onDetach method of my Fragment.
However when the replacement Fragment gets created, this new Fragment is firstly attached to the calling Activity and then, the old one gets destroyed.
Having in mind, that i inject my presenter into the Fragment instance, my presenter will be never null at the time that new Fragment gets attached and consequently, and considering that i create a new instance of my Presenter when it is null, the presenter instance that is being injected into the fragment is not aware of the new 'View' object.
As a result, when the results reach the UI thread through the callback this view object is 'not Added'.
Somebody told me the following, but I am a bit perplexed.
Please, would you be able to confirm or dispute it?
(the Fragment is not retained via setRetainInstance()
At the moment it is a common practice to initialize views in Fragments like this:
private lateinit var myTextView: TextView
fun onViewCreated(view: View, bundle: Bundle) {
...
myTextView = view.findViewById(R.id.myTextViewId)
...
}
And then we never nullify this property. Though this is a common practise, it is causing a memory leak.
Background to this:
Let's say, FragmentA has a reference to a childView of it's View, as an instance field.
Navigation from fragment A to B is executed by FragmentManager using a specific FragmentTransaction. Depending on the type of transaction, the Manager might want to kill the View only but still persist the instance of FragmentA(see below lifecycle part where it says "The fragment returns to the layout from the back stack"). When user navigates back from FragmentB to FragmentA, the previous instance of FragmentA will be brought to the front, but a new View will be created.
The issue is that if we keep instance to our view in the lateinit property and never clear the reference to it, the view cannot be fully destroyed, causing memory leak.
Please, is there an official answer on the matter?
An official answer regarding this matter is,
The Memory Profiler is a component in the Android Profiler that helps
you identify memory leaks and memory churn that can lead to stutter,
freezes, and even app crashes.
In this documentation, android officials teach you how to figure out memory leak by yourself so that they don't have to answer on each and every test cases a user may perform. Besides you can use LeakCanary which does a great job in detecting memory leak.
For your convenience, I performed a heap analysis (a similar but extended version of your use case). Before showing the analysis report I would like to give a step by step basic overview of how the memories will be de/allocated in your case,
On open FragmentA: It's content/root View and the TextView will be allocated into the memory.
On Navigate to FragmentB: onDestroyView() of FragmentA will be
called, but FragmentA's View can not be destroyed because the
TextView holds a strong reference to it and FragmentA
holds a strong reference to TextView.
On Navigate back to FragmentA from FragmentB: The previous
allocation of the View and TextView will be cleared. At the same time, they will get new allocations as the onCreateView() is called.
On Back press from FragmentA: The new allocations will be cleared as
well.
Answer to your question:
In step 2, we can see there is a memory leak as the retain memory of View is not freed up what it was supposed to be. On the other hand, from step 3 we can see that the memory is recovered as soon as the user returns back to the Fragment. So we can figure out that this kind of memory leak persist till the FragmentManager brings the Fragment back.
Example / Statical Analysis
To test your case, I have created an application. My application has an Activity with a Button and a FrameLayour which is the container for fragments. Pressing the Button will replace the container with FragmentA. FragmentA contains a Button, pressing that will replace the container with FragmentB. FragmentB has a TextView which is stored in the fragment as an instance field.
This report is based on the following operation performed on the above application (Only the Views that I created i.e. ConstraintLayout, Framelayout, Button and TextView are taken in consideration),
Opened the app: Activity visible
Pressed the Button in the Activity: FragmentA visible
Pressed the Button in FragmentA: FragmentB visible and FragmentA onDestroyView()
Pressed the Button in the Activity: 2nd instance FragmentA visible and FragmentB onDestroyView(). (This is the same as step 2 in the previous example except, FragmentB acts as A and the 2nd instance of FragmentA acts as B)
Pressed the Button in the 2nd instance FragmentA: 2nd instance of FragmentB visible and 2nd instance of FragmentA onDestroyView().
Pressed Back Button: 2nd instance of FragmentA visible and 2nd instance of FragmentB onDetach()
Pressed Back Button: 1st instance of FragmentB visible and 2nd instance of FragmentA onDetach()
Pressed Back Button: 1st instance of FragmentAvisible and 1st instance of FragmentB onDetach()
Pressed Back Button: 1st instance of FragmentA onDetach()
Pressed Back Button: which closed the application.
Observation
If you look into the report you can see, in step 1, each and every view lived until the app is closed. In step 2, the View of FragmentA i.e. FrameLayout and it's child, Button are allocated and got both cleared in step 3 which is expected. In step 3, the View of FragmentB i.e. FrameLayout and its child TextView is allocated but did not get cleared in step 4 hence, caused memory leak but cleared in step 7 when it's View is created again and allocated newly created View. On the other hand, the Views that are created in step 5 just got cleared in step 6 causing no memory leak, because the fragment was detached and they didn't prevent the fragment from being cleared up.
Conclusion
We observed that the leak from saving views in fragment lasts until the user returns back to the fragment. When the fragment is brought back i.e. onCreateView() is called, the leak is recovered. On the other hand, no leak happens when the fragment is on top and can only go back. Based on it, we can make the following conclusion,
When there is no forward transaction from a fragment, there is nothing wrong with saving views as a strong reference as they will be cleared in onDetach()
If there is a forward transaction we can store weak references of views so that they are cleared in onDestroyView()
P.S. If you don't understand heap dump, please watch Google I/O 2011: Memory management for Android Apps. Also, this link provides valuable information about memory leak.
I hope my answer helped you clear your confusion. Let me know if you still have confusion?
Disclaimer:
I will post another answer because I think I couldn't extract the exact use case from the question in the first read. I am waiting for the approval of the edit request I made to this question to know I understood the question properly. I am keeping this answer because I believe there are some useful tips and links that might help someone. On the other hand, in certain cases my answer is right.
Keeping a reference to a View in a Fragment causes memory leaks?
ABSOLUTELY NOT since the field is declared private, no object from outside of the Fragment i.e. Activity can access a hard reference of it from the Fragment object. Therefore, it will not prevent the Fragment object from being garbage collected.
You might ask, will it cause memory leak when I use this reference in an async callback?
My answer would be, yes it will cause memory leak for keeping reference
inside the async callback but not due to keeping its reference in
the Fragment. However, this memory leak will also happen even if you don't store the View reference in the Fragment.
In general, to avoid memory leak, you should watch out the following simple patterns,
Never reference views inside Async callbacks
Never reference views from static objects
Avoid storing views in a collection that stores values as hard references
This official video, DO NOT LEAK VIEWS (Android Performance Patterns Season 3 ep6) will help you understand it better.
Edit2
Indirect official information:
methods of FragmentTransaction commitAllowingStateLoss() and commitNowAllowingStateLoss() indicate that the Fragment added to backstack with saved state if no explicitly stated otherwise.
method of Fragment setRetainInstance(boolean retain) indicates that Fragment can save its state between orientation changes and other Activity recreations - it means that FragmentManager exists relatively independently of Activitys life cycle and can store Fragment state even if Activity is destroyed.
A small remark in the description of onDestroyView
Internally it is called after the view's state has been saved but before it has been removed from its parent.
Which indicates the exact time of saving Fragment state.
All these points combined almost explicitly state that there is a state of a Fragment view and it is stored in a memory between navigation events.
Edit2 end
The first issue is in your question.
You state that the code you provided is a common practice to initialise views in Fragments. Well it is not a common practice at all. It is an old and outdated way Google only uses in its samples and examples. While it is enough for samples it is no good for the production.
One of the current official standards for Kotlin from Google is to init views in fragments or activities via synthetics. Google even use this approach in its modern samples and examples. There is a method called clearFindViewByIdCache() that is able to get rid of all strong synthetic references when you need it(most commonly in onDestroyView).
The second standard is to use Android Data Binding via <layout></layout>, <data></data> tags in layout xml files and ViewModels in code. It is applicable both for Kotlin and Java and pretty easy and straightforward. One of the reasons it was done is to get rid of memory leaks while using the old "standard way", make it easy to retain state on configuration changes and unify approach to modern UI layer implementation. To completely get rid of possible memory leak you will have to nullify bindings in onDestroyView though. If implemented well! it handles all the stuff out of the box including memory leaks(their absence), retaining view state on config changes, updating UI with relevant data from either network or database via LivaData, general communication with the UI, handling Android JetPack features and many more. Along with the rest of JetPack features currently it is a Google recommended way to create Android apps
There is a third semiofficial approach - usage of Butterknife. If implemented well it also is able to handle correct releasing of UI resources to avoid common memory leaks related to UI. The library has bind()(in onCreateView) and unbind()(in onDestroyView) methods that handle the stuff you mentioned in your question.
The last in this answer(but not in production)) method is to use WeakReference, SoftReference or PhantomReference - it is a general Java programming technique to avoid memory leaks and to allow GC of objects. It is not very common practice in Android but it is still a good way to handle strong references locks.
Bonus!)
Not to worry about onDestroyView you can use delegation technique and AutoClearedValue in Kotlin.
We can declare auto-clearing properties like this:
var myTextView by autoCleared<TextView>()
…and set their value just like we would for a simple property:
myTextView = view.findViewById(R.id.myTextViewId)
So now regarding whether the code in your question causes memory leaks. Well it most certainly do. It is not even a subject to dispute. It is not stated anywhere officially because it is considered as a common knowledge since it is just how the JWM and Android base classes work.
Edit
Some people in answers claim that there is no leak. Well in the traditional Android understanding - there is no leak - not Activity nor Fragment are leaked - fragment references are alive and placed where they need to be - in fragment managers back stack.
The problem is - the leak still persists. It is not a traditional one thus LeakCanary won't find it. But you can find it in debug and profiling. It is still a leak though. Strong references to the views inside a fragment retain during the back stack transaction thus - they store their objects. While ordinary text views or buttons are not that heavy for the heap - the ones that store images - are quite the opposite - they can fill the heap pretty fast. It happens because Android wants to save the most of the fragments view state to restore it as fast as it possible - so the user won't see the blank screen. There may be also an issue when two layouts of the same fragment are present in the view hierarchy and the references refer to the old layout that is quite below and currently invisible. It was my bugs since I handled navigation and storing states in a bad and a wrong manner, but it shows that the old view may be present in the heap.
Before the Android Jet Pack era this leak was one to ignore, since there were no that extensive fragments usage and navigation between them. So the heap could handle the resources. But now with single Activity approach this may become one of the main reasons of OutOfMemoryError using content heavy fragments without clearing resources in onDestroyView().
Hope it clarifies some corners.
Edit end
Hope it helps.
I don't think there is a direct official answer, but the answer is in the documentation and it can be easily proven. If you check fragment lifecycle and description of onDestroyView method, it says:
After onDestroyView is called the layout is detached and next time fragment attaches the view is recreated.
The fragment can actually come back from the back stack and onCreateView is called in this case.
That basically means if you keep any view references in the fragment instance while the fragment is in back stack, you are preventing these views from being garbage collected.
If you are still not convinced, you can take a look at the source code of AOSP. For example ListFragment and PreferenceFragment both set all views references to null in onDestroyView().
In addition it's pretty easy to prove there is a memory leak. Just create a fragment with an image view, which displays a full-size image. Keep a reference in your fragment and navigate to another fragment with a fragment transaction. Then tap force garbage collection button and create a heap dump with the memory profiler tool and you'll see your fragment is actually holding image view and the image itself, preventing them from being collected. If you repeat the same steps, but setting the reference to null in onDestroyView(), you'll see your fragment's retained size is much smaller and no ImageView instance is present in the heap anymore.
From a practical point of View: The main reason for memory leaks is keeping static fields, in particular static Context, which should be avoided. Within an attached Fragment, this usually is not even required. static fields should be called .close() and set to null before super.onDetach() or super.onDestroy(). One does not even have keep handles to any views, when using data-binding. In Kotlin there are also synthetic accessors for that. Keeping handles to views is not required at all, which renders the question obsolete. This was required before both of these existed. Use lint, ktlint or leakcanary or memory-profiler to find potential memory leaks.
There's a Fragment lifecycle method called onDestroyView which you should override to release any reference to the views.
Generally you should only use lateinit var view references if your Fragment is permanently added to the Activity and it will not be removed.
Kotlin View Binding extensions already solve this problem by automatically clearing view cache inside onDestroyView.
If you use view binding, you should implement it like in the docs. Like so,
private var _binding: ResultProfileBinding? = null
// This property is only valid between onCreateView and
// onDestroyView.
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = ResultProfileBinding.inflate(inflater, container, false)
val view = binding.root
return view
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
This fix the View memory leak with LeakCanary for me.
I'm trying to understand Kotlin Android extensions and this article says says:
When asked for a view, it will try to find it in the cache. If it’s not there, it will find it and add it to the cache. Pretty simple indeed.
Besides, it adds a function to clear the cache: clearFindViewByIdCache. You can use it for instance if you have to rebuild the view, as the old views won't be valid anymore.
Why won't the old views be valid anymore once you rebuild the view after onDestroy()? The view references are still going to be there once you rebuild the view, for example in OnActivityCreated().
The important note here is that Fragments usually have a longer lifecycle then their views. Let's consider the following flow:
You created Fragment A and placed it into a container
Fragment A view got created (this onCreateView + onViewCreated)
You created Fragment B and replace Fragment A with the new fragment in the container + keep back stack
Fragment A views will be destroyed at this moment, but the fragment will be kept inside of FragmentManager.
You back press and Fragment B got fully destroyed because you logically leaving that. At this moment system replaces it back with Fragment A and it creates a new view again.
So here are a few important notes:
At step #4 if you keep any links to subviews, then your memory just leaks because all of them already got detached out of your view hierarchy and can't be used anymore. Basically, you still hold all those views in memory, even though they are not in use anymore. Usually, ppl don't notice that and all views will get rebound again with new onViewCreated (and findViewById), but kotlin extensions keep them in a cache and basically do not execute this findViewById again, so you can get old (read as dead) views that are not those views that just got created.
If you use ButterKnife at the moment for instance, they have description about that as well in Binding Reset section. (http://jakewharton.github.io/butterknife/#reset).
Last important note is that Kotlin extensions do this job for you, so you don't really need to call it inside of onDestroyView, you just use it and it works (as magic...)
I faced with that problem when I started to use ViewPager. As every page is nested Fragment, I can't call setRetainInstance(true) for it. So, I need to store Fragment's state to a Bundle and cancel/recall remote API methods onViewAttached/Detached which I don't want to.
What I learn about this situation:
I can use RecyclerViewPager to avoid using nested Fragments, but I still can't keep an instance of Presenter. One of the ways is to use a static field, but the same thing can I do with the Fragment.
Create some kind of rootViewPager under MainActivity and use it in Fragments via setVisibility(GONE/VISIBLE) and replacing Adapter. So, every Fragment placed into this ViewPager will not have parent Fragment and I will solve my case. Suitable and elegant, but not the best solution as I think.
Any other variants?
In Mosby 3.0 Presenters can be retained even without setRetainInstance(true) ... I would suggest to wait until 3.0 release ...
So, I need to store Fragment's state to a Bundle and cancel/recall
remote API methods onViewAttached/Detached which I don't want to.
Mosby 2.0 does exactly that for you but you have to make your ViewState and your data implement Parcelable. In that case the presenter instance won't survive screen orientation changes, but presenter will "resume" on the same state / point (a new presenter instance will be created, async tasks etc. might be restarted too). See RestorableViewState (javadoc is slightly outdated, because it mentions that this is the only way to work with activities which since Mosby 2.0 is not true anymore)
Fragment transaction has method add(Fragment fragment, String tag), which does not place fragment to container, so it cannot have view. For what it can be used?
From the Android Documentation:
However, a fragment is not required to be a part of the activity layout; you may also use a fragment without its own UI as an invisible worker for the activity.
How about this purpose ?
Simple example: an Activity starts an AsyncTask, but when device rotated activity restarts, causing AsyncTask to lose connection with the UI Thread. But this Activity can hold a Fragment (invisible, with no UI at all) that can handle all the AsyncTask work. When Activity recreated the Android OS takes care reattaching the Fragment, thus no data loss will occur.
For Dialogs you don't have any container on normal app layer. It is directly added on Window with WindowManager(See WindowManager.LayoutParams for various types of layers).
DialogFragment has an API like DialogFragment.html#show(android.app.FragmentManager, java.lang.String) which corresponds to this.
You can use fragments without UI (container) as a background worker (one benefit is that you can retain it during rotations etc) and for retaining data during rotations and other changes.
Reading http://developer.android.com/guide/components/fragments.html is strongly recommended.
Example of instance retaining: https://android.googlesource.com/platform/development/+/master/samples/ApiDemos/src/com/example/android/apis/app/FragmentRetainInstance.java
Also, here are similar questions (so this questions seems to be a duplicated but cannot be flagged due to bounty):
What is the use case for a Fragment with no UI?
Android non-UI Fragment usage
As #Lucius Hipan mentions, it can be used to prevent data loss.
Almost always this king of fragments are used as Retained container ( setRetainInstance(true) called in onCreate method), then after device configuration changes (e.g. orientation changing) fragment will not be recreated but remembers previous state.
It's recommended way to use asynctask.
Here is an example:
There is login activity. The user enters their credentials and presses the Login button. After that configuration change occurs (user rotates phone). So, network task was completed, but your handlers was not listening for it now. If you show any login animation, it can be stored via savedInstance, but listeners not. And instead of creating service you can simply create new retained fragment with persistant asynctask and interface to communicate with activity.
This method is a good compromise for small projects where using bus libraries is overstatement.
By calling the method add(Fragment fragment, String tag) internally calls add(int containerId, Fragment fragment, String tag) with a 0 containerId.That will be add(0, fragment, tag).
If 0 is supplied as containerId, it will not be placed the fragment in a container.