26 ตุลาคม 2558

[Android Code] Save/Restore กับ Instance State ใน Fragment ควรทำอย่างไรกันนะ?



        มาแล้ว!! ห่างหายนอนตายกับงานมาหลายสัปดาห์หลังจากปล่อยบทความ [Android Code] มา Save/Restore กับ Instance State บน Activity ให้ถูกต้องกันเถอะ คราวนี้ก็ถึงคราวของ Fragment กันบ้าง ว่าการจัดการกับ Instance State บน Fragment เค้าทำยังไงกัน เพราะการจัดการ Save/Restore Instance State ของ Fragment นั้นไม่ได้เหมือนกับบน Activity เป๊ะๆซักเท่าไร

Save/Restore Instance State ของ Fragment ต่างกับ Activity ยังไง?

        ยังจำภาพนี้กันได้ใช่มั้ยครับ?


        อันนี้เป็นภาพ Cycle เวลาที่ Activity ทำการ Save/Restore Instance State (ถ้าจำไม่ได้ให้เขกโต๊ะ 3 ทีเป็นการลงโทษ)

        ถึงแม้ว่า Fragment จะมี Life Cycle คล้ายกับ Activity แต่ก็มีความต่างอยู่บ้าง ดังนั้นจึงทำให้ลำดับการทำงานเมื่อ Save/Restore ต่างจาก Activity แบบนี้


        จะเห็นว่า Fragment มี onSavedInstanceState แต่ทว่ากลับไม่มี onRestoreInstanceState ให้เรียกใช้งาน Fragment [Android Developer] เพราะเวลา Restore จะทำใน onActivityCreated แทน

        โดยที่ onCreate, onCreateView และ onActivityCreated จะมี Bundle ที่ชื่อว่า savedInstanceState ส่งเข้ามาให้ด้วย เผื่อเวลามีการ Restore Instance State

public void onCreate(Bundle savedInstanceState) {
    ...
}

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    ...
}

@Override
public void onActivityCreated(Bundle savedInstanceState) {
    ...
}

        ถึงแม้ว่าทั้ง 3 เมธอดนี้จะส่ง Bundle จากการ Restore มาให้ แต่เวลา Restore ข้อมูลจริงๆให้ทำใน onActivityCreated นะ

เตรียม Fragment ให้พร้อม 

        เพื่อให้ไปทีละขั้นทีละตอน ก่อนอื่นก็ต้องเตรียม Fragment ขึ้นมาก่อนเนอะ ก็สร้างขึ้นมาง่ายๆเลย แล้วเอาไปแปะลงใน Activity โดยใช้คำสั่ง Replace ซะ (เป็นวิธีแปะ Fragment ที่ส่วนใหญ่ใช้กัน)

        สร้าง Fragment ที่ชื่อว่า CategoryFragment (นามสมมติ) ขึ้นมา

CategoryFragment.java
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.ViewGroup;

public class CategoryFragment extends Fragment {
    public static CategoryFragment newInstance() {
        return new CategoryFragment();
    }

    public CategoryFragment() { }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
    } 

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        return inflater.inflate(R.layout.fragment_category, container, false);
    }

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

        แปะลงใน Activity แบบนี้

MainActivity.java
import android.support.v4.app.Fragment;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;

public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        if (savedInstanceState == null) {
            replaceCategoryFragment();
        }
    }

    public void replaceCategoryFragment() {
        replaceFragment(CategoryFragment.newInstance());
    }
    
    public void replaceFragment(Fragment fragment) {
        getSupportFragmentManager().beginTransaction()
                .replace(R.id.layout_fragment_container, fragment)
                .addToBackStack(null)
                .commit();
    }
}

        โดย Activity ก็ไม่มีอะไร มีแค่ FrameLayout ที่เอาไว้แปะ Fragment อย่างเดียว

activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <FrameLayout
        android:id="@+id/layout_fragment_container"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</LinearLayout>


        พอเริ่มทำงานก็จะได้ออกมาประมาณนี้


