Como salvar corretamente o estado da instância de fragmentos na pilha traseira?

489

Encontrei muitos casos de uma pergunta semelhante no SO, mas infelizmente nenhuma resposta atende aos meus requisitos.

Tenho layouts diferentes para retrato e paisagem e estou usando a pilha traseira, o que me impede de usar setRetainState()e truques usando rotinas de alteração de configuração.

Eu mostro certas informações para o usuário no TextViews, que não são salvas no manipulador padrão. Ao escrever meu aplicativo apenas usando o Activities, o seguinte funcionou bem:

TextView vstup;

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.whatever);
    vstup = (TextView)findViewById(R.id.whatever);
    /* (...) */
}

@Override
public void onSaveInstanceState(Bundle state) {
    super.onSaveInstanceState(state);
    state.putCharSequence(App.VSTUP, vstup.getText());
}

@Override
public void onRestoreInstanceState(Bundle state) {
    super.onRestoreInstanceState(state);
    vstup.setText(state.getCharSequence(App.VSTUP));
}

Com Fragments, isso funciona apenas em situações muito específicas. Especificamente, o que quebra horrivelmente é substituir um fragmento, colocá-lo na pilha de trás e girar a tela enquanto o novo fragmento é mostrado. Pelo que entendi, o fragmento antigo não recebe uma chamada ao onSaveInstanceState()ser substituído, mas permanece ligado de alguma forma ao Activitye esse método é chamado posteriormente quando Viewnão existe mais, portanto, procurando por qualquer um dos meus TextViewresultados em a NullPointerException.

Além disso, descobri que manter a referência à minha TextViewsnão é uma boa ideia com Fragments, mesmo que esteja OK com Activity's. Nesse caso, onSaveInstanceState()na verdade salva o estado, mas o problema reaparece se eu girar a tela duas vezes quando o fragmento estiver oculto, pois onCreateView()ele não é chamado na nova instância.

Pensei em salvar o estado em onDestroyView()algum Bundleelemento membro da classe (na verdade são mais dados, não apenas um TextView) e salvá- lo , onSaveInstanceState()mas existem outras desvantagens. Principalmente, se o fragmento é atualmente mostrado, a ordem de chamar as duas funções é invertida, então eu precisaria considerar duas situações diferentes. Deve haver uma solução mais limpa e correta!

The Vee
fonte
1
Aqui está um exemplo muito bom com uma explicação detalhada. Emuneee.com/blog/2013/01/07/saving-fragment-states
Hesam
1
Eu segundo o link emunee.com. Ele resolveu um problema de interface do usuário para mim!
Reenactor Rob

Respostas:

541

Para salvar corretamente o estado da instância Fragment, faça o seguinte:

1. No fragmento, salve o estado da instância substituindo onSaveInstanceState()e restaurando em onActivityCreated():

class MyFragment extends Fragment {

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        ...
        if (savedInstanceState != null) {
            //Restore the fragment's state here
        }
    }
    ...
    @Override
    public void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);

        //Save the fragment's state here
    }

}

2. E um ponto importante , na atividade, você deve salvar a instância do fragmento onSaveInstanceState()e restaurá-la onCreate().

class MyActivity extends Activity {

    private MyFragment 

    public void onCreate(Bundle savedInstanceState) {
        ...
        if (savedInstanceState != null) {
            //Restore the fragment's instance
            mMyFragment = getSupportFragmentManager().getFragment(savedInstanceState, "myFragmentName");
            ...
        }
        ...
    }

    @Override
    protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);

        //Save the fragment's instance
        getSupportFragmentManager().putFragment(outState, "myFragmentName", mMyFragment);
    }

}

Espero que isto ajude.

ThanhHH
fonte
7
Isso funcionou perfeitamente para mim! Sem soluções alternativas, sem hacks, apenas faz sentido dessa maneira. Obrigado por isso, fez horas de pesquisa bem-sucedidas. SaveInstanceState () de seus valores em fragmento, em seguida, salvar fragmento na Atividade segurando o fragmento, em seguida, restaurar :)
MattMatt
77
O que é mContent?
Wizurd
14
@wizurd mContent é um fragmento, é uma referência à instância do fragmento atual na atividade.
ThanhHH
13
Você pode explicar como isso salvará o estado da instância de um fragmento na pilha traseira? Foi o que o OP pediu.
Hitmaneidos 5/11
51
Isso não está relacionado à pergunta, onSaveInstance não é chamado quando fragmento é putted para backstack
Tadas Valaitis
87

É assim que estou usando neste momento ... é muito complicado, mas pelo menos lida com todas as situações possíveis. Caso alguém esteja interessado.

public final class MyFragment extends Fragment {
    private TextView vstup;
    private Bundle savedState = null;

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        View v = inflater.inflate(R.layout.whatever, null);
        vstup = (TextView)v.findViewById(R.id.whatever);

