Fragment onCreateView und onActivityCreated werden zweimal aufgerufen

100

Ich entwickle eine App mit Android 4.0 ICS und Fragmenten.

Betrachten Sie dieses modifizierte Beispiel aus der Demo-Beispiel-App der ICS 4.0.3 (API Level 15) API:

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

}}

Hier ist die Ausgabe, die beim Ausführen dieses Beispiels und beim anschließenden Drehen des Telefons abgerufen wurde:

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

Meine Frage ist, warum onCreateView und onActivityCreated zweimal aufgerufen werden. Das erste Mal mit einem Bundle mit dem gespeicherten Status und das zweite Mal mit einem null savedInstanceState?

Dies verursacht Probleme beim Beibehalten des Zustands des Fragments bei Rotation.

Dave
quelle
2
Ich denke, diese Frage kann mit stackoverflow.com/a/8678705/404395
Marioosh

Antworten:

45

Ich habe mir auch eine Weile am Kopf gekratzt, und da Daves Erklärung etwas schwer zu verstehen ist, werde ich meinen (anscheinend funktionierenden) Code veröffentlichen:

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) {
    }
}

Wie Sie sehen können, ähnelt es dem Android-Beispiel, abgesehen davon, dass es sich nicht im Konstruktor löst und anstelle von Hinzufügen Ersetzen verwendet .

Nach langem Headscratching und Versuch und Irrtum stellte ich fest, dass das Finden des Fragments im Konstruktor das doppelte onCreateView-Problem auf magische Weise verschwinden lässt (ich gehe davon aus, dass es für onTabSelected nur null ist, wenn es über den Pfad ActionBar.setSelectedNavigationItem () aufgerufen wird, wenn Status speichern / wiederherstellen).

Staffan
quelle
Funktioniert perfekt! Du hast mir den Schlaf gerettet! Vielen Dank :)
Jaibatrik
Sie können auch fragment.getClass (). getName () verwenden, wenn Sie die Klassenvariable entfernen und einen Parameter aus dem Aufruf entfernen möchten
Ben Sewards
Funktioniert perfekt mit dem Android-Beispiel "Vorheriger Ref. TabListener" - tnx. Das neueste Android "TabListener ref. Sample" [wie am 4. April 2013] ist wirklich, wirklich falsch.
Grzegorz Dev
Wo ist der Methodenaufruf ft.commit ()?
MSaudi
1
@ MuhammadBabar, siehe stackoverflow.com/questions/23248789/… . Wenn Sie addanstelle von replaceund drehen Sie den Bildschirm, haben Sie viele Fragmente onCreateView().
CoolMind
26

Ok, hier ist was ich herausgefunden habe.

Was ich nicht verstanden habe, ist, dass alle Fragmente, die an eine Aktivität angehängt werden, wenn eine Konfigurationsänderung stattfindet (Telefon dreht sich), neu erstellt und der Aktivität wieder hinzugefügt werden. (was Sinn macht)

Im TabListener-Konstruktor wurde die Registerkarte getrennt, wenn sie gefunden und an die Aktivität angehängt wurde. Siehe unten:

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

Später in der Aktivität onCreate wurde die zuvor ausgewählte Registerkarte aus dem Status der gespeicherten Instanz ausgewählt. Siehe unten:

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"));
}

Wenn die Registerkarte ausgewählt wurde, wurde sie im onTabSelected-Rückruf erneut zugeordnet.

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);
    }
}

Das angehängte Fragment ist der zweite Aufruf der Methoden onCreateView und onActivityCreated. (Das erste Mal, wenn das System die Aktivität und alle angehängten Fragmente neu erstellt.) Das erste Mal hätte das onSavedInstanceState-Bundle Daten gespeichert, aber nicht das zweite Mal.

