Fragment onCreateView e onActivityCreated chamados duas vezes

101

Estou desenvolvendo um aplicativo usando Android 4.0 ICS e fragmentos.

Considere este exemplo modificado do aplicativo de exemplo de demonstração da API ICS 4.0.3 (API nível 15):

public class FragmentTabs extends Activity {

private static final String TAG = FragmentTabs.class.getSimpleName();

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

    final ActionBar bar = getActionBar();
    bar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS);
    bar.setDisplayOptions(0, ActionBar.DISPLAY_SHOW_TITLE);

    bar.addTab(bar.newTab()
            .setText("Simple")
            .setTabListener(new TabListener<SimpleFragment>(
                    this, "mysimple", SimpleFragment.class)));

    if (savedInstanceState != null) {
        bar.setSelectedNavigationItem(savedInstanceState.getInt("tab", 0));
        Log.d(TAG, "FragmentTabs.onCreate tab: " + savedInstanceState.getInt("tab"));
        Log.d(TAG, "FragmentTabs.onCreate number: " + savedInstanceState.getInt("number"));
    }

}

@Override
protected void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);
    outState.putInt("tab", getActionBar().getSelectedNavigationIndex());
}

public static class TabListener<T extends Fragment> implements ActionBar.TabListener {
    private final Activity mActivity;
    private final String mTag;
    private final Class<T> mClass;
    private final Bundle mArgs;
    private Fragment mFragment;

    public TabListener(Activity activity, String tag, Class<T> clz) {
        this(activity, tag, clz, null);
    }

    public TabListener(Activity activity, String tag, Class<T> clz, Bundle args) {
        mActivity = activity;
        mTag = tag;
        mClass = clz;
        mArgs = args;

        // Check to see if we already have a fragment for this tab, probably
        // from a previously saved state.  If so, deactivate it, because our
        // initial state is that a tab isn't shown.
        mFragment = mActivity.getFragmentManager().findFragmentByTag(mTag);
        if (mFragment != null && !mFragment.isDetached()) {
            Log.d(TAG, "constructor: detaching fragment " + mTag);
            FragmentTransaction ft = mActivity.getFragmentManager().beginTransaction();
            ft.detach(mFragment);
            ft.commit();
        }
    }

    public void onTabSelected(Tab tab, FragmentTransaction ft) {
        if (mFragment == null) {
            mFragment = Fragment.instantiate(mActivity, mClass.getName(), mArgs);
            Log.d(TAG, "onTabSelected adding fragment " + mTag);
            ft.add(android.R.id.content, mFragment, mTag);
        } else {
            Log.d(TAG, "onTabSelected attaching fragment " + mTag);
            ft.attach(mFragment);
        }
    }

    public void onTabUnselected(Tab tab, FragmentTransaction ft) {
        if (mFragment != null) {
            Log.d(TAG, "onTabUnselected detaching fragment " + mTag);
            ft.detach(mFragment);
        }
    }

    public void onTabReselected(Tab tab, FragmentTransaction ft) {
        Toast.makeText(mActivity, "Reselected!", Toast.LENGTH_SHORT).show();
    }
}

public static class SimpleFragment extends Fragment {
    TextView textView;
    int mNum;

    /**
     * When creating, retrieve this instance's number from its arguments.
     */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Log.d(FragmentTabs.TAG, "onCreate " + (savedInstanceState != null ? ("state " + savedInstanceState.getInt("number")) : "no state"));
        if(savedInstanceState != null) {
            mNum = savedInstanceState.getInt("number");
        } else {
            mNum = 25;
        }
    }

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        Log.d(TAG, "onActivityCreated");
        if(savedInstanceState != null) {
            Log.d(TAG, "saved variable number: " + savedInstanceState.getInt("number"));
        }
        super.onActivityCreated(savedInstanceState);
    }

    @Override
    public void onSaveInstanceState(Bundle outState) {
        Log.d(TAG, "onSaveInstanceState saving: " + mNum);
        outState.putInt("number", mNum);
        super.onSaveInstanceState(outState);
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        Log.d(FragmentTabs.TAG, "onCreateView " + (savedInstanceState != null ? ("state: " + savedInstanceState.getInt("number")) : "no state"));
        textView = new TextView(getActivity());
        textView.setText("Hello world: " + mNum);
        textView.setBackgroundDrawable(getResources().getDrawable(android.R.drawable.gallery_thumb));
        return textView;
    }
}

}