        /* (...) */

        /* If the Fragment was destroyed inbetween (screen rotation), we need to recover the savedState first */
        /* However, if it was not, it stays in the instance from the last onDestroyView() and we don't want to overwrite it */
        if(savedInstanceState != null && savedState == null) {
            savedState = savedInstanceState.getBundle(App.STAV);
        }
        if(savedState != null) {
            vstup.setText(savedState.getCharSequence(App.VSTUP));
        }
        savedState = null;

        return v;
    }

    @Override
    public void onDestroyView() {
        super.onDestroyView();
        savedState = saveState(); /* vstup defined here for sure */
        vstup = null;
    }

    private Bundle saveState() { /* called either from onDestroyView() or onSaveInstanceState() */
        Bundle state = new Bundle();
        state.putCharSequence(App.VSTUP, vstup.getText());
        return state;
    }

    @Override
    public void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        /* If onDestroyView() is called first, we can use the previously savedState but we can't call saveState() anymore */
        /* If onSaveInstanceState() is called first, we don't have savedState, so we need to call saveState() */
        /* => (?:) operator inevitable! */
        outState.putBundle(App.STAV, (savedState != null) ? savedState : saveState());
    }

    /* (...) */

}

Como alternativa , é sempre possível manter os dados exibidos em Views passivos nas variáveis ​​e usar os Views apenas para exibi-los, mantendo as duas coisas sincronizadas. Mas não considero a última parte muito limpa.

The Vee
fonte
68
Esta é a melhor solução que encontrei até agora, mas ainda resta um problema (um tanto exótico): se você tem dois fragmentos Ae B, onde Aestá atualmente no backstack e Bé visível, perde o estado de A(o invisível um) se você girar a tela duas vezes . O problema é que onCreateView()não é chamado apenas nesse cenário onCreate(). Então, mais tarde, onSaveInstanceState()não há visualizações para salvar o estado. Seria necessário armazenar e salvar o estado passado onCreate().
devconsole
7
@devconsole Eu gostaria de poder dar 5 votos a este comentário! Essa coisa de rotação duas vezes me mata há dias.
DroidT
Obrigado pela ótima resposta! Ainda tenho uma dúvida. Onde é o melhor lugar para instanciar o objeto de modelo (POJO) nesse fragmento?
21315
7
Para ajudar a economizar tempo para outras pessoas, App.VSTUPe App.STAVsão duas tags de seqüência de caracteres que representam os objetos que estão tentando obter. Exemplo: savedState = savedInstanceState.getBundle(savedGamePlayString);ousavedState.getDouble("averageTime")
Tanner Hallman 22/10
1
Isso é algo belíssimo.
Ivan
61

Na biblioteca de suporte mais recente, nenhuma das soluções discutidas aqui é mais necessária. Você pode brincar com seus Activityfragmentos conforme desejar FragmentTransaction. Apenas verifique se seus fragmentos podem ser identificados com um ID ou tag.

Os fragmentos serão restaurados automaticamente, desde que você não tente recriá-los a cada chamada para onCreate(). Em vez disso, você deve verificar se savedInstanceStatenão é nulo e, nesse caso, encontrar as referências antigas aos fragmentos criados.

Aqui está um exemplo:

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    if (savedInstanceState == null) {
        myFragment = MyFragment.newInstance();
        getSupportFragmentManager()
                .beginTransaction()
                .add(R.id.my_container, myFragment, MY_FRAGMENT_TAG)
                .commit();
    } else {
        myFragment = (MyFragment) getSupportFragmentManager()
                .findFragmentByTag(MY_FRAGMENT_TAG);
    }
...
}

Observe, no entanto, que existe atualmente um erro ao restaurar o estado oculto de um fragmento. Se você estiver ocultando fragmentos em sua atividade, será necessário restaurar esse estado manualmente neste caso.

Ricardo
fonte
2
Essa correção foi algo que você notou ao usar a biblioteca de suporte ou leu sobre ela em algum lugar? Existe mais alguma informação que você possa fornecer sobre isso? Obrigado!
Piovezan 02/02
1
@Piovezan, pode ser inferido implicitamente a partir dos documentos. Por exemplo, o documento beginTransaction () tem a seguinte redação: "Isso ocorre porque a estrutura cuida de salvar seus fragmentos atuais no estado (...)". Eu também tenho codificado meus aplicativos com esse comportamento esperado já há algum tempo.
Ricardo
1
@ Ricardo isso se aplica se estiver usando um ViewPager?
Derek Beattie
1
Normalmente sim, a menos que você tenha alterado o comportamento padrão na sua implementação de FragmentPagerAdapterou FragmentStatePagerAdapter. Se você olhar o código de FragmentStatePagerAdapter, por exemplo, verá que o restoreState()método restaura os fragmentos do FragmentManagerparâmetro que você passou como ao criar o adaptador.
1111 Ricardo
4
Eu acho que essa contribuição é a melhor resposta para a pergunta original. Também é o que, na minha opinião, está melhor alinhado com o funcionamento da plataforma Android. Eu recomendaria marcar essa resposta como "Aceita" para ajudar melhor os futuros leitores.
dbm
17

