Как я могу поддерживать состояние фрагмента при добавлении в задний стек?

Я написал фиктивную активность, которая переключается между двумя фрагментами. Когда вы переходите из FragmentA в FragmentB, FragmentA добавляется в задний стек. Однако, когда я возвращаюсь к FragmentA (при нажатии назад), создается совершенно новый FragmentA, и состояние, в котором оно было, теряется. У меня такое чувство, что я следую тому же вопросу, что и этот, но я включил полный образец кода, чтобы помочь устранить проблему:

public class FooActivity extends Activity {
  @Override public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    final FragmentTransaction transaction = getFragmentManager().beginTransaction();
    transaction.replace(android.R.id.content, new FragmentA());
    transaction.commit();
  }

  public void nextFragment() {
    final FragmentTransaction transaction = getFragmentManager().beginTransaction();
    transaction.replace(android.R.id.content, new FragmentB());
    transaction.addToBackStack(null);
    transaction.commit();
  }

  public static class FragmentA extends Fragment {
    @Override public View onCreateView(LayoutInflater inflater, ViewGroup container,
        Bundle savedInstanceState) {
      final View main = inflater.inflate(R.layout.main, container, false);
      main.findViewById(R.id.next_fragment_button).setOnClickListener(new View.OnClickListener() {
        public void onClick(View v) {
          ((FooActivity) getActivity()).nextFragment();
        }
      });
      return main;
    }

    @Override public void onSaveInstanceState(Bundle outState) {
      super.onSaveInstanceState(outState);
      // Save some state!
    }
  }

  public static class FragmentB extends Fragment {
    @Override public View onCreateView(LayoutInflater inflater, ViewGroup container,
        Bundle savedInstanceState) {
      return inflater.inflate(R.layout.b, container, false);
    }
  }
}

При добавлении некоторых сообщений журнала:

07-05 14:28:59.722 D/OMG     ( 1260): FooActivity.onCreate
07-05 14:28:59.742 D/OMG     ( 1260): FragmentA.onCreateView
07-05 14:28:59.742 D/OMG     ( 1260): FooActivity.onResume
<Tap Button on FragmentA>
07-05 14:29:12.842 D/OMG     ( 1260): FooActivity.nextFragment
07-05 14:29:12.852 D/OMG     ( 1260): FragmentB.onCreateView
<Tap 'Back'>
07-05 14:29:16.792 D/OMG     ( 1260): FragmentA.onCreateView

Он никогда не вызывает FragmentA.onSaveInstanceState и создает новый FragmentA при ударе. Однако, если я нахожусь на FragmentA, и я блокирую экран, FragmentA.onSaveInstanceState вызывает вызов. Настолько странно... Я ошибаюсь в ожидании фрагмента, добавленного в задний стек, чтобы не требовалось повторного создания? Здесь docs говорят:

В то время как если вы вызываете addToBackStack() при удалении фрагмента, то фрагмент останавливается и будет возобновлен, если пользователь выполнит навигацию назад.

Ответ 1

Если вы вернетесь к фрагменту из заднего стека, он не воссоздает фрагмент, а повторно использует тот же экземпляр и начинает с onCreateView() в жизненном цикле фрагмента, см. Жизненный цикл фрагментов.

Итак, если вы хотите сохранить состояние, вы должны использовать переменные экземпляра и не полагаться на onSaveInstanceState().

Ответ 2

Сравнение с Apple UINavigationController и UIViewController, Google не очень хорошо работает в архитектуре программного обеспечения Android. И документ Android о Fragment не очень помогает.

Когда вы вводите FragmentB из FragmentA, существующий экземпляр FragmentA не будет уничтожен. Когда вы нажимаете Назад в FragmentB и возвращаетесь к FragmentA, мы не создаем новый экземпляр FragmentA. Будет вызываться существующий экземпляр FragmentA onCreateView().

Главное, мы не должны снова раздувать представление в FragmentA onCreateView(), потому что мы используем существующий экземпляр FragmentA. Нам нужно сохранить и повторно использовать rootView.

Следующий код работает хорошо. Он не только сохраняет состояние фрагмента, но также уменьшает нагрузку на ОЗУ и ЦП (потому что мы только раздуваем макет, если это необходимо). Я не могу поверить, что пример кода Google и документ никогда не упоминаются, но всегда раздувают макет.

Версия 1 (не используйте версию 1. Используйте версию 2)

