I'm trying to understand the internal behavior of Android fragments. Got doubts between exact difference between onDestroy(), onDetach() and
void onDestroy ()
Called when the fragment is no longer in use. This is called after onStop() and before onDetach().
void onDetach ()
Called when the fragment is no longer attached to its activity. This is called after onDestroy().
Query :
If fragment is not longer in use,means that we can remove that fragment from Activity right?
In this case why to call onDestroy () first then onDetach () next,We can use only one method to indicate the state that "Fragment is no longer in use,can be removed activity"
onDestroy() : onDestroy() called to do final clean up of the fragment’s state but Not guaranteed to be called by the Android platform.
(Called when the fragment is no longer in use, after onStop and before onDetach())
onDetach() : onDetach() called after onDestroy(), to notify that the fragment has been disassociated from its hosting activity. (Called when the fragment is no longer attached to its activity)
ref: android-fragment-lifecycle, onDestroy,onDetach
take a look at Fragment class (line 1564), performDestroy is called first if f.mRetaining is false :
if (DEBUG) Log.v(TAG, "movefrom CREATED: " + f);
if (!f.mRetaining) {
//performDestroy is called first if f.mRetaining is false, else not
f.performDestroy();
dispatchOnFragmentDestroyed(f, false);
} else {
f.mState = Fragment.INITIALIZING;
}
//then performDetach
f.performDetach();
dispatchOnFragmentDetached(f, false);
if (!keepActive) {
if (!f.mRetaining) {
makeInactive(f);
} else {
f.mHost = null;
f.mParentFragment = null;
f.mFragmentManager = null;
}
}
And here is the code of performDestroy and performDetach:
void performDestroy() {
mLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY);
if (mChildFragmentManager != null) {
mChildFragmentManager.dispatchDestroy();
}
mState = INITIALIZING;
mCalled = false;
mIsCreated = false;
onDestroy();
if (!mCalled) {
throw new SuperNotCalledException("Fragment " + this
+ " did not call through to super.onDestroy()");
}
mChildFragmentManager = null;
}
void performDetach() {
mCalled = false;
onDetach();
mLayoutInflater = null;
if (!mCalled) {
throw new SuperNotCalledException("Fragment " + this
+ " did not call through to super.onDetach()");
}
// Destroy the child FragmentManager if we still have it here.
// We won't unless we're retaining our instance and if we do,
// our child FragmentManager instance state will have already been saved.
if (mChildFragmentManager != null) {
if (!mRetaining) {
throw new IllegalStateException("Child FragmentManager of " + this + " was not "
+ " destroyed and this fragment is not retaining instance");
}
mChildFragmentManager.dispatchDestroy();
mChildFragmentManager = null;
}
}
If you looked into whole fragment lifecycle then
onAttach() onCreate()
are counterpart of
onDetach() onDestroy()
lifecycle method respectively.
So in order to not break design consistency lifecycle method calling sequence is as following
onAttach()
onCreate()
onDestroy()
onDetach()
now let's move on your query
Query : If fragment is not longer in use, means that we can remove that fragment from Activity right?
In this case why to call onDestroy () first then onDetach () next,We can use only one method to indicate the state that "Fragment is no longer in use,can be removed activity"
Android always try to maintain its counterpart. answer for your query why onAttached first gives your query's answer
Fragment is designed to be activity independent.The onAttach() provides an interface to determine the state/type/(other detail that matters to the fragment) of the containing activity with reference to the fragment before you initialize a fragment.
Related
I'm trying to change the text from a TextView that is in a Fragment when onLocationChanged function is called.
I know that i could implement LocationListener when creating HomeFragment but i want this to be modularized.
public void onLocationChanged(Location location) {
Log.i(TAG,"onLocationChanged method called");
HomeFragment hf = new HomeFragment();
if(hf == null)
{
Log.i(TAG,"hf is null");
}
else {
if(hf.getView().findViewById(R.id.speed_box) == null)
{
Log.i(TAG,"findViewById failed");
}
else {
TextView speedBox = (TextView) hf.getView().findViewById(R.id.speed_box);
if (location == null) {
if (speedBox != null) {
speedBox.setText("unknown m/s");
} else {
Log.i(TAG, "speedBox object is null");
}
} else {
Log.i(TAG, "onLocationChanged method called, the speed is: " + location.getSpeed());
float speed = location.getSpeed();
if (speedBox != null) {
speedBox.setText(location.getSpeed() + " m/s");
}
{
Log.i(TAG, "speedBox object is null");
}
}
}
}
}
You create an instance of HomeFragment, but it's not attached to the layout yet, that's why you get null from getView.
The fragment needs to be attached to Activity by a transaction from FragmentManager, then fragment.onCreateView is called, then getView will not return null.
To me, the reason you don't want to use listener is not what it should be. Location callback should be global in a location-aware app, and any component needs to listen to location changed can register a listener anywhere.
Here is how I will implement it:
Have a singleton AppLocationManager class that holds location logic, it keep the list of LocationListener and fire event to all listener if location changed. AppLocationManager doesn't need to know about its dependencies or what they are, it only does 1 job.
HomeFragment registers the listener to AppLocationManager in onCreateView, listen to the change and update its TextView.
Any other component can register listener to AppLocationManager if they want just like HomeFragment.
First of all, you probably don't want to initialize every time your Fragment class, instead of that, you should instantiate this class only once and check accessibility of this Fragment, so a few options:
option - in this case, you instantiate Fragment class only once, and use this method as variable holder
private HomeFragment hf;
public Fragment getHomeFragment() {
if (hf == null) {
hf = new HomeFragment();
}
return hf;
}
Find already visible fragment:
Fragment currentFragment = getFragmentManager().findFragmentById(R.id.fragment_container);
if (currentFragment != null) {
if (currentFragment instanceof HomeFragment) {
//do your action
}
}
At least, try to post the whole class, where you have your onLocationChanged method
According to fragment lifecycle onAttach() is called before onCreate() so that it assigns hosting activity to the fragment. So, I wanted to know what if it is not overridden. Does a default definition for all the fragment callbacks already exists?
From the documentation:
void onAttach (Activity activity)
called once the fragment is associated with its activity. This method was deprecated in API level
23. Use onAttach(Context) instead.
If you override this method you must call through to the superclass
implementation.
void onAttach (Context context)
Called when a fragment is first attached to its context. onCreate(Bundle) will be called after this.
This is a lifecyle design for the fragment. There is nothing wrong when you don't override the method.
Does a default definition for all the fragment callbacks already exists?
No, you need to create the fragment callback by yourself. onAttach() method is usually overriden to make sure the parent activity of the fragment is implementing the fragment callback. Something like this (read more at Communicating with Other Fragments):
public class HeadlinesFragment extends ListFragment {
OnHeadlineSelectedListener mCallback;
// Container Activity must implement this interface
public interface OnHeadlineSelectedListener {
public void onArticleSelected(int position);
}
#Override
public void onAttach(Context context) {
super.onAttach(context);
// This makes sure that the container activity has implemented
// the callback interface. If not, it throws an exception
try {
mCallback = (OnHeadlineSelectedListener) context;
} catch (ClassCastException e) {
throw new ClassCastException(context.toString()
+ " must implement OnHeadlineSelectedListener");
}
}
...
}
When the parent activity is not implementing OnHeadlineSelectedListener, the application will crash and throwing must implement OnHeadlineSelectedListener. Hence it will preventing you introducing a logic error in your code.
UPDATE
What the purpose of onAttach()?
According to fragment lifecycle onAttach() is called before onCreate()
so that it assigns hosting activity to the fragment.
What the meaning of that actually?
Simple answer: It's a lifecyle of Fragment where we can know when the Fragment has been attached to it's parent activity.
More details:
From the the following source code of onAttach():
/**
* Called when a fragment is first attached to its context.
* {#link #onCreate(Bundle)} will be called after this.
*/
#CallSuper
public void onAttach(Context context) {
mCalled = true;
final Activity hostActivity = mHost == null ? null : mHost.getActivity();
if (hostActivity != null) {
mCalled = false;
onAttach(hostActivity);
}
}
/**
* #deprecated Use {#link #onAttach(Context)} instead.
*/
#Deprecated
#CallSuper
public void onAttach(Activity activity) {
mCalled = true;
}
We will see nothing except the documentation about our previous question and
mHost.
on the source code of Fragment at https://github.com/android/platform_frameworks_base/blob/master/core/java/android/app/Fragment.java#L435, we can know that the mhost is actually a FragmentHostCallback:
// Activity this fragment is attached to.
FragmentHostCallback mHost;
But if we scanning through all the source code Fragment, we won't get any clue where the mhost is initialized.
We know that from the Fragment lifecyle diagram that the lifecyle is start when the fragment is added:
Programatically, we add the Fragment with:
FragmentManager fragmentManager = getFragmentManager();
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
ExampleFragment fragment = new ExampleFragment();
fragmentTransaction.add(R.id.fragment_container, fragment);
fragmentTransaction.commit();
Checking the FragmentManager source code at line 1200 to 1229 from method moveToState():
void moveToState(Fragment f, int newState, int transit, int transitionStyle,
boolean keepActive) {
}
we have the following code:
f.mHost = mHost;
f.mParentFragment = mParent;
f.mFragmentManager = mParent != null
? mParent.mChildFragmentManager : mHost.getFragmentManagerImpl();
// If we have a target fragment, push it along to at least CREATED
// so that this one can rely on it as an initialized dependency.
if (f.mTarget != null) {
if (mActive.get(f.mTarget.mIndex) != f.mTarget) {
throw new IllegalStateException("Fragment " + f
+ " declared target fragment " + f.mTarget
+ " that does not belong to this FragmentManager!");
}
if (f.mTarget.mState < Fragment.CREATED) {
moveToState(f.mTarget, Fragment.CREATED, 0, 0, true);
}
}
dispatchOnFragmentPreAttached(f, mHost.getContext(), false);
f.mCalled = false;
f.onAttach(mHost.getContext());
if (!f.mCalled) {
throw new SuperNotCalledException("Fragment " + f
+ " did not call through to super.onAttach()");
}
if (f.mParentFragment == null) {
mHost.onAttachFragment(f);
} else {
f.mParentFragment.onAttachFragment(f);
}
Now we know that mHost and onAttach() of Fragment is initialized and called by the FragmentManager.
Nothing happens if you don't call OnAttach(). It is a lifecycle method provided to you if want to do something when the fragment is attached to its activity or context.
However, the fragment class does have a default implementation of OnAttach (which doesn't do anything). If you are curious, check out the source code.
I have an activity defined with a fragment described in XML. In the activity, I retrieve a reference to the fragment:
#Override
public void onStart() {
super.onStart();
FragmentManager manager = getSupportFragmentManager();
mFragment = (DetailActivityFragment) manager.findFragmentById(R.id.fragment);
...
}
I call the fragment's (custom) updateUI() method, and it has a different ID than that in the activity:
DetailActivity: tbm 440; got fragment: (0x038c6009) (invisible)
DetailActivityFragment: tbm 162; fragment: (0x05f54af9) updating UI
Then, when the fragment is destroyed:
DetailActivityFragment: tbm 131; destroying fragment (0x038c6009)
i.e. the same fragment that was created inside the activity, and different from the actual fragment.
In case it matters, here's how I log the fragment IDs:
Log.d(TAG, String.format("tbm 162; (0x%08x) updating UI", System.identityHashCode(this));
Also, the ID shown in the 'tbm 162' log statement is always the same, and always matches the ID of the first time the fragment was instantiated.
Is there any particular reason for this perversity? How can a fragment's im-memory ID change between the time it is instantiated and the time the fragment is referenced inside itself? The issue of course is that inside the updateUI method, UI elements are referenced that are contained within the (now hidden and detached) original fragment, so the UI never actually changes visibly.
TIA!
Edit:
The updateUI() method does nothing but display some fields, so I don't think it is relevant, but as requested in case I'm missing something:
public void updateUi(final InformativeBean bean) {
Log.d(TAG, String.format("tbm 162; (0x%08x) (%s) updating UI",
System.identityHashCode(this),
this));
if (mView == null || bean == null) {
return;
}
if (bean.getBattery() != null) {
TextView battery_info = (TextView) mView.findViewById(R.id.battery_field);
battery_info.setText(String.format("%s%%", bean.getBattery().getPercentage()));
}
ImageView burnin_view = (ImageView) mView.findViewById(R.id.burnin_flag_image);
if (bean.isBurninComplete()) {
burnin_view.setImageResource(R.drawable.ic_flag_burnin_complete);
} else {
burnin_view.setImageResource(R.drawable.ic_flag_burnin_incomplete);
}
Program program = bean.getProgram();
Program new_program = bean.getPendingProgram();
TextView current_program_view = (TextView) mView.findViewById(R.id.current_program_field);
current_program_view.setBackgroundColor(mTransparentColor);
if (new_program == null) {
if (program == null) {
return;
}
current_program_view.setText(program.toString());
} else if (program == null || program.equals(new_program)) {
current_program_view.setText(new_program.toString());
} else {
current_program_view.setText(String.format("%s -> %s", program, new_program));
current_program_view.setBackgroundColor(mProgramBackgroundColor);
}
}
Edit:
Even weirder, the fragment changes ID even within itself:
DetailActivityFragment: tbm 065; (0x06a9e492) (DetailActivityFragment{6a9e492 #0 id=0x7f0d0075}) in attach
DetailActivityFragment: tbm 104; (0x06a9e492) (DetailActivityFragment{6a9e492 #0 id=0x7f0d0075}) in onCreateView
DetailActivity: tbm 463; got fragment: (0x06a9e492) (DetailActivityFragment{6a9e492 #0 id=0x7f0d0075}) (invisible)
DetailActivityFragment: tbm 162; (0x068d3e1a) (DetailActivityFragment{68d3e1a}) updating UI
so it's 0x06a9e492 in onAttach and onCreateView (and that's the reference the activity receives), but stubbornly 0x068d3e1a inside updateUI.
After much investigation it turns out that a field inside the activity containing the errant fragment was holding a reference to that activity. Changing the field so it is passed the current activity instance (vs holding an implicit reference to the field's outer activity) fixed the issue. I'm still somewhat confused as to how a fragment's ID can change even while it is 'active', but at least I can make progress.
This helped a lot: http://www.androiddesignpatterns.com/2013/01/inner-class-handler-memory-leak.html. I didn't spot the problem at first as being a kind of leak.
Effectively, this was the (original, errant) code:
DetailActivity
...
some_field = new Listener() {
// The outer activity is retained, causing its fragment to be (partially?) re-used.
onSomeEvent() {
Toast.makeText(DetailActivity.this, "Got Some Event", ...).show();
}
};
...
And the new, working code is more like:
DetailActivity
...
some_field = ListenerWithActivity.getInstance(this);
...
ListenerWithActivity:
public static ListenerWithActivity getInstance(Activity activity) {
ListenerWithActivity existing = getExisting();
if (existing == null) {
existing = new ListenerWithActivity();
}
existing.setActivity(activity);
return existing;
}
public void onSomeEvent() {
Toast.makeText(getActivity(), "Got Some Event", ...).show();
}
In my activities I frequently use this idiom:
#Override
public void onDestroy() {
super.onDestroy();
if (isFinishing() != true) return;
// do some final cleanup since we're going away for good
}
Fragment has an onDestroy() method, but what is the equivalent of isFinishing()? Should I just check getActivity().isFinishing() from within the fragment's onDestroy()?
EDITED TO ADD:
Here are the callbacks (in order) I get under various circumstances, along with whether getActivity() returns null or non-null and, if non-null, the value of getActivity().isFinishing():
Scenario #1: DialogFragment is showing and user taps back button (i.e. need to release references):
onDismiss(): activity = non-null, finishing = false
onDestroy(): activity = non-null, finishing = false
Scenario #2: DialogFragment is showing and parent activity finishes (i.e. need to release references):
onDestroy(): activity = non-null, finishing = true
onDismiss(): activity = null, finishing = n/a
Scenario #3: DialogFragment is showing and OS temporarily destroys parent activity (i.e. should not release references):
onDestroy(): activity = non-null, finishing = false
onDismiss(): activity = null, finishing = n/a
Fragments have a method called isRemoving() which is true when the fragment is being removed from the Activity:
Return true if this fragment is currently being removed from its activity. This is not whether its activity is finishing, but rather whether it is in the process of being removed from its activity.
Thanks for the help guys. I ended up adding these to my DialogFragment:
private void doReleaseReferences() {
// release the stuff
}
#Override
public void onDestroy() {
super.onDestroy();
if (getActivity().isFinishing())
doReleaseReferences();
}
#Override
public void onDismiss(DialogInterface dialog) {
super.onDismiss(dialog);
if (getActivity() != null)
doReleaseReferences();
}
Based on the callback behavior I added to the original question I think this serves my purposes. When the back button is pressed or the parent activity finishes the references are released; when the parent activity is destroyed by the OS with the intention of being revived later they are not released.
If you would like to clean up fragment references on per basis you could use this helper method for fragment replacement
public abstract class BaseActivity extends AppCompatActivity {
#Override
protected void onDestroy() {
if (isFinishing()) {
notifyStackedFragmentsFinishing();
}
super.onDestroy();
}
protected void changeFragment(Fragment fragment) {
notifyStackedFragmentsFinishing();
getSupportFragmentManager().beginTransaction()
.replace(R.id.fragment, fragment)
.commit();
}
private void notifyStackedFragmentsFinishing() {
List<Fragment> fragments = getSupportFragmentManager().getFragments();
if (fragments != null && !fragments.isEmpty()) {
for (Fragment fragment : fragments) {
((BaseFragment) fragment).releaseOnFinishing();
}
}
}
}
Now you can extend all your activities from BaseActivity and all your fragments from BaseFragment. Also if you stacking fragments you should probably extend more i.e. onBackPressed()
I think this kotlin extension function will do the trick. At least if you are using the "Navigation framework"
fun Fragment.isFinishing(): Boolean {
val name = this.javaClass.name
val thisEntry = findNavController().backQueue.find {
val destination = it.destination
destination is FragmentNavigator.Destination && destination.className == name
}
return thisEntry == null
}
Given a fragment which loads (a lot of) data from the database using a loader.
Problem :
I have a pager adapter which destroys the fragment when the user moves away from the tab holding it and recreates it when user gets back to that tab. Because of this recreation, a new loader is created everytime and the data gets loaded everytime.
Question :
To avoid recreating loader everytime the fragment is created, is it ok to use getActivity.getSupportLoaderManager.initLoader(loaderId, null, false) in the onActivityCreated method of the fragment?
I have tried it, tested it and it seems to be working fine. But I'm not convinced that it is right.
Actually, checking the source code, you end up doing the same.
Fragment.getLoaderManager:
/**
* Return the LoaderManager for this fragment, creating it if needed.
*/
public LoaderManager getLoaderManager() {
if (mLoaderManager != null) {
return mLoaderManager;
}
if (mActivity == null) {
throw new IllegalStateException("Fragment " + this + " not attached to Activity");
}
mCheckedForLoaderManager = true;
mLoaderManager = mActivity.getLoaderManager(mWho, mLoadersStarted, true);
return mLoaderManager;
}
mWho is basically the fragment ID.
final void setIndex(int index, Fragment parent) {
mIndex = index;
if (parent != null) {
mWho = parent.mWho + ":" + mIndex;
} else {
mWho = "android:fragment:" + mIndex;
}
}
The difference in Activity.getLoaderManager() is that who will be (root)
So even though you can do what you are asking, calling it directly from the Fragment might be a better approach
Activity source code
Fragment source code
Disclaimer: I only checked the source code in the latest version, but I don't expect it to be very different
May i ask why you are simply not retaining the Fragment? It seems that what you need is to create the Loader in the Fragment and create the fragment with setRetainInstance(true).
In this case remember to provide a TAG when you add the fragment.
This way the fragment will survive even to activity config changes and only the view will be recreated leaving your loader alive.