Aqui está a saída recuperada da execução deste exemplo e da rotação do telefone:

06-11 11:31:42.559: D/FragmentTabs(10726): onTabSelected adding fragment mysimple
06-11 11:31:42.559: D/FragmentTabs(10726): onCreate no state
06-11 11:31:42.559: D/FragmentTabs(10726): onCreateView no state
06-11 11:31:42.567: D/FragmentTabs(10726): onActivityCreated
06-11 11:31:45.286: D/FragmentTabs(10726): onSaveInstanceState saving: 25
06-11 11:31:45.325: D/FragmentTabs(10726): onCreate state 25
06-11 11:31:45.340: D/FragmentTabs(10726): constructor: detaching fragment mysimple
06-11 11:31:45.340: D/FragmentTabs(10726): onTabSelected attaching fragment mysimple
06-11 11:31:45.348: D/FragmentTabs(10726): FragmentTabs.onCreate tab: 0
06-11 11:31:45.348: D/FragmentTabs(10726): FragmentTabs.onCreate number: 0
06-11 11:31:45.348: D/FragmentTabs(10726): onCreateView state: 25
06-11 11:31:45.348: D/FragmentTabs(10726): onActivityCreated
06-11 11:31:45.348: D/FragmentTabs(10726): saved variable number: 25
06-11 11:31:45.348: D/FragmentTabs(10726): onCreateView no state
06-11 11:31:45.348: D/FragmentTabs(10726): onActivityCreated

Minha pergunta é: por que onCreateView e onActivityCreated são chamados duas vezes? A primeira vez com um Bundle com o estado salvo e a segunda vez com um SavedInstanceState nulo?

Isso está causando problemas com a retenção do estado do fragmento na rotação.

Dave
fonte
2
Acho que esta pergunta pode estar relacionada a stackoverflow.com/a/8678705/404395
marioosh

Respostas:

45

Eu estava coçando minha cabeça sobre isso também por um tempo e, como a explicação de Dave é um pouco difícil de entender, postarei meu código (aparentemente funcionando):

private class TabListener<T extends Fragment> implements ActionBar.TabListener {
    private Fragment mFragment;
    private Activity mActivity;
    private final String mTag;
    private final Class<T> mClass;

    public TabListener(Activity activity, String tag, Class<T> clz) {
        mActivity = activity;
        mTag = tag;
        mClass = clz;
        mFragment=mActivity.getFragmentManager().findFragmentByTag(mTag);
    }

    public void onTabSelected(Tab tab, FragmentTransaction ft) {
        if (mFragment == null) {
            mFragment = Fragment.instantiate(mActivity, mClass.getName());
            ft.replace(android.R.id.content, mFragment, mTag);
        } else {
            if (mFragment.isDetached()) {
                ft.attach(mFragment);
            }
        }
    }

    public void onTabUnselected(Tab tab, FragmentTransaction ft) {
        if (mFragment != null) {
            ft.detach(mFragment);
        }
    }

    public void onTabReselected(Tab tab, FragmentTransaction ft) {
    }
}

Como você pode ver, é muito parecido com o exemplo do Android, exceto por não desanexar no construtor e usar substituir em vez de adicionar .

Depois de muito headscratching e tentativa e erro, descobri que encontrar o fragmento no construtor parece fazer o problema duplo onCreateView desaparecer magicamente (presumo que acaba sendo nulo para onTabSelected quando chamado pelo caminho ActionBar.setSelectedNavigationItem () quando salvando / restaurando estado).

Staffan
fonte
Funciona perfeitamente bem! Você salvou minha noite de sono! Obrigado :)
jaibatrik
você também pode usar fragment.getClass (). getName () se quiser remover a variável de classe e remover um parâmetro da chamada
Ben Sewards
Funciona perfeitamente com a amostra do Android "ref. TabListener" - tnx. O mais novo Android "TabListener ref. Sample" [como em 4 ix 2013] está muito, muito errado.
Grzegorz Dev
onde está a chamada do método ft.commit () ??
MSaudi
1
@MuhammadBabar, consulte stackoverflow.com/questions/23248789/… . Se você usar em addvez de replacee girar a tela, terá muitos fragmentos ' onCreateView().
CoolMind
26

Ok, aqui está o que descobri.