public class FragmentA extends Fragment {
    View _rootView;
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
            Bundle savedInstanceState) {
        if (_rootView == null) {
            // Inflate the layout for this fragment
            _rootView = inflater.inflate(R.layout.fragment_a, container, false);
            // Find and setup subviews
            _listView = (ListView)_rootView.findViewById(R.id.listView);
            ...
        } else {
            // Do not inflate the layout again.
            // The returned View of onCreateView will be added into the fragment.
            // However it is not allowed to be added twice even if the parent is same.
            // So we must remove _rootView from the existing parent view group
            // (it will be added back).
            ((ViewGroup)_rootView.getParent()).removeView(_rootView);
        }
        return _rootView;
    }
}

------ Обновление от 3 мая 2005 года: -------

Как упоминалось в комментариях, иногда _rootView.getParent() имеет значение null в onCreateView, что приводит к сбою. Версия 2 удаляет _rootView в onDestroyView(), как предположил dell116. Протестировано на Android 4.0.3, 4.4.4, 5.1.0.

Версия 2

public class FragmentA extends Fragment {
    View _rootView;
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
            Bundle savedInstanceState) {
        if (_rootView == null) {
            // Inflate the layout for this fragment
            _rootView = inflater.inflate(R.layout.fragment_a, container, false);
            // Find and setup subviews
            _listView = (ListView)_rootView.findViewById(R.id.listView);
            ...
        } else {
            // Do not inflate the layout again.
            // The returned View of onCreateView will be added into the fragment.
            // However it is not allowed to be added twice even if the parent is same.
            // So we must remove _rootView from the existing parent view group
            // in onDestroyView() (it will be added back).
        }
        return _rootView;
    }

    @Override
    public void onDestroyView() {
        if (_rootView.getParent() != null) {
            ((ViewGroup)_rootView.getParent()).removeView(_rootView);
        }
        super.onDestroyView();
    }
}

ВНИМАНИЕ!!!

Это ХЭЙ! Хотя я использую его в своем приложении, вам нужно внимательно протестировать и прочитать комментарии.

Ответ 3

Я думаю, что есть альтернативный способ достичь того, что вы ищете. Я не говорю, что это полное решение, но оно послужило цели в моем случае.

То, что я сделал, вместо замены фрагмента, я только что добавил целевой фрагмент. Таким образом, в основном вы будете использовать метод add() вместо replace().

Что еще я сделал. Я скрываю свой текущий фрагмент и также добавляю его в backstack.

Следовательно, он перекрывает новый фрагмент поверх текущего фрагмента, не разрушая его вид (проверьте, что его метод onDestroyView() не вызывается. Плюс добавление его в backstate дает мне преимущество возобновления фрагмента.

Вот код:

Fragment fragment=new DestinationFragment();
FragmentManager fragmentManager = getFragmentManager();
android.app.FragmentTransaction ft=fragmentManager.beginTransaction();
ft.add(R.id.content_frame, fragment);
ft.hide(SourceFragment.this);
ft.addToBackStack(SourceFragment.class.getName());
ft.commit();

Система AFAIK вызывает onCreateView(), только если представление уничтожено или не создано. Но здесь мы сохранили вид, не удаляя его из памяти. Так что это не создаст новый вид.

И когда вы вернетесь из целевого фрагмента, он вытолкнет последний FragmentTransaction, удалив верхний фрагмент, который заставит самый верхний вид (SourceFragment) появиться на экране.

КОММЕНТАРИЙ: Как я уже сказал, это не полное решение, поскольку оно не удаляет вид фрагмента источника и, следовательно, занимает больше памяти, чем обычно. Но все же, служить цели. Кроме того, мы используем совершенно другой механизм сокрытия вида вместо его замены, что не является традиционным.

Так что дело не в том, как вы поддерживаете состояние, а в том, как вы поддерживаете представление.

Ответ 4

Я столкнулся с этой проблемой во Фрагменте, содержащем карту, в которой слишком много настроек для сохранения/перезагрузки. Мое решение состояло в том, чтобы в целом поддерживать этот Фрагмент активным все время (похоже на то, что упомянуто @kaushal).

Скажите, что у вас есть текущий фрагмент A и вы хотите отобразить фрагмент B. Обобщая последствия:

  • replace() - удалить фрагмент A и заменить его фрагментом B. Фрагмент A будет воссоздан после того, как он снова появится на передней панели.
  • add() - (create and) добавить фрагмент B и перекрывать фрагмент A, который по-прежнему активен в фоновом режиме
  • remove() - может использоваться для удаления фрагмента B и возврата в A. Фрагмент B будет воссоздан при вызове позже

Следовательно, если вы хотите сохранить оба сохраненных фрагмента, просто переключите их с помощью функции hide()/show().

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

Ответ 5

onSaveInstanceState() вызывается только при изменении конфигурации.

Поскольку изменение одного фрагмента на другой изменяет конфигурацию, поэтому вызов onSaveInstanceState() не существует. Какое состояние не спасает? Можете ли вы указать?

Если вы введете какой-либо текст в EditText, он будет сохранен автоматически. Любой элемент пользовательского интерфейса без идентификатора - это элемент, состояние которого не сохраняется.

Ответ 6

Я бы предложил очень простое решение.

Возьмите ссылочную переменную View и установите представление в OnCreateView. Проверьте, существует ли представление в этой переменной, а затем верните то же представление.

   private View fragmentView;

   public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        super.onCreateView(inflater, container, savedInstanceState);

        if (fragmentView != null) {
            return fragmentView;
        }
        View view = inflater.inflate(R.layout.yourfragment, container, false);
        fragmentView = view;
        return view;
    }