Die Lösung besteht darin, das Fragment nicht im TabListener-Konstruktor zu trennen, sondern nur angehängt zu lassen. (Sie müssen es immer noch im FragmentManager anhand seines Tags finden.) Außerdem überprüfe ich in der onTabSelected-Methode, ob das Fragment getrennt ist, bevor ich es anhänge. Etwas wie das:

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
quelle
4
Die erwähnten Lösungen "Das Fragment im TabListener-Konstruktor nicht trennen" bewirken, dass sich die Tab-Fragmente überlappen. Ich kann den Inhalt der anderen Fragmente sehen. Es funktioniert nicht bei mir.
Aksel Fatih
@ flock.dux Ich bin mir nicht sicher, was du mit Überlappung meinst. Android kümmert sich darum, wie sie angeordnet sind. Wir geben lediglich das Anhängen oder Trennen an. Es muss mehr los sein. Wenn Sie eine neue Frage mit Beispielcode stellen, können wir vielleicht herausfinden, was für Sie los ist.
Dave
1
Ich hatte das gleiche Problem (mehrere Fragmentkonstruktoraufrufe von Android). Ihr Befund löst mein Problem: Was ich nicht verstanden habe, ist, dass alle Fragmente, die bei einer Konfigurationsänderung (Telefon dreht sich) an eine Aktivität angehängt werden, neu erstellt und der Aktivität wieder hinzugefügt werden. (was Sinn macht)
Eugene
26

Ich hatte das gleiche Problem mit einer einfachen Aktivität, die nur ein Fragment trug (das manchmal ersetzt wurde). Dann wurde mir klar, dass ich onSaveInstanceState nur im Fragment (und onCreateView, um nach savedInstanceState zu suchen) und nicht in der Aktivität verwende.

Beim Einschalten des Geräts wird die Aktivität mit den Fragmenten neu gestartet und onCreated wird aufgerufen. Dort habe ich das gewünschte Fragment angehängt (was beim ersten Start richtig ist).

Auf dem Gerät wird Android zuerst das sichtbare Fragment neu erstellt und dann onCreate der enthaltenen Aktivität aufgerufen, an die mein Fragment angehängt wurde, wodurch das ursprünglich sichtbare ersetzt wird.

Um dies zu vermeiden, habe ich einfach meine Aktivität geändert, um nach savedInstanceState zu suchen:

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
 }

 }

Ich habe nicht einmal onSaveInstanceState der Aktivität überschrieben.

Gunnar Bernstein
quelle
Danke dir. Es hat mir bei AppCompatActivity + PreferenceFragmentCompat geholfen und ist abgestürzt, während Dialoge im Präferenzfragment nach einer Orientierungsänderung angezeigt wurden, da der Fragmentmanager bei der zweiten Fragmenterstellung null war.
RoK
12

Die beiden positiv bewerteten Antworten hier zeigen Lösungen für eine Aktivität mit Navigationsmodus NAVIGATION_MODE_TABS, aber ich hatte das gleiche Problem mit a NAVIGATION_MODE_LIST. Dadurch verloren meine Fragmente unerklärlicherweise ihren Zustand, als sich die Bildschirmausrichtung änderte, was wirklich ärgerlich war. Zum Glück konnte ich es aufgrund ihres hilfreichen Codes herausfinden.

Grundsätzlich muss bei Verwendung einer Listennavigation "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 ()" zweimal aufgerufen werden!

Siehe meine onNavigationItemSelected()Implementierung unten.

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

Ich lieh mir Inspiration für diese Lösung von hier .

XåpplI'-I0llwlg'I -
quelle
Diese Lösung funktioniert für mein ähnliches Problem mit einer Navigationsschublade. Ich finde das vorhandene Fragment anhand der ID und überprüfe, ob es dieselbe Klasse wie das neue Fragment hat, bevor ich es neu erstelle.
William
8

Für mich sieht es so aus, als ob Sie Ihren TabListener jedes Mal instanziieren ... also erstellt das System Ihr Fragment aus dem savedInstanceState neu und dann tun Sie es erneut in Ihrem onCreate.

Sie sollten das in a if(savedInstanceState == null)einschließen, damit es nur ausgelöst wird, wenn kein savedInstanceState vorhanden ist.

Barak
quelle
Ich denke nicht, dass das richtig ist. Wenn ich meinen addTab-Code in den if-Block einbinde, wird das Fragment an die Aktivität angehängt, aber es gibt keine Registerkarten. Es scheint, dass Sie die Registerkarten jedes Mal in der onCreate-Methode hinzufügen müssen. Ich werde mich weiter damit befassen und mehr posten, wenn ich es besser verstehe.
Dave