Проблемы с обратным стеком Android Fragment

121

У меня огромная проблема с тем, как работает backstack фрагментов Android, и я буду очень благодарен за любую предложенную помощь.

Представьте, что у вас есть 3 фрагмента

[1] [2] [3]

Я хочу, чтобы пользователь мог перемещаться, [1] > [2] > [3]но на обратном пути (нажатие кнопки возврата) [3] > [1].

Как я и предполагал, это можно сделать, если не вызывать addToBackStack(..)при создании транзакции, которая переносит фрагмент [2]в держатель фрагмента, определенный в XML.

На самом деле, кажется, что если я не хочу [2]появляться снова, когда пользователь нажимает кнопку «Назад» [3], я не должен вызывать addToBackStackтранзакцию, которая показывает фрагмент [3]. Это кажется совершенно нелогичным (возможно, из мира iOS).

В любом случае, если я сделаю это таким образом, когда я выйду [1] > [2]и вернусь назад, я вернусь, [1]как и ожидалось.

Если я пойду [1] > [2] > [3]и нажму «назад», я вернусь к [1](как и ожидалось). Теперь странное поведение происходит, когда я пытаюсь [2]снова перейти из [1]. Прежде всего [3]кратко отображается, прежде чем [2]попадет в поле зрения. Если я вернусь в этот момент [3], отображается, и если я снова вернусь, приложение закроется.

Может ли кто-нибудь помочь мне понять, что здесь происходит?


А вот XML-файл макета для моей основной деятельности:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
          android:layout_width="fill_parent"
          android:layout_height="fill_parent"
          android:orientation="vertical" >

<fragment
        android:id="@+id/headerFragment"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        class="com.fragment_test.FragmentControls" >
    <!-- Preview: layout=@layout/details -->
</fragment>
<FrameLayout
        android:id="@+id/detailFragment"
        android:layout_width="match_parent"
        android:layout_height="fill_parent"

        />



Обновление. Это код, который я использую для сборки по иерархии навигации.

    Fragment frag;
    FragmentTransaction transaction;


    //Create The first fragment [1], add it to the view, BUT Dont add the transaction to the backstack
    frag = new Fragment1();

    transaction = getSupportFragmentManager().beginTransaction();
    transaction.replace(R.id.detailFragment, frag);
    transaction.commit();

    //Create the second [2] fragment, add it to the view and add the transaction that replaces the first fragment to the backstack
    frag = new Fragment2();

    transaction = getSupportFragmentManager().beginTransaction();
    transaction.replace(R.id.detailFragment, frag);
    transaction.addToBackStack(null);
    transaction.commit();


    //Create third fragment, Dont add this transaction to the backstack, because we dont want to go back to [2] 
    frag = new Fragment3();
    transaction = getSupportFragmentManager().beginTransaction();
    transaction.replace(R.id.detailFragment, frag);
    transaction.commit();


     //END OF SETUP CODE-------------------------
    //NOW:
    //Press back once and then issue the following code:
    frag = new Fragment2();
    transaction = getSupportFragmentManager().beginTransaction();
    transaction.replace(R.id.detailFragment, frag);
    transaction.addToBackStack(null);
    transaction.commit();

    //Now press back again and you end up at fragment [3] not [1]

Большое спасибо

Крис Берч
источник
но фрагмент перекрывается, когда я нажимаю обратно с фрагмента C на фрагмент A.
Приянка
У меня такая же проблема, как ее исправить?
Приянка
обратитесь к моему ответу ... это может вам помочь < stackoverflow.com/questions/14971780/… >
MD Khali

Ответы:

203

Пояснение: что здесь происходит?

Если иметь в виду, что .replace()это равно тому, .remove().add()что мы знаем из документации:

Заменить существующий фрагмент, добавленный в контейнер. По сути, это то же самое, что и вызов remove(Fragment)всех добавленных в данный момент фрагментов, которые были добавлены с теми же, containerViewIdа затем add(int, Fragment, String)с теми же аргументами, приведенными здесь.

тогда что происходит примерно так (я добавляю числа к фрагменту, чтобы было понятнее):

// transaction.replace(R.id.detailFragment, frag1);
Transaction.remove(null).add(frag1)  // frag1 on view

// transaction.replace(R.id.detailFragment, frag2).addToBackStack(null);
Transaction.remove(frag1).add(frag2).addToBackStack(null)  // frag2 on view

// transaction.replace(R.id.detailFragment, frag3);
Transaction.remove(frag2).add(frag3)  // frag3 on view

(здесь начинают происходить все вводящие в заблуждение)

Помните, что .addToBackStack()сохраняется только транзакция, а не сам фрагмент ! Итак, теперь у нас есть frag3макет:

