Problemas com a pilha traseira do fragmento Android

121

Eu tenho um problema enorme com o modo como o backstack do fragmento android parece funcionar e ficaria muito grato por qualquer ajuda oferecida.

Imagine que você tem 3 fragmentos

[1] [2] [3]

Quero que o usuário possa navegar, [1] > [2] > [3]mas no caminho de volta (pressionando o botão Voltar) [3] > [1].

Como eu imaginava, isso seria realizado se não fosse chamada addToBackStack(..)ao criar a transação que traz fragmento [2]para o detentor de fragmento definido em XML.

A realidade disso parece que, se eu não quiser [2]aparecer novamente quando o usuário pressionar o botão Voltar [3], não devo chamar addToBackStacka transação que mostra o fragmento [3]. Isso parece completamente contra-intuitivo (talvez proveniente do mundo iOS).

De qualquer forma, se eu fizer dessa maneira, quando eu for [1] > [2]e pressionar, chego de volta [1]como esperado.

Se eu for [1] > [2] > [3]e depois pressionar, pulo de volta para [1](como esperado). Agora o comportamento estranho acontece quando tento pular de [2]novo para [1]. Antes de tudo, [3]é exibido brevemente antes de aparecer [2]. Se eu pressionar novamente neste momento, [3]será exibido e se eu pressionar novamente, o aplicativo será encerrado.

Alguém pode me ajudar a entender o que está acontecendo aqui?


E aqui está o arquivo xml de layout para minha atividade principal:

<?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"

        />



Atualizar Este é o código que estou usando para criar pela hierarquia de navegação

    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]

Muito Obrigado

Chris Birch
fonte
mas fragmento é sobrepostos, quando i backpress de Fragmento C de fragmento A.
Priyanka
Eu tenho o mesmo problema, como você corrige isso?
Priyanka
consulte a minha ans .. pode ser ajudá-lo < stackoverflow.com/questions/14971780/… >
MD Khali

Respostas:

203

Explicação: sobre o que está acontecendo aqui?

Se tivermos em mente que .replace()é igual ao .remove().add()que sabemos pela documentação:

Substitua um fragmento existente que foi adicionado a um contêiner. Isso é essencialmente o mesmo que chamar remove(Fragment)todos os fragmentos adicionados atualmente que foram adicionados com o mesmo containerViewIde, em seguida, add(int, Fragment, String)com os mesmos argumentos fornecidos aqui.

então o que está acontecendo é assim (estou adicionando números à frag para deixar mais claro):

// 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

(aqui todas as coisas enganosas começam a acontecer)

Lembre-se de que .addToBackStack()está salvando apenas a transação e não o fragmento como ele mesmo! Então agora temos frag3o layout:

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

Solução possível

Considere implementar FragmentManager.BackStackChangedListenerpara observar as mudanças na pilha traseira e aplicar sua lógica no onBackStackChanged()método:

Arvis
fonte
Boa explicação @arvis. No entanto, como podemos evitar esse comportamento sem recorrer a formas hacky como a de DexterMoon ou a de Nemanja usando popBackStack que mostra o fragmento enquanto reproduz a animação de transição?
momo
@momo que você pode implementar FragmentManager.BackStackChangedListenerpara observar as alterações na pilha de trás. Monitore todas as suas transações com o onBackStackChanged()método e aja conforme necessário: por ex. rastrear uma contagem de transações no BackStack; verifique transação específica por nome ( FragmentTransaction addToBackStack (String name)) etc.
Arvis
Obrigado por responder. Na verdade, tentei isso mais cedo hoje, registrei o ouvinte e removi o fragmento do onBackstackChange. Enquanto a transição de popping é executada, o fragmento se torna uma área branca vazia. Eu acho que o método é disparado quando popping inicia a animação e não quando termina ...
momo
4
Evite usar pilhas de costas! isso realmente não ajuda com a eficiência geral! use plain replace () ou, melhor ainda, remova / adicione toda vez que quiser navegar!
stack_ved
@Arvis u pls pode me ajudar a obter o mesmo problema .... contagem de pilhas 0, mas ainda assim meu fragmento é visível?
Erum 12/10
33

Certo!!! depois de muito puxar o cabelo, finalmente descobri como fazer isso funcionar corretamente.

Parece que o fragmento [3] não é removido da visualização quando a tecla é pressionada, então você deve fazê-lo manualmente!

Primeiro de tudo, não use replace (), mas use remove e adicione separadamente. Parece que replace () não funciona corretamente.

A próxima parte disso é substituir o método onKeyDown e remover o fragmento atual toda vez que o botão Voltar é pressionado.

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

}

Espero que isto ajude!

Chris Birch
fonte
Isso funciona, mas isso não pode ser usado no meu projeto, pois o fragmento é recarregado! alguma sugestão?
TharakaNirmana
Mas, se eu estou fazendo isso. Estou ficando tela em branco quando eu estou voltando de C para A para Fragment C.
Nigam Patro
Eu suspeito @Arvis resposta deve lançar alguma luz sobre o seu problema
Chris Birch
16

Antes de tudo, agradeço à @Arvis por uma explicação que abre os olhos.

Prefiro uma solução diferente à resposta aceita aqui para este problema. Eu não gosto de mexer com o comportamento anulador de volta mais do que o absolutamente necessário e quando tentei adicionar e remover fragmentos por conta própria sem que a pilha traseira padrão aparecesse quando o botão Voltar fosse pressionado, eu me encontrei no inferno :) Se você. adicione f2 sobre f1 quando você o remover f1 não chamará nenhum dos métodos de retorno de chamada, como onResume, onStart etc. e isso pode ser muito lamentável.