สิ่งที่ควรรู้ในการ Save/Restore Instance State บน Fragment

        อย่างแรกก็คือ ใน onSaveInstanceState ถ้าผู้ที่หลงเข้ามาอ่านไม่ได้สั่งให้เก็บข้อมูลใดๆ Fragment ตัวนั้นก็จะไม่มีการ Save/Restore Instance State ดังนั้นอย่าแปลกใจถ้าทำไม Fragment ไม่ยอม Restore โดยที่ไม่ได้เก็บข้อมูลอะไรลงไป (ถ้าไม่ได้ยัดอะไรมา จะให้ Restore ทำมะเขือเผือกอะไรเล่าเนอะ)

        อย่างที่สอง Fragment ใดๆก็ตามที่แปะบน Activity เดี๋ยว Activity มันจะ Restore Fragment ตัวนั้นให้เอง (แล้ว Fragment มันก็จะ Restore Instance State ที่อยู่ในตัวมันอีกที)โดยที่ผู้ที่หลงเข้ามาอ่านไม่ต้องทำอะไร และคำสั่งแปะ Fragment ให้เรียกใช้ตอนที่ Activity เริ่มทำงานเท่านั้น (สาเหตุที่ต้องเช็ค savedInstanceState == null นั่นเอง) แต่ถ้าไม่ได้แปะ Fragment ตั้งแต่ตอนสร้าง Activity ขึ้นมา ก็ไม่ต้องทำอะไร

@Override
protected void onCreate(Bundle savedInstanceState) {
    ...

    if (savedInstanceState == null) {
        // แปะ Fragment เริ่มต้นที่นี่
    }
}

        อย่างที่สาม ควร Restore Instance State ใน onActivityCreated ครับ เพราะเป็นหลังจากที่ View ถูกสร้างขึ้นมาเรียบร้อยแล้ว

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

    if (savedInstanceState == null) {
        // Fragment ถูกสร้างขึ้นมาครั้งแรก
    } else {
        // Fragment ถูก Restore ขึ้นมา
    }
}

        และอย่างสุดท้าย ให้ Restore แค่ Instance ที่ใช้ใน Fragment เท่านั้น ส่วน View ที่อยู่ใน Fragment นั้นควร Restore State ได้ด้วยตัวเอง (เหมือนกับบทความของ Activity นั่นเอง)

ใช้ตัวอย่างแบบบทความที่แล้ว

        บทความที่แล้วเจ้าของบล็อกจำลองคำสั่งขึ้นมาเพื่อสมมติว่าเป็นคำสั่งเรียกข้อมูลจากเซิฟเวอร์ ในบทความนี้ก็จะขอหยิบมาใช้เหมือนเดิม (Reuse นี่เอง) รวมไปถึง Parceler กับ Otto ก็จะเอามาใช้ในนี้ด้วยเลย

        ก่อนอื่นก็เริ่มจาก Model Class ขอตั้งเป็นชื่อ Category เนอะ

Category.java
import org.parceler.Parcel;

@Parcel
public class Category {
    String name;
    String id;
    String date;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getDate() {
        return date;
    }

    public void setDate(String date) {
        this.date = date;
    }
}

        สร้าง Event Bus ไว้ให้พร้อม

BusProvider.java
import com.squareup.otto.Bus;

public class BusProvider {
    private static Bus bus = new Bus();

    public static Bus getInstance() {
        return bus;
    }
}

        แล้วก็จำลองคำสั่งดึงข้อมูลจากเซิฟเวอร์เหมือนเดิม

NetworkConnectionManager.java
import android.os.Handler;

public class NetworkConnectionManager {
    public static void getCategoty() {
        new Handler().postDelayed(new Runnable() {
            @Override
            public void run() {
                Category category = new Category();
                category.setId("620395");
                category.setName("Electronics");
                category.setDate("25/10/2015");
                BusProvider.getInstance().post(category);
            }
        }, 3000);
    }

    public interface OnServerCallback {
        void onCategoryCallback(Category category);

    }
}

        โดยที่คำสั่ง getCategory จะถูกเรียกใช้ใน onActivityCreated เมื่อ Fragment ถูกสร้างขึ้นแค่ครั้งแรกเท่านั้น

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

    if (savedInstanceState == null) {
        NetworkConnectionManager.getUserProfile();
    } 
}

เพิ่มคำสั่งต่างๆให้ครบ รวมไปถึงคำสั่ง Save/Restore Instance State 

        เรียกใช้คำสั่งดึงข้อมูลจากเซิฟเวอร์ (จำลอง) ใน onActivityCreated ซะ แล้วก็จัดการ Save/Restore ให้เรียบร้อย

CategoryFragment.java
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;

import com.squareup.otto.Subscribe;

import org.parceler.Parcels;

public class CategoryFragment extends Fragment {
    public static final String KEY_CATEGORY = "key_category";

