วิธีการบันทึกสถานะของชิ้นส่วนอินสแตนซ์ในสแต็กหลังอย่างถูกต้อง


489

ฉันพบคำถามที่คล้ายกันจำนวนมากใน SO แต่ไม่มีคำตอบที่ตรงกับความต้องการของฉัน

ฉันมีเลย์เอาต์ที่แตกต่างกันสำหรับแนวตั้งและแนวนอนและฉันใช้แบ็คสแต็กซึ่งทั้งสองป้องกันไม่ให้ฉันใช้setRetainState()และลูกเล่นโดยใช้รูทีนการเปลี่ยนแปลงการกำหนดค่า

ฉันแสดงข้อมูลบางอย่างแก่ผู้ใช้ใน TextViews ซึ่งไม่ได้รับการบันทึกในตัวจัดการเริ่มต้น เมื่อเขียนแอปพลิเคชันของฉัน แต่เพียงผู้เดียวโดยใช้กิจกรรมสิ่งต่อไปนี้ใช้ได้ดี:

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

ด้วยFragments สิ่งนี้ใช้ได้เฉพาะในสถานการณ์ที่เฉพาะเจาะจงเท่านั้น โดยเฉพาะสิ่งที่แตกอย่างน่ากลัวคือแทนที่ชิ้นส่วนวางไว้ในกองหลังแล้วหมุนหน้าจอในขณะที่ชิ้นส่วนใหม่จะปรากฏขึ้น จากสิ่งที่ฉันเข้าใจส่วนเก่าไม่ได้รับสายไปonSaveInstanceState()เมื่อถูกแทนที่ แต่การเข้าพักที่เชื่อมโยงอย่างใดไปActivityและวิธีการนี้เรียกว่าต่อมาเมื่อมันViewไม่ได้อยู่อีกต่อไปดังนั้นมองหาใด ๆ ของฉันTextViewผล s NullPointerExceptionเป็น

นอกจากนี้ฉันยังพบว่าการอ้างอิงถึงฉันTextViewsไม่ใช่ความคิดที่ดีกับFragments ถึงแม้ว่ามันจะโอเคกับActivityก็ตาม ในกรณีนี้onSaveInstanceState()จริง ๆ แล้วบันทึกสถานะ แต่ปัญหาปรากฏขึ้นอีกครั้งถ้าฉันหมุนหน้าจอสองครั้งเมื่อชิ้นส่วนถูกซ่อนเนื่องจากonCreateView()ไม่ได้รับการเรียกในอินสแตนซ์ใหม่

ผมคิดว่าการออมของรัฐในonDestroyView()เป็นบางส่วนBundleองค์ประกอบสมาชิกระดับชนิด (เป็นข้อมูลจริงมากขึ้นไม่เพียงหนึ่งTextView) และบันทึกว่าในonSaveInstanceState()แต่มีข้อบกพร่องอื่น ๆ ในขั้นต้นถ้าส่วนย่อยถูกแสดงอยู่ลำดับของการเรียกฟังก์ชั่นทั้งสองจะกลับด้านดังนั้นฉันต้องพิจารณาสถานการณ์ที่แตกต่างกันสองสถานการณ์ จะต้องมีวิธีที่สะอาดและถูกต้อง!


1
นี่คือตัวอย่างที่ดีมากพร้อมคำอธิบายโดยละเอียดเช่นกัน emuneee.com/blog/2013/01/07/saving-fragment-states
Hesam

1
ฉันสองลิงค์ emunee.com มันแก้ไขปัญหา UI ติดสำหรับฉัน!
Reenactor Rob

คำตอบ:


541

หากต้องการบันทึกสถานะอินสแตนซ์ของFragmentคุณอย่างถูกต้องคุณควรทำสิ่งต่อไปนี้:

1.ในส่วนให้บันทึกสถานะอินสแตนซ์โดยการแทนที่onSaveInstanceState()และกู้คืนใน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.และจุดสำคัญในกิจกรรมที่คุณต้องบันทึกเช่นชิ้นส่วนในและเรียกคืนในonSaveInstanceState()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);
    }

}

หวังว่านี่จะช่วยได้


7
มันทำงานได้อย่างสมบูรณ์แบบสำหรับฉัน! ไม่มีวิธีแก้ไขปัญหาไม่มีแฮ็คแค่ใช้วิธีนี้ ขอบคุณสำหรับสิ่งนี้ทำให้การค้นหาประสบความสำเร็จหลายชั่วโมง SaveInstanceState () ของค่าของคุณเป็นแฟรกเมนต์จากนั้นบันทึกแฟรกเมนต์ในกิจกรรมที่เก็บ
แฟรกเมนต์

77
mContent คืออะไร
wizurd

14
@wizurd mContent เป็นแฟรกเมนต์อ้างอิงถึงอินสแตนซ์ของแฟรกเมนต์ปัจจุบันในกิจกรรม
ThanhHH