Ответ 7

Здесь, поскольку onSaveInstanceState в фрагменте не вызывается, когда вы добавляете фрагмент в backstack. Жизненный цикл фрагмента в backstack при восстановлении start onCreateView и end onDestroyView, а onSaveInstanceState вызывается между onDestroyView и onDestroy. Мое решение - создать переменную экземпляра экземпляра и init в onCreate. Пример кода:

private boolean isDataLoading = true;
private ArrayList<String> listData;
public void onCreate(Bundle savedInstanceState){
     super.onCreate(savedInstanceState);
     isDataLoading = false;
     // init list at once when create fragment
     listData = new ArrayList();
}

И проверьте его в onActivityCreated:

public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
    super.onViewCreated(view, savedInstanceState);
    if(isDataLoading){
         fetchData();
    }else{
         //get saved instance variable listData()
    }
}

private void fetchData(){
     // do fetch data into listData
}

Ответ 8

getSupportFragmentManager().addOnBackStackChangedListener(new FragmentManager.OnBackStackChangedListener()
    {
        @Override
        public void onBackStackChanged()
        {
            if (getSupportFragmentManager().getBackStackEntryCount() == 0)
            {
                //setToolbarTitle("Main Activity");
            }
            else
            {
                Log.e("fragment_replace11111", "replace");
            }
        }
    });


YourActivity.java
@Override
public void onBackPressed()
{
 Fragment fragment = getSupportFragmentManager().findFragmentById(R.id.Fragment_content);
  if (fragment instanceof YourFragmentName)
    {
        fragmentReplace(new HomeFragment(),"Home Fragment");
        txt_toolbar_title.setText("Your Fragment");
    }
  else{
     super.onBackPressed();
   }
 }


public void fragmentReplace(Fragment fragment, String fragment_name)
{
    try
    {
        fragmentTransaction = fragmentManager.beginTransaction();
        fragmentTransaction.replace(R.id.Fragment_content, fragment, fragment_name);
        fragmentTransaction.setCustomAnimations(R.anim.enter_from_right, R.anim.exit_to_left, R.anim.enter_from_left, R.anim.exit_to_right);
        fragmentTransaction.addToBackStack(fragment_name);
        fragmentTransaction.commitAllowingStateLoss();
    }
    catch (Exception e)
    {
        e.printStackTrace();
    }
}

Ответ 9

Моя проблема была похожа, но я победил меня, не сохранив фрагмент. Предположим, что у вас есть активность, в которой есть 2 фрагмента - F1 и F2. F1 запускается изначально и позволяет говорить, что содержит некоторую информацию о пользователе, а затем при некотором условии F2 появляется, запрашивая у пользователя дополнительный атрибут - их номер телефона. Затем вы хотите, чтобы этот номер телефона возвращался в F1 и завершал регистрацию, но вы понимаете, что все предыдущие данные пользователя потеряны, и у вас нет их предыдущих данных. Фрагмент воссоздается с нуля, и даже если вы сохранили эту информацию в onSaveInstanceState, пакет возвращается обратно в onActivityCreated.

Решение: Сохраните требуемую информацию в качестве переменной экземпляра в активности вызова. Затем передайте эту переменную экземпляра в ваш фрагмент.

@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
    super.onActivityCreated(savedInstanceState);

    Bundle args = getArguments();

    // this will be null the first time F1 is created. 
    // it will be populated once you replace fragment and provide bundle data
    if (args != null) {
        if (args.get("your_info") != null) {
            // do what you want with restored information
        }
    }
}

