When are views attached and detached? - android

This question is not about how to detect if a view is attached or detached.
In general, when is a view attached or detached? Is there a lifecycle diagram for this?
To clarify, I'm looking for answers to what happens when: Activity sent to background, opaque view placed on top, visibility set to GONE, view inflated, parent detached, etc. This is not an exhaustive list - I just want to understand how attaching and detaching of views works, at a fundamental level.
Update with more examples of what I'm trying to get at:
What about fragments vs. activities?
What about nested views - in what order are views attached/detached (parent->child or child->parent)?
Are views measured before they are attached or after?
What about using addView() to a ViewGroup manually?
Edit: Summary:
For Activities, views are attached in setContentView(). Views are detached in onDestroy() or when setContentView() is called with a different view.
For Fragments, views are attached after onViewCreated() finishes, and are detached after onDestroyView() finishes.
For ViewGroups, views are attached in addView() and detached in removeView()
setVisibility() does not affect the attached state of a view

From the official documentation:
An activity is a single, focused thing that the user can do. Almost
all activities interact with the user...
The first thing that needs to be noted is that it is not a must for activities to be associated with a layout. You can have an activity with no UI (and hence no View). Android even specifies a no UI theme for this.
Moving on to your question - a View gets attached to an Activity at the moment you call setContentView(view). This is usually called within the onCreate() method. The reason you usually have this in the onCreate() method is because most of the initialization is done there. And how could you initialize your widgets if the view has not been inflated and attached to the Activity? Hence, if you have a view, you almost invariable end up calling setContentView() inside your onCreate() method preceding all other initialization.
But does that mean that the view (if it exists) must be tied to the
activity only within the onCreate() method?
To answer this question, let's see what the lifecycle of an Activity looks like. You start your app:
onCreate() -> onStart() -> onResume() // They get called consecutively
The stage you are now in is where all the widgets have been initialized.
So why not inflate and attach the activity in onResume() and do all the
initializations there?
Of course, you could. But imagine what happens when a dialog (a partially opaque view) shows up? The Activity is now partially covered and is in the background. The onPause() method is called. The layout is still attached to the Activity at this point. You take some action and dismiss the dialog. onResume() gets called. The layout would be inflated again. All the initializations would happen again and you would lose your state. Even if you did not have much in the way of initialization, you would still be making a pretty expensive call by calling onCreate() again. And you want to avoid this in resource limited mobile devices.
What happens when an opaque view comes up and the Activity is now in
the background but still running (like an incoming phone call or opening another activity)?
Now the following callbacks happen:
onPause() -> onStop()
When you move back to the original activity
onRestart() -> onStart() -> onResume()
For the same reason as I mentioned in onPause() you do not want to inflate and attach a layout here.
But what happens to the layout itself when an Activity is in the
background. Is the layout still attached?
Yes, it very much is. If another activity comes up which uses the same layout as the original activity, then the new activity has it's own layout and there is no sharing of layouts.
What happens if the user terminates the activity by pressing the Back
button?
Assuming the onBackPressed() method is not overridden to achieve custom behavior (in which case, it is up for grabs), onDestroy() is called and the activity is destroyed and there is no View associated with it anymore.
What happens when the activity is in the background and the Android GC
decides to destroy the activity and reclaim resources?
According to the activity lifecycle within the documentation, onDestroy() will be called. But there is no guarantee of this. At this point, the activity and it's associated view are simply garbage collected and there is no connection.The next time you start the app, onCreate() will be called as usual and you simply start from the beginning again.
What happens when I rotate my device?
The way Android works is to actually destroy the current activity and inflate the new layout again and start from the onCreate() method again. So what technically happens is:
onPause() -> onStop() -> onDestroy() -> onCreate() -> onStart() ->
onResume()
Because of this, you can even have a different layout and view while in landscape mode.
EDIT: Added the relationship between activities, fragments and views
A fragment represents a portion (or behavior) on the screen. A fragment can be made to occupy the full screen or you can have multiple fragments within an Activity. Fragments have their own life cycle but it is closely tied to the host activity's life cycle (and beyond the scope of this answer). Since we are specifically talking about views, I will limit this answer to two methods of interest:
onCreateView()
onViewCreated()
The methods are called in the following order:
onAttach() -> onCreate() -> onCreateView() -> onViewCreated()
You do the actual layout inflation within the onCreateView() and then you do the initializations within the onViewCreated() method. The result of the onCreateView() method is used by Android to inflate the view.
So when is the fragment created in the first place by the Activity?
There are two ways to display fragments - one is to put them within the xml layout of the activity (just like any regular widget where instead of the widget name, you will be using the fully qualified package name of the fragment class) or you can do the addition using FragmentManager programmatically (which is the preferred method).
If you were defining the fragment within the xml layout, you should know that the fragment cannot be removed programmatically. It would be hard to modify this and reuse that screen space for other fragments. Also in this case, the view is attached and tied to the activity. In this case, you will inflate the xml layout of the Activity within the onCreate() method of the activity. So now, the flow will looks something like:
onCreate() [Activity] -> onAttach() [Fragment] -> onCreate()
[Fragment] -> onCreateView() [Fragment] -> onViewCreated() [Fragment]
-> onStart() [Activity] -> onResume() [Activity] -> onActivityCreated() [Fragment]
So first the fragment view is instantiated and attached to the fragment before the onStart() method of the activity is created.
If the fragment is added programmatically, if it is added within the onCreate() method, then it follows the same flow. It can be started anywhere. You simply have to substitute the life cycle of the fragment within the activity in the appropriate place. When you add fragments programmatically, while the fragment is hosted in the activity, the view is attached to the activity. When the fragment is removed from the activity, onDetach() is called and the view is no longer a part of the Activity. The resources taken by the fragment can be released.
What about Nested Views, Nested Fragments, etc.?
In nested views, like one layout container inside another, the rules of the parent container apply to the immediate child container. Always the parent gets initialized first. So for a widget inside a LinearLayout, the parent LinearLayout is constructed first immediately followed by the child. When destroying such views, everything goes when the parent ceases to exist. I have not read about any documentation as to an order in which this may happen. The Android GC may have rules but I am not sure if they are documented anywhere.
You can have nested fragments too - in this case, the parent fragment gets initialized before the child fragment does (and it makes sense doesn't it?). When a parent fragment ceases to exist, the child will also cease to exist. The child cannot exist without the parent but you can have the parent without the child.
The bottom line for nested views is that once the parent view is
destroyed, it takes the child view immediately with it.
Are views measured before they are attached or after?
Views are measure after they are attached. Calling getMeausredWidth() or getMeasuredHeight() will return zero prior to this. But what you can do is call neasure() directly on the view before it is attached and pass MeasureSpecs (I would suggest you read up more on this in the official docs) to set some constraints. But this option is not fool proof as it depends on the parent ViewGroup enforcing it's own constraints which take higher precedence. To simply answer your question, views are measured after they are attached.
What about using addView() to add a view to a ViewGroup manually?
This is simply the same as nested views. The child exist only when they are added and this is controlled by the user. In nested views defined in layout xml, the children are inflated immediately after their parent. Here the control is more in the hands of the user. When the parent view in this case is destroyed, it takes the child view with it.
As a last point, I'd also like to mention that you should not use static handles for views as this would lead to a lot of headaches with the tearing down of views.

Related

Advantage of fragments on Screen rotation

I wonder if there is any advantage of fragments, on screen rotation.
Generally fragments get destroyed followed by activity. Is there something that fragments retain while doing so?
onDestroy() method is called both in the activity and fragments.
I can try to figure out advantage of Fragment on Screen rotation.
Realtime app problem is:
Android is the potentially frequent destruction and reconstruction of an Activity. The most common time this occurs is when the user rotates the device between horizontal and portrait orientations (Screen rotation).
This crashing usually occurs because device orientation changes cause the Android framework to tear down the displayed Activity along within any contained Views, and then to fully reconstruct the Activity/View hierarchy. Any references to the Activity or to the Views within the Activity suddenly become invalid. Similarly any references within the Activity or Views that were set as a result of a user action or similar are now lost.
There are a number of ways to deal with this issue but one of the easiest is to take advantage of Fragments.
Things to keep in mind:
Fragments won’t automatically resolve this issue because, by default, when the Activity is torn-down in response to an orientation change the Fragment contained within the Activity is also torn down along with any contained Views.
The solution lies in an underused method: Fragment.setRetainInstance with a value of true.
why?
Calling setRetainInstance with a value of true causes Android to preserve the Fragment across the teardown/reconstruction cycle of an Activity. Along with the Fragment, the Views or other object references contained within the Fragment or Views remain.
With setRetainInstance(true) called on a Fragment instance.when an orientation change occurs, Android…
Holds a reference to the Fragment instance
Tears down the old Activity instance
Creates a new Activity instance
Attaches the preserved Fragment instance to the new Activity instance
you must add
android:configChanges="keyboardHidden|orientation|screenSize"
in parent Activity also calling
setRetainInstance(true)
in onCreate of fragment

Re-inflate different layout View in same Fragment in onResume

I have a Fragment, MainFragment, which can contain two, three, or four nested fragments. The specific fragments that will be shown can be changed by the user in the settings.
There is a different layout for each number of fragments. For instance, layout_3 is used when the user chooses three nested fragments.
What I need to do is dynamically update MainFragment's layout, and which fragments will be nested within that layout, in onResume() (i.e. Once the user comes back from the settings). There are about 10 fragments the user can choose from, and I need to be able to swap them in and out of MainFragment dynamically.
I'm having trouble doing this. The only way to update the layout/view once I return from the settings is to leave MainFragment and then come back (which calls onCreateView()).
Here is an example of what I do in onCreateView() to initialize the layouts (two nested fragments is the default):
mView = mInflater.inflate(R.layout.layout_2, mParent, false);
getChildFragmentManager().beginTransaction().add(R.id.fragmentContainer1, fragment1).commit();
getChildFragmentManager().beginTransaction().add(R.id.fragmentContainer2, fragment2).commit();
return view;
Suppose the user then goes to the settings and chooses to have three nested fragments. This is what I've tried in onResume(), to no effect:
mView = mInflater.inflate(R.layout.layout_3, mParent, false);
getChildFragmentManager().beginTransaction().add(R.id.fragmentContainer1, fragment1)).commit();
getChildFragmentManager().beginTransaction().add(R.id.fragmentContainer2, fragment2).commit();
getChildFragmentManager().beginTransaction().add(R.id.fragmentContainer3, fragment3).commit();
I'm not sure if I'm doing something wrong. Ideally, I would just force MainFragment to call onCreateView() again, but none of the solutions for that problem seem to work.
Any ideas? Thanks for the help.
Edit: I believe the problem is with inflating the new View, rather than replacing the fragments.
For instance, suppose the default screen is layout_4, with four fragment containers. The user then goes to the settings un-checks all four default fragments, and chooses three new fragments. In onResume(), we try to inflate layout_3, and then add the fragments. I think layout_3 never inflates, but because my fragment containers have the same style id across layouts (i.e. fragmentContainer1 - fragmentContainer4), the first three fragment containers are updated. The fourth one remains as it was, since I assumed we were in layout_3 and did not try to update it.
This behavior is confirmed and results in a crash when the user tries to increase the number of fragments, rather than decrease. Above, when the user switched from four fragments to three fragments, there was no crash because all three fragment containers I tried to update exist in layout_4. But if the user is in layout_2 and then goes to the settings to select a third fragment, we'll try to add a fragment to fragmentContainer3 when we resume. This results in a crash because layout_3 fails to inflate.
java.lang.RuntimeException: Unable to resume activity
java.lang.IllegalArgumentException: No view found for id 0x7f0c005f
Any ideas how to fix this? The call to re-inflate mView in onResume() does not seem to have any effect.
Edit 2: I've tried calling mParent.addView(mView) after inflating, but still experience the same behavior as above, for the most part.
When you return from the settings, onResume() should be called in the MainFragment and subsequently any nested fragments that were already loaded in the MainFragment. Can you not include any update logic in the nested fragments' onResume() instead of only in onCreateView()?
Otherwise you can create a different code path and put update logic there: make public methods in the classes for fragment1, fragment2, fragment3 that include all your update logic, and call those methods from somewhere in MainFragment. (You could create an interface and have the nested fragment classes inherit that interface, if they are different classes and you want to have a cleaner design.)
Be careful about whether or not the nested fragments have been resumed yet—calling methods on View objects when nested fragments' onResume() hasn't been called yet could be problematic.

