Preventing Fragment Transition Issues

http://justmobiledev.com/wp-content/uploads/2017/11/fragment-transition.pngPreventing Fragment Transition Issues

The Problem

If you are using Fragments in your Android app, there’s a good chance you will eventually see the error message like the one below.
Man, that’s ugly! Debugging Java stacktraces can be a daunting task, so lets look at the different pieces step by step.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Exception java.lang.IllegalStateException: Fatal Exception thrown
on Scheduler.
io.reactivex.android.schedulers.HandlerScheduler$ScheduledRunnable.run
(HandlerScheduler.java)
android.os.Handler.handleCallback (Handler.java:751)
android.os.Handler.dispatchMessage (Handler.java:95)
android.os.Looper.loop (Looper.java:154)
android.app.ActivityThread.main (ActivityThread.java:6682)
java.lang.reflect.Method.invoke (Method.java)
com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run
(ZygoteInit.java:1520)
com.android.internal.os.ZygoteInit.main (ZygoteInit.java:1410)

Caused by: java.lang.IllegalStateException: Fragment d{c44110b} not attached to Activity
android.support.v4.app.Fragment.m (Fragment.java)

Let’s start with the ‘Caused by’ line, which identifies the root cause of the exception:

1
2
Caused by: java.lang.IllegalStateException: Fragment d{c44110b} not attached to Activity
android.support.v4.app.Fragment.m (Fragment.java)
From this error message the issue seems to be that the Activity was trying to perform an action on a Fragment, but the Fragment was not attached to the Activity at the time.

In the Fragment life cycle, the Fragment gets attached to the Activity in the ‘OnAttach()’ method and detached in the ‘OnDetach()’ method.
These events are triggered by the FragmentManager, which is used to add, replace or remove Fragments from the Activity.
However, these actions can only be performed when the Activity is in ‘Running’ state, so when it is in the foreground. In case the Activity is in the background, adding or replacing a Fragment will fail.

So the root issue for the exception above was that we were trying to perform Fragment transaction after the Activity.onSaveInstanceState() method has been called.

This can easily happen in the following scenario:

  1. User clicks a search button in Fragment A
  2. The search is performed asynchronously on a different thread
  3. Search takes a while, so the user gets bored and decides to check his/her email. The app is placed in the background. The Activity moves into Paused and Stopped state
  4. The search returns and the FragmentManager is invokes to display Fragment B with the search results. However, since the Activity is stopped, Fragment transactions cannot be performed and the exception occurs.

The Solution

How can the exception be avoided? The idea is to make the Activity become aware of its state and only perform Fragment transactions when it is in Running state.

If you have a parent class for all your Activites, add a new flag to check the Activity state.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public abstract class BaseActivity extends AppCompatActivity {
    public boolean isRunning = false;
   
    @Override
    protected void onResume() {
        super.onResume();

        isRunning = true;
    }
   
    @Override
    public void onPause()
    {
        super.onPause();

        isRunning = false;
    }
}

Next, when the Activity is in paused state, we want to defer any Fragment transactions until the Activity is in Running state again. So let’s create a new class for this purpose.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public abstract class DeferredFragmentTransaction {

    private int contentFrameId;
    private Fragment replacingFragment;

    public abstract void commit();

    public int getContentFrameId() {
        return contentFrameId;
    }

    public void setContentFrameId(int contentFrameId) {
        this.contentFrameId = contentFrameId;
    }

    public Fragment getReplacingFragment() {
        return replacingFragment;
    }

    public void setReplacingFragment(Fragment replacingFragment) {
        this.replacingFragment = replacingFragment;
    }
}

Next, in your BaseActivity class, when adding or replacing Fragments, you want to check if the Activity is running or not. In case it is, you can perform the Fragment transaction, in case it is not, you want to add the Fragment to the deferred queue:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public abstract class BaseActivity extends AppCompatActivity {
    private Queue<DeferredFragmentTransaction> queueDeferredFragmentTransactions = new ArrayDeque<>();

    ....
    public void addFragment(final @IdRes int id, final Fragment fragment) {

        if (isRunning)
        {
            FragmentManager fragmentManager = getSupportFragmentManager();

            // set Exit transition for the current visible fragment
            Fragment exitFragment = fragmentManager.findFragmentById(id);
            if (exitFragment != null && exitFragment.isVisible()) {
                exitFragment.setExitTransition(new AutoTransition());
            }

            FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
            fragmentTransaction.replace(id, fragment, fragment.toString());
            fragmentTransaction.commit();
        }
        else {
            // In case the activity is not running anymore, add the fragment transaction to the deferred transaction
            DeferredFragmentTransaction deferredFragmentTransaction = new DeferredFragmentTransaction() {
                @Override
                public void commit() {
                    addFragment(id, fragment);
                }
            };

            deferredFragmentTransaction.setContentFrameId(id);
            deferredFragmentTransaction.setReplacingFragment(fragment);

            queueDeferredFragmentTransactions.add(deferredFragmentTransaction);
        }
    }
    ...

Finally, when the Activity resumes, you want to process all Fragment transition requests from the deferred queue and an perform the transitions.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public abstract class BaseActivity extends AppCompatActivity {
    ...
    @Override
    protected void onResume() {
        super.onResume();

        isRunning = true;

        // Perform all deferred fragment transitions
        while (!queueDeferredFragmentTransactions.isEmpty()) {
            queueDeferredFragmentTransactions.remove().commit();
        }
    }
    ...
}

The credit for the deferred fragment queue idea goes to Ben Daniel A..

Thanks for reading. Please comment and rate the post.

Author Description

justmobiledev

There are 1 comments. Add yours

  1. 4th September 2018 | Rebekah Schulenberg says: Reply
    With thanks! Valuable information!

Join the Conversation