    private TextView tvCategoryName;
    private TextView tvCategoryId;

    private Category category;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        ...
        BusProvider.getInstance().register(this);
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.fragment_category, container, false);
        tvCategoryName = (TextView) view.findViewById(R.id.tv_category_name);
        tvCategoryId = (TextView) view.findViewById(R.id.tv_category_id);
        return view;
    }

    @Override
    public void onDestroy() {
        ...
        BusProvider.getInstance().unregister(this);
    }

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        ...
        if (savedInstanceState == null) {
            NetworkConnectionManager.getUserProfile();
        } else {
            category = Parcels.unwrap(savedInstanceState.getParcelable(KEY_CATEGORY));
        }
    }

    @Subscribe
    public void onCategoryResult(Category category) {
        this.category = category;
        setCategory(category);
    }

    private void setCategory(Category category) {
        if (category != null) {
            tvCategoryName.setText(category.getName());
            tvCategoryId.setText(category.getId());
        }
    }

    @Override
    public void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        outState.putParcelable(KEY_CATEGORY, Parcels.wrap(category));
    }
}

        เข้าใจไม่ยากเนอะ? สรุปลำดับสิ่งที่ต้องทำคร่าวๆได้ดังนี้

        • ถ้ามีคำสั่งที่ต้องการให้ทำงานตอน Fragment ถูกสร้างขึ้นเท่านั้น ให้เรียกใช้งานใน onActivityCreated โดยเช็คเงื่อนไขว่า savedInstanceState มีค่าเป็น Null

        • ข้อมูลที่ต้องการ Save/Restore โยนขึ้น Global ทุกครั้งไม่ว่าจะเป็น Null หรือไม่ (เผื่อค่ามีการเปลี่ยนแปลง)

        • ประกาส onSaveInstanceState ขึ้นมาเพื่อเก็บข้อมูลที่ต้องการ

        • คำสั่ง Restore ให้ใส่ไว้ใน onActivityCreated โดยที่ savedInstanceState จะต้องไม่เป็น Null

        • อย่าลืมแยกฟังก์ชันให้ย่อยที่สุดตาม Concept ทำไม method สั้นๆ จึงดีกว่า !! ดังนั้นคำสั่งในส่วนของ onActivityCreated จะถูกย่อยออกมาเป็นฟังก์ชันดังนี้

CategoryFragment.java
public class CategoryFragment extends Fragment {
    ...

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

        if (savedInstanceState == null) {
            getCategory();
        } else {
            restoreInstanceState(savedInstanceState);
        }
    }

    private void getCategory() {
        NetworkConnectionManager.getCategory();
    }

    private void restoreInstanceState(Bundle savedInstanceState) {
        category = Parcels.unwrap(savedInstanceState.getParcelable(KEY_CATEGORY));
        
        ...
    }

    ...
}

        และ Layout ของ CategoryFragment ก็มีแค่ TextView อยู่กลางจอ 2 ตัวที่ไม่มีข้อความอะไรแบบนี้

fragment_category.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center"
    android:orientation="vertical">

    <TextView
        android:id="@+id/tv_category_name"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:freezesText="true"
        android:text="Loading..."
        android:textSize="30sp" />

    <TextView
        android:id="@+id/tv_category_id"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:freezesText="true"
        android:textSize="26sp" />

</LinearLayout>

        พอลองทดสอบดูก็จะเห็นว่า Fragment มีการ Restore ข้อมูลอย่างที่ควรจะเป็น