Should accessing views done from onStart or onActivityCreated method?

I have read several documents about this but still fail to conclude where should I write my codes to set some values in textviews/edittext ...
what i have read and seen in video tutorials is, both onStart and onActivityCreated methods get called with different actions (like after fragment initiated, or orientation changed etc.). Moreover, both of them get called after Activity's onCreate method, which means views are available from both Fragment methods.
Anybody can give me some advise regarding this?
(p.s. Currently I put all codes accessing xml views inside onStart, and my application is running without any issue)
I don't know of any potential problems with accessing your layout's views in onStart or onActivityCreated.
Personally, I usually set references to my layout's views and set initial values in a fragment's #onViewCreated(). This is the first opportunity after the layout has been inflated that you have to access a layout's children. The View that was inflated is passed as a parameter so you even have direct access to the parent layout object if you needed it for some reason.
According to the fragment lifecycle onActivityCreated() will be called next and then onStart(). All of these will be executed in that same order when a fragment is returned from the back stack -- so it seems to be personal preference.

Programmatically removing a ViewPager, when are (or how can I ensure) contained Fragments are destroyed?

Having searched regarding this issue beforehand, I can find many discussions regarding dynamically adding and removing selected Fragments from a ViewPager. What I'm actually concerned about here however is how I can programmatically remove an entire ViewPager 'cleanly' from its containing ViewGroup, when that ViewPager has been used to display Fragments via a FragmentPagerAdapter, and ensure that the contained Fragments are destroyed properly.
To expand on the question a bit more, I have a landscape two-pane layout where a selection is made from a list within a Fragment on the left-hand-side, and chosen content is then placed on the right within a FrameLayout. The key thing is that the content may or may not be paginated. Therefore, the content must either be displayed in a ViewPager format, or if it is not paginated then it shall be represented by a single Fragment directly.
To show a single Fragment, I simply perform a FragmentTransaction as you normally would in order to place the Fragment into the FrameLayout container. If on the other hand it's paginated content to be shown, then instead I create a ViewPager and add it as a child of the FrameLayout.
When I need to change the content, then if the previous content was a stand-alone Fragment then I can simply remove it via FragmentTransaction .remove(). When I do this, the Fragment goes through the onPause() ... onDestroy() cycle as expected. If the previous content was a ViewPager then I remove it from the FrameLayout using .removeAllViews(). Here I come to the problem: I don't see any of the onPause() ... onDestroy() methods being called in any of the Fragments that were held within that ViewPager via the FragmentPagerAdapter.
From a user point of view, the application works fine. After several rounds of ViewPager being removed, I can see the GC reclaiming memory. However, I don't like the fact that those Fragments' end of life methods aren't called as I can't do any cleanup within them, and it just doesn't seem 'right'.
Is there a method I can hook into in order to remove the ViewPager's Fragments when the ViewPager is detached from its parent, perhaps? In other words, when I know that the ViewGroup is no longer in used, I would perform FragmentTransactions somewhere (perhaps in the FragmentPagerAdapter) to remove those Fragments.
Alternatively, I realise that I could just keep the ViewPager on the right permanently, and dynamically swap the Fragments within it. Of course it simply would not matter that at certain times it would only hold one page. If this would be a better way to go then I shall refactor my code to do this, but I would appreciate opinions.
However, I don't like the fact that those Fragments' end of life methods aren't called as I can't do any cleanup within them, and it just doesn't seem 'right'.
They should get cleaned up when the activity is destroyed, if that is not too late for you (e.g., heap issues).
In other words, when I know that the ViewGroup is no longer in used, I would perform FragmentTransactions somewhere (perhaps in the FragmentPagerAdapter) to remove those Fragments.
You did not execute the transactions to put the fragments there. Hence, you cannot readily execute the transactions to remove the fragments. If you switch to FragmentStatePagerAdapter, and call setAdapter(null), it should cause all existing fragments in the pager to be destroyed, by my reading of the source code. FragmentPagerAdapter never uses remove(), but FragmentStatePagerAdapter does, from its destroyItem() method, and all extant fragments are destroyed via destroyItem() when a new adapter (or null) is supplied to setAdapter().

Fragment LIfecycle in Custom Tab View

I have a custom widget that performs FragmentTransaction.replace when buttons are pressed. Currently, my code is set up such that the first time a fragment is created, it attaches a bunch of stuff to the view that isn't originally part of the xml layout file.
When the app first launches, all my fragments show stuff correctly, however, let's say I start on Fragment A. I can then transition to Fragment B (with B showing up correctly), however, when I transition back to Fragment A, all the stuff I have attached to the view of Fragment A is now gone. I know this happens because onCreateView is called which probably means the Fragment's view is re-generated when FragmentTransaction.replace is called.
Is there a way where I can keep my fragments around instead of having them re-generate their views when FragmentTransaction.replace is called?
Thanks!
Instead of using fragmentTransaction.replace, use fragmentTransaction.show and fragmentTransaction.hide.
That will keep your fragments from being destroyed.

Categories

Resources