Fragment onCreateView dan onActivityCreated dipanggil dua kali

101

Saya mengembangkan aplikasi menggunakan Android 4.0 ICS dan fragmen.

Pertimbangkan contoh yang dimodifikasi ini dari aplikasi contoh demo API ICS 4.0.3 (API level 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;
    }
}

}

Berikut adalah keluaran yang diambil dari menjalankan contoh ini dan kemudian memutar telepon:

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

Pertanyaan saya adalah, mengapa onCreateView dan onActivityCreated dipanggil dua kali? Pertama kali dengan Bundle dengan status tersimpan dan kedua kalinya dengan null storedInstanceState?

Ini menyebabkan masalah dengan mempertahankan status fragmen saat diputar.

Dave
sumber
2
Saya pikir pertanyaan ini dapat terkait dengan stackoverflow.com/a/8678705/404395
marioosh

Jawaban:

45

Saya juga menggaruk-garuk kepala tentang ini untuk sementara waktu, dan karena penjelasan Dave agak sulit untuk dipahami, saya akan memposting kode saya (tampaknya berfungsi):

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

Seperti yang Anda lihat, ini sangat mirip dengan contoh Android, selain tidak melepaskan di konstruktor, dan menggunakan replace daripada menambahkan .

Setelah banyak headscratching dan trial-and-error, saya menemukan bahwa menemukan fragmen di konstruktor tampaknya membuat masalah onCreateView ganda menghilang secara ajaib (saya berasumsi itu hanya berakhir menjadi null untuk onTabSelected saat dipanggil melalui jalur ActionBar.setSelectedNavigationItem () saat menyimpan / memulihkan negara).

Staffan
sumber
Bekerja dengan sangat baik! Anda menyelamatkan tidur malam saya! Terima kasih :)
jaibatrik
Anda juga dapat menggunakan fragment.getClass (). getName () jika Anda ingin menghapus variabel kelas dan menghapus parameter dari panggilan
Ben Sewards
Bekerja sempurna dengan "referensi sebelumnya TabListener" Android sample - tnx. Android terbaru "TabListener ref. Sample" [seperti pada 4 ix 2013] benar-benar salah.
Grzegorz Dev
di mana pemanggilan metode ft.commit () ??
MSaudi
1
@MuhammadBabar, lihat stackoverflow.com/questions/23248789/… . Jika Anda menggunakan addbukannya replacedan memutar layar, Anda akan memiliki banyak fragmen ' onCreateView().
CoolMind
26

Oke, Ini yang saya temukan.

Yang tidak saya mengerti adalah bahwa semua fragmen yang dilampirkan ke aktivitas saat terjadi perubahan konfigurasi (ponsel berputar) dibuat ulang dan ditambahkan kembali ke aktivitas. (yang masuk akal)

Apa yang terjadi di konstruktor TabListener adalah tab terlepas jika ditemukan dan dilampirkan ke aktivitas. Lihat di bawah:

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

Kemudian di aktivitas onCreate, tab yang dipilih sebelumnya dipilih dari status instance tersimpan. Lihat di bawah:

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

Saat tab dipilih, itu akan dipasang kembali di callback 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);
    }
}

Fragmen yang dilampirkan adalah panggilan kedua ke metode onCreateView dan onActivityCreated. (Yang pertama adalah saat sistem membuat ulang aktivitas dan semua fragmen yang dilampirkan) Pertama kali Bundel onSavedInstanceState akan menyimpan data tetapi tidak untuk yang kedua kalinya.