< press back button >
// System pops the back stack and find the following saved back entry to be reversed:
// [Transaction.remove(frag1).add(frag2)]
// so the system makes that transaction backward!!!
// tries to remove frag2 (is not there, so it ignores) and re-add(frag1)
// make notice that system doesn't realise that there's a frag3 and does nothing with it
// so it still there attached to view
Transaction.remove(null).add(frag1) //frag1, frag3 on view (OVERLAPPING)

// transaction.replace(R.id.detailFragment, frag2).addToBackStack(null);
Transaction.remove(frag3).add(frag2).addToBackStack(null)  //frag2 on view

< press back button >
// system makes saved transaction backward
Transaction.remove(frag2).add(frag3) //frag3 on view

< press back button >
// no more entries in BackStack
< app exits >

Возможное решение

Подумайте о реализации, FragmentManager.BackStackChangedListenerчтобы отслеживать изменения в заднем стеке и применять свою логику в onBackStackChanged()методе:

Arvis
источник
Хорошее объяснение @arvis. Однако как мы можем предотвратить такое поведение, не прибегая к хакерским приемам, таким как метод DexterMoon или способ от Nemanja, использующий popBackStack, который показывает фрагмент во время воспроизведения анимации перехода?
momo
@momo, который вы можете реализовать, FragmentManager.BackStackChangedListenerчтобы следить за изменениями в заднем стеке. Следите за всеми своими транзакциями с помощью onBackStackChanged()метода и действуйте по мере необходимости: например. отслеживать количество транзакций в BackStack; проверить конкретную транзакцию по имени ( FragmentTransaction addToBackStack (String name)) и т. д.
Арвис
Спасибо за ответы. Я действительно пробовал это сегодня, зарегистрировал слушателя и удалил фрагмент из onBackstackChange. Пока воспроизводится переход, фрагмент становится белой пустой областью. Я предполагаю, что метод срабатывает, когда всплывающее окно запускает анимацию, а не когда заканчивается ...
momo
4
Избегайте использования задних стеков! это не очень помогает с общей эффективностью! используйте простой replace () или, что еще лучше, удаляйте / добавляйте каждый раз, когда хотите перемещаться!
stack_ved
@Arvis может ли вы помочь мне получить ту же проблему .... количество стека 0, но все же мой фрагмент виден?
Erum
33

Правильно!!! после долгого выдергивания волос я наконец-то понял, как заставить это работать должным образом.

Кажется, что фрагмент [3] не удаляется из представления при нажатии back, так что вам придется делать это вручную!

Прежде всего, не используйте replace (), а вместо этого используйте отдельно удалить и добавить. Кажется, что replace () не работает должным образом.

Следующая часть этого - переопределение метода onKeyDown и удаление текущего фрагмента при каждом нажатии кнопки возврата.

@Override
public boolean onKeyDown(int keyCode, KeyEvent event)
{
    if (keyCode == KeyEvent.KEYCODE_BACK)
    {
        if (getSupportFragmentManager().getBackStackEntryCount() == 0)
        {
            this.finish();
            return false;
        }
        else
        {
            getSupportFragmentManager().popBackStack();
            removeCurrentFragment();

            return false;
        }



    }

    return super.onKeyDown(keyCode, event);
}


public void removeCurrentFragment()
{
    FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();

    Fragment currentFrag =  getSupportFragmentManager().findFragmentById(R.id.detailFragment);


    String fragName = "NONE";

    if (currentFrag!=null)
        fragName = currentFrag.getClass().getSimpleName();


    if (currentFrag != null)
        transaction.remove(currentFrag);

    transaction.commit();

}

Надеюсь это поможет!

Крис Берч
источник
это работает, но это не может быть использовано в моем проекте, так как фрагмент перезагружается! какие-либо предложения?
TharakaNirmana
Но, если я это делаю. У меня пустой экран, когда я возвращаюсь от C к A для фрагмента C.
Нигам Патро
Я подозреваю, что ответ @Arvis должен пролить свет на вашу проблему
Крис Берч,
16

Прежде всего, спасибо @Arvis за объяснение, открывающее глаза.

Я предпочитаю другое решение принятого здесь ответа для этой проблемы. Мне не нравится возиться с переопределением обратного поведения больше, чем это абсолютно необходимо, и когда я пытался добавлять и удалять фрагменты самостоятельно, без всплытия заднего стека по умолчанию при нажатии кнопки возврата, я обнаружил себя в аду фрагментов :) Если вы. добавьте f2 вместо f1 при его удалении. f1 не будет вызывать какие-либо методы обратного вызова, такие как onResume, onStart и т. д., и это может быть очень неудачным.

Во всяком случае, вот как я это делаю:

В настоящее время на виду только фрагмент f1.

