public final class

FragmentScenario<F extends Fragment>

extends java.lang.Object

 java.lang.Object

↳androidx.fragment.app.testing.FragmentScenario<F>

Overview

FragmentScenario provides API to start and drive a Fragment's lifecycle state for testing. It works with arbitrary fragments and works consistently across different versions of the Android framework.

FragmentScenario only supports androidx.fragment.app.Fragment. If you are using a deprecated fragment class such as android.support.v4.app.Fragment or , please update your code to androidx.fragment.app.Fragment.

If your testing Fragment has a dependency to specific theme such as Theme.AppCompat, use the theme ID parameter in FragmentScenario.launch(Class) method.

Summary

Methods
public static FragmentScenario<Fragment>launch(java.lang.Class<Fragment> fragmentClass)

Launches a Fragment hosted by an empty FragmentActivity and waits for it to reach the resumed state.

public static FragmentScenario<Fragment>launch(java.lang.Class<Fragment> fragmentClass, Bundle fragmentArgs)

Launches a Fragment with given arguments hosted by an empty FragmentActivity and waits for it to reach the resumed state.

public static FragmentScenario<Fragment>launch(java.lang.Class<Fragment> fragmentClass, Bundle fragmentArgs, FragmentFactory factory)

Launches a Fragment with given arguments hosted by an empty FragmentActivity using the given FragmentFactory and waits for it to reach the resumed state.

public static FragmentScenario<Fragment>launch(java.lang.Class<Fragment> fragmentClass, Bundle fragmentArgs, int themeResId, FragmentFactory factory)

Launches a Fragment with given arguments hosted by an empty FragmentActivity themed by themeResId, using the given FragmentFactory and waits for it to reach the resumed state.

public static FragmentScenario<Fragment>launchInContainer(java.lang.Class<Fragment> fragmentClass)

Launches a Fragment in the Activity's root view container android.R.id.content, hosted by an empty FragmentActivity and waits for it to reach the resumed state.

public static FragmentScenario<Fragment>launchInContainer(java.lang.Class<Fragment> fragmentClass, Bundle fragmentArgs)

Launches a Fragment in the Activity's root view container android.R.id.content, with given arguments hosted by an empty FragmentActivity and waits for it to reach the resumed state.

public static FragmentScenario<Fragment>launchInContainer(java.lang.Class<Fragment> fragmentClass, Bundle fragmentArgs, FragmentFactory factory)

Launches a Fragment in the Activity's root view container android.R.id.content, with given arguments hosted by an empty FragmentActivity using the given FragmentFactory and waits for it to reach the resumed state.

public static FragmentScenario<Fragment>launchInContainer(java.lang.Class<Fragment> fragmentClass, Bundle fragmentArgs, int themeResId, FragmentFactory factory)

Launches a Fragment in the Activity's root view container android.R.id.content, with given arguments hosted by an empty FragmentActivity themed by themeResId, using the given FragmentFactory and waits for it to reach the resumed state.

public FragmentScenario<Fragment>moveToState(Lifecycle.State newState)

Moves Fragment state to a new state.

public FragmentScenario<Fragment>onFragment(FragmentScenario.FragmentAction<Fragment> action)

Runs a given action on the current Activity's main thread.

public FragmentScenario<Fragment>recreate()

Recreates the host Activity.

from java.lang.Objectclone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait

Methods

public static FragmentScenario<Fragment> launch(java.lang.Class<Fragment> fragmentClass)

Launches a Fragment hosted by an empty FragmentActivity and waits for it to reach the resumed state.

Parameters:

fragmentClass: a fragment class to instantiate

public static FragmentScenario<Fragment> launch(java.lang.Class<Fragment> fragmentClass, Bundle fragmentArgs)

Launches a Fragment with given arguments hosted by an empty FragmentActivity and waits for it to reach the resumed state.

This method cannot be called from the main thread.

Parameters:

fragmentClass: a fragment class to instantiate
fragmentArgs: a bundle to passed into fragment

public static FragmentScenario<Fragment> launch(java.lang.Class<Fragment> fragmentClass, Bundle fragmentArgs, FragmentFactory factory)