O que eu não entendi é que todos os fragmentos que são anexados a uma atividade quando ocorre uma mudança de configuração (o telefone gira) são recriados e adicionados de volta à atividade. (o que faz sentido)

O que estava acontecendo no construtor TabListener era que a guia havia sido desanexada se fosse encontrada e anexada à atividade. Ver abaixo:

mFragment = mActivity.getFragmentManager().findFragmentByTag(mTag);
    if (mFragment != null && !mFragment.isDetached()) {
        Log.d(TAG, "constructor: detaching fragment " + mTag);
        FragmentTransaction ft = mActivity.getFragmentManager().beginTransaction();
        ft.detach(mFragment);
        ft.commit();
    }

Posteriormente, na atividade onCreate, a guia selecionada anteriormente foi selecionada a partir do estado da instância salva. Ver abaixo:

if (savedInstanceState != null) {
    bar.setSelectedNavigationItem(savedInstanceState.getInt("tab", 0));
    Log.d(TAG, "FragmentTabs.onCreate tab: " + savedInstanceState.getInt("tab"));
    Log.d(TAG, "FragmentTabs.onCreate number: " + savedInstanceState.getInt("number"));
}

Quando a guia era selecionada, ela era reanexada no retorno de chamada onTabSelected.

public void onTabSelected(Tab tab, FragmentTransaction ft) {
    if (mFragment == null) {
        mFragment = Fragment.instantiate(mActivity, mClass.getName(), mArgs);
        Log.d(TAG, "onTabSelected adding fragment " + mTag);
        ft.add(android.R.id.content, mFragment, mTag);
    } else {
        Log.d(TAG, "onTabSelected attaching fragment " + mTag);
        ft.attach(mFragment);
    }
}

O fragmento que está sendo anexado é a segunda chamada para os métodos onCreateView e onActivityCreated. (O primeiro é quando o sistema está recriando a atividade e todos os fragmentos anexados) Na primeira vez, o pacote onSavedInstanceState teria salvado os dados, mas não na segunda vez.

A solução é não desanexar o fragmento no construtor TabListener, apenas deixá-lo anexado. (Você ainda precisa encontrá-lo no FragmentManager por sua tag) Além disso, no método onTabSelected, eu verifico se o fragmento foi destacado antes de anexá-lo. Algo assim:

public void onTabSelected(Tab tab, FragmentTransaction ft) {
            if (mFragment == null) {
                mFragment = Fragment.instantiate(mActivity, mClass.getName(), mArgs);
                Log.d(TAG, "onTabSelected adding fragment " + mTag);
                ft.add(android.R.id.content, mFragment, mTag);
            } else {

                if(mFragment.isDetached()) {
                    Log.d(TAG, "onTabSelected attaching fragment " + mTag);
                    ft.attach(mFragment);
                } else {
                    Log.d(TAG, "onTabSelected fragment already attached " + mTag);
                }
            }
        }
Dave
fonte
4
As soluções mencionadas "para não desanexar o fragmento no construtor TabListener" fazem com que os fragmentos da guia se sobreponham. Posso ver o conteúdo dos outros fragmentos. Não funciona para mim.
Aksel Fatih
@ flock.dux Não tenho certeza do que você quer dizer com sobreposição. O Android cuida de como eles são dispostos, portanto, apenas especificamos anexar ou desanexar. Deve haver mais coisas acontecendo. Talvez se você fizer uma nova pergunta com um código de exemplo, possamos descobrir o que está acontecendo para você.
Dave
1
Eu tive o mesmo problema (várias chamadas de construtor de fragmento do Android). Sua descoberta resolve meu problema: O que eu não entendi é que todos os fragmentos que são anexados a uma atividade quando ocorre uma mudança de configuração (o telefone gira) são recriados e adicionados de volta à atividade. (o que faz sentido)
eugene
26

Eu tive o mesmo problema com uma atividade simples carregando apenas um fragmento (que era substituído às vezes). Então percebi que uso onSaveInstanceState apenas no fragmento (e onCreateView para verificar o savedInstanceState), não na atividade.

Ao ligar o dispositivo, a atividade que contém os fragmentos é reiniciada e onCreated é chamado. Lá eu anexei o fragmento necessário (que está correto na primeira inicialização).

No dispositivo, ligue o Android primeiro recriou o fragmento que estava visível e depois chamou onCreate da atividade que continha o meu fragmento, substituindo assim o fragmento visível original.