De qualquer forma, é assim que eu faço:

Atualmente em exibição, é apenas o fragmento f1.

f1 -> f2

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

Nada fora do comum aqui. Do que no fragmento f2, esse código leva você ao fragmento f3.

f2 -> f3

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

Não sei ao ler os documentos se isso deve funcionar, é dito que esse método de transação pop-up é assíncrono e talvez uma maneira melhor seria chamar popBackStackImmediate (). Mas, até onde eu sei, nos meus dispositivos está funcionando perfeitamente.

A referida alternativa seria:

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

Aqui, na verdade, haverá um breve retorno a f1 antes de passar para f3, portanto, uma pequena falha.

Na verdade, isso é tudo o que você precisa fazer, sem necessidade de substituir o comportamento da pilha traseira ...

Nemanja Kovacevic
fonte
6
mas a falha, isso é um problema
Zyoo
Parece menos "hack-ey" do que ouvir mudanças no BackStack. Obrigado.
ahaisting
13

Eu sei que é uma quetion antiga, mas eu tenho o mesmo problema e corrijo assim:

Primeiro, adicione Fragment1 ao BackStack com um nome (por exemplo, "Frag1"):

frag = new Fragment1();

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

E então, sempre que você quiser voltar ao Fragmento1 (mesmo depois de adicionar 10 fragmentos acima), basta chamar popBackStackImmediate com o nome:

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

Espero que ajude alguém :)

Shirane85
fonte
1
Ótima resposta. Eu tive que usar getSupportFragmentManager (). PopBackStackImmediate ("Frag1", FragmentManager.POP_BACK_STACK_INCLUSIVE); para obtê-lo para trabalhar para o meu caso de uso embora
eliasbagley
1
Onde eu tenho que colocar esse código ???? getSupportFragmentManager (). popBackStackImmediate ("Frag1", 0); On MainActivty ou onBackPress
pavel
não está funcionando. :( este é o meu código, no meu caso, o fragmento está sobreposto. ele abre o fragmento A, mas o fragmento A está sobreposto no fragmento B.
Priyanka
FragmentManager fragmentManager = getActivity (). GetSupportFragmentManager (); fragmentManager.popBackStack (FragmentA.class.getName (), FragmentManager.POP_BACK_STACK_INCLUSIVE); FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction (); FragmentE fragmentE = novo FragmentE (); fragmentTransaction.replace (R.id.fragment_content, fragmentE, fragmentE.getClass (). getName ()); fragmentTransaction.commit ();
Priyanka
5

Após a resposta do @Arvis, decidi me aprofundar ainda mais e escrevi um artigo sobre isso aqui: http://www.andreabaccega.com/blog/2015/08/16/how-to-avoid-fragments-overlapping- devido-a-pesadelo-pesadelo-no-android /

Para os desenvolvedores preguiçosos ao redor. Minha solução consiste em adicionar sempre as transações ao backstack e executar um extra FragmentManager.popBackStackImmediate()quando necessário (automaticamente).

O código tem muito poucas linhas de código e, no meu exemplo, eu queria pular de C para A sem voltar para "B" se o usuário não se aprofundar no backstack (ex: C navega para D).

Portanto, o código anexado funcionaria da seguinte forma: A -> B -> C (traseira) -> A e A -> B -> C -> D (traseira) -> C (traseira) -> B (traseira) -> A

Onde

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

foram emitidos de "B" a "C" como na pergunta.

Ok, ok, aqui está o código :)

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();
        }
      }
    }
  });
}
Andrea Baccega
fonte
1
obtendo falha nesta linha fragmentManager.popBackStackImmediate (); erro: java.lang.IllegalStateException: FragmentManager já está executando transações em com.example.myapplication.FragmentA $ 2.onBackStackChanged (FragmentA.java:43)
Priyanka
1

Se você está lutando com addToBackStack () e popBackStack (), basta usar

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

Em sua atividade No OnBackPressed () descubra a remessa por tag e faça suas coisas

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

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

Para obter mais informações https://github.com/DattaHujare/NavigationDrawer , nunca uso addToBackStack () para manipular fragmentos.

Dattu Hujare
fonte
0

Eu acho que, quando li sua história, [3] também está nos bastidores. Isso explica por que você o vê piscando.

A solução seria nunca colocar [3] na pilha.

jdekeij
fonte
Oi jdekei Obrigado pela sua contribuição. O problema é que não consigo ver onde estou adicionando [3] ao backstack. Adicionei outro pedaço de código que demonstra (programaticamente) exatamente a navegação que eu estava executando usando os botões.
Chris Birch
Isso também me ajuda, mas eu uso apenas removeCurFragment do seu código e um pouco outro verificador de fragmentos. Substituir método funciona para mim bem, pode ser, tudo s old method issue but now itbem. Graças
Viktor V.
0

Eu tive um problema semelhante em que tinha três fragmentos consecutivos no mesmo Activity[M1.F0] -> [M1.F1] -> [M1.F2] seguido por uma chamada para um novoActivity [M2]. Se o usuário pressionou um botão em [M2], eu queria retornar a [M1, F1] em vez de [M1, F2], que é o que o comportamento da contrapressão já fazia.

Para fazer isso, removo [M1, F2], chamo show em [M1, F1], confirmo a transação e, em seguida, adiciono [M1, F2] chamando-o com hide. Isso removeu a pressão extra adicional que, de outra forma, teria sido deixada para trás.

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

Oi Depois de fazer este código: Não consigo ver o valor do Fragment2 ao pressionar a tecla Voltar. Meu código:

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
fonte
0

executePendingTransactions(), commitNow()não funcionou (

Trabalhou no androidx (jetpack).

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
fonte