04 October 2015

มา Save/Restore กับ Instance State บน Activity ให้ถูกต้องกันเถอะ

Updated on


        บทความนี้เป็นเรื่องของการ Save และ Restore Instance State บน Android โดยเจ้าของบล็อกเขียนเพื่อเพิ่มเติมจากที่พี่เนย NuuNeoI เคยเขียนให้อ่านกันใน Best Practices ของการ Save/Restore State ของ Activity และ Fragment เพื่อให้ผู้ที่หลงเข้ามาอ่านหลายๆคนได้เข้าใจมากขึ้น

ทำไมต้องจัดการกับ Instance State?

        ผู้ที่หลงเข้ามาอ่านรู้กันอยู่แล้วว่า Activity ของ Android นั้นมี Life Cycle ใช่มั้ยล่ะ (ถ้ายังไม่รู้ให้รีบกลับไปศึกษาด่วนเลย) และเวลาที่เขียนโค๊ดกัน โค๊ดก็จะอยู่ภายใน Activity ซึ่งรวมไปถึงตัวแปรต่างๆที่ใช้สำหรับ View เพื่อแสดงผลลัพธ์ให้ผู้ใช้เห็น

        อย่างเช่น ข้อมูลจากเซิฟเวอร์ ซึ่งเป็นข้อมูลของผู้ใช้ไม่ว่าจะเป็น ID, Name หรือรายละเอียดต่างๆ เพื่อแสดงให้ผู้ใช้เห็นในหน้า Profile


        โดยทั่วไปก็จะเอามาแสดงบน Layout ที่เตรียมไว้เพื่อให้ผู้ใช้เห็นข้อมูลตามที่ต้องการ อาจจะมีการใส่ Logic Code เล็กน้อยเพื่อกำหนดรูปแบบในการแสดงผล

        แต่ทว่าถ้าผู้ใช้หมุนหน้าจอล่ะ?

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

         ข้อมูลผู้ใช้ (ที่ยกตัวอย่างไว้) ก็จะหายไปเช่นกัน เพราะว่า Instance ตัวดังกล่าวไม่ได้เก็บค่าไว้ก่อนที่จะหมุนหน้าจอ ดังนั้นพอหมุนจอเสร็จมันจะกลายเป็น Null ทันที

        ดังนั้นจึงเป็นหน้าที่ของนักพัฒนาที่จะต้องคอยสั่งด้วยว่าจะต้องมาจัดการกับ Instance เหล่านี้ เพื่อให้ยังคงมีข้อมูลอยู่หลังจากการหมุนหน้าจอ

        การหมุนหน้าจอถือว่าเป็นกรณีพื้นฐานที่สุดที่จะต้องจัดการกับ Instance เลยนะเออ

        ซึ่งบน Activity ก็จะมี Method ที่ชื่อว่า onSaveInstanceState กับ onRestoreInstanceState เพื่อให้นักพัฒนาจัดการกับ Instance ได้ โดยจะมีลำดับการทำงานเมื่อหมุนหน้าจอแบบนี้


        หลังจากที่ onPause ทำงานก็จะเข้าสู่ onSaveInstanceState ทันที เพื่อเก็บค่าตัวแปรไว้จากนั้นก็ถูกทำลายแล้วสร้างขึ้นมาใหม่โดยก่อนที่จะเข้าสู่ onResume จะเข้า onRestoreInstanceState เพื่อคืนค่าตัวแปรกลับมาแล้วค่อยทำงานต่อตามปกติของ Life Cycle

        ดังนั้นจำให้ขึ้นใจเลยว่า "Save ก่อนจะถูกทำลาย แล้ว Restore ก่อนจะเริ่มทำงานใหม่"

        หมายเหตุ - ภาพข้างบนนี้ไม่ใช่ Activity Life Cycle นะครับ แต่ภาพนี้เป็นลำดับการทำงานของ Activity เมื่อมีการ Save/Restore Instance State จึงไม่มีพวก onStart หรือ onStop อยู่ในภาพ

ลองดูตัวอย่างกันเลยดีกว่า

        ก่อนจะเริ่มตัวอย่างก็ต้องอธิบายสิ่งที่ต้องการก่อนเนอะ สมมติว่ามีดังนี้

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

Model Class นั้นสำคัญมาก

        ในการเขียนโค๊ดบนแอนดรอยด์จะใช้ MVC Pattern ดังนั้นข้อมูลผู้ใช้จะต้องอยู่ในรูปของ Model Class แบบนี้

Profile.java
import org.parceler.Parcel;

/**
 * Created by Akexorcist on 10/4/15 AD.
 */
@Parcel
public class Profile {
    String id;
    Character character;

    public String getId() {
        return id;
    }

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

    public Character getCharacter() {
        return character;
    }

    public void setCharacter(Character character) {
        this.character = character;
    }

    @Parcel
    public static class Character {
        String name;
        String job;
        String tribe;
        int level;
        int exp;

        public String getName() {
            return name;
        }

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

        public String getJob() {
            return job;
        }

        public void setJob(String job) {
            this.job = job;
        }

        public String getTribe() {
            return tribe;
        }

        public void setTribe(String tribe) {
            this.tribe = tribe;
        }

        public int getLevel() {
            return level;
        }

        public void setLevel(int level) {
            this.level = level;
        }

        public int getExp() {
            return exp;
        }