Eu só quero dar a solução que eu criei que lida com todos os casos apresentados neste post que eu derivei do Vasek e do devconsole. Essa solução também lida com o caso especial quando o telefone é girado mais de uma vez enquanto os fragmentos não são visíveis.

Aqui é onde eu armazeno o pacote para uso posterior, pois onCreate e onSaveInstanceState são as únicas chamadas feitas quando o fragmento não está visível

MyObject myObject;
private Bundle savedState = null;
private boolean createdStateInDestroyView;
private static final String SAVED_BUNDLE_TAG = "saved_bundle";

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    if (savedInstanceState != null) {
        savedState = savedInstanceState.getBundle(SAVED_BUNDLE_TAG);
    }
}

Como destroyView não é chamado na situação de rotação especial, podemos ter certeza de que, se ele criar o estado, devemos usá-lo.

@Override
public void onDestroyView() {
    super.onDestroyView();
    savedState = saveState();
    createdStateInDestroyView = true;
    myObject = null;
}

Esta parte seria a mesma.

private Bundle saveState() { 
    Bundle state = new Bundle();
    state.putSerializable(SAVED_BUNDLE_TAG, myObject);
    return state;
}

Agora, aqui está a parte complicada. No meu método onActivityCreated, instanciamos a variável "myObject", mas a rotação ocorre onActivity e onCreateView não são chamados. Portanto, myObject será nulo nessa situação quando a orientação girar mais de uma vez. Eu resolvo isso reutilizando o mesmo pacote que foi salvo no onCreate como o pacote que está saindo.

    @Override
public void onSaveInstanceState(Bundle outState) {

    if (myObject == null) {
        outState.putBundle(SAVED_BUNDLE_TAG, savedState);
    } else {
        outState.putBundle(SAVED_BUNDLE_TAG, createdStateInDestroyView ? savedState : saveState());
    }
    createdStateInDestroyView = false;
    super.onSaveInstanceState(outState);
}

Agora, onde você quiser restaurar o estado, basta usar o pacote salvoState

  @Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    ...
    if(savedState != null) {
        myObject = (MyObject) savedState.getSerializable(SAVED_BUNDLE_TAG);
    }
    ...
}
DroidT
fonte
Você pode me dizer ... O que é "MyObject" aqui?
kavie
2
Qualquer coisa que você queira que seja. É apenas um exemplo que representa algo que seria salvo no pacote.
DroidT 18/10/2014
3

Graças ao DroidT , fiz isso:

Sei que se o fragmento não executar onCreateView (), sua exibição não será instanciada. Portanto, se o fragmento na pilha traseira não criar suas visualizações, eu salvo o último estado armazenado, caso contrário, construo meu próprio pacote com os dados que desejo salvar / restaurar.

1) Estenda esta classe:

import android.os.Bundle;
import android.support.v4.app.Fragment;

public abstract class StatefulFragment extends Fragment {

    private Bundle savedState;
    private boolean saved;
    private static final String _FRAGMENT_STATE = "FRAGMENT_STATE";

    @Override
    public void onSaveInstanceState(Bundle state) {
        if (getView() == null) {
            state.putBundle(_FRAGMENT_STATE, savedState);
        } else {
            Bundle bundle = saved ? savedState : getStateToSave();

            state.putBundle(_FRAGMENT_STATE, bundle);
        }

        saved = false;

        super.onSaveInstanceState(state);
    }

    @Override
    public void onCreate(Bundle state) {
        super.onCreate(state);

        if (state != null) {
            savedState = state.getBundle(_FRAGMENT_STATE);
        }
    }

    @Override
    public void onDestroyView() {
        savedState = getStateToSave();
        saved = true;

        super.onDestroyView();
    }

    protected Bundle getSavedState() {
        return savedState;
    }

    protected abstract boolean hasSavedState();

    protected abstract Bundle getStateToSave();

}

2) No seu fragmento, você deve ter o seguinte:

@Override
protected boolean hasSavedState() {
    Bundle state = getSavedState();

    if (state == null) {
        return false;
    }

    //restore your data here

    return true;
}

3) Por exemplo, você pode chamar hasSavedState em onActivityCreated:

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

    if (hasSavedState()) {
        return;
    }

    //your code here
}
Noturno
fonte
-6
final FragmentTransaction ft = getFragmentManager().beginTransaction();
ft.hide(currentFragment);
ft.add(R.id.content_frame, newFragment.newInstance(context), "Profile");
ft.addToBackStack(null);
ft.commit();
Nilesh Savaliya
fonte
1
Não está relacionado à pergunta original.
Jorge E. Hernández