13
คุณช่วยอธิบายได้ว่าวิธีนี้จะบันทึกสถานะอินสแตนซ์ของแฟรกเมนต์ในแบ็คสแต็กหรือไม่ นั่นคือสิ่งที่ OP ถาม
hitmaneidos

51
นี้ไม่เกี่ยวข้องกับคำถาม onSaveInstance จะไม่ถูกเรียกเมื่อชิ้นส่วนถูก backstack
Tadas Valaitis

87

นี่คือวิธีที่ฉันใช้ในขณะนี้ ... มันซับซ้อนมาก แต่อย่างน้อยก็จัดการกับสถานการณ์ที่เป็นไปได้ทั้งหมด ในกรณีที่ใครสนใจ

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

    /* (...) */

}

หรืออาจเป็นไปได้ที่จะเก็บข้อมูลที่แสดงในรูปแบบพาสซีฟViewในตัวแปรและใช้Views เพื่อแสดงข้อมูลเท่านั้นโดยทำให้ทั้งสองสิ่งสอดคล้องกัน ฉันไม่คิดว่าส่วนสุดท้ายสะอาดมาก แต่


68
นี่คือทางออกที่ดีที่สุดที่ฉันได้พบเพื่อให้ห่างไกล แต่มียังคงเป็นหนึ่ง (ที่ค่อนข้างแปลกใหม่) ปัญหาที่เหลือ: ถ้าคุณมีสองชิ้นส่วนAและBที่Aเป็นอยู่ในปัจจุบันใน backstack และBจะมองเห็นแล้วคุณจะสูญเสียสถานะของA(มองไม่เห็น อย่างใดอย่างหนึ่ง) ถ้าคุณหมุนจอแสดงผลเป็นครั้งที่สอง ปัญหาคือว่าไม่ได้รับการเรียกว่าในสถานการณ์นี้เท่านั้นonCreateView() onCreate()ดังนั้นในภายหลังในonSaveInstanceState()มุมมองที่ไม่มีการบันทึกรัฐจาก หนึ่งจะต้องเก็บและจากนั้นบันทึกสถานะที่ผ่านonCreate()มา
devconsole

7
@devconsole ฉันหวังว่าฉันจะให้ 5 คะแนนโหวตสำหรับความคิดเห็นนี้! การหมุนรอบสองครั้งนี้ทำให้ฉันต้องฆ่าฉันหลายวัน
DroidT

ขอบคุณสำหรับคำตอบที่ยอดเยี่ยม! ฉันมีคำถามหนึ่งข้อ ที่ที่ดีที่สุดในการสร้างอินสแตนซ์ออบเจ็กต์โมเดล (POJO) ในส่วนนี้อยู่ที่ใด
Renjith

7
เพื่อช่วยประหยัดเวลาสำหรับผู้อื่นApp.VSTUPและApp.STAVเป็นทั้งแท็กสตริงที่แสดงถึงวัตถุที่พวกเขากำลังพยายามที่จะได้รับ ตัวอย่าง: savedState = savedInstanceState.getBundle(savedGamePlayString);หรือsavedState.getDouble("averageTime")
Tanner Hallman

1
นี่คือเรื่องของความงาม
Ivan

61

ในไลบรารีการสนับสนุนล่าสุดไม่มีวิธีแก้ไขปัญหาที่กล่าวถึงที่นี่มีความจำเป็นอีกต่อไป คุณสามารถเล่นกับเศษของตามที่คุณต้องการใช้Activity FragmentTransactionเพียงตรวจสอบให้แน่ใจว่าชิ้นส่วนของคุณสามารถระบุได้ด้วย id หรือแท็ก

onCreate()ชิ้นส่วนที่จะถูกเรียกคืนโดยอัตโนมัติตราบใดที่คุณไม่ได้พยายามที่จะสร้างพวกเขาในการเรียกร้องให้ทุก แต่คุณควรตรวจสอบว่าsavedInstanceStateไม่ใช่โมฆะและค้นหาการอ้างอิงเก่าไปยังแฟรกเมนต์ที่สร้างขึ้นในกรณีนี้

นี่คือตัวอย่าง:

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

โปรดทราบว่าขณะนี้มีข้อผิดพลาดเมื่อกู้คืนสถานะที่ซ่อนอยู่ของชิ้นส่วน หากคุณซ่อนชิ้นส่วนในกิจกรรมของคุณคุณจะต้องเรียกคืนสถานะนี้ด้วยตนเองในกรณีนี้


2
การแก้ไขนี้เป็นสิ่งที่คุณสังเกตเห็นในขณะที่ใช้ไลบรารีการสนับสนุนหรือคุณอ่านมันเกี่ยวกับที่อื่นหรือไม่? มีข้อมูลเพิ่มเติมที่คุณสามารถให้เกี่ยวกับมันได้หรือไม่ ขอบคุณ!
Piovezan