Solusinya adalah dengan tidak melepaskan fragmen di konstruktor TabListener, biarkan saja terpasang. (Anda masih perlu menemukannya di FragmentManager dengan tag itu) Selain itu, dalam metode onTabSelected saya memeriksa untuk melihat apakah fragmen terlepas sebelum saya memasangnya. Sesuatu seperti ini:

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
sumber
4
Solusi "tidak melepaskan fragmen dalam konstruktor TabListener" yang disebutkan menyebabkan fragmen tab saling tumpang tindih. Saya bisa melihat isi fragmen lainnya. Itu tidak berhasil untuk saya.
Aksel Fatih
@ flock.dux Saya tidak yakin apa yang Anda maksud dengan saling tumpang tindih. Android menangani bagaimana mereka ditata jadi, kami hanya menentukan lampirkan atau lepas. Pasti ada lebih banyak hal yang terjadi. Mungkin jika Anda mengajukan pertanyaan baru dengan kode contoh, kami dapat mengetahui apa yang terjadi pada Anda.
Dave
1
Saya memiliki masalah yang sama (beberapa panggilan konstruktor fragmen dari Android). Temuan Anda memecahkan masalah saya: Yang tidak saya mengerti adalah bahwa semua fragmen yang dilampirkan ke suatu aktivitas saat terjadi perubahan konfigurasi (telepon berputar) dibuat ulang dan ditambahkan kembali ke aktivitas. (yang masuk akal)
eugene
26

Saya memiliki masalah yang sama dengan Activity sederhana yang hanya membawa satu fragmen (yang terkadang diganti). Saya kemudian menyadari bahwa saya menggunakan onSaveInstanceState hanya di fragmen (dan onCreateView untuk memeriksa saveInstanceState), bukan dalam aktivitas.

Di perangkat, aktifkan aktivitas yang berisi fragmen akan dimulai ulang dan onCreated dipanggil. Di sana saya melampirkan fragmen yang diperlukan (yang benar pada awal pertama).

Pada perangkat, Android pertama-tama membuat ulang fragmen yang terlihat, lalu memanggil onCreate dari aktivitas penampung tempat fragmen saya dipasang, sehingga menggantikan yang asli yang terlihat.

Untuk menghindarinya, saya cukup mengubah aktivitas saya untuk memeriksa disimpanInstanceState:

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
 }

 }

Saya bahkan tidak menimpa aktivitas onSaveInstanceState.

Gunnar Bernstein
sumber
Terima kasih. Ini membantu saya dengan AppCompatActivity + PreferenceFragmentCompat dan mengalami error saat menampilkan dialog di fragmen preferensi setelah orientasi berubah, karena pengelola fragmen bernilai null pada pembuatan fragmen kedua.
RoK
12

Dua jawaban yang diberi suara positif di sini menunjukkan solusi untuk Aktivitas dengan mode navigasi NAVIGATION_MODE_TABS, tetapi saya memiliki masalah yang sama dengan a NAVIGATION_MODE_LIST. Itu menyebabkan Fragmen saya kehilangan statusnya secara tak terduga ketika orientasi layar berubah, yang sangat mengganggu. Untungnya, karena kode mereka yang membantu, saya berhasil mengetahuinya.

Pada dasarnya, saat menggunakan navigasi daftar, `` 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 () `dipanggil dua kali!

Lihat saya onNavigationItemSelected() penerapan bawah ini.

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

Saya meminjam inspirasi untuk solusi ini dari sini .

XåpplI'-I0llwlg'I -
sumber
Solusi ini berfungsi untuk masalah serupa saya dengan laci navigasi. Saya menemukan fragmen yang ada berdasarkan ID dan memeriksa apakah itu memiliki kelas yang sama dengan fragmen baru sebelum membuatnya kembali.
William
8

Bagi saya sepertinya itu karena Anda membuat instance TabListener Anda setiap saat ... jadi sistem membuat ulang fragmen Anda dari storedInstanceState dan kemudian Anda melakukannya lagi di onCreate Anda.

Anda harus membungkusnya if(savedInstanceState == null)sehingga hanya aktif jika tidak ada storedInstanceState.

Barak
sumber
Saya tidak berpikir itu benar. Ketika saya membungkus kode addTab saya di blok if, fragmen dilampirkan ke aktivitas tetapi tidak ada tab. Tampaknya Anda harus menambahkan tab setiap kali dalam metode onCreate. Saya akan terus melihat ini dan memposting lebih banyak karena saya lebih mengerti.
Dave