Итак, следуя моему примеру: перед отображением F2 я сохраняю данные пользователя в переменной экземпляра с помощью обратного вызова. Затем я запускаю F2, пользователь заполняет номер телефона и нажимает save. Я использую другой обратный вызов в действии, собираю эту информацию и заменяю свой фрагмент F1, на этот раз он имеет связанные данные, которые я могу использовать.

@Override
public void onPhoneAdded(String phone) {
        //replace fragment
        F1 f1 = new F1 ();
        Bundle args = new Bundle();
        yourInfo.setPhone(phone);
        args.putSerializable("you_info", yourInfo);
        f1.setArguments(args);

        getFragmentManager().beginTransaction()
                .replace(R.id.fragmentContainer, f1).addToBackStack(null).commit();

    }
}

Более подробную информацию о вызовах можно найти здесь: https://developer.android.com/training/basics/fragments/communicating.html

Ответ 10

first: просто используйте метод add вместо метода замены класса FragmentTransaction, тогда вам нужно добавить secondFragment в стек методом addToBackStack

второй: при обратном нажатии вы должны вызвать popBackStackImmediate()

Fragment sourceFragment = new SourceFragment ();
final Fragment secondFragment = new SecondFragment();
final FragmentTransaction ft = getChildFragmentManager().beginTransaction();
ft.add(R.id.child_fragment_container, secondFragment );
ft.hide(sourceFragment );
ft.addToBackStack(NewsShow.class.getName());
ft.commit();

((SecondFragment)secondFragment).backFragmentInstanceClick = new SecondFragment.backFragmentNewsResult()
{
        @Override
        public void backFragmentNewsResult()
        {                                    
            getChildFragmentManager().popBackStackImmediate();                                
        }
};

Ответ 11

Замените фрагмент, используя следующий код:

Fragment fragment = new AddPaymentFragment();
getSupportFragmentManager().beginTransaction().replace(R.id.frame, fragment, "Tag_AddPayment")
                .addToBackStack("Tag_AddPayment")
                .commit();

Деятельность onBackPressed():

  @Override
public void onBackPressed() {
    android.support.v4.app.FragmentManager fm = getSupportFragmentManager();
    if (fm.getBackStackEntryCount() > 1) {

        fm.popBackStack();
    } else {


        finish();

    }
    Log.e("popping BACKSTRACK===> ",""+fm.getBackStackEntryCount());

}

Ответ 12

Public void replaceFragment(Fragment mFragment, int id, String tag, boolean addToStack) {
        FragmentTransaction mTransaction = getSupportFragmentManager().beginTransaction();
        mTransaction.replace(id, mFragment);
        hideKeyboard();
        if (addToStack) {
            mTransaction.addToBackStack(tag);
        }
        mTransaction.commitAllowingStateLoss();
    }
replaceFragment(new Splash_Fragment(), R.id.container, null, false);

Ответ 13

Идеальное решение, чтобы найти старый фрагмент в стеке и загрузить его, если он существует в стеке.

/**
     * replace or add fragment to the container
     *
     * @param fragment pass android.support.v4.app.Fragment
     * @param bundle pass your extra bundle if any
     * @param popBackStack if true it will clear back stack
     * @param findInStack if true it will load old fragment if found
     */
    public void replaceFragment(Fragment fragment, @Nullable Bundle bundle, boolean popBackStack, boolean findInStack) {
        FragmentManager fm = getSupportFragmentManager();
        FragmentTransaction ft = fm.beginTransaction();
        String tag = fragment.getClass().getName();
        Fragment parentFragment;
        if (findInStack && fm.findFragmentByTag(tag) != null) {
            parentFragment = fm.findFragmentByTag(tag);
        } else {
            parentFragment = fragment;
        }
        // if user passes the @bundle in not null, then can be added to the fragment
        if (bundle != null)
            parentFragment.setArguments(bundle);
        else parentFragment.setArguments(null);
        // this is for the very first fragment not to be added into the back stack.
        if (popBackStack) {
            fm.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE);
        } else {
            ft.addToBackStack(parentFragment.getClass().getName() + "");
        }
        ft.replace(R.id.contenedor_principal, parentFragment, tag);
        ft.commit();
        fm.executePendingTransactions();
    }

используйте его как

Fragment f = new YourFragment();
replaceFragment(f, null, boolean true, true);