Bagaimana cara menyimpan instance Fragmen dengan benar di tumpukan belakang?

489

Saya telah menemukan banyak contoh pertanyaan serupa pada SO tetapi sayangnya tidak ada jawaban yang memenuhi persyaratan saya.

Saya memiliki tata letak yang berbeda untuk potret dan lansekap dan saya menggunakan tumpukan belakang, yang keduanya mencegah saya menggunakan setRetainState()dan trik menggunakan rutin perubahan konfigurasi.

Saya menunjukkan informasi tertentu kepada pengguna di TextViews, yang tidak disimpan di penangan default. Saat menulis aplikasi saya hanya menggunakan Aktivitas, yang berikut ini berfungsi dengan baik:

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

Dengan Fragments, ini hanya berfungsi dalam situasi yang sangat spesifik. Secara khusus, yang rusak adalah mengganti sebuah fragmen, meletakkannya di tumpukan belakang dan kemudian memutar layar ketika fragmen baru ditampilkan. Dari apa yang saya mengerti, fragmen lama tidak menerima panggilan onSaveInstanceState()ketika diganti tetapi entah bagaimana tetap terhubung dengan Activitydan metode ini dipanggil nanti ketika Viewtidak ada lagi, jadi mencari salah satu TextViewhasil saya menjadi a NullPointerException.

Juga, saya menemukan bahwa menjaga referensi ke saya TextViewsbukan ide yang baik dengan Fragments, bahkan jika tidak apa - apa dengan Activity. Dalam hal ini, onSaveInstanceState()sebenarnya menyelamatkan negara tetapi masalah muncul kembali jika saya memutar layar dua kali ketika fragmen disembunyikan, karena onCreateView()itu tidak dipanggil dalam contoh baru.

Saya pikir menyelamatkan negara dalam onDestroyView()ke dalam beberapa Bundle-jenis unsur anggota kelas (itu sebenarnya lebih banyak data, bukan hanya satu TextView) dan tabungan yang di onSaveInstanceState()tetapi ada kelemahan lainnya. Terutama, jika fragmen yang saat ini ditampilkan, urutan memanggil dua fungsi dibalik, jadi saya akan perlu untuk account untuk dua situasi yang berbeda. Harus ada solusi yang lebih bersih dan benar!

Vee
sumber
1
Berikut adalah contoh yang sangat bagus dengan penjelasan detail juga. emuneee.com/blog/2013/01/07/saving-fragment-states
Hesam
1
Saya kedua tautan emunee.com. Ini memecahkan masalah stick UI bagi saya!
Reenactor Rob

Jawaban:

541

Untuk menyimpan keadaan instance dengan benar, FragmentAnda harus melakukan yang berikut:

1. Dalam fragmen, simpan status instance dengan mengganti onSaveInstanceState()dan mengembalikan 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. Dan poin penting , dalam aktivitas, Anda harus menyimpan instance fragmen onSaveInstanceState()dan mengembalikannya 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);
    }

}

Semoga ini membantu.

ThanhHH
sumber
7
Ini bekerja dengan baik untuk saya! Tidak ada solusi, tidak ada peretasan, cukup masuk akal dengan cara ini. Terima kasih untuk ini, membuat berjam-jam pencarian berhasil. SaveInstanceState () dari nilai-nilai Anda di fragmen, lalu simpan fragmen di Aktivitas memegang fragmen, lalu pulihkan :)
MattMatt
77
Apa itu mContent?
wizurd
14
@wizurd mContent adalah Fragmen, merujuk pada instance fragmen saat ini dalam aktivitas.
ThanhHH
13
Bisakah Anda menjelaskan bagaimana ini akan menyimpan keadaan instance dari fragmen di tumpukan belakang? Itulah yang diminta OP.
hitmaneidos
51
Ini tidak terkait dengan pertanyaan, onSaveInstance tidak dipanggil ketika fragmen dimasukkan ke backstack
Tadas Valaitis
87

Ini adalah cara yang saya gunakan saat ini ... ini sangat rumit tetapi setidaknya menangani semua situasi yang mungkin. Jika ada yang tertarik.

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

    /* (...) */

}

Atau , selalu ada kemungkinan untuk menjaga data ditampilkan Viewdalam variabel pasif s dan menggunakan data Viewhanya untuk menampilkannya, menjaga dua hal tetap sinkron. Saya tidak menganggap bagian terakhir sangat bersih.

Vee
sumber
68
Ini adalah solusi terbaik yang saya temukan sejauh ini tetapi masih ada satu (agak eksotis) masalah yang tersisa: jika Anda memiliki dua fragmen, Adan B, di mana Asaat ini di backstack dan Bterlihat, maka Anda kehilangan status A(yang tidak terlihat) satu) jika Anda memutar layar dua kali . Masalahnya adalah bahwa onCreateView()tidak dipanggil dalam skenario ini, hanya saja onCreate(). Jadi nanti, di onSaveInstanceState()sana tidak ada pandangan untuk menyelamatkan negara. Seseorang harus menyimpan dan kemudian menyelamatkan negara lewat onCreate().
devconsole
7
@devconsole Saya berharap saya bisa memberi Anda 5 suara untuk komentar ini! Rotasi ini dua kali telah membunuh saya selama berhari-hari.
DroidT
Terima kasih atas jawaban yang bagus! Saya punya satu pertanyaan. Di mana tempat terbaik untuk instantiate objek model (POJO) dalam fragmen ini?
Renjith
7
Untuk membantu menghemat waktu bagi orang lain, App.VSTUPdan App.STAVkeduanya adalah tag string yang mewakili objek yang mereka coba peroleh. Contoh: savedState = savedInstanceState.getBundle(savedGamePlayString);atausavedState.getDouble("averageTime")
Tanner Hallman
1
Ini adalah hal yang indah.
Ivan
61