f1 -> f2

Fragment2 f2 = new Fragment2();
this.getActivity().getSupportFragmentManager().beginTransaction().replace(R.id.main_content,f2).addToBackStack(null).commit();

здесь нет ничего необычного. Затем во фрагменте f2 этот код переносит вас на фрагмент f3.

f2 -> f3

Fragment3 f3 = new Fragment3();
getActivity().getSupportFragmentManager().popBackStack();
getActivity().getSupportFragmentManager().beginTransaction().replace(R.id.main_content, f3).addToBackStack(null).commit();

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

Указанная альтернатива будет:

final FragmentActivity activity = getActivity();
activity.getSupportFragmentManager().popBackStackImmediate();
activity.getSupportFragmentManager().beginTransaction().replace(R.id.main_content, f3).addToBackStack(null).commit();

Здесь на самом деле будет краткое возвращение к f1, прежде чем переходить к f3, так что небольшой сбой.

На самом деле это все, что вам нужно сделать, нет необходимости переопределять поведение обратного стека ...

Неманья Ковачевич
источник
6
но глюк, это проблема
Zyoo
Это кажется менее «хакерским», чем прислушиваться к изменениям BackStack. Спасибо.
ahaisting
13

Я знаю, что это старый вопрос, но у меня та же проблема, и я исправляю ее следующим образом:

Сначала добавьте фрагмент 1 в BackStack с именем (например, «Frag1»):

frag = new Fragment1();

transaction = getSupportFragmentManager().beginTransaction();
transaction.replace(R.id.detailFragment, frag);
transaction.addToBackStack("Frag1");
transaction.commit();

А затем, когда вы хотите вернуться к Fragment1 (даже после добавления 10 фрагментов над ним), просто вызовите popBackStackImmediate с именем:

getSupportFragmentManager().popBackStackImmediate("Frag1", 0);

Надеюсь, это кому-то поможет :)

Shirane85
источник
1
Отличный ответ. Мне пришлось использовать getSupportFragmentManager (). PopBackStackImmediate ("Frag1", FragmentManager.POP_BACK_STACK_INCLUSIVE); чтобы заставить его работать для моего
варианта
1
Куда мне поместить этот код ???? getSupportFragmentManager (). popBackStackImmediate ("Frag1", 0); На MainActivty или onBackPress
павел
это не работает. :( это мой код, в моем случае фрагмент перекрывается. он открывает фрагмент A, но фрагмент A перекрывается с фрагментом B.
Приянка
FragmentManager fragmentManager = getActivity (). GetSupportFragmentManager (); fragmentManager.popBackStack (FragmentA.class.getName (), FragmentManager.POP_BACK_STACK_INCLUSIVE); FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction (); FragmentE fragmentE = новый FragmentE (); fragmentTransaction.replace (R.id.fragment_content, fragmentE, fragmentE.getClass (). getName ()); fragmentTransaction.commit ();
Приянка
5

После ответа @Arvis я решил копнуть еще глубже и написал об этом техническую статью: http://www.andreabaccega.com/blog/2015/08/16/how-to-avoid-fragments-overlapping- из - за к backstack-кошмар-в-андроид /

Для ленивых разработчиков. Мое решение состоит в том, чтобы всегда добавлять транзакции в backstack и FragmentManager.popBackStackImmediate()при необходимости выполнять дополнительные операции (автоматически).

Код представляет собой очень мало строк кода, и в моем примере я хотел перейти от C к A, не возвращаясь к «B», если пользователь не углубился в бэкстэк (например, из C переходит к D).

Следовательно, прикрепленный код будет работать следующим образом: A -> B -> C (назад) -> A & A -> B -> C -> D (назад) -> C (назад) -> B (назад) -> A

где

fm.beginTransaction().replace(R.id.content, new CFragment()).commit()

были выданы от «B» до «C», как в вопросе.

Хорошо, вот код :)

public static void performNoBackStackTransaction(FragmentManager fragmentManager, String tag, Fragment fragment) {
  final int newBackStackLength = fragmentManager.getBackStackEntryCount() +1;

  fragmentManager.beginTransaction()
      .replace(R.id.content, fragment, tag)
      .addToBackStack(tag)
      .commit();

  fragmentManager.addOnBackStackChangedListener(new FragmentManager.OnBackStackChangedListener() {
    @Override
    public void onBackStackChanged() {
      int nowCount = fragmentManager.getBackStackEntryCount();
      if (newBackStackLength != nowCount) {
        // we don't really care if going back or forward. we already performed the logic here.
        fragmentManager.removeOnBackStackChangedListener(this);

        if ( newBackStackLength > nowCount ) { // user pressed back
          fragmentManager.popBackStackImmediate();
        }
      }
    }
  });
}
Андреа Бачега
источник
1
получение сбоя в этой строке fragmentManager.popBackStackImmediate (); ошибка: java.lang.IllegalStateException: FragmentManager уже выполняет транзакции в com.example.myapplication.FragmentA $ 2.onBackStackChanged (FragmentA.java:43)
Приянка
1