แล้ว Fragment ที่ซ้อนกันเป็น Stack หลายๆชั้นล่ะ?

        ถึงแม้ว่าตัวอย่างที่ยกมาข้างต้นนั้นสามารถจัดการกับ Instance State บน Fragment ได้ก็จริง แต่เวลาเขียนโค๊ดที่ต้องมีการ Replace Fragment ทับของเก่าเรื่อยๆจนทำให้ Fragment ซ้อนกันเป็น Stack จะยังทำงานได้ปกติมั้ยนะ

        ลองนึกดูว่า จากเดิมเจ้าของบล็อกมีแค่ Category Fragment อยู่ตัวเดียว แต่ถ้ามี Product Fragment โผล่ขึ้นมาทับแทนที่ล่ะ?

        เป็นธรรมดาที่ Category Fragment จะถูกซ้อนอยู่ชั้นล่างสุดของ Back Stack แล้วที่หน้าจอจะแสดงเป็น Product Fragment (อยู่ชั้นบนสุดของ Back Stack)

        จะเกิดอะไรขึ้นถ้าระหว่างนั้นมีการหมุนหน้าจอ?

        ถ้าขี้เกียจลองก็ดูภาพประกอบข้างล่างนี้เลย (แต่แนะนำว่าให้ลองไปเขียนทดสอบดูจะได้เข้าใจง่ายขึ้น)


        ปกติ Fragment ที่ถูกแสดงอยู่ เมื่อมีการหมุนหน้าจอก็จะทำการ Save Instance State > Destroy View > Destroy และหลังจากหมุนหน้าจอเสร็จก็จะทำการ Create > Create View > Activity Created เนอะ

        แต่สำหรับ Fragment ที่ซ้อนอยู่ใน Back Stack เมื่อมีการหมุนหน้าจอจะไม่มีการ Destroy View และ Create View (ก็มันถูกทำลายไปตั้งแต่ตอนที่ Fragment ตัวใหม่มาแทนที่แล้วนั่นเอง) ดังนั้นจึงมีแค่การ Save Instance State > Destroy > Create เท่านั้น

        จำได้มั้ยครับว่าเจ้าของบล็อกใส่คำสั่ง Restore Instance State ไว้ที่ไหน?

        onActivityCreated นั่นเอง ซึ่งมันจะไม่ถูกเรียกในกรณีที่ Fragment ซ้อนอยู่ใน Back Stack

        ถ้าเจ้าของบล็อกหมุนหน้าจอทีเดียวแล้วกดกลับไปที่ Category Fragment จะทำให้ onActivityCreated ถูกเรียก (เพราะ View ของ Cateogry Fragment ถูกสร้างขึ้นมา) จึงทำให้สามารถ Restore Instance State ได้ตามปกติอยู่

        แต่ถ้าเกิดซนขึ้นมา เปิด Product Fragment ขึ้นมาทับ Category Fragment แล้วหมุนหน้าจอไปมาหลายๆครั้งล่ะ?


        Save Instance State > Destroy > Create วัฏจักรแบบนี้ก็จะเกิดขึ้นไปเรื่อยๆ แต่อย่าลืมว่าคำสั่ง Restore เจ้าของบล็อกใส่ไว้ใน onActivityCreated ซึ่งในภาพข้างบนนี้จะเห็นว่ามันไม่ได้ถูกเรียกขึ้นมาทำงานเลย

        ผลก็คือ Instance State หายไปทันทีจ้า กลายเป็น Null  

        จะแก้ไขปัญหานี้ยังไงดี? มี 2 วิธีด้วยกันดังนี้

วิธีแรก ย้ายคำสั่ง Restore Instance State ไปไว้ใน onCreate สิ!!

        เมื่อ onActivityCreated มันไม่ได้ทำงาน แต่ onCreate นั้นทำงาน ก็แค่ย้ายคำสั่งไปไว้ใน onCreate ก็จบแล้วเนอะ?

CategoryFragment.java
public class CategoryFragment extends Fragment {
    ...

    @Override
    public void onCreate(Bundle savedInstanceState) {
        ...

        if (savedInstanceState != null) {
            restoreInstanceState(savedInstanceState);
        } 
    }

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

        if (savedInstanceState == null) {
            getCategory();
        }
    }

    ...
}

        เพียงเท่านี้ Instance State ก็สามารถ Restore ได้ปกติแล้ว

วิธีที่สอง กำหนดให้ Retain Fragment ตัวนั้นไว้

        Fragment จะมีคำสั่ง setRetainInstance เพื่อให้ Fragment ถูกเก็บไว้ไม่โดนทำลายทิ้ง ซึ่งวิธีใช้ก็คือแค่ประกาศไว้ใน onCreate ของ Fragment นั้นๆ

CategoryFragment.java
public class CategoryFragment extends Fragment {
    ...

    @Override
    public void onCreate(Bundle savedInstanceState) {
        ... 

        setRetainInstance(true);
        
        ...        
    }

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