Para evitar isso, simplesmente mudei minha atividade para verificar o savedInstanceState:

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

if (savedInstanceState != null) {
/**making sure you are not attaching the fragments again as they have 
 been 
 *already added
 **/
 return; 
 }
 else{
  // following code to attach fragment initially
 }

 }

Eu nem mesmo sobrescrevi emSaveInstanceState da atividade.

Gunnar Bernstein
fonte
Obrigado. Isso me ajudou com AppCompatActivity + PreferenceFragmentCompat e travando ao mostrar diálogos no fragmento de preferência após a mudança de orientação, pois o gerenciador de fragmento era nulo na criação do segundo fragmento.
RoK
12

As duas respostas votadas aqui mostram soluções para uma atividade com modo de navegação NAVIGATION_MODE_TABS, mas tive o mesmo problema com a NAVIGATION_MODE_LIST. Isso fez com que meus Fragments perdessem inexplicavelmente seu estado quando a orientação da tela mudasse, o que era realmente irritante. Felizmente, devido ao código útil deles, consegui descobrir.

Basicamente, ao usar uma navegação de lista, `` onNavigationItemSelected () is automatically called when your activity is created/re-created, whether you like it or not. To prevent your Fragment'sonCreateView () from being called twice, this initial automatic call toonNavigationItemSelected () should check whether the Fragment is already in existence inside your Activity. If it is, return immediately, because there is nothing to do; if it isn't, then simply construct the Fragment and add it to the Activity like you normally would. Performing this check prevents your Fragment from needlessly being created again, which is what causesonCreateView () `deve ser chamado duas vezes!

Veja minha onNavigationItemSelected()implementação abaixo.

public class MyActivity extends FragmentActivity implements ActionBar.OnNavigationListener
{
    private static final String STATE_SELECTED_NAVIGATION_ITEM = "selected_navigation_item";

    private boolean mIsUserInitiatedNavItemSelection;

    // ... constructor code, etc.

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

        if (savedInstanceState.containsKey(STATE_SELECTED_NAVIGATION_ITEM))
        {
            getActionBar().setSelectedNavigationItem(savedInstanceState.getInt(STATE_SELECTED_NAVIGATION_ITEM));
        }
    }

    @Override
    public void onSaveInstanceState(Bundle outState)
    {
        outState.putInt(STATE_SELECTED_NAVIGATION_ITEM, getActionBar().getSelectedNavigationIndex());

        super.onSaveInstanceState(outState);
    }

    @Override
    public boolean onNavigationItemSelected(int position, long id)
    {    
        Fragment fragment;
        switch (position)
        {
            // ... choose and construct fragment here
        }

        // is this the automatic (non-user initiated) call to onNavigationItemSelected()
        // that occurs when the activity is created/re-created?
        if (!mIsUserInitiatedNavItemSelection)
        {
            // all subsequent calls to onNavigationItemSelected() won't be automatic
            mIsUserInitiatedNavItemSelection = true;

            // has the same fragment already replaced the container and assumed its id?
            Fragment existingFragment = getSupportFragmentManager().findFragmentById(R.id.container);
            if (existingFragment != null && existingFragment.getClass().equals(fragment.getClass()))
            {
                return true; //nothing to do, because the fragment is already there 
            }
        }

        getSupportFragmentManager().beginTransaction().replace(R.id.container, fragment).commit();
        return true;
    }
}

Eu peguei inspiração para esta solução daqui .

XåpplI'-I0llwlg'I -
fonte
Esta solução funciona para meu problema semelhante com uma gaveta de navegação. Eu encontro o fragmento existente por ID e verifico se ele tem a mesma classe do novo fragmento antes de recriá-lo.
William
8

Parece-me que é porque você está instanciando seu TabListener todas as vezes ... então o sistema está recriando seu fragmento de savedInstanceState e então você está fazendo isso novamente em seu onCreate.

Você deve envolvê-lo em um, de if(savedInstanceState == null)modo que só seja acionado se não houver savedInstanceState.

Barak
fonte
Eu não acho isso correto. Quando envolvo meu código addTab no bloco if, o fragmento é anexado à atividade, mas não há guias. Parece que você tem que adicionar as guias todas as vezes no método onCreate. Vou continuar olhando para isso e postar mais conforme entender melhor.
Dave de