Android Fragments and Transactions

Problem

Sometimes it is confusing how fragments work in Android and interesting side effects like:

  • Why have Fragments empty constructors?
  • Why shouldn’t I pass state variables like they are?
  • Why do I need this Fragment transactions?
  • What does addToBackStack mean and do I need it?
  • Why is sometimes the Activity null in my fragment?

Overview

Fragments are used to wrap functionality into an own reusable UI element. The life cycle is well documented by google. More interesting is, that android may recreate fragments during its life cycle which leads to interesting implications.

Furthermore Fragments are not directly added an visible, as requesting them to show only queues this task. Nevertheless, they will be shown later on.

Sometimes it is also confusing that creating a fragment inside a fragment will always pass the common activity of both fragments into it.

@Override
public void onAttach(Context context) {
    super.onAttach(context); // context is always the activity
}

Dynamic recreation

As android can recreate the fragment during the resume process of an activity. As so state in member variables needs to be persisted. In general, fragments should always be created using a bundle or be dismissed by the activity in the onPause method:

public class MyDialogFragment extends DialogFragment {
    private static final String ARG_PARAM1 = "param1";
    private String mParam1;

    public MyDialogFragment() {} // required

    // Create fragment with call parameters
    public static MyDialogFragment newInstance(String param1) {
        MyDialogFragment fragment = new MyDialogFragment();
        Bundle args = new Bundle();
        args.putString(ARG_PARAM1, param1);
        fragment.setArguments(args);
        return fragment;
    }
    // restore any initial passed arguments as needed
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        if (getArguments() != null) {
            mParam1 = getArguments().getString(ARG_PARAM1);
        }
    }
    // if the param may change during the life cycle of the fragment and need to be stored
    // usually not needed
    @Override
    public void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        outState.putString(ARG_PARAM1, mParam1);
    }
}

As long as the fragment is restored, the activity of it may be null. As so it may happen that own threads still running in the fragment may encounter null pointer exceptions.

The easiest way is either:

  • Stop all running threads in the onPause
  • Remove all listeners for the time being, but keep the threads, e.g. if you want to keep a socket connection open
  • save in a boolean variable the fact that your fragment was paused and wait for the resume

Hide and show DialogFragments

Showing a fragment is straight forward, by calling the show method. Now we can pass a second parameter, which is used to find this fragment later on. We can decide by storing the reference in a field variable or just to remember the name. Usually the last is the better approach.

But how now correctly to dismiss the fragment?

// show a fragment
MyDialogFragment dialogFragment = MyDialogFragment.newInstance("init");
dialogFragment.show(getFragmentManager(), "dialog");

// hide a fragment again
Fragment dialog = getFragmentManager().findFragmentByTag("dialog");
if (dialog != null) {
    FragmentTransaction ft = getFragmentManager().beginTransaction();
    ft.remove(dialog);
    ft.commit();
}

The above code example works for any kind of fragment. Of course, it is also possible to cast the fragment into a DialogFragment and invoke the dismiss method.

addToBackStack or not?

The main purpose of the addToBackStack method in the fragment transaction is to allow the user to use consistently the back button. If we modify the code above to:

Fragment dialog = getFragmentManager().findFragmentByTag("dialog");
if (dialog != null) {
    FragmentTransaction ft = getFragmentManager().beginTransaction();
    ft.remove(dialog);
    ft.addToBackStack(null); // remember this change
    ft.commit();
}

Then the user may open the dialog again with the back button on the phone. This is very interesting if you move between different fragments/dialog fragments. E.g.: you have an edit dialog fragment which opens a date time picker dialog. Then you can remove the detail dialog fragment and add the date picker fragment in one transaction. As soon the user hits the back button the detail dialog fragment is again visible.

As the name already indicated, it is a stack of view states, each change to the view is seen as a transaction. The user may pop the stack by using the back button.

In short:

  • Cancel or just close the dialog/fragment –> don’t add it to the back stack
  • Navigate to a Fragment like to an activity –> add it to the back stack, so the user can go back

Prevent closing of a Dialog Fragment using the back button

After the fragment was created set cancelable to false.

dialogFragment.setCancelable(false);

Is isVisible or isAdded reliable?

This happens to be the most tricky question. As showing a dialog will not always show the dialog immediately. It only schedules the show operation for the Fragment Manager.

As soon as you happen to see errors like:

java.lang.IllegalStateException: Fragment already added: XxxxxDialog{xxx XxxxxDialog}
       at android.app.FragmentManagerImpl.addFragment(FragmentManager.java:1236)
       at android.app.BackStackRecord.run(BackStackRecord.java:715)
       at android.app.FragmentManagerImpl.execPendingActions(FragmentManager.java:1552)
       at android.app.FragmentManagerImpl$1.run(FragmentManager.java:487)
       at android.os.Handler.handleCallback(Handler.java:739)
       at android.os.Handler.dispatchMessage(Handler.java:95)

The first solution by just calling getFragmentManager().executePendingTransactions() won’t work.

The reason is typically a multithreading issue, calling/ scheduling multiple show operations for the same fragment from a background thread. The best fix would be to find why this is called multiple times and check if this can be avoided. If not, then an AtomicBoolean could be the rescue e.g.:

public class MyDialog extends DialogFragment {
    private final AtomicBoolean isShowing = new AtomicBoolean(false);

    @Override
    public void show(FragmentManager manager, String tag) {
        isShowing.set(true); // ensure we update our "show" flag
        super.show(manager, tag);
    }

    @Override
    public void onResume() {
        super.onResume();
        isShowing.set(true);
    }
    @Override
    public void onDismiss(DialogInterface dialog) {
        super.onDismiss(dialog);
        isShowing.set(false);
    }
    /**
     * reader for the boolean flag, if show was already called
     */
    public boolean getIsShowing() {
        return isShowing.get();
    }
}

Using the AtomicBoolean enables you now to verify in a multithreaded environment if show was already called on the dialog.

Paul Sterl has written 54 articles

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>