        if (savedInstanceState == null) {
            getCategory();
        } else (savedInstanceState != null) {
            restoreInstanceState(savedInstanceState);
        } 
    }

    ...
}

        ผลที่เกิดขึ้นก็คือเมื่อมีการหมุนหน้าจอครั้งแรกจะทำการ Save Instance State เพียงอย่างเดียว จากนั้นจะหมุนหน้าจอไปมายังไงก็ตาม Fragment ตัวนั้นๆก็จะไม่ทำอะไร (ยังคงรักษาสภาพไว้อยู่) จึงไม่ต้องห่วงว่าจะโดนทำลายทิ้งจน Instance State หายไป

        เพียงเท่านี้ก็สามารถทำการ Restore Instance State ที่ onActivityCreated ต่อได้เลย

แบบไหนดีกว่ากันนะ?

        เพื่อให้เป็นไปในแนวทางที่ควรจะเป็นของแอนดรอยด์ ควรใช้วิธีที่สองครับ ใช้คำสั่ง setRetainInstance ให้เป็น True เพื่อให้การ Restore Instance State ยังคงอยู่ใน onActivityCreated ดังเดิม

Fragment ที่อยู่ใน View Pager จะต้องจัดการยังไง?

        อาจจะมีผู้ที่หลงเข้ามาอ่านหลายๆคนสงสัยว่า ถ้าเอา Fragment ไปแปะไว้บน View Pager แล้วจะจัดการเรื่อง Save/Restore Instance State ยังไง

        ก่อนอื่นให้มองว่ามันก็คือการเอา Fragment ไปแปะอยู่ที่ใดที่หนึ่ง ไม่ต่างอะไรกับ Activity ดังนั้นพฤติกรรมส่วนใหญ่นั้นจึงคล้ายๆกันครับ แต่จะมีบางอย่างที่แตกต่างอยู่ ซึ่งขึ้นอยู่กับว่าใช้ Fragment Pager Adapter หรือ Fragment State Pager Adapter นั่นเอง เพราะ Adapter ทั้งสองนี้มีความต่างกันในการจัดการ State ของ Fragment

        ซึ่ง Fragment Pager Adapter จะพยายามรักษา Fragment ที่แสดงอยู่ไว้ไม่ให้ถูกทำลายทิ้ง (ทำลายแค่ View)


        เวลากลับมาแสดงใหม่อีกครั้งก็จะแค่สร้าง View ขึ้นมาใหม่



        ส่วน Fragment State Pager Adapter จะจัดการ State ของ Fragment ที่ไม่ได้ Active ด้วยการทำลายทิ้ง แต่ก็ยังคงเก็บ Instance State ไว้ให้อยู่


        และเมื่อ Fragment ตัวนั้นกลับมา Active ใหม่อีกครั้งก็จะทำการสร้างขึ้นมาและ Restore Instance State ได้เลย


การจัดการ Instance State บน Fragment Pager Adapter

        ให้มองว่ามันเหมือนกับการซ้อน Fragment เป็น Stack โดยตัวที่ไม่ได้แสดงอยู่ก็จะถูกทำลาย View ทิ้ง ดังนั้นก็จะเกิดกรณีที่ว่าผู้ใช้หมุนไปมาแล้ว Instance State หาย ซึ่งเจ้าของบล็อกได้บอกวิธีแก้ปัญหาสองวิธีแล้ว และก็แนะนำวิธี Retain Instance

        ดังนั้น Fragment ที่อยู่ใน Fragment Pager Adapter ควรกำหนด Retain Instance ด้วยเพื่อให้ Instance State ไม่หายไปจ้า

การจัดการ Instance State บน Fragment State Pager Adapter

        ในกรณีนี้เรียกได้ว่าแทบจะไม่ต้องทำอะไร แค่ Save/Restore Instance State ให้กับ Fragment ตามปกติได้เลย ไม่จำเป็นต้อง Retain Instance แต่อย่างใด เพราะ Fragment State Pager Adapter ทำลายปุ๊ป มันก็จะ Save Instance State โดยทันที จนกว่าจะกลับมา Active

ปัญหาเรื่อง Fragment ถูกทำลายไปแล้ว แต่คำสั่งบางอย่างยังทำงานอยู่

        ลองนึกภาพว่า Category Fragment ของเจ้าของบล็อกมันดึงข้อมูลจากเซิฟเวอร์ใช่มั้ยล่ะครับ แล้วถ้าเอามันมาแสดงใน View Pager ล่ะ มันก็ทำงานได้ปกติอยู่เนอะ?

        แต่จะเกิดอะไรขึ้นถ้าเรียกข้อมูลจากเซิฟเวอร์


        แล้วผู้ใช้เกิดเลื่อน View Pager ไปเป็น Fragment ตัวอื่น แล้วข้อมูลจากเซิฟเวอร์มันดันส่งกลับมาล่ะ?


        ย้อนความทรงจำเกี่ยวกับคำสั่งตรงส่วนนี้หน่อยดีกว่า