Launches a Fragment with given arguments hosted by an empty FragmentActivity using the given FragmentFactory and waits for it to reach the resumed state.

This method cannot be called from the main thread.

Parameters:

fragmentClass: a fragment class to instantiate
fragmentArgs: a bundle to passed into fragment
factory: a fragment factory to use or null to use default factory

public static FragmentScenario<Fragment> launch(java.lang.Class<Fragment> fragmentClass, Bundle fragmentArgs, int themeResId, FragmentFactory factory)

Launches a Fragment with given arguments hosted by an empty FragmentActivity themed by themeResId, using the given FragmentFactory and waits for it to reach the resumed state.

This method cannot be called from the main thread.

Parameters:

fragmentClass: a fragment class to instantiate
fragmentArgs: a bundle to passed into fragment
themeResId: a style resource id to be set to the host activity's theme
factory: a fragment factory to use or null to use default factory

public static FragmentScenario<Fragment> launchInContainer(java.lang.Class<Fragment> fragmentClass)

Launches a Fragment in the Activity's root view container android.R.id.content, hosted by an empty FragmentActivity and waits for it to reach the resumed state.

This method cannot be called from the main thread.

Parameters:

fragmentClass: a fragment class to instantiate

public static FragmentScenario<Fragment> launchInContainer(java.lang.Class<Fragment> fragmentClass, Bundle fragmentArgs)

Launches a Fragment in the Activity's root view container android.R.id.content, with given arguments hosted by an empty FragmentActivity and waits for it to reach the resumed state.

This method cannot be called from the main thread.

Parameters:

fragmentClass: a fragment class to instantiate
fragmentArgs: a bundle to passed into fragment

public static FragmentScenario<Fragment> launchInContainer(java.lang.Class<Fragment> fragmentClass, Bundle fragmentArgs, FragmentFactory factory)

Launches a Fragment in the Activity's root view container android.R.id.content, with given arguments hosted by an empty FragmentActivity using the given FragmentFactory and waits for it to reach the resumed state.

This method cannot be called from the main thread.

Parameters:

fragmentClass: a fragment class to instantiate
fragmentArgs: a bundle to passed into fragment
factory: a fragment factory to use or null to use default factory

public static FragmentScenario<Fragment> launchInContainer(java.lang.Class<Fragment> fragmentClass, Bundle fragmentArgs, int themeResId, FragmentFactory factory)

Launches a Fragment in the Activity's root view container android.R.id.content, with given arguments hosted by an empty FragmentActivity themed by themeResId, using the given FragmentFactory and waits for it to reach the resumed state.

This method cannot be called from the main thread.

Parameters:

fragmentClass: a fragment class to instantiate
fragmentArgs: a bundle to passed into fragment
themeResId: a style resource id to be set to the host activity's theme
factory: a fragment factory to use or null to use default factory

public FragmentScenario<Fragment> moveToState(Lifecycle.State newState)

Moves Fragment state to a new state.

If a new state and current state are the same, this method does nothing. It accepts CREATED, STARTED, RESUMED, and DESTROYED. DESTROYED is a terminal state. You cannot move to any other state after the Fragment reaches that state.

This method cannot be called from the main thread.

Note: Moving state to STARTED is not supported on Android API level 23 and lower. java.lang.UnsupportedOperationException will be thrown.

public FragmentScenario<Fragment> recreate()

Recreates the host Activity.

After this method call, it is ensured that the Fragment state goes back to the same state as its previous state.

This method cannot be called from the main thread.

public FragmentScenario<Fragment> onFragment(FragmentScenario.FragmentAction<Fragment> action)

Runs a given action on the current Activity's main thread.

Note that you should never keep Fragment reference passed into your action because it can be recreated at anytime during state transitions.

Throwing an exception from action makes the host Activity crash. You can inspect the exception in logcat outputs.

This method cannot be called from the main thread.

Source

