Ссылка фрагмента на mActivity становится пустой после изменения ориентации. Неэффективное поддержание состояния фрагмента

Мое приложение состоит из нескольких фрагментов. До сих пор у меня были ссылки на них, хранящиеся в настраиваемом объекте Application, но я начинаю думать, что делаю что-то не так.

Мои проблемы начались, когда я понял, что все ссылки моего фрагмента на mActivity становятся нулевыми после изменения ориентации. Поэтому, когда я вызываю getActivity () после изменения ориентации, возникает исключение NullPointerException. Я проверил, что мой фрагмент onAttach () вызывается, прежде чем я вызываю getActivity (), но он по-прежнему возвращает значение null.

Ниже представлена ​​урезанная версия моей MainActivity, которая является единственным действием в моем приложении.

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();
        }
    }
}

Ниже представлена ​​урезанная версия CustomApplication. Мои мысли, стоящие за этой реализацией, заключались в том, чтобы гарантировать только один экземпляр каждого фрагмента на протяжении жизненного цикла моего приложения.

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;
    }
}

Мне очень интересны советы о том, как лучше всего реализовать несколько фрагментов и как поддерживать их состояния. К вашему сведению, мое приложение пока состоит из 15+ фрагментов. Я провел некоторое исследование, и мне кажется, что FragmentManager.findFragmentByTag () - хорошая ставка, но мне не удалось его успешно реализовать.

Моя реализация, похоже, работает хорошо, за исключением того факта, что ссылки mActivity становятся нулевыми после изменения ориентации, что позволяет мне полагать, что у меня также могут быть проблемы с утечкой памяти.

Если вам нужно увидеть больше кода, дайте мне знать. Я намеренно избегал включения фрагмента кода, поскольку я твердо уверен, что проблемы связаны с моими реализациями Activity и Application, но могу ошибаться.

Спасибо за ваше время.


person Morten Salte    schedule 01.04.2013    source источник


Ответы (6)


Мои мысли, стоящие за этой реализацией, заключались в том, чтобы гарантировать только один экземпляр каждого фрагмента на протяжении жизненного цикла моего приложения.

Вероятно, это часть, если не все, источника ваших затруднений.

При изменении конфигурации Android воссоздает ваши фрагменты, используя общедоступный конструктор без аргументов для создания нового экземпляра. Следовательно, ваши фрагменты с глобальной областью видимости не будут «гарантировать только один экземпляр каждого фрагмента».

Удалите этот пользовательский Application класс. Пожалуйста, позвольте фрагментам воссоздаться естественным образом, или, если они должны жить в течение жизни одного действия, используйте setRetainInstance(true). Не пытайтесь повторно использовать фрагменты в разных действиях.

person CommonsWare    schedule 01.04.2013
comment
Спасибо за ваш быстрый ответ! Я внесу необходимые изменения в соответствии с этим :) Всего один быстрый вопрос: когда мое приложение будет уничтожено Android для ресурсных целей, как я могу гарантировать, что когда пользователь повторно запускает мое приложение, последний фрагмент, который он посетил (до того, как приложение было уничтожено) отображается вместо стартового фрагмента по умолчанию? Мне также нужно, чтобы в моем фрагменте меню была выбрана правильная позиция, когда это происходит. На ум приходят предпочтения, но может быть, у вас есть идея получше? - person Morten Salte; 01.04.2013
comment
@MortenSalte: как я могу гарантировать, что, когда пользователь повторно запускает мое приложение, отображается последний фрагмент, который он посетил (до того, как приложение было уничтожено) - в onPause() или onStop(), запишите информацию о состоянии (SharedPreferences, файл, база данных), которая будет сохраняться после завершения вашего процесса. На ум приходят предпочтения, но может быть, у вас есть идея получше? - предпочтения были бы типичным решением для чего-то вроде этого, но любое постоянное хранилище должно работать. - person CommonsWare; 01.04.2013

Я не понимаю, где вы используете ссылку на mActivity. Но не ссылайтесь на это. Всегда используйте getActivity, так как Activity можно воссоздать после изменения ориентации. Кроме того, никогда не устанавливайте поля фрагмента сеттерами или назначением всегда используйте Bundle и Arguments.

Лучшая практика создания экземпляра нового фрагмента Android

Также вы можете использовать setRetainInstance (true), чтобы сохранить все члены фрагмента во время изменения ориентации.

Общие сведения о setRetainInstance фрагмента (логическое значение)

person Yaroslav Mytkalyk    schedule 01.04.2013

Чтобы решить эту проблему, вы должны использовать объект активности, предоставленный методом фрагмента onAttach, поэтому при изменении ориентации фрагмент воссоздается, поэтому onAttach предоставит вам текущую ссылку

person Kiran Choudhary    schedule 14.05.2013

вы можете использовать onAttach(Context context) для создания частной переменной контекста во фрагменте, подобном этому

 @Override
public void onAttach(Context context) {
    this.context = context;
    super.onAttach(context);
}

при изменении ориентации onAttach дает вам новую ссылку на контекст, если вы хотите ссылку на действие, вы можете привести контекст к действию.

person Rishabh Khanna    schedule 30.08.2017

Контекст также может быть переназначен внутри onCreate по фрагментам, поскольку OnCreate вызывается при повороте устройства.

private Context mContext; @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); //get new activity reference here mContext = getActivity(); }

передать этот mContext по всему фрагменту

person coderBox    schedule 13.10.2018

Если вы не setRetainInstance(true) в onCreate ... коллекция, например List<Object>, Vector<Object> в классе Application, получит значение null. Убедитесь, что вы setRetainInstance(true) оживили их.

person user2705819    schedule 29.08.2013