@Subscribe
public void onCategoryResult(Category category) {
    this.category = category;
    if(isVisible()) {
        setCategory(category);
    }
}

private void setCategory(Category category) {
    if (category != null) {
        tvCategoryName.setText(category.getName());
        tvCategoryId.setText(category.getId());
    }
}

        จะเห็นว่าเมื่อมีข้อมูลส่งเข้ามาก็จะนำไปกำหนดให้กับ Text View ทั้ง 2 ตัวทันที

        แต่ในเงื่อนไขที่เจ้าของบล็อกยกตัวอย่างนี้จะทำให้เกิด NullPointerException ขึ้นมา เพราะว่า View ของ Fragment ตัวนี้ถูกทำลายไปแล้วนั่นเอง (Category ไม่เป็น Null นะ)

        ผู้ที่หลงเข้ามาอ่านจะเช็คตรงนี้ว่า Fragment นั้น Visible อยู่หรือไม่ก็ได้ แต่ก็ไม่ค่อยแนะนำซักเท่าไร เพราะในตอนที่ Restore Instance State ก็ต้องกลับมานั่งกำหนด Category ลงใน Text View ด้วยทุกครั้ง เพื่อป้องกันตอนที่ข้อมูลส่งกลับมาแล้วไม่ได้กำหนดให้แสดงใน Text View

        ดังนั้นในกรณีเจ้าของบล็อกแนะนำว่าควรยกเลิกคำสั่งดึงข้อมูลจากเซิฟเวอร์เมื่อ Fragment ไม่ได้ Active ดีกว่า และเมื่อกลับมา Active ใหม่อีกครั้งก็ค่อยให้ดึงข้อมูลจากเซิฟเวอร์ใหม่อีกครั้งแทน เพื่อลดความซับซ้อนในการทำงาน

public class CategoryFragment extends Fragment {
    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);

        if(category == null) {
            getCategory();
        }

        if (savedInstanceState != null) {
            restoreInstanceState(savedInstanceState);
        }
    }

    @Override
    public void onDestroy() {
        ...
        BusProvider.getInstance().unregister(this);
    }
}

        พอดีว่าคำสั่งดึงข้อมูลของเจ้าของบล็อกแค่จำลองขึ้นมา จึงไม่มีคำสั่งยกเลิก ก็ขอใช้วิธียกเลิก Event Bus ใน onDestroy แทนนะ

        ไม่ยากใช่มั้ยล่ะ?

สรุป

        ในการจัดการกับ Instance State ของ Fragment ไม่ใช่เรื่องยาก เพราะในความเป็นจริงมันก็ใช้หลักการเดียวกับ Activity นั่นเอง แต่ทว่าสิ่งที่เข้าใจยากกว่าคือการเข้าใจและรู้ State ของ Fragment ในทุกๆกรณีมากกว่า

        ดังนั้นการจะจัดการ Instance State บน Fragment ให้ได้อย่างมีประสิทธิภาพ ควรเริ่มจากการเข้าใจการทำงานของ Fragment ก่อนนะ

        สำหรับตัวอย่างที่แนะนำในบทความนี้ก็เป็นส่วนหนึ่งของการจัดการกับ Fragment ที่มีการรับข้อมูลจากเซิฟเวอร์ แต่ทว่าผู้ที่หลงเข้ามาอ่านจะใช้งานยังไงก็เอาไปปรับเปลี่ยนตามความเหมาะสมนะครับ แต่สิ่งที่สำคัญที่อยากจะให้จำไว้ในเรื่องนี้ก็คือ

        "Fragment ควรจัดการ Instance State แค่ของ Fragment เท่านั้น ส่วน View ก็ควรจัดการได้ด้วยตัวเอง และ Activity ก็ด้วยเช่นกัน"

        <ไฟล์โค๊ดตัวอย่าง รอซักพักนะจ๊ะ>




เหล่าพันธมิตรแอนดรอยด์

Devahoy Layer Net NuuNeoI The Cheese Factory Somkiat CC Mart Routine Artit-K Arnondora Kamonway Try to be android developer Oatrice Benz Nest Studios Kotchaphan@Medium Jirawatee@Medium Travispea@Medium