Di perpustakaan dukungan terbaru, tidak ada solusi yang dibahas di sini yang diperlukan lagi. Anda dapat bermain dengan Activityfragmen Anda sesuka Anda menggunakan FragmentTransaction. Pastikan saja bahwa fragmen Anda dapat diidentifikasi dengan id atau tag.

Fragmen akan dipulihkan secara otomatis selama Anda tidak mencoba membuatnya kembali pada setiap panggilan onCreate(). Sebagai gantinya, Anda harus memeriksa apakah savedInstanceStatebukan nol dan menemukan referensi lama ke fragmen yang dibuat dalam kasus ini.

Berikut ini sebuah contoh:

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

Namun perlu dicatat bahwa saat ini ada bug ketika memulihkan keadaan tersembunyi suatu fragmen. Jika Anda menyembunyikan fragmen dalam aktivitas Anda, Anda harus mengembalikan keadaan ini secara manual dalam kasus ini.

Ricardo
sumber
2
Apakah ini memperbaiki sesuatu yang Anda perhatikan saat menggunakan perpustakaan dukungan atau apakah Anda membacanya di suatu tempat? Apakah ada informasi lain yang bisa Anda berikan tentang itu? Terima kasih!
Piovezan
1
@Piovezan dapat disimpulkan secara tersirat dari dokumen. Sebagai contoh, dokumen beginTransaction () berbunyi sebagai berikut: "Ini karena kerangka kerja menyimpan bagian Anda saat ini di negara bagian (...)". Saya juga telah mengkode aplikasi saya dengan perilaku yang diharapkan ini untuk beberapa waktu sekarang.
Ricardo
1
@ Ricardo apakah ini berlaku jika menggunakan ViewPager?
Derek Beattie
1
Biasanya ya, kecuali jika Anda mengubah perilaku default pada implementasi FragmentPagerAdapteratau FragmentStatePagerAdapter. Jika Anda melihat kode FragmentStatePagerAdapter, misalnya, Anda akan melihat bahwa restoreState()metode mengembalikan fragmen dari yang FragmentManagerAnda berikan sebagai parameter saat membuat adaptor.
Ricardo
4
Saya pikir kontribusi ini adalah jawaban terbaik untuk pertanyaan awal. Itu juga yang - menurut saya - paling sesuai dengan cara kerja platform Android. Saya akan merekomendasikan menandai jawaban ini sebagai "Diterima" untuk lebih membantu pembaca masa depan.
dbm
17

Saya hanya ingin memberikan solusi yang saya temukan yang menangani semua kasus yang disajikan dalam posting ini yang saya dapatkan dari Vasek dan devconsole. Solusi ini juga menangani kasus khusus ketika ponsel diputar lebih dari satu kali sementara fragmen tidak terlihat.

Di sini adalah saya menyimpan bundel untuk digunakan kemudian karena onCreate dan onSaveInstanceState adalah satu-satunya panggilan yang dibuat ketika fragmen tidak terlihat

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

Karena destroyView tidak dipanggil dalam situasi rotasi khusus, kami dapat yakin bahwa jika itu menciptakan keadaan, kami harus menggunakannya.

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

Bagian ini akan sama.

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

Sekarang inilah bagian yang sulit. Dalam metode onActivityCreated saya, saya instantiate variabel "myObject" tetapi rotasi terjadi padaActivity dan onCreateView tidak dipanggil. Karenanya, myObject akan menjadi nol dalam situasi ini ketika orientasi berputar lebih dari satu kali. Saya menyiasatinya dengan menggunakan kembali bundel yang sama yang disimpan di onCreate sebagai bundel keluar.

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

Sekarang di mana pun Anda ingin mengembalikan negara hanya menggunakan bundel StateState

  @Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    ...
    if(savedState != null) {
        myObject = (MyObject) savedState.getSerializable(SAVED_BUNDLE_TAG);
    }
    ...
}
DroidT
sumber
Bisakah Anda memberi tahu saya ... Apa itu "MyObject" di sini?
kavie
2
Apa pun yang Anda inginkan. Itu hanyalah sebuah contoh yang mewakili sesuatu yang akan disimpan dalam bundel.
DroidT
3

Berkat DroidT , saya membuat ini:

Saya menyadari bahwa jika Fragmen tidak mengeksekusi onCreateView (), pandangannya tidak instantiated. Jadi, jika fragmen di tumpukan belakang tidak membuat pandangannya, saya menyimpan status tersimpan terakhir, jika tidak, saya membangun bundel saya sendiri dengan data yang ingin saya simpan / pulihkan.

1) Perpanjang kelas ini:

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) Dalam Fragmen Anda, Anda harus memiliki ini:

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

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

    //restore your data here

    return true;
}

3) Misalnya, Anda dapat memanggil hasSavedState di onActivityCreated:

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

    if (hasSavedState()) {
        return;
    }

    //your code here
}
Noturno
sumber
-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
sumber
1
Tidak terkait dengan pertanyaan awal.
Jorge E. Hernández