1
@Piovezan มันสามารถอนุมานจากเอกสารโดยปริยาย ตัวอย่างเช่นdoc startTransaction ()อ่านดังนี้: "นี่เป็นเพราะกรอบงานจะดูแลการบันทึกชิ้นส่วนปัจจุบันของคุณในสถานะ (... )" ฉันได้เขียนโค้ดแอพของฉันด้วยพฤติกรรมที่คาดหวังนี้มาระยะหนึ่งแล้ว
Ricardo

1
@Ricardo ใช้กับ ViewPager ได้ไหม?
Derek Beattie

1
ปกติใช่เว้นแต่คุณจะเปลี่ยนพฤติกรรมเริ่มต้นในการดำเนินงานของคุณหรือFragmentPagerAdapter FragmentStatePagerAdapterหากคุณดูที่รหัสของFragmentStatePagerAdapterคุณจะเห็นว่าrestoreState()วิธีการกู้คืนชิ้นส่วนจากFragmentManagerคุณผ่านเป็นพารามิเตอร์เมื่อสร้างอะแดปเตอร์
Ricardo

4
ฉันคิดว่าการมีส่วนร่วมนี้เป็นคำตอบที่ดีที่สุดสำหรับคำถามเดิม ในความคิดของฉันก็เป็นแนวที่ดีที่สุดกับการทำงานของแพลตฟอร์ม Android ฉันขอแนะนำให้ทำเครื่องหมายคำตอบนี้เป็น "ยอมรับ" เพื่อช่วยผู้อ่านในอนาคตได้ดียิ่งขึ้น
dbm

17

ฉันแค่ต้องการที่จะแก้ปัญหาที่เกิดขึ้นกับที่จัดการทุกกรณีที่นำเสนอในโพสต์นี้ที่ฉันได้รับจาก Vasek และ devconsole วิธีการแก้ปัญหานี้ยังจัดการกรณีพิเศษเมื่อโทรศัพท์หมุนมากกว่าหนึ่งครั้งในขณะที่มองไม่เห็นชิ้นส่วน

ที่นี่ฉันถูกเก็บมัดเพื่อใช้ในภายหลังตั้งแต่ onCreate และ onSaveInstanceState เป็นสายเดียวที่เกิดขึ้นเมื่อชิ้นส่วนไม่สามารถมองเห็นได้

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

เนื่องจาก destroyView ไม่ได้ถูกเรียกในสถานการณ์การหมุนพิเศษเราจึงมั่นใจได้ว่าหากมันสร้างสถานะที่เราควรใช้

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

ส่วนนี้จะเหมือนกัน

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

ตอนนี้ที่นี่เป็นส่วนที่ยุ่งยาก ในเมธอด onActivityCreated ของฉันฉันสร้างอินสแตนซ์ของตัวแปร "myObject" แต่การหมุนเกิดขึ้นบนแอคทีฟและ onCreateView ไม่ได้ถูกเรียก ดังนั้น myObject จะเป็นโมฆะในสถานการณ์นี้เมื่อการวางแนวหมุนมากกว่าหนึ่งครั้ง ฉันได้รับสิ่งนี้โดยนำชุดเดิมที่บันทึกไว้ใน onCreate กลับมาใช้เป็นชุดกลุ่มที่กำลังออกไป

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

ตอนนี้ทุกที่ที่คุณต้องการคืนค่าสถานะเพียงใช้บันเดิลที่บันทึกไว้

  @Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    ...
    if(savedState != null) {
        myObject = (MyObject) savedState.getSerializable(SAVED_BUNDLE_TAG);
    }
    ...
}

คุณบอกฉันได้ไหม ... "MyObject" ที่นี่คืออะไร
kavie

2
ทุกสิ่งที่คุณต้องการให้เป็น มันเป็นเพียงตัวอย่างที่แสดงถึงสิ่งที่จะถูกบันทึกไว้ในชุดข้อมูล
DroidT

3

ขอบคุณDroidTฉันทำสิ่งนี้:

ฉันรู้ว่าหาก Fragment ไม่ได้เรียกใช้งาน onCreateView () มุมมองของมันจะไม่ถูกสร้างขึ้นมา ดังนั้นหากแฟรกเมนต์บนสแต็กหลังไม่ได้สร้างมุมมองฉันจะบันทึกสถานะที่เก็บไว้ล่าสุดมิฉะนั้นฉันจะสร้างบันเดิลของตัวเองด้วยข้อมูลที่ฉันต้องการบันทึก / กู้คืน

1) ขยายคลาสนี้:

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) ในแฟรกเมนต์ของคุณคุณต้องมีสิ่งนี้:

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

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

    //restore your data here

    return true;
}

3) ตัวอย่างเช่นคุณสามารถโทร hasSavedState ใน onActivityCreated:

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

    if (hasSavedState()) {
        return;
    }

    //your code here
}

-6
final FragmentTransaction ft = getFragmentManager().beginTransaction();
ft.hide(currentFragment);
ft.add(R.id.content_frame, newFragment.newInstance(context), "Profile");
ft.addToBackStack(null);
ft.commit();

1
ไม่เกี่ยวข้องกับคำถามเดิม
Jorge E. Hernández
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.