/*
 * Copyright (C) 2018 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package androidx.fragment.app.testing;

import static androidx.annotation.RestrictTo.Scope.LIBRARY;
import static androidx.core.util.Preconditions.checkNotNull;
import static androidx.core.util.Preconditions.checkState;
import static androidx.test.core.app.ApplicationProvider.getApplicationContext;

import android.annotation.SuppressLint;
import android.content.ComponentName;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;

import androidx.annotation.IdRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.annotation.StyleRes;
import androidx.core.util.Preconditions;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import androidx.fragment.app.FragmentFactory;
import androidx.fragment.testing.R;
import androidx.lifecycle.Lifecycle.State;
import androidx.lifecycle.ViewModel;
import androidx.lifecycle.ViewModelProvider;
import androidx.test.core.app.ActivityScenario;

/**
 * FragmentScenario provides API to start and drive a Fragment's lifecycle state for testing. It
 * works with arbitrary fragments and works consistently across different versions of the Android
 * framework.
 * <p>
 * FragmentScenario only supports {@link Fragment androidx.fragment.app.Fragment}. If you are using
 * a deprecated fragment class such as {@code android.support.v4.app.Fragment} or
 * {@link android.app.Fragment android.app.Fragment}, please update your code to
 * {@link Fragment androidx.fragment.app.Fragment}.
 * <p>
 * If your testing Fragment has a dependency to specific theme such as {@code Theme.AppCompat},
 * use the theme ID parameter in {@link #launch} method.
 *
 * @param <F> The Fragment class being tested
 *
 * @see ActivityScenario a scenario API for Activity
 */
public final class FragmentScenario<F extends Fragment> {

    private static final String FRAGMENT_TAG = "FragmentScenario_Fragment_Tag";
    @SuppressWarnings("WeakerAccess") /* synthetic access */
    final Class<F> mFragmentClass;
    private final ActivityScenario<EmptyFragmentActivity> mActivityScenario;

    /**
     * An empty activity inheriting FragmentActivity. This Activity is used to host Fragment in
     * FragmentScenario.
     *
     * @hide
     */
    @RestrictTo(LIBRARY)
    public static class EmptyFragmentActivity extends FragmentActivity {

        @NonNull
        public static final String THEME_EXTRAS_BUNDLE_KEY =
                "androidx.fragment.app.testing.FragmentScenario.EmptyFragmentActivity"
                        + ".THEME_EXTRAS_BUNDLE_KEY";

        @Override
        @SuppressLint("RestrictedApi")
        protected void onCreate(@Nullable Bundle savedInstanceState) {
            setTheme(getIntent().getIntExtra(THEME_EXTRAS_BUNDLE_KEY,
                    R.style.FragmentScenarioEmptyFragmentActivityTheme));

            // Checks if we have a custom FragmentFactory and set it.
            FragmentFactory factory = FragmentFactoryHolderViewModel.getInstance(this)
                    .getFragmentFactory();
            if (factory != null) {
                getSupportFragmentManager().setFragmentFactory(factory);
            }

            // FragmentFactory needs to be set before calling the super.onCreate, otherwise the
            // Activity crashes when it is recreating and there is a fragment which has no
            // default constructor.
            super.onCreate(savedInstanceState);
        }
    }

    /**
     * A view-model to hold a fragment factory.
     *
     * @hide
     */
    @RestrictTo(LIBRARY)
    public static class FragmentFactoryHolderViewModel extends ViewModel {

        private static final ViewModelProvider.Factory FACTORY = new ViewModelProvider.Factory() {
            @NonNull
            @Override
            @SuppressWarnings("unchecked")
            public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
                FragmentFactoryHolderViewModel viewModel = new FragmentFactoryHolderViewModel();
                return (T) viewModel;
            }
        };

        @NonNull
        static FragmentFactoryHolderViewModel getInstance(@NonNull FragmentActivity activity) {
            ViewModelProvider viewModelProvider = new ViewModelProvider(activity, FACTORY);
            return viewModelProvider.get(FragmentFactoryHolderViewModel.class);
        }

        @Nullable private FragmentFactory mFragmentFactory;

        void setFragmentFactory(@Nullable FragmentFactory factory) {
            mFragmentFactory = factory;
        }

