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
}
Related
I'm working on the project initially created by another developer. There is a root Activity (let's call it CustomActivity) with code below.
private static SomeOtherClass instance = null;
#Override
public void onCreate(final Bundle savedInstanceState)
{
super.onCreate(null);
if (!(Thread.getDefaultUncaughtExceptionHandler() instanceof CustomExceptionHandler)) {
Thread.setDefaultUncaughtExceptionHandler(new CustomExceptionHandler());
}
// #see http://stackoverflow.com/questions/19545889/app-restarts-rather-than-resumes)
if (!isTaskRoot() && getIntent().hasCategory(Intent.CATEGORY_LAUNCHER) && null != getIntent().getAction()
&& getIntent().getAction().equals(Intent.ACTION_MAIN)) {
finish();
return;
}
instance = new SomeOtherClass();
this.screen = new CustomFragment();
this.getFragmentManager.beginTransaction()
.replace(R.id.content_frame, screen).commit();
...
}
#Override
protected void onDestroy()
{
this.instance = null;
...
}
static public SomeOtherClass getInstance()
{
return instance;
}
this.screen has a button with CustomActivity.getInstance().methodCall() on tap. And one of beta testers said he just tapped that button and got crash with this method in stacktrace: Attempt to invoke virtual method '...CustomActivity.getInstance().methodCall()' on a null object reference.
I don't understand - how it's possible due to Activity lifecycle.
According to stacktrace, onCreate shouldn't be called after some previous onDestroy. Even if the last one can happen when we were in another Activity (and yep, this.screen is not nullified anywhere), but this.screen fragment can't be rendered without Activity recreation. Am I right?
P.S.: there is no more instance variable management at all and SomeOtherClass has no custom parent class (just default object).
P.P.S.: nope, device wasn't locked / app just launched. Tester worked with it, rotated phone to remove sim card, removed, rotated back and saw crash alert.
P.P.P.S: don't know why null in super.onCreate() but anyhow this Activity has no code to support saved states.
I was wondering, what is the Fragment lifecycle methods, I should commit FragmentTransaction to avoid famous
java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState
According to http://www.androiddesignpatterns.com/2013/08/fragment-transaction-commit-state-loss.html, it gives great tip, on how to avoid such exception, by commit FragmentTransaction
FragmentActivity
onCreate()
onResumeFragments()
onPostResume()
Fragment
???
However, how about Fragment? What is the suitable Fragment lifecycle we should commit our fragment? For instance, under very rare situation, I will get exception from Google Play Console crash report, while trying to commit Fragment in another Fragment's onCreate.
public class BuyPortfolioFragment extends Fragment {
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
final FragmentManager fm = this.getFragmentManager();
// Check to see if we have retained the worker fragment.
this.statusBarUpdaterFragment = (StatusBarUpdaterFragment)fm.findFragmentByTag(STATUS_BAR_UPDATER_FRAGMENT);
if (this.statusBarUpdaterFragment == null) {
this.statusBarUpdaterFragment = StatusBarUpdaterFragment.newInstance();
this.statusBarUpdaterFragment.setTargetFragment(this, 0);
// java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState
fm.beginTransaction().add(statusBarUpdaterFragment, STATUS_BAR_UPDATER_FRAGMENT).commit();
} else {
statusBarUpdaterFragment.setTargetFragment(this, 0);
}
p/s I know I can avoid such exception by using commitAllowingStateLoss. I want to use it as last resource.
Fragment's lifecycle state not always matches Activity's. Fragment's method getFragmentManager() returns the FragmentManager of it's hosting Activity (unless it's a child Fragment, if so this method returns the child fragment manager of a hosting Fragment). You may never know in which state is Fragment's hosting Activity unless you make tracking code. So it's really possible that the transaction eventually may be committed after Activity onSaveInstanceState() was called.
I suggest using getChildFragmentManager() and deal with child fragments from fragments.
Or if your intention was really to control Activity Fragments, make accessors for controlling it's state, like
// Activity method
public void showSomeFragment() {
if (mFragmentTransactionsAllowed) {
// do transaction
}
}
// And track the boolean
#Override
protected void onCreate(Bundle b) {
super.onCreate(b);
// override on onCreate() in case if Activity object is reused and state was true
mFragmentTransactionsAllowed = true;
}
#Override
protected void onStart() {
super.onStart();
// override here so that if activity goes foreground but not yet destroyed
mFragmentTransactionsAllowed = true;
}
#Override
protected void onResume() {
super.onResume();
mFragmentTransactionsAllowed = true;
}
#Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
mFragmentTransactionsAllowed = false;
}
I have an Activity in which I have a ProgressBar,an ImageView and a TextView,I update all three from an AsyncTask.All three get updated when the screen is completely in one orientation when the task is running,but the ImageView and TextView are not displayed and the ProgressBar freezes when the screen orientation changes from one orientation to another.
Adding the attach and detach methods to the task and using retainNonConfigurationInstance to return the task when the Activity and using getLastNonConfigurationInstance is destroyed has had no effect.I have also implement three methods for getting the various progress values from the AsyncTask to no effect.
MyActivity looks like this:
static final String TAG="ImageUpdateActivity";
TextView txt_currentOp;
ImageView img_currentOp;
ImageUpdatingTask task;
CustomProgressBar updatebar;
#SuppressWarnings("deprecation")
#Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.layout_imageupdate);
txt_currentOp=(TextView)findViewById(R.id.txt_currentOp);
img_currentOp=(ImageView)findViewById(R.id.img_updateOp);
updatebar=(CustomProgressBar)findViewById(R.id.progressbar_update);
String filename=getIntent().getStringExtra("pathName");
task=(ImageUpdatingTask)getLastNonConfigurationInstance();
if(task!=null)
{
task.attach(this);
if(task.getStatus()==AsyncTask.Status.RUNNING)
{
Log.d(TAG, "The progress description is: "+task.getProgressDesc());
txt_currentOp.setText(task.getProgressDesc());
img_currentOp.setImageBitmap(task.getProgressBitmap());
updatebar.setProgress(task.getProgress());
}
}
else
{
task=new ImageUpdatingTask(this);
task.execute(filename);
}
}
public Object retainNonConfigurationInstance()
{
task.detach();
return task;
}
public boolean onKeyDown(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_BACK && event.getRepeatCount() == 0) {
if(task.getStatus()!=AsyncTask.Status.FINISHED)
{
task.cancel(true);
task=null;
}
Intent i=new Intent(this,ImagePreviewActivity.class);
startActivity(i);
}
return super.onKeyDown(keyCode, event);
}
This is how I update the progress from my doInBackground method where
int progress=0;
Bitmap progressBitmap=null;
String progressDesc=null;
are global variables.
mOperation=BITMAP_TO_PIX;
progressDesc=getValueFromOperation(mOperation);
Pix pix=convertBitmapToPix(bitmap);
mOperation=CONVERT_TO_8;
progressDesc=getValueFromOperation(mOperation);
Pix pix2=convertOperation(pix);
temp=pix2.copy();
tempImg=convertPixToBitmap(temp);
progressBitmap=tempImg;
temp=null;
progress+=10;//60
publishProgress(tempImg);
And in my publishProgress I use:
#Override
protected void onProgressUpdate(Bitmap... values) {
// TODO Auto-generated method stub
super.onProgressUpdate(values);
int oldOperation=0,oldProgress=0;
if(mOperation!=oldOperation)
{
String progressText=getValueFromOperation(mOperation);
Log.d(TAG, progressText);
activity.txt_currentOp.setText(progressText);
oldOperation=mOperation;
}
if(oldProgress!=progress)
{
Log.d(TAG,"Update the progress: "+progress);
activity.updatebar.setProgress(progress);
oldProgress=progress;
}
activity.img_currentOp.setImageBitmap(values[0]);
}
And the Activity,is passed to the task using the constructor:
public ImageUpdatingTask(ImageUpdateActivity activity)
{
this.activity=activity;
}
These are the methods that take care of interaction between the AsyncTask and the Activity:
public void attach(ImageUpdateActivity activity)
{
this.activity=activity;
}
public void detach()
{
activity=null;
}
public int getProgress()
{
return progress;
}
public Bitmap getProgressBitmap()
{
return progressBitmap;
}
public String getProgressDesc()
{
return progressDesc;
}
When orientation changes your activity gets is destroyed and recreated. Fragments are hosted by an activity.
By default, Fragments are destroyed and recreated along with their parent Activitys when a configuration change occurs. Calling Fragments setRetainInstance(true) allows us to bypass this destroy-and-recreate cycle, signaling the system to retain the current instance of the fragment when the activity is recreated.
public void setRetainInstance (boolean retain)
Added in API level 11
Control whether a fragment instance is retained across Activity re-creation (such as from a configuration change). This can only be used with fragments not in the back stack. If set, the fragment lifecycle will be slightly different when an activity is recreated:
onDestroy() will not be called (but onDetach() still will be, because the fragment is being detached from its current activity).
onCreate(Bundle) will not be called since the fragment is not being re-created.
onAttach(Activity) and onActivityCreated(Bundle) will still be called.
You can check this blog for a workaround suggested . Uses interface as callback to the activity.
http://www.androiddesignpatterns.com/2013/04/retaining-objects-across-config-changes.html
and the source code for the same is available at
https://github.com/alexjlockwood/worker-fragments
Quoting from the blog
Flow of Events
When the MainActivity starts up for the first time, it instantiates and adds the TaskFragment to the Activity's state. The TaskFragment creates and executes an AsyncTask and proxies progress updates and results back to the MainActivity via the TaskCallbacks interface. When a configuration change occurs, the MainActivity goes through its normal lifecycle events, and once created the new Activity instance is passed to the onAttach(Activity) method, thus ensuring that the TaskFragment will always hold a reference to the currently displayed Activity instance even after the configuration change. The resulting design is both simple and reliable; the application framework will handle re-assigning Activity instances as they are torn down and recreated, and the TaskFragment and its AsyncTask never need to worry about the unpredictable occurrence of a configuration change.
I have an activity with dynamic fragments in it. I need to run some code after a fragment is removed but remove(myFragment).commit() is executed asynchronously and i cant know when exactly the fragment is removed.Here is my code:
final FragmentTransaction ft = getFragmentManager().beginTransaction();
ft.remove(myFragment).commit();
//wait until the fragment is removed and then execute rest of my code
From the documentation:
public abstract int commit ()
Schedules a commit of this transaction. The commit does not happen
immediately; it will be scheduled as work on the main thread to be
done the next time that thread is ready.
What if you use the fragment's onDetach method to call the activity and tell it its done?
class MyFrag extends Fragment {
private Activity act;
#Override
public void onAttach(Activity activity) {
act = activity;
}
#Override
public void onDetatch() {
act.fragGone();
}
}
And in the activity:
public void fragGone() {
//do something, update a boolean, refresh view, etc.
}
You could try using the onDetached() callback of the fragment. This will be called whenever it is removed from its Activity.
Use onAttach() to check when the fragment is attached to the Activity and use onDettach() to check when the fragment is dettached to the activity.
Using the onDettach() you can also check to update or not views, data, etc in this way:
#Override
public void onDetach() {
super.onDetach();
synchronized (mLock) {
mReady = false;
}
}
My application consists of several fragments. Up until now I've had references to them stored in a custom Application object, but I am beginning to think that I'm doing something wrong.
My problems started when I realized that all my fragment's references to mActivity becomes null after an orientation change. So when I call getActivity() after an orientation change, a NullPointerException is thrown.
I have checked that my fragment's onAttach() is called before I make the call to getActivity(), but it still returns null.
The following is a stripped version of my MainActivity, which is the only activity in my application.
public class MainActivity extends BaseActivity implements OnItemClickListener,
OnBackStackChangedListener, OnSlidingMenuActionListener {
private ListView mSlidingMenuListView;
private SlidingMenu mSlidingMenu;
private boolean mMenuFragmentVisible;
private boolean mContentFragmentVisible;
private boolean mQuickAccessFragmentVisible;
private FragmentManager mManager;
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
/*
* Boolean variables indicating which of the 3 fragment slots are visible at a given time
*/
mMenuFragmentVisible = findViewById(R.id.menuFragment) != null;
mContentFragmentVisible = findViewById(R.id.contentFragment) != null;
mQuickAccessFragmentVisible = findViewById(R.id.quickAccessFragment) != null;
if(!savedInstanceState != null) {
if(!mMenuFragmentVisible && mContentFragmentVisible) {
setupSlidingMenu(true);
} else if(mMenuFragmentVisible && mContentFragmentVisible) {
setupSlidingMenu(false);
}
return;
}
mManager = getSupportFragmentManager();
mManager.addOnBackStackChangedListener(this);
final FragmentTransaction ft = mManager.beginTransaction();
ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN);
if (!mMenuFragmentVisible && mContentFragmentVisible) {
/*
* Only the content fragment is visible, will enable sliding menu
*/
setupSlidingMenu(true);
onToggle();
ft.replace(R.id.contentFragment, getCustomApplication().getSportsFragment(), SportsFragment.TAG);
} else if (mMenuFragmentVisible && mContentFragmentVisible) {
setupSlidingMenu(false);
/*
* Both menu and content fragments are visible
*/
ft.replace(R.id.menuFragment, getCustomApplication().getMenuFragment(), MenuFragment.TAG);
ft.replace(R.id.contentFragment, getCustomApplication().getSportsFragment(), SportsFragment.TAG);
}
if (mQuickAccessFragmentVisible) {
/*
* The quick access fragment is visible
*/
ft.replace(R.id.quickAccessFragment, getCustomApplication().getQuickAccessFragment());
}
ft.commit();
}
private void setupSlidingMenu(boolean enable) {
/*
* if enable is true, enable sliding menu, if false
* disable it
*/
}
#Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
// launch the fragment that was clicked from the menu
}
#Override
public void onBackPressed() {
// Will let the user press the back button when
// the sliding menu is open to display the content.
if (mSlidingMenu != null && mSlidingMenu.isMenuShowing()) {
onShowContent();
} else {
super.onBackPressed();
}
}
#Override
public void onBackStackChanged() {
/*
* Change selected position when the back stack changes
*/
if(mSlidingMenuListView != null) {
mSlidingMenuListView.setItemChecked(getCustomApplication().getSelectedPosition(), true);
}
}
#Override
public void onToggle() {
if (mSlidingMenu != null) {
mSlidingMenu.toggle();
}
}
#Override
public void onShowContent() {
if (mSlidingMenu != null) {
mSlidingMenu.showContent();
}
}
}
The following is a stripped version of the CustomApplication. My thoughts behind this implementation was to guarantee only one instance of each fragment throughout my application's life cycle.
public class CustomApplication extends Application {
private Fragment mSsportsFragment;
private Fragment mCarsFragment;
private Fragment mMusicFragment;
private Fragment mMoviesFragment;
public Fragment getSportsFragment() {
if(mSsportsFragment == null) {
mSsportsFragment = new SportsFragment();
}
return mSsportsFragment;
}
public Fragment getCarsFragment() {
if(mCarsFragment == null) {
mCarsFragment = new CarsFragment();
}
return mCarsFragment;
}
public Fragment getMusicFragment() {
if(mMusicFragment == null) {
mMusicFragment = new MusicFragment();
}
return mMusicFragment;
}
public Fragment getMoviesFragment() {
if(mMoviesFragment == null) {
mMoviesFragment = new MoviesFragment();
}
return mMoviesFragment;
}
}
I am very interested in tips on how to best implement multiple fragments and how to maintain their states. For your information, my applicaion consists of 15+ fragments so far.
I have done some research and it seems that FragmentManager.findFragmentByTag() is a good bet, but I haven't been able to successfully implement it.
My implementation seems to work good except for the fact that mActivity references become null after orientation changes, which lets me to believe that I may have some memory leak issues as well.
If you need to see more code, please let me know. I purposely avoided including fragment code as I strongly believe issues are related to my Activity and Application implementations, but I may be wrong.
Thanks for your time.
My thoughts behind this implementation was to guarantee only one instance of each fragment throughout my application's life cycle
This is probably part, if not all, of the source of your difficulty.
On a configuration change, Android will re-create your fragments by using the public zero-argument constructor to create a new instance. Hence, your global-scope fragments will not "guarantee only one instance of each fragment".
Please delete this custom Application class. Please allow the fragments to be re-created naturally, or if they need to live for the life of a single activity, use setRetainInstance(true). Do not attempt to reuse fragments across activities.
I don't see where are you using the reference to mActivity. But don't hold a reference to it. Always use getActivity since the Activity can be recreated after orientation change. Also, don't ever set the fragment's fields by setters or by assigning always use a Bundle and Arguments
Best practice for instantiating a new Android Fragment
Also you can use setRetainInstance(true) to keep all the fragment's members during orientation change.
Understanding Fragment's setRetainInstance(boolean)
To resolve this problem you have to use the activity object provided by onAttach method of fragment so when you change the orientation fragment is recreated so onAttach give you the current reference
you can use onAttach(Context context) to create a private context variable in fragment like this
#Override
public void onAttach(Context context) {
this.context = context;
super.onAttach(context);
}
on changing orientation, onAttach gives you new reference to the context, if you want reference to activity, you can typecast context to activity.
Context can also be reassigned inside onCreate in fragments as OnCreate is called when device is rotated
private Context mContext;
#Override
public void onCreate(#Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//get new activity reference here
mContext = getActivity();
}
pass this mContext throughout the fragment
If you don't setRetainInstance(true) in onCreate ... the collection e.g List<Object>, Vector<Object> in Application class will get null. Make sure you setRetainInstance(true) to make them alive.