Если вы боретесь с addToBackStack () и popBackStack (), просто используйте

FragmentTransaction ft =getSupportFragmentManager().beginTransaction();
ft.replace(R.id.content_frame, new HomeFragment(), "Home");
ft.commit();`

В своей деятельности в OnBackPressed () найдите фрагмент по тегу, а затем выполните свои действия

Fragment home = getSupportFragmentManager().findFragmentByTag("Home");

if (home instanceof HomeFragment && home.isVisible()) {
    // do you stuff
}

Для получения дополнительной информации https://github.com/DattaHujare/NavigationDrawer Я никогда не использую addToBackStack () для обработки фрагментов.

Датту Худжаре
источник
0

Когда я читаю ваш рассказ, я думаю, что [3] тоже находится на заднем плане. Это объясняет, почему вы видите его мигание.

Решением было бы никогда не помещать [3] в стек.

jdekeij
источник
Привет, jdekei Спасибо за ваш вклад. Проблема в том, что я не вижу, куда добавляю [3] в backstack. Я добавил еще один фрагмент кода, который демонстрирует (программно) именно ту навигацию, которую я выполнял с помощью кнопок.
Крис Берч,
Мне это тоже помогает, но я использую только removeCurFragment из вашего кода и немного другую программу проверки фрагментов. Метод замены работает у меня нормально, может быть, это s old method issue but now itнормально. Спасибо
Виктор В.
0

У меня была аналогичная проблема, когда у меня было 3 последовательных фрагмента в одном и том же Activity[M1.F0] -> [M1.F1] -> [M1.F2] с последующим вызовом нового Activity[M2]. Если пользователь нажимал кнопку в [M2], я хотел вернуться к [M1, F1] вместо [M1, F2], что уже делало поведение обратного нажатия.

Для этого я удаляю [M1, F2], вызываю show на [M1, F1], фиксирую транзакцию, а затем добавляю [M1, F2] обратно, вызывая ее с помощью hide. Это устранило лишний пресс со спины, который иначе остался бы позади.

// Remove [M1.F2] to avoid having an extra entry on back press when returning from M2
final FragmentTransaction ftA = fm.beginTransaction();
ftA.remove(M1F2Fragment);
ftA.show(M1F1Fragment);
ftA.commit();
final FragmentTransaction ftB = fm.beginTransaction();
ftB.hide(M1F2Fragment);
ftB.commit();

Привет, после выполнения этого кода: я не могу видеть значение Fragment2 при нажатии клавиши «Назад». Мой код:

FragmentTransaction ft = fm.beginTransaction();
ft.add(R.id.frame, f1);
ft.remove(f1);

ft.add(R.id.frame, f2);
ft.addToBackStack(null);

ft.remove(f2);
ft.add(R.id.frame, f3);

ft.commit();

@Override
    public boolean onKeyDown(int keyCode, KeyEvent event){

        if(keyCode == KeyEvent.KEYCODE_BACK){
            Fragment currentFrag =  getFragmentManager().findFragmentById(R.id.frame);
            FragmentTransaction transaction = getFragmentManager().beginTransaction();

            if(currentFrag != null){
                String name = currentFrag.getClass().getName();
            }
            if(getFragmentManager().getBackStackEntryCount() == 0){
            }
            else{
                getFragmentManager().popBackStack();
                removeCurrentFragment();
            }
       }
    return super.onKeyDown(keyCode, event);
   }

public void removeCurrentFragment()
    {
        FragmentTransaction transaction = getFragmentManager().beginTransaction();
        Fragment currentFrag =  getFragmentManager().findFragmentById(R.id.frame);

        if(currentFrag != null){
            transaction.remove(currentFrag);
        }
        transaction.commit();
    }
Iñaqui
источник
0

executePendingTransactions(), commitNow()не работал (

Работал в androidx (джетпак).

private final FragmentManager fragmentManager = getSupportFragmentManager();

public void removeFragment(FragmentTag tag) {
    Fragment fragmentRemove = fragmentManager.findFragmentByTag(tag.toString());
    if (fragmentRemove != null) {
        fragmentManager.beginTransaction()
                .remove(fragmentRemove)
                .commit();

        // fix by @Ogbe
        fragmentManager.popBackStackImmediate(tag.toString(), 
            FragmentManager.POP_BACK_STACK_INCLUSIVE);
    }
}
tim4dev
источник