        @Nullable
        FragmentFactory getFragmentFactory() {
            return mFragmentFactory;
        }

        @Override
        protected void onCleared() {
            super.onCleared();
            mFragmentFactory = null;
        }
    }

    private FragmentScenario(
            @NonNull Class<F> fragmentClass,
            @NonNull ActivityScenario<EmptyFragmentActivity> activityScenario) {
        this.mFragmentClass = fragmentClass;
        this.mActivityScenario = activityScenario;
    }

    /**
     * Launches a Fragment hosted by an empty {@link FragmentActivity} and waits for it to reach
     * the resumed state.
     *
     * @param fragmentClass a fragment class to instantiate
     */
    @NonNull
    public static <F extends Fragment> FragmentScenario<F> launch(
            @NonNull Class<F> fragmentClass) {
        return launch(fragmentClass, /*fragmentArgs=*/ null);
    }

    /**
     * Launches a Fragment with given arguments hosted by an empty {@link FragmentActivity} and
     * waits for it to reach the resumed state.
     * <p>
     * This method cannot be called from the main thread.
     *
     * @param fragmentClass a fragment class to instantiate
     * @param fragmentArgs a bundle to passed into fragment
     */
    @NonNull
    public static <F extends Fragment> FragmentScenario<F> launch(
            @NonNull Class<F> fragmentClass, @Nullable Bundle fragmentArgs) {
        return launch(fragmentClass, fragmentArgs, /*factory=*/null);
    }

    /**
     * Launches a Fragment with given arguments hosted by an empty {@link FragmentActivity} using
     * the given {@link FragmentFactory} and waits for it to reach the resumed state.
     * <p>
     * This method cannot be called from the main thread.
     *
     * @param fragmentClass a fragment class to instantiate
     * @param fragmentArgs a bundle to passed into fragment
     * @param factory a fragment factory to use or null to use default factory
     */
    @NonNull
    public static <F extends Fragment> FragmentScenario<F> launch(
            @NonNull Class<F> fragmentClass, @Nullable Bundle fragmentArgs,
            @Nullable FragmentFactory factory) {
        return launch(fragmentClass, fragmentArgs,
                R.style.FragmentScenarioEmptyFragmentActivityTheme, factory);
    }

    /**
     * Launches a Fragment with given arguments hosted by an empty {@link FragmentActivity} themed
     * by {@code themeResId}, using the given {@link FragmentFactory} and waits for it to reach the
     * resumed state.
     * <p>
     * This method cannot be called from the main thread.
     *
     * @param fragmentClass a fragment class to instantiate
     * @param fragmentArgs a bundle to passed into fragment
     * @param themeResId a style resource id to be set to the host activity's theme
     * @param factory a fragment factory to use or null to use default factory
     */
    @NonNull
    public static <F extends Fragment> FragmentScenario<F> launch(
            @NonNull Class<F> fragmentClass, @Nullable Bundle fragmentArgs,
            @StyleRes int themeResId, @Nullable FragmentFactory factory) {
        return internalLaunch(fragmentClass, fragmentArgs, themeResId, factory,
                /*containerViewId=*/ 0);
    }

    /**
     * Launches a Fragment in the Activity's root view container {@code android.R.id.content},
     * hosted by an empty {@link FragmentActivity} and waits for it to reach the resumed state.
     * <p>
     * This method cannot be called from the main thread.
     *
     * @param fragmentClass a fragment class to instantiate
     */
    @NonNull
    public static <F extends Fragment> FragmentScenario<F> launchInContainer(
            @NonNull Class<F> fragmentClass) {
        return launchInContainer(fragmentClass, /*fragmentArgs=*/ null);
    }

    /**
     * Launches a Fragment in the Activity's root view container {@code android.R.id.content}, with
     * given arguments hosted by an empty {@link FragmentActivity} and waits for it to reach the
     * resumed state.
     * <p>
     * This method cannot be called from the main thread.
     *
     * @param fragmentClass a fragment class to instantiate
     * @param fragmentArgs a bundle to passed into fragment
     */
    @NonNull
    public static <F extends Fragment> FragmentScenario<F> launchInContainer(
            @NonNull Class<F> fragmentClass, @Nullable Bundle fragmentArgs) {
        return launchInContainer(fragmentClass, fragmentArgs, /*factory=*/null);
    }

    /**
     * Launches a Fragment in the Activity's root view container {@code android.R.id.content}, with
     * given arguments hosted by an empty {@link FragmentActivity} using the given
     * {@link FragmentFactory} and waits for it to reach the resumed state.
     * <p>
     * This method cannot be called from the main thread.
     *
     * @param fragmentClass a fragment class to instantiate
     * @param fragmentArgs a bundle to passed into fragment
     * @param factory a fragment factory to use or null to use default factory
     */
    @NonNull
    public static <F extends Fragment> FragmentScenario<F> launchInContainer(
            @NonNull Class<F> fragmentClass, @Nullable Bundle fragmentArgs,
            @Nullable FragmentFactory factory) {
        return launchInContainer(fragmentClass, fragmentArgs,
                R.style.FragmentScenarioEmptyFragmentActivityTheme, factory);
    }

    /**
     * Launches a Fragment in the Activity's root view container {@code android.R.id.content}, with
     * given arguments hosted by an empty {@link FragmentActivity} themed by {@code themeResId},
     * using the given {@link FragmentFactory} and waits for it to reach the resumed state.
     * <p>
     * This method cannot be called from the main thread.
     *
     * @param fragmentClass a fragment class to instantiate
     * @param fragmentArgs a bundle to passed into fragment
     * @param themeResId a style resource id to be set to the host activity's theme
     * @param factory a fragment factory to use or null to use default factory
     */
    @NonNull
    public static <F extends Fragment> FragmentScenario<F> launchInContainer(
            @NonNull Class<F> fragmentClass, @Nullable Bundle fragmentArgs,
            @StyleRes int themeResId, @Nullable FragmentFactory factory) {
        return internalLaunch(
                fragmentClass, fragmentArgs, themeResId, factory, android.R.id.content);
    }

    @NonNull
    @SuppressLint("RestrictedApi")
    private static <F extends Fragment> FragmentScenario<F> internalLaunch(
            @NonNull final Class<F> fragmentClass, final @Nullable Bundle fragmentArgs,
            @StyleRes int themeResId, @Nullable final FragmentFactory factory,
            @IdRes final int containerViewId) {
        Intent startActivityIntent =
                Intent.makeMainActivity(
                        new ComponentName(getApplicationContext(),
                                EmptyFragmentActivity.class))
                        .putExtra(EmptyFragmentActivity.THEME_EXTRAS_BUNDLE_KEY, themeResId);
        FragmentScenario<F> scenario = new FragmentScenario<>(
                fragmentClass,
                ActivityScenario.<EmptyFragmentActivity>launch(startActivityIntent));
        scenario.mActivityScenario.onActivity(
                new ActivityScenario.ActivityAction<EmptyFragmentActivity>() {
                    @Override
                    public void perform(EmptyFragmentActivity activity) {
                        if (factory != null) {
                            FragmentFactoryHolderViewModel.getInstance(activity)
                                    .setFragmentFactory(factory);
                            activity.getSupportFragmentManager().setFragmentFactory(factory);
                        }
                        Fragment fragment = activity.getSupportFragmentManager()
                                .getFragmentFactory().instantiate(
                                        Preconditions.checkNotNull(fragmentClass.getClassLoader()),
                                        fragmentClass.getName());
                        fragment.setArguments(fragmentArgs);
                        activity.getSupportFragmentManager()
                                .beginTransaction()
                                .add(containerViewId, fragment, FRAGMENT_TAG)
                                .commitNow();
                    }
                });
        return scenario;
    }

    /**
     * Moves Fragment state to a new state.
     * <p> If a new state and current state are the same, this method does nothing. It accepts
     * {@link State#CREATED CREATED}, {@link State#STARTED STARTED}, {@link State#RESUMED RESUMED},
     * and {@link State#DESTROYED DESTROYED}. {@link State#DESTROYED DESTROYED} is a terminal state.
     * You cannot move to any other state after the Fragment reaches that state.
     * <p> This method cannot be called from the main thread.
     * <p><em>Note: Moving state to {@link State#STARTED STARTED} is not supported on Android API
     * level 23 and lower. {@link UnsupportedOperationException} will be thrown.</em>
     */
    @NonNull
    public FragmentScenario<F> moveToState(@NonNull State newState) {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N && newState == State.STARTED) {
            throw new UnsupportedOperationException(
                    "Moving state to STARTED is not supported on Android API level 23 and lower."
                    + " This restriction comes from the combination of the Android framework bug"
                    + " around the timing of onSaveInstanceState invocation and its workaround code"
                    + " in FragmentActivity. See http://issuetracker.google.com/65665621#comment3"
                    + " for more information.");
        }
        if (newState == State.DESTROYED) {
            mActivityScenario.onActivity(
                    new ActivityScenario.ActivityAction<EmptyFragmentActivity>() {
                        @Override
                        public void perform(EmptyFragmentActivity activity) {
                            Fragment fragment =
                                    activity.getSupportFragmentManager().findFragmentByTag(
                                            FRAGMENT_TAG);
                            // Null means the fragment has been destroyed already.
                            if (fragment != null) {
                                activity
                                        .getSupportFragmentManager()
                                        .beginTransaction()
                                        .remove(fragment)
                                        .commitNowAllowingStateLoss();
                            }
                        }
                    });
        } else {
            mActivityScenario.onActivity(
                    new ActivityScenario.ActivityAction<EmptyFragmentActivity>() {
                        @Override
                        public void perform(EmptyFragmentActivity activity) {
                            Fragment fragment =
                                    activity.getSupportFragmentManager().findFragmentByTag(
                                            FRAGMENT_TAG);
                            checkNotNull(fragment,
                                    "The fragment has been removed from FragmentManager already.");
                        }
                    });
            mActivityScenario.moveToState(newState);
        }
        return this;
    }

    /**
     * Recreates the host Activity.
     * <p>
     * After this method call, it is ensured that the Fragment state goes back to the same state
     * as its previous state.
     * <p>
     * This method cannot be called from the main thread.
     */
    @NonNull
    public FragmentScenario<F> recreate() {
        mActivityScenario.recreate();
        return this;
    }

    /**
     * FragmentAction interface should be implemented by any class whose instances are intended to
     * be executed by the main thread. A Fragment that is instrumented by the FragmentScenario is
     * passed to {@link FragmentAction#perform} method.
     * <p>
     * You should never keep the Fragment reference as it will lead to unpredictable behaviour.
     * It should only be accessed in {@link FragmentAction#perform} scope.
     */
    public interface FragmentAction<F extends Fragment> {
        /**
         * This method is invoked on the main thread with the reference to the Fragment.
         *
         * @param fragment a Fragment instrumented by the FragmentScenario.
         */
        void perform(@NonNull F fragment);
    }

    /**
     * Runs a given {@code action} on the current Activity's main thread.
     * <p>
     * Note that you should never keep Fragment reference passed into your {@code action}
     * because it can be recreated at anytime during state transitions.
     * <p>
     * Throwing an exception from {@code action} makes the host Activity crash. You can
     * inspect the exception in logcat outputs.
     * <p>
     * This method cannot be called from the main thread.
     */
    @NonNull
    public FragmentScenario<F> onFragment(@NonNull final FragmentAction<F> action) {
        mActivityScenario.onActivity(
                new ActivityScenario.ActivityAction<EmptyFragmentActivity>() {
                    @Override
                    public void perform(EmptyFragmentActivity activity) {
                        Fragment fragment = activity.getSupportFragmentManager().findFragmentByTag(
                                FRAGMENT_TAG);
                        checkNotNull(fragment,
                                "The fragment has been removed from FragmentManager already.");
                        checkState(mFragmentClass.isInstance(fragment));
                        action.perform(Preconditions.checkNotNull(mFragmentClass.cast(fragment)));
                    }
                });
        return this;
    }
}