        public void setExp(int exp) {
            this.exp = exp;
        }
    }
}

        โค๊ดดูยาวไปหน่อย แต่ก็อยากสมมติให้สมจริงไปเลย โดยมี Model ซ้อนอยู่ข้างในอีกชั้น (ขอทำเป็น Inner Class เพื่อให้ดูได้ง่าย) ซึ่ง Model Class ตัวนี้จะใช้เก็บข้อมูลของผู้ใช้ที่ส่งมาจากเซิฟเวอร์ เพื่อเรียกใช้งานภายในแอพ

        หมายเหตุ - ขออนุญาตมักง่ายด้วยการใช้ Parceler Library เข้ามาช่วยนะครับ เป็นไลบรารีช่วยทำให้ Model Class กลายเป็น Parcelable ได้ โดยไม่ต้องเขียนโค๊ด Boiledplate สามารถอ่านเพิ่มเติมได้ที่บทความ [Dev] ห่อให้ด้วย~!! แนะนำการใช้งาน Parceler Library สำหรับ Android ไม่เช่นนั้นโค๊ดจะยาวกว่านี้อีกเยอะ

ขี้เกียจเขียนโค๊ดดึงข้อมูลจากเซิฟเวอร์ ขอใช้โค๊ดจำลองแทนละกัน

        เพราะโค๊ดมันเยอะ เลยไม่อยากให้มาปะปนกับตัวอย่างในบทความนี้มากนัก เดี๋ยวผู้ที่หลงเข้ามาอ่านจะดูแล้วสับสนว่าโค๊ดอันไหนที่ต้องสนใจจริงๆ ส่วนขี้เกียจเป็นเรื่องรอง (จริงๆนะ)

        โค๊ดจำลองที่ว่าก็ไม่ได้มีอะไรมากนักนอกจาก สร้าง Class ตัวหนึ่งที่จะส่งข้อมูลผู้ใช้มาเป็นคลาส Profile โดยจะใช้เวลา 2-3 วินาทีถึงจะส่งข้อมูลกลับมาเป็น Callback เพราะงั้นจะจำลองแบบโคตรง่ายแบบนี้

NetworkConnectionManager.java
import android.os.Handler;

public class NetworkConnectionManager {

    public static void getUserProfile(final OnServerCallback callback) {
        new Handler().postDelayed(new Runnable() {
            @Override
            public void run() {
                Profile.Character character = new Profile.Character();
                character.setName("Anonymous Wizard");
                character.setJob("Wizard");
                character.setLevel(98);
                character.setExp(1234566);
                character.setTribe("DEVIL");
                Profile profile = new Profile();
                profile.setId("sleepingforless");
                profile.setCharacter(character);
                if (callback != null)
                    callback.onUserProfileCallback(profile);
            }
        }, 3000);
    }

    public interface OnServerCallback {
        void onUserProfileCallback(Profile profile);

    }
}

      คำเตือน - คำสั่งนี้เป็นการจำลองว่าดึงข้อมูลจากเซิฟเวอร์นะครับ แต่ไม่ใช่โค๊ดแบบนี้ ><

      ดังนั้นเวลาจะดึงข้อมูลจากเซิฟเวอร์ก็จะเรียกผ่านคำสั่ง

NetworkConnectionManager.getUserProfile(callback);

เรียกใช้คำสั่งใน Activity กันได้เลย

        สมมติว่าเจ้าของบล็อกจัด Layout ไว้แบบนี้



        แต่เวลารอข้อมูลจากเซิฟเวอร์ส่งกลับมาจะต้องขึ้นข้อความรอโหลดข้อมูลก่อนแบบนี้


        ก็ใช้วิธีกำหนด Visibility ของ Layout เอาง่ายๆเนอะ

        สรุปความต้องการได้ดังนี้

        • ระหว่างรอข้อมูลส่งกลับมาให้แสดง Loading
        • เมื่อข้อมูลส่งมาแล้วให้ซ่อน Loading แล้วแสดงข้อมูลที่ได้

        ซึ่งส่วนใหญ่ก็จะเขียนแบบนี้กัน

DetailActivity.java
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.LinearLayout;
import android.widget.TextView;

public class DetailActivity extends AppCompatActivity implements NetworkConnectionManager.OnServerCallback {
    private LinearLayout layoutProgressLoading;
    private LinearLayout layoutUserProfile;
    private TextView tvUserName;
    private TextView tvUserJob;
    private TextView tvUserLevel;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_detail);

        layoutProgressLoading = (LinearLayout) findViewById(R.id.layout_progress_loading);
        layoutUserProfile = (LinearLayout) findViewById(R.id.layout_user_profile);
        tvUserName = (TextView) findViewById(R.id.tv_user_name);
        tvUserJob = (TextView) findViewById(R.id.tv_user_job);
        tvUserLevel = (TextView) findViewById(R.id.tv_user_level);

        layoutUserProfile.setVisibility(View.GONE);
        layoutProgressLoading.setVisibility(View.VISIBLE);

        NetworkConnectionManager.getUserProfile(this);
    }

    @Override
    public void onUserProfileCallback(Profile profile) {
        Profile.Character character = profile.getCharacter();
        tvUserName.setText(character.getName());
        tvUserJob.setText(character.getJob());
        tvUserLevel.setText(String.valueOf(character.getLevel()));

        if(character.getTribe().equalsIgnoreCase("DEVIL")) {
            tvUserLevel.setBackgroundResource(R.drawable.shape_oval_red);
        } else {
            tvUserLevel.setBackgroundResource(R.drawable.shape_oval_gray);
        }

        layoutUserProfile.setVisibility(View.VISIBLE);
        layoutProgressLoading.setVisibility(View.GONE);
    }
}

        ซึ่งการเขียนแบบนี้เป็นการเขียนแบบทั่วๆไปที่ไม่ได้จัดการกับ Instance State เลย ดังนั้นเวลาหมุนหน้าจอก็จะทำให้ต้องโหลดข้อมูลใหม่ทุกครั้ง (เพราะคำสั่งอยู่ใน onCreate) แบบวีดีโอตัวอย่างนี้


        และก่อนจะจัดการกับ Instance State สิ่งสำคัญที่เจ้าของบล็อกอยากให้ทำก่อนก็คือ แบ่งคำสั่งออกเป็น Method แยกย่อยตามหน้าที่ โดยอ้างอิงจาก ทำไม method สั้นๆ จึงดีกว่า !! ดังนั้นกลุ่มคำสั่งไหนแยกออกมาเป็น Method ได้ก็ให้ทำซะ

DetailActivity.java
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.LinearLayout;
import android.widget.TextView;

public class DetailActivity extends AppCompatActivity implements NetworkConnectionManager.OnServerCallback {
    private LinearLayout layoutProgressLoading;
    private LinearLayout layoutUserProfile;
    private TextView tvUserName;
    private TextView tvUserJob;
    private TextView tvUserLevel;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_detail);
        bindView();
        callServer();
    }

    private void bindView() {
        layoutProgressLoading = (LinearLayout) findViewById(R.id.layout_progress_loading);
        layoutUserProfile = (LinearLayout) findViewById(R.id.layout_user_profile);
        tvUserName = (TextView) findViewById(R.id.tv_user_name);
        tvUserJob = (TextView) findViewById(R.id.tv_user_job);
        tvUserLevel = (TextView) findViewById(R.id.tv_user_level);
    }

    private void callServer() {
        showLoading();
        NetworkConnectionManager.getUserProfile(this);
    }

    @Override
    public void onUserProfileCallback(Profile profile) {
        setUserProfile(profile);
        showUserProfile();
    }

    private void showLoading() {
        layoutUserProfile.setVisibility(View.GONE);
        layoutProgressLoading.setVisibility(View.VISIBLE);
    }

    private void showUserProfile() {
        layoutUserProfile.setVisibility(View.VISIBLE);
        layoutProgressLoading.setVisibility(View.GONE);
    }

    private void setUserProfile(Profile profile) {
        Profile.Character character = profile.getCharacter();
        tvUserName.setText(character.getName());
        tvUserJob.setText(character.getJob());
        tvUserLevel.setText(String.valueOf(character.getLevel()));
        tvUserLevel.setBackgroundResource(getUserTribeColor(character));
    }

    private int getUserTribeColor(Profile.Character character) {
        if(character.getTribe().equalsIgnoreCase("DEVIL")) {
            return R.drawable.shape_oval_red;
        } else {
            return R.drawable.shape_oval_gray;
        }
    }
}

        ถูกหรือป่าวหว่า? ช่างมันเถอะ แต่ที่ต้องทำแบบนี้ก็เพราะว่าเดี๋ยวตอนจัดการกับ Instance State การจัด Method แบบนี้จะทำให้จัดการง่ายขึ้นเยอะเลยนะ

ปรับโค๊ดใหม่ จัดการกับ Instance State ให้เรียบร้อย 

        ทีนี้ต้องมาดูกันต่อว่า "ต้องเก็บตัวแปรหรือข้อมูลตัวไหนบ้าง" ซึ่งค่าบางอย่างที่เกี่ยวกับ Properties ของ View ไม่จำเป็นต้องจัดการ เพราะว่าตัว View เองมันจัดการเรียบร้อยแล้ว ดังนั้นในตัวอย่างนี้สิ่งที่ต้องเก็บคือ "ข้อมูลผู้ใช้" หรือ Profile Instance ที่ได้จาก onUserProfileCallback นั่นเอง

        Save/Restore มันให้เรียบร้อยซะ

DetailActivity.java
public class DetailActivity extends AppCompatActivity implements NetworkConnectionManager.OnServerCallback {
    public static final String KEY_PROFILE = "key_profile";

    ...

    private Profile profile;

    ...

    @Override
    protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        outState.putParcelable(KEY_PROFILE, Parcels.wrap(profile));
    }

    @Override
    protected void onRestoreInstanceState(Bundle savedInstanceState) {
        super.onRestoreInstanceState(savedInstanceState);
        profile = Parcels.unwrap(savedInstanceState.getParcelable(KEY_PROFILE));
        setUserProfile(profile);
    }

    ...

    @Override
    public void onUserProfileCallback(Profile profile) {
        this.profile = profile;
        ...
    }

    ...
}

        หมายเหตุ - Parcels.unwrap และ Parcels.wrap เป็นคำสั่งของ Parceler Library ที่แปลง Model Class ให้กลายเป็น Parceler จ๊ะ (ไปดูวิธีใช้เองอีกทีนะ)

        สำหรับการ Save/Restore นั้นทำไม่ยาก โดยสังเกตว่าใน onSaveInstanceState จะมี outState มาให้ด้วย ให้ผู้ที่หลงเข้ามาอ่านเก็บข้อมูลที่ต้องการลงไปใน outState นี่ล่ะ

        จากนั้นเวลาเข้า onRestoreInstanceState ก็จะส่ง savedInstanceState มาให้ด้วย ให้ผู้ที่หลงเข้ามาอ่านดึงข้อมูลที่เก็บไว้ออกมา โดยมีหัวใจสำคัญคือ Key String ที่เอาไว้กำหนดเวลาที่ Save และ Restore เพราะถ้า Key ไม่ตรงกันมันก็จะดึงข้อมูลไม่ได้

        ซึ่งเจ้าของบล็อกก็ได้สร้างไว้เป็นชื่อว่า KEY_PROFILE ไปเลย เป็น Key สำหรับ Save/Restore Profile Instance โดยเฉพาะ (กรณีที่มี Instance มากกว่าหนึ่งตัวให้ใช้ Key คนละชื่อกัน)

        และจะเห็นว่าเวลาที่ทำ Save/Restore นั้น Instance จะต้องถูกเก็บไว้ที่ Global อย่างช่วยไม่ได้

เก็บข้อมูลประเภทไหนได้บ้าง?

        เนื่องจากเวลาที่ Save/Restore จะต้องเก็บข้อมูลไว้ใน savedInstanceState และ outState ที่เป็น Bundle Class นั่นจึงหมายความว่า ประเภทของข้อมูลที่จะเก็บได้ต้องเป็นประเภทที่ Bundle Class รองรับนั่นเอง ซึ่งมีดังนี้

        • Boolean - boolean
        • Boolean Array - boolean[]
        • Byte - byte
        • Byte Array - byte[]
        • Short - short
        • Short Array - short[]
        • Integer - int
        • Interger Array - int[]
        • Integer Array List - ArrayList<Integer>
        • Float - float
        • Float Array - float[]
        • Long - long
        • Long Array - long[]
        • Double - double
        • Double Array - double[]
        • Char - char
        • Char Array - char[]
        • Char Sequence - CharSequence
        • Char Sequence Array - CharSequence[]
        • Char Sequence Array List - ArrayList<CharSequence>
        • String - String
        • String Array - String[]
        • String Array List - ArrayList<String>
        • Binder - IBinder
        • Size - Size
        • SizeF - SizeF
        • Parcelable - Parcelable
        • Parcelable Array - Parcelable[]
        • Parcelable Array List - ArrayList<Parcelable>
        • Spare Parcelable Array - SpareArray<Any Parcelable Object>
        • Serializable - Serializable
        • Bundle - Bundle

        จะเห็นว่าค่าที่เก็บได้นั้นจะเป็นพวก Primitive Data หรือมี Primitive Data เป็นส่วนประกอบอยู่ข้างใน จึงหมายความว่านอกเหนือจากนี้จะไม่สามารถเก็บได้ (ใช่ครับ ไม่สามารถบ้าจี้ยัด View Class ลงไปเก็บไว้ได้หรอกนะ) รวมไปถึงยัด Bundle ลงไปได้อีกด้วย จึงทำให้เก็บข้อมูลแบบซับซ้อนได้เลย ซึ่งทุกตัวจะต้องมีการกำหนด Key String ด้วย และควรเป็น Key คนละชื่อกันนะ

        จึงเป็นที่มาว่าทำไมนักพัฒนาต้องเขียน Model Class แบบ MVC (จะเขียนเป็นแบบ MVP หรือ MVVM ก็ได้นะจ๊ะ) เพื่อที่ว่า Model Class จะได้ยัดลงใน Bundle ได้นั่นเอง เพราะ Bundle จะถูกใช้ในการส่งข้อมูลเป็นหลัก ไม่ว่าจะ Intent หรือ Save/Restore Instance State เป็นต้น

กลับมาดูวิธีการนำข้อมูลที่ Save/Restore ไปใช้งานกันต่อ

        จะเห็นว่าตัวอย่างข้างต้นมีแต่การพล่ามถึงขั้นตอน Save/Restore Instance State แล้วทีนี้จะเอาไปใช้งานยังไงล่ะ? มาดูความต้องการกันก่อนเลย

        • เรียกข้อมูลจากเซิฟเวอร์ เมื่อเปิด Activity ครั้งแรก (เพราะไม่มี Instance ให้ Restore)
        • ดึงข้อมูลทีเก็บไว้มาแสดงแทน ถ้าผู้ใช้หมุนหน้าจอ

        ผู้ที่หลงเข้ามาอ่านจะรู้ได้ไงว่ามีการ Save/Restore เมื่อไร? อย่างแรกเลยคือ onSaveInstanceState กับ onRestoreInstanceState จะทำงานเมื่อมีการ Save/Restore นั่นเอง ดังนั้นเวลาที่ Activity พึ่งเปิดขึ้นมา ทั้งสอง Method นี้จะไม่ทำงาน

        และเคยสังเกตกันมั้ยว่า onCreate จะมี Instance ของ Bundle ให้มาด้วยทุกครั้ง (ที่ชื่อ savedInstanceState น่ะ) แต่ทว่าผู้ที่หลงเข้ามาอ่านไม่เคยจะได้เรียกใช้มันเลย เวลาส่งข้อมูลผ่าน Intent ก็ดันใช้คำสั่ง getIntent แทน

        แล้วมันจะมีไว้ทำอะไรล่ะ?

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

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

       เวลาที่มีการ Save/Restore Instance State ข้อมูลจะไม่ได้ถูกส่งไปที่ onRestoreInstanceState เท่านั้น แต่รวมไปถึง savedInstanceState ใน onCreate ด้วย ดังนั้นจึงสามารถรู้ได้ว่า onCreate ที่ทำงานนั้นเกิดมาจาก Activity พึ่งถูกสร้างขึ้นมาหรือว่าเกิดจากการ Save/Restore โดยเช็คว่าเป็น Null หรือป่าว


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

public class DetailActivity extends AppCompatActivity implements NetworkConnectionManager.OnServerCallback {
    ...

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

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

    ...

    private void callServer() {
        showLoading();
        NetworkConnectionManager.getUserProfile(this);
    }

    ...
}

        แล้วถ้าหมุนหน้าจอล่ะ? ควรจะใช้คำสั่งตรงไหน?

         ไม่ใช่ใน onCreate นะครับ เพราะอย่าลืมว่า onCreate ทำงานก่อน onRestoreInstanceState ดังนั้นในตอนที่ onCreate ทำงาน ข้อมูลที่ Global จะยังเป็น Null อยู่ เพราะว่ายังไม่ได้ Restore

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

public class DetailActivity extends AppCompatActivity implements NetworkConnectionManager.OnServerCallback {
    public static final String KEY_PROFILE = "key_profile";

    ..

    private Profile profile;

    ...

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

        profile = Parcels.unwrap(savedInstanceState.getParcelable(KEY_PROFILE));
        setUserProfile(profile);
    }

    ...

    private void showUserProfile() {
        layoutUserProfile.setVisibility(View.VISIBLE);
        layoutProgressLoading.setVisibility(View.GONE);
    }

    private void setUserProfile(Profile profile) {
        Profile.Character character = profile.getCharacter();
        tvUserName.setText(character.getName());
        tvUserJob.setText(character.getJob());
        tvUserLevel.setText(String.valueOf(character.getLevel()));
        tvUserLevel.setBackgroundResource(getUserTribeColor(character));
        showUserProfile();
    }

    private int getUserTribeColor(Profile.Character character) {
        if(character.getTribe().equalsIgnoreCase("DEVIL")) {
            return R.drawable.shape_oval_red;
        } else {
            return R.drawable.shape_oval_gray;
        }
    }
}

        เพียงเท่านี้แอพของผู้ที่หลงเข้ามาอ่านก็รองรับการหมุนหน้าจอได้แล้ว เย้ เย้



        ว่าแต่... ลืมอะไรกันไปหรือป่าวหว่า?

        ถ้าหมุนหน้าจอระหว่างที่กำลังดึงข้อมูลจากเซิฟเวอร์ล่ะ?

        ....

Instance State กับ Asynchronous Code

        สิ่งที่นักพัฒนาหลายๆคนนั้นลืมคำนึงถึงก็คือเวลาเทสโค๊ดอย่างในตัวอย่างนี้ สิ่งที่ทำคือ

        • กดเข้าสู่หน้าดึงข้อมูลจากเซิฟเวอร์
        • รอโหลดข้อมูลจนเสร็จ
        • ลองหมุนหน้าจอ
        • ลองหมุนหน้าจออีกครั้ง
        • เย้ ทำงานได้ปกติไม่มีปัญหา เย้!!

        แต่ในความเป็นจริงนั้นผู้ใช้อาจจะหมุนหน้าจอระหว่างที่โหลดข้อมูลอยู่ก็ได้

        หมุนหน้าจอระหว่างโหลดข้อมูลอยู่ จะเกิดอะไรขึ้นล่ะ?

        ข้อมูลจากเซิฟเวอร์ยังคงส่งกลับมาใช่หรือไม่ - ใช่

        แล้ว onUserProfileCallback ที่ประกาศไว้จะทำงานเมื่อข้อมูลโหลดเสร็จหรือไม่ - ไม่

        นั่นก็เพราะว่า Listener หรือ Callback ที่ประกาศไว้ในตอนเรียกคำสั่งจะถูกเคลียร์ทิ้งไปด้วย แต่ทว่าการดึงข้อมูลจากเซิฟเวอร์ยังคงทำงานอยู่ ทำให้เมื่อโหลดข้อมูลเสร็จแล้วก็ไม่เกิดอะไรขึ้น เพราะ Callback หลังจากหมุนหน้าจอเป็นคนละตัวกับก่อนหมุนหน้าจอ

        ซึ่งในกรณีนี้เกิดขึ้นได้กับ Asynchronous Code เกือบทั้งหมด ดังนั้นวิธีเดิมๆที่ใช้กันจึงเป็นไปไม่ได้เลยที่จะทำงานต่อเมื่อหมุนหน้าจอ ดังนั้นจึงต้องเปลี่ยนไปใช้วิธีอื่นแทน

AsyncTaskLoader คือทางออก (หรือใช้ 3rd-party Library ไปเลย)

        โดย AsyncTaskLoader เป็น Loader แบบหนึ่งที่ทำงานอยู่เบื้องหลังคล้ายกับ AsycnTask แต่พิเศษตรงที่ตัวมันเองผูกเข้ากับ Activity หรือ Fragment ที่เรียกใช้ จึงทำให้มันไม่ถูกเคลียร์ทิ้งเมื่อมีการหมุนหน้าจอ และมันจะหยุดทำงานเมื่อ Activity หรือ Fragment ที่เรียกใช้งานนั้นหยุดทำงาน

        สำหรับรายละเอียดเกี่ยวกับ AsyncTaskLoader แนะนำให้อ่านเพิ่มเติมที่ สารพันเรื่องราวของ "Thread" บนแอนดรอยด์ การปะทะกันของ Thread, AsyncTask, AsyncTaskLoader และ IntentService

        หมายเหตุ - วิธีใช้ AsyncTaskLoader ก็ไปดูจากบทความที่แปะให้ละกันเนอะ ขี้เกียจอธิบาย

        เพิ่มเติม - ถ้าใช้รับส่งข้อมูลระหว่างเซิฟเวอร์จริงๆ ไม่แนะนำให้เขียนเอง ลองใช้ 3rd-party Network Library แทน เช่น Retrofit, OkHttp, Volley, Ion, RoboSpice หรือ AsyncHttpClient เป็นต้น เพราะ Library เหล่านี้จะจัดการในส่วนของการเชื่อมต่อให้หมดแล้ว

        แต่ทว่า AsyncTaskLoader จะไม่สามารถส่งข้อมูลจากตัวมันเองมาที่ Activity หรือ Fragment ได้โดยตรง หรือ Network Library ที่ส่งข้อมูลกลับมา Activity/Fragment ด้วยวิธี Callback ก็ยังคงประสบการณ์ปัญหาเดิมคือ Callback ถูกเคลียร์ทิ้งไปแล้ว

        ซึ่งเดิมทีจะต้องใช้วิธีที่เรียกว่า Broadcast Receiver เพื่อส่งข้อมูลแทน แต่มันก็ต้องเขียนโค๊ดเพิ่มอีกมากพอสมควร ดังนั้นเจ้าของบล็อกจึงขอแนะนำวิธี Event Bus แทน

Event Bus จงเจริญ

        โดย Event Bus เป็น Concept อย่างหนึ่งในการส่งข้อมูลจากอีกที่หนึ่งไปยังอีกที่หนึ่ง โดยที่ทั้งสองที่ไม่จำเป็นต้องผูกมัดกัน เช่น ส่งข้อมูลจาก Fragment ไปยัง Activity โดยที่ Fragment ไม่จำเป็นต้องประกาศเรียกชื่อ Activity เลยซักกะแอะ รวมไปถึงฝั่ง Activity ด้วยเช่นกัน ซึ่ง Event Bus จะเป็นตัวกลางในการรับข้อมูลเพื่อส่งต่อไปยังอีกฝั่งให้ โดยที่ Concept นี้เหมาะเจาะกับกรณีนี้โคตรๆ


        สำหรับ Event Bus Library ที่เจ้าของบล็อกใช้จะชื่อว่า Otto ของ Square (Network Library ก็ใช้ Retrofit ของ Square) ซึ่งผู้ที่หลงเข้ามาอ่านสามารถเข้าไปอ่านบทความของ Otto ได้ที่ มานั่งรถ Bus ด้วย Otto กันเถอะ

เปลี่ยนไปใช้ Otto แทนการทำ Callback

        ถึงแม้ว่าจะใช้โค๊ดจำลองก็ตาม แต่วิธี Callback มันก็ไม่เวิร์กเวลาหมุนหน้าจอน่ะแหละ ดังนั้นต้องเปลี่ยนมาใช้ Otto อยู่ดี

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 getUserProfile() {
        new Handler().postDelayed(new Runnable() {
            @Override
            public void run() {
                Profile.Character character = new Profile.Character();
                character.setName("Anonymous Wizard");
                character.setJob("Wizard");
                character.setLevel(98);
                character.setExp(1234566);
                character.setTribe("DEVIL");
                Profile profile = new Profile();
                profile.setId("sleepingforless");
                profile.setCharacter(character);

                BusProvider.getInstance().post(profile);
            }
        }, 3000);
    }
}

        และใน Activity ก็ต้องเปลี่ยนไปใช้ Otto เช่นกัน

DetailActivity.java
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.LinearLayout;
import android.widget.TextView;

import com.squareup.otto.Subscribe;

import org.parceler.Parcels;

public class DetailActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ...

        BusProvider.getInstance().register(this);

        ...
    }

    @Override
    protected void onDestroy() {
        ...

        BusProvider.getInstance().unregister(this);
    }

    ...

    @Subscribe
    public void onUserProfileCallback(Profile profile) {
        this.profile = profile;
        setUserProfile(profile);
    }

    ...
}

        เอาล่ะ เปลี่ยนเรียบร้อย

จัดการกับ Instance State ในขณะที่กำลังโหลดข้อมูลอยู่

        สิ่งที่ต้องทำเพิ่มหลังจากเปลี่ยนไปใช้ Otto ก็คือ!!!!

        ระวัง Profile Class เป็น Null ตอน Restore เพราะผู้ใช้หมุนหน้าจอเล่นก่อนที่จะโหลดข้อมูลเสร็จ

DetailActivity.java
public class DetailActivity extends AppCompatActivity {
    ...

    private void setUserProfile(Profile profile) {
        if(profile != null) {
            Profile.Character character = profile.getCharacter();
            tvUserName.setText(character.getName());
            tvUserJob.setText(character.getJob());
            tvUserLevel.setText(String.valueOf(character.getLevel()));
            tvUserLevel.setBackgroundResource(getUserTribeColor(character));
            showUserProfile();
        }
    }

    ...
}
     
        เพิ่มโค๊ดแค่นิดเดียวเท่านั้นเอง~ เท่านี้ก็รองรับการหมุนหน้าจอเต็มรูปแบบแล้ว


มาทำให้มันถูกต้องกว่านี้กันดีกว่า

        อะไรนะ? วิธีนี้ยังไม่ถูกต้องอีกหรอ?

        ก็ใช่นะ เพราะว่าในตัวอย่างที่ผ่านมานี้เจ้าของบล็อกได้ Save/Restore กับ Profile Class แล้วตอนที่ Restore ก็เอาค่าไปกำหนดให้กับ View ใหม่อีกครั้ง

         แต่ทว่า Concept จริงๆนั้น View จะต้องสามารถ Save/Restore ได้ด้วยตัวเอง โดยที่ไม่จำเป็นต้องกำหนดค่าตอน Restore จาก Activity/Fragment เลย ดังนั้นบน Activity/Fragment จึงควรทำแค่ Restore Profile Class มาเก็บไว้ใช้งานเท่านั้น

        มันคงไม่สนุกซักเท่าไรถ้า Activity หนึ่งตัวต้องมานั่งจัดการกับ Instance State ของ View ที่อยู่ในนั้นทั้งหมด จึงเป็นที่มาว่าทำไม Activity ต้องแยกออกจาก View

จะทำให้ View สามารถ Restore ตัวเองได้อย่างไร?

         จากบทความของ NuuNeoI ในเรื่อง Best Practices ของการ Save/Restore State ของ Activity และ Fragment ได้ยกตัวอย่างของ TextView ไว้ ว่าสามารถกำหนด android:freezeText ได้ว่าจะให้ TextView นั้นเก็บข้อความไว้หรือป่าว

<TextView
    android:id="@+id/tv_user_level"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:freezesText="true">

        ดังนั้นคำสั่ง setText จึงไม่จำเป็นต้องใช้ตอน Restore เลย เพราะเดี๋ยว TextView มันจัดการของมันเอง

        แล้วค่าอื่นๆที่ยังต้องกำหนดอยู่ล่ะ? อย่าง Visibility หรือ BackgroundColorResource 

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

        ดังนั้นกรณีของ LinearLayout ทั้งสองตัวที่ใช้ในตัวอย่างนี้ เจ้าของบล็อกก็ต้องทำเป็น Custom View แล้วจัดการเพิ่มอีกที

MyLinearLayout.java
import android.annotation.TargetApi;
import android.content.Context;
import android.os.Build;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.AttributeSet;
import android.widget.LinearLayout;

public class MyLinearLayout extends LinearLayout {
    public MyLinearLayout(Context context) {
        super(context);
    }

    public MyLinearLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public MyLinearLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    public MyLinearLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }

    @Override
    public Parcelable onSaveInstanceState() {
        Parcelable superState = super.onSaveInstanceState();
        SavedState ss = new SavedState(superState);
        ss.visibility = this.getVisibility();
        return ss;
    }

    @Override
    public void onRestoreInstanceState(Parcelable state) {
        if (!(state instanceof SavedState)) {
            super.onRestoreInstanceState(state);
            return;
        }
        SavedState ss = (SavedState) state;
        super.onRestoreInstanceState(ss.getSuperState());
        this.setVisibility(ss.visibility);
    }

    private static class SavedState extends BaseSavedState {
        int visibility;

        SavedState(Parcelable superState) {
            super(superState);
        }

        private SavedState(Parcel in) {
            super(in);
            this.visibility = in.readInt();
        }

        @Override
        public void writeToParcel(Parcel out, int flags) {
            super.writeToParcel(out, flags);
            out.writeInt(this.visibility);
        }

        public static final Creator<SavedState> CREATOR = new Creator<SavedState>() {
            public SavedState createFromParcel(Parcel in) {
                return new SavedState(in);
            }

            public SavedState[] newArray(int size) {
                return new SavedState[size];
            }
        };
    }
}

        อาห์ Boiledplate Code ที่น่าเบื่อ...

        จากนั้นก็เปลี่ยนมาใช้ MyLinearLayout แทน LinearLayout ซะ ก็จะได้ LinearLayout ที่ Save/Restore Visibility เองได้แล้ว ไม่ต้องมานั่งใช้คำสั่งแสดง/ซ่อนหลังจาก Restore Profile Class อีกต่อไป

<com.akexorcist.androidinstancestate.view.MyLinearLayout
    android:id="@+id/layout_user_profile"
    ... 
    >

        อ๊ะๆ ยังเหลือ TextView อีกตัวหนึ่งที่ใช้คำสั่งกำหนดสีพื้นหลังอยู่นะเออ


        ดังนั้นเจ้าของบล็อกจึงต้องเปลี่ยนให้ TextView ธรรมดาๆตัวนี้กลายเป็น Custom View ซะ เพื่อที่จะได้ Save/Restore สีพื้นหลังได้ เพราะตัวนี้เจ้าของบล็อกใช้วิธีกำหนดสีพื้นหลังด้วยการเอา Shape Drawable มากำหนด

MyLevelTextView.java
import android.annotation.TargetApi;
import android.content.Context;
import android.os.Build;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.AttributeSet;
import android.widget.TextView;

public class MyLevelTextView extends TextView {
    private int backgroundResourceId;

    public MyLevelTextView(Context context) {
        super(context);
    }

    public MyLevelTextView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public MyLevelTextView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    public MyLevelTextView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }

    @Override
    public void setBackgroundResource(int resid) {
        super.setBackgroundResource(resid);
        backgroundResourceId = resid;
    }

    @Override
    public Parcelable onSaveInstanceState() {
        Parcelable superState = super.onSaveInstanceState();
        SavedState ss = new SavedState(superState);
        ss.backgroundResourceId = backgroundResourceId;
        return ss;
    }

    @Override
    public void onRestoreInstanceState(Parcelable state) {
        if (!(state instanceof SavedState)) {
            super.onRestoreInstanceState(state);
            return;
        }
        SavedState ss = (SavedState) state;
        super.onRestoreInstanceState(ss.getSuperState());
        this.setBackgroundResource(ss.backgroundResourceId);
    }

    private static class SavedState extends BaseSavedState {
        int backgroundResourceId;

        SavedState(Parcelable superState) {
            super(superState);
        }

        private SavedState(Parcel in) {
            super(in);
            this.backgroundResourceId = in.readInt();
        }

        @Override
        public void writeToParcel(Parcel out, int flags) {
            super.writeToParcel(out, flags);
            out.writeInt(this.backgroundResourceId);
        }

        public static final Creator<SavedState> CREATOR = new Creator<SavedState>() {
            public SavedState createFromParcel(Parcel in) {
                return new SavedState(in);
            }

            public SavedState[] newArray(int size) {
                return new SavedState[size];
            }
        };
    }
}

        วุ่นวายเล็กน้อยก็ตรงที่มันไม่มีคำสั่ง getBackgroundResource ให้ใช้ ดังนั้นก็เลยต้อง Override คำสั่ง setBackgroundResource เพื่อคอยเก็บค่า ID ไว้แทน จากนั้นก็ Save/Restore ให้เรียบร้อย

        สำหรับคำสั่งก็ยังคงเรียกไปที่ LinearLayout กับ TextView ได้เลย ขอแค่ว่าใน Layout XML เรียกใช้งาน Custom View ที่ได้สร้างขึ้นมาก็พอ

DetailActivity.java
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.LinearLayout;
import android.widget.TextView;

import com.akexorcist.androidinstancestate.R;
import com.akexorcist.androidinstancestate.bus.BusProvider;
import com.akexorcist.androidinstancestate.model.Profile;
import com.akexorcist.androidinstancestate.network.NetworkConnectionManager;
import com.akexorcist.androidinstancestate.view.MyLevelTextView;
import com.squareup.otto.Subscribe;

import org.parceler.Parcels;

public class DetailActivity extends AppCompatActivity {
    public static final String KEY_PROFILE = "key_profile";

    private LinearLayout layoutProgressLoading;
    private LinearLayout layoutUserProfile;
    private TextView tvUserLevel;
    private TextView tvUserName;
    private TextView tvUserJob;

    private Profile profile;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_detail);
        BusProvider.getInstance().register(this);
        bindView();

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

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

    private void bindView() {
        layoutProgressLoading = (LinearLayout) findViewById(R.id.layout_progress_loading);
        layoutUserProfile = (LinearLayout) findViewById(R.id.layout_user_profile);
        tvUserName = (TextView) findViewById(R.id.tv_user_name);
        tvUserJob = (TextView) findViewById(R.id.tv_user_job);
        tvUserLevel = (MyLevelTextView) findViewById(R.id.tv_user_level);
    }

    @Override
    protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        outState.putParcelable(KEY_PROFILE, Parcels.wrap(profile));
    }

    @Override
    protected void onRestoreInstanceState(Bundle savedInstanceState) {
        super.onRestoreInstanceState(savedInstanceState);
        profile = Parcels.unwrap(savedInstanceState.getParcelable(KEY_PROFILE));
    }

    private void callServer() {
        showLoading();
        NetworkConnectionManager.getUserProfile();
    }

    @Subscribe
    public void onUserProfileCallback(Profile profile) {
        if (profile != null) {
            this.profile = profile;
            showUserProfile();
            setProfileDetail(profile);
        }
    }

    private void setProfileDetail(Profile profile) {
        Profile.Character character = profile.getCharacter();
        tvUserName.setText(character.getName());
        tvUserJob.setText(character.getJob());
        tvUserLevel.setText(String.valueOf(character.getLevel()));
        tvUserLevel.setBackgroundResource(getUserTribeColor(character));
    }

    private void showLoading() {
        layoutUserProfile.setVisibility(View.GONE);
        layoutProgressLoading.setVisibility(View.VISIBLE);
    }

    private void showUserProfile() {
        layoutUserProfile.setVisibility(View.VISIBLE);
        layoutProgressLoading.setVisibility(View.GONE);
    }

    private int getUserTribeColor(Profile.Character character) {
        if (character.getTribe().equalsIgnoreCase("DEVIL")) {
            return R.drawable.shape_oval_red;
        } else {
            return R.drawable.shape_oval_gray;
        }
    }
}

        ถึงตอนนี้แล้วขอบอกว่า โค๊ดเสร็จจริงๆแล้วนะ ฮาๆๆ เท่านี้ View ก็จัดการในส่วนของตัวมันเอง และ Activity ก็จัดการแค่ Model Class ที่ใช้งานเท่านั้น ไม่ต้องไปยุ่งเกี่ยวกับ View ให้วุ่นวาย

ยุ่งยากแบบนี้ทำไมไม่กำหนด Orientation ใน Activity Configuration Change?

        เจ้าของบล็อกเชื่อว่าผู้ที่หลงเข้ามาอ่านต้องเคยใช้วิธีแบบนี้เพื่อแก้ปัญหาการหมุนหน้าจอแน่ๆ

<activity
    android:name=".DetailActivity"
    android:configChanges="orientation|keyboardHidden|screenSize" />

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

        แต่ทว่าวิธีนี้กลับไม่ใช่วิธีที่ถูกต้องซักเท่าไรนัก ไม่ถูกต้องตามธรรมเนียมครรลองของ Android (เวอร์มะ)

        ถ้าแอพถูกย่อและตายลง (อาจจะเพราะถูกคืนแรม) กรณีนี้จะไม่สามารถ Save Instance State ก่อนที่จะถูกปิดลงได้ ดังนั้นตอนกลับมาเปิดใหม่อีกครั้งก็ไม่สามารถ Restore ได้เลย

        และไม่สามารถออกแบบหน้าจอแนวตั้งและแนวนอนเป็นคนละแบบได้ เนื่องจากวิธีนี้จะไม่มีการเปลี่ยนแปลง View ดังนั้นรูปแบบ Layout ที่จัดไว้ก็ยังคงเป็นแบบก่อนหน้า ซึ่งนักพัฒนาส่วนใหญ่ที่ใช้วิธีนี้จะแก้ปัญหาด้วยการจัด Layout ให้ Responsive กับหน้าจอหลายๆขนาด

        ดังนั้นถ้าเป็นไปได้ก็ลองปรับเปลี่ยนวิธีดูนะครับ

สรุป

        บทความนี้ขออ้างอิงบทความจากหลายๆที่เสียหน่อย เพราะจะอธิบายทั้งหมดก็เกรงว่าเนื้อหาจะยาวกว่านี้อีกมาก ขอสรุปเป็นข้อๆไว้ดังนี้

        • ถ้าเบื่อ Boiledplate บน Parcelable ให้ใช้ Parceler ดู
        • ถ้ามีพวก Background Task ในนั้นด้วยก็ลองใช้ Otto ดู
        • การ Save/Restore ควรแยกหน้าที่ให้ถูกต้อง View ก็จัดการตัวเอง Activity ก็จัดการ Model Class
        • ถ้าบางอย่างของ View ที่ใช้งานอยู่มันไม่ Restore ให้ ก็ให้ Inherit ออกมาเป็น Custom Class แล้วจัดการเองให้เรียบร้อย
        • ถึงแม้ว่าจะต้องเสียเวลาสร้าง Custom Class เอง แต่ก็ดีกว่าปล่อยให้โค๊ดกลายเป็น Spaghetti Code ที่มีการทำงานในแต่ละส่วนปนกันมั่วไปหมด
        • เลี่ยงการใช้ Configuration Change
        • ลองทดสอบด้วยการหมุนหน้าจอทุกกรณีที่ทำงาน รวมไปถึงการลองย่อแอพให้แอพตายเพราะถูกคืนแรมไปให้แอพตัวอื่นด้วย

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

        สำหรับโค๊ดตัวอย่างที่ใช้ในบทความนี้สามารถดาวน์โหลดไปดูกันได้นะครับ

        • Android Instance State [Google Drive]
        • Android Instance State [Sleeping For Less]

        ขอบคุณพี่เนย NuuNeoI สำหรับคำแนะนำ และพี่เฟิร์ส artit-k สำหรับการตรวจทานเนื้อหาครับ