Best Practices ของการ Save/Restore State ของ Activity และ Fragment (StatedFragment deprecated แล้วจ้า)

Posted on 28 Apr 2015 03:20 | 8834 reads | 0 shares
 

รอบที่แล้วเรานำเสนอ วิธีการ Save/Restore Fragment State ด้วย StatedFragment ที่เราเขียนขึ้นมาไป ได้รับการตอบรับเยอะมาก ต้องขอขอบพระคุณทุกท่านครับ

อย่างไรก็ตาม StatedFragment เป็นการ Break Pattern ของ Android ไปพอสมควร ด้วยจุดประสงค์ทางแนวคิดว่า "มันคงจะเข้าใจง่ายขึ้นสำหรับมือใหม่ถ้าการ Save/Restore State ของ Fragment มีลักษณะการใช้งานเหมือน Activity ที่รวมเรื่อง View State และ Instance State เข้าไว้ที่เดียวกัน" เลยอยากทำการทดลองขึ้นมาว่าถ้ามันออกมาเหมือนกัน นักพัฒนาจะเข้าใจง่ายขึ้นหรือเปล่า และจะเป็น Pattern ที่น่าใช้ขึ้นหรือเปล่า

และถือเป็นการดีมากที่โพสต์ Blog ครั้งที่แล้วไป ทำให้ได้ Feedback จากนักพัฒนาทั่วโลก ต้องขอขอบพระคุณทุกคนมากมา ณ ที่นี้ด้วยครับ หลังจากทดลองใช้ StatedFragment มา 2 เดือนก็พบว่าถึง StatedFragment จะเข้าใจง่ายขึ้นก็จริง แต่แนวคิดกลับไม่ตรงตามที่ Android ถูกออกแบบไว้ ซึ่งอาจส่งผลต่อการทำโครงแอพฯที่ไม่ดีได้ในระยะยาว ซึ่งแม้แต่ตัวเราเองยังรู้สึกแปลกหน่อยๆกับโค้ดที่สร้างขึ้นมาเอง (แหะๆ)

ด้วยเหตุนี้ เราเลยขอตัดสินใจ Deprecated StatedFragment ทิ้งไป และขอนำเสนอวิธีการ Save/Restore Fragment State แบบถูกต้องตามที่แอนดรอยด์ถูกออกแบบไว้กันครับ =)

เข้าใจธรรมชาติการ Save/Restore State บน Activity

โดยปกติแล้วบน Activity เมื่อ onSaveInstanceState ถูกเรียก Activity จะวิ่งเข้าไปถาม View ทุกตัวที่แปะอยู่บน Activity โดยอัตโนมัติว่ามีอะไรจะเก็บสถานะไว้มั้ย หาก View ถูกเขียนการ Save/Restore View State ภายในไว้แล้ว Activity ก็จะเก็บค่าสถานะเหล่านั้นไว้ และเมื่อ onRestoreInstanceState ถูกเรียก ค่าเหล่านั้นก็จะถูกกระจายกลับไปยัง View ที่ถูกประกาศในชื่อ android:id เดียวกันกับตอนเก็บมา

มาดูกันแบบ Visualize กันเล้ยยย

activitysavestate_

activityrestorestate_

นี่คือสาเหตุว่าทำไม EditText ถึงยังมีข้อความที่พิมพ์ไว้อยู่ภายในถึงแม้ Activity และ EditText นั้นจะถูกทำลายไปแล้วแถมเราก็ยังไม่ได้ทำการบันทึกค่าอะไรไว้เลย เพราะมันมีการบันทึก State ในระดับ View ไว้โดยอัตโนมัตินั่นเอง และนี่ก็เป็นเหตุผลว่าทำไมถ้า View ไม่ได้ถูกประกาศ android:id ไว้จะไม่สามารถคืนสถานะของ View ได้ เพราะระบบโยนค่าที่บันทึกไว้กลับไม่ถูกนั่นเอง

ส่วนตัวแปรต่างๆที่อยู่ใน Activity จะถูกทำลายทิ้งหมดสิ้น ค่าเหล่านี้เองที่เราต้องบันทึกในคำสั่ง onSaveInstanceState และ onRestoreInstanceState

public class MainActivity extends AppCompatActivity {

    // These variable are destroyed along with Activity
    private int someVarA;
    private String someVarB;

    ...

    @Override
    protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        outState.putInt("someVarA", someVarA);
        outState.putString("someVarB", someVarB);
    }

    @Override
    protected void onRestoreInstanceState(Bundle savedInstanceState) {
        super.onRestoreInstanceState(savedInstanceState);
        someVarA = savedInstanceState.getInt("someVarA");
        someVarB = savedInstanceState.getString("someVarB");
    }

}

เข้าใจธรรมชาติการ Save/Restore State บน Fragment

พอเป็น Fragment กระบวนการเดิมก็ยังเกิดขึ้นในกรณีที่ Fragment ถูกทำลายโดยระบบ จะเกิดภาพแบบเดียวกับด้านบนทุกประการ

fragmentstatesaving

fragmentstaterestoring_

ซึ่งแน่นอน ตัวแปรทั้งหมดใน Fragment ก็จะหายไปด้วยพร้อม Fragment เราต้องทำการบันทึกและคืนค่ากลับมาผ่านคำสั่ง onSaveInstanceState และ onActivityCreated ตามลำดับ

public class MainFragment extends Fragment {

    // These variable are destroyed along with Activity
    private int someVarA;
    private String someVarB;

    ...

    @Override
    public void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        outState.putInt("someVarA", someVarA);
        outState.putString("someVarB", someVarB);
    }

    @Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        someVarA = savedInstanceState.getInt("someVarA");
        someVarB = savedInstanceState.getString("someVarB");
    }

}

แต่สำหรับ Fragment จะมีกรณีพิเศษเพิ่มขึ้นมาจาก Activity หน่อยนึงคือ เมื่อ Fragment มีการ Back ขึ้นมาจาก Backstack ปรากฎว่า View ภายในจะถูกทำลายและสร้างใหม่ขึ้นมา

แต่ในกรณีนี้ Fragment ไม่ได้ถูกทำลายไปด้วย มีแค่เพียง View ภายในเท่านั้นที่ถูกทำลาย ผลคือไม่มีการ Save State ใดๆในระดับ Fragment เกิดขึ้น แต่อย่างไรก็ตามระบบภายใน Fragment มีการ Save/Restore View State ไว้เรียบร้อย ทำให้ View ที่มีการเขียนส่วนของ Save/Restore View ภายในไว้ ยกตัวอย่างเช่น EditText จะสามารถกลับมาเหมือนเดิมได้ แต่กับ View ที่ไม่ได้ทำตรงนี้ไว้ หน้าตาจะกลับเป็นเหมือนที่ประกาศไว้ใน Layout XML เช่น TextView ที่ไม่ได้ประกาศ android:freezeText เป็น true ไว้ เป็นต้น

fragmentfrombackstack_

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

public class MainFragment extends Fragment {

    // These variable still persist in this case
    private int someVarA;
    private String someVarB;

    ...

}

ตรงนี้อาจจะสังเกตเห็นได้แล้วว่า ถ้า View ทุกตัวที่อยู่ใน Fragment มีการ Save/Restore View State ภายในไว้ การที่ View ใน Fragment ถูกทำลายและสร้างใหม่จะไม่สร้างปัญหาใดๆเลยและเราก็ไม่ต้องทำอะไรเพิ่มเลย เพราะ View ก็จะกลับมาเป็นเหมือนเดิม ส่วนตัวแปรต่างๆใน Fragment ก็จะยังอยู่ครบ

ดังนั้นเงื่อนไขแรกของ Best Practices การ Save/Restore State ของ Fragment จึงเป็น ...

View ทุกตัวที่ใช้ในแอพฯต้องมีการ Save/Restore View State ภายใน

View ทุกตัวในโลกของแอนดรอยด์มีความสามารถในการบันทึกและคืนค่า View State ภายในด้วยคำสั่ง onSaveInstanceState และ onRestoreInstanceState ขึ้นอยู่กับว่าผู้พัฒนาได้ Implement การ Save/Restore ไว้หรือไม่เท่านั้นเอง

public class CustomView extends View {

    ...

    @Override
    public Parcelable onSaveInstanceState() {
        Bundle bundle = new Bundle();
        // Save current View's state here
        return bundle;
    }

    @Override
    public void onRestoreInstanceState(Parcelable state) {
        super.onRestoreInstanceState(state);
        // Restore View's state here
    }

    ...

}

เบื้องต้นแล้ว View ทุกตัวที่เป็น View มาตรฐานได้ Implement ตรงนี้ไว้หมดทุกตัวแล้ว แค่ว่าบางตัวอาจจะต้องเปิดใช้งานหน่อย ยกตัวอย่างเช่น TextView ต้องใส่ android:freezeText="true" ก่อนถึงจะใช้งานตรงนี้ได้

แต่ถ้าพูดถึง 3rd Party Custom View ที่มีแจกทั่วไปใน Internet แล้ว บางอันก็ทำตรงนี้ไว้แต่บางอันก็ไม่ได้ทำ ทำให้อาจเกิดปัญหาในการใช้งานได้

หากพบเจอ 3rd Party Custom View ที่ไม่ได้ Save/Restore View State ไว้ภายใน วิธีที่ดีที่สุดคือ ให้สร้างคลาสใหม่ที่ Inherit class ที่ต้องการใช้แล้ว Implement onSaveInstanceState/onRestoreInstanceState ให้เรียบร้อยก่อนเอาไปใช้งาน

//
// Assumes that SomeSmartButton is a 3rd Party view that
// View State Saving/Restoring are not implemented internally
//
public class SomeBetterSmartButton extends SomeSmartButton {

    ...

    @Override
    public Parcelable onSaveInstanceState() {
        Bundle bundle = new Bundle();
        // Save current View's state here
        return bundle;
    }

    @Override
    public void onRestoreInstanceState(Parcelable state) {
        super.onRestoreInstanceState(state);
        // Restore View's state here
    }

    ...

}

และหากคุณทำ Custom View ใช้เองก็อย่าลืม Implement สองคำสั่งนี้ด้วย เป็นสิ่งที่สำคัญมากๆและต้องมีในทุกๆประเภทของ View ครับ

สุดท้าย อย่าลืมใส่ android:id ให้กับทุก View ที่มีการบันทึกและคืนค่า View State ภายในด้วย มิฉะนั้นระบบจะคืนค่าให้กับ View ไม่ถูก ส่งผลให้ View ไม่สามารถคืนสภาพเดิมได้ในที่สุด

    <EditText
        android:id="@+id/editText1"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

    <EditText
        android:id="@+id/editText2"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

    <CheckBox
        android:id="@+id/cbAgree"
        android:text="I agree"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

เพียงเท่านี้การ Save/Restore Fragment State ก็เสร็จไปกว่าครึ่งแล้ว !

แยก Fragment State กับ View State ออกจากกันให้ชัดเจน

ในการดีไซน์โครงสร้างของโค้ด เราควรจะแยก View State และ Fragment State ออกจากกัน หากคุณสมบัติใดเป็นของ View ก็ให้บันทึกและคืนค่าภายใน View แต่ถ้ามี Field ไหนเป็นตัวแปรและคุณสมบัติของ Fragment ให้จัดการเก็บบันทึกค่าไว้ใน onSaveInstanceState ก่อนจะคืนค่ากลับที่เดิมที่คำสั่ง onActivityCreated ยกตัวอย่างเช่นตัวแปรที่ได้มาจากการโหลดข้อมูลจาก Web Service ระหว่างรัน

public class MainFragment extends Fragment {

    ...

    private String dataGotFromServer;
    
    @Override
    public void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        outState.putString("dataGotFromServer", dataGotFromServer);
    }

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        dataGotFromServer = savedInstanceState.getString("dataGotFromServer");
    }

    ...

}

ขอเน้นย้ำว่า เพื่อให้ทุกอย่างเป็นไปตาม Structure ที่ถูกต้อง ห้ามทำการบันทึกหรือคืนค่า View State ที่ Fragment เป็นอันขาด ให้ทำในระดับ View เท่านั้น จงแยก View State และ Fragment State ออกจากกันให้ชัดเจนครับ

เรียบร้อยครับ แค่นี้ก็เรียบร้อย เป็น Best Practices ของการ Save/Restore State สำหรับ Fragment ที่ทุกอย่างจะกลับมาเหมือนเดิมไม่ว่าจะเป็นตัวแปรหรือหน้าตาของ View ครับ =)

บ้ายบาย StatedFragment สวัสดี NestedActivityResultFragment

ด้านบนนี้เป็นการอธิบายวิธีที่ Android ถูกออกแบบมาให้บันทึกและคืนค่า State ของ Activity และ Fragment ให้ทำตามวิธีด้านบนเลยจะดีที่สุดครับ ดังนั้นทางเราจึงขอ Deprecated StatedFragment ไป

อย่างไรก็ตาม ฟังก์ชั่นการรับ onActivityResult ใน Nested Fragment ขอ StatedFragment ยังคงมีประโยชน์อยู่ แต่เพื่อไม่ให้เป็นการสับสน ดังนั้นเราเลยสร้างคลาสใหม่ขึ้นมาชื่อว่า NestedActivityResultFragment ตั้งแต่ในเวอร์ชั่น 0.10.0 เป็นต้นไป ให้ใช้งานแทน StatedFragment สำหรับผู้ที่ต้องการใช้ฟังก์ชั่นนี้ครับ

วิธีใช้งานสามารถดูได้ที่ https://github.com/nuuneoi/StatedFragment ครับผม

หวังว่าจะเห็นภาพกันมากขึ้นนะครับ ต้องขอโทษด้วยที่ทำให้สับสนใน Blog ก่อนหน้านี้ ^^"

ผู้เขียน: nuuneoi (Android GDE, CTO & CEO at The Cheese Factory)
นักพัฒนาแบบ Full-Stack ที่มีประสบการณ์ในการพัฒนาแอพฯแอนดรอยด์มากว่า 6 ปีและอยู่ในวงการพัฒนาแอพฯมือถือมากว่า 12 ปี มีความสนใจทางด้าน Infrastucture, Service Side, Design, UI&UX, Hardware, Optimization, Cooking, Photographing, Blogging, Training, Public Speaking และรักที่จะแชร์เรื่องราวให้ผู้คนได้อ่านได้ฟังกันผ่าน Blog