เรื่องสำคัญที่เรามองข้ามไปกับ Save Restore State

สวัสดีครับ เหล่าผู้ว่างมากทั้งหลาย วันนี้ผมมาแชร์ประสบการณ์ที่เคยพบเจอในการเขียนแอพ android ที่กำลังจะเล่าคือส่วนที่สำคัญมากๆ แต่หลายๆ คนไม่สนใจกัน มากนัก มันคือส่วนที่อยู่ใน onCreate ของ Activity

เมื่อเราเปิดดูใน Activity จะพบว่ามี Argument ที่ชื่อว่า Bundle savedInstanceState

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

savedInstanceState เป็น Bundle ที่เก็บ State ของ Activity ในตอนที่ onSaveInstanceState ทำงาน

@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
}

ใน method นี่แหละที่หลายคนมองข้ามไป สำคัญอย่างมากที่เราควร save ค่าตัวแปรต่างๆ ที่อยู่ในระบบ Activity เพราะว่าเวลา Activity คืนชีพกลับมาจะเป็นเหมือนเดิมทุกประการ พูดอย่างนี้อาจจะไม่เห็นภาพ งั้นเรามาลองทำดูกันเลยครับ อันดับแรกให้สร้าง project ขึ้นมา จากนั้น ให้สร้าง Layout ตามนี้ครับ

ข้อแนะนำ: แนะนำอย่า Hard String ใน android:text แบบผมนะครับ :D ควรเก็บ String แยกใน file String ดีกว่า และควรเก็บพวกค่าต่างๆ เช่นค่าความกว้างของ EditText ไว้ใน dimens.xml แต่เพื่อความรวดเร็วผมเลยขออนุญาตทำแบบนี้นะครับ

ต่อมาทำการประกาศตัวแปรไว้ใน Global Variable ให้เรียบร้อยใน Class Activity

private EditText editText;
private Button btnSave, btnShow;

private String name = "";

จากนั้นทำการ Bind View ให้ครบ

editText = (EditText) findViewById(R.id.edt_simple_text);
btnSave = (Button) findViewById(R.id.btn_save);
btnShow = (Button) findViewById(R.id.btn_show);

และทำการ set listener ให้กับปุ่ม โดย เมื่อกดปุ่ม Save จะเก็บค่าที่ได้จาก EditText เก็บไว้ในตัวแปร name ที่เราประกาศเป็น Global Variable

btnSave.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
name = editText.getText().toString();
}
});

และปุ่ม show ให้ทำการ Toast เพื่อแสดงค่า String ที่เก็บไว้ในตัวแปร มาแสดงที่หน้าจอ

btnShow.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(MainActivity.this, "Hello: " + name, Toast.LENGTH_SHORT).show();
}
});

ลองกัน Run ดูครับ โดยให้พิมพ์ข้อความลงใน EditText จากนั้นกด Save และกดปุ่ม Show ก็จะแสดงข้อความที่เราพิมพ์

จากนั้นลองหมุนหน้าจอ และกด Show ครับ

จะเห็นได้ว่าตอนนี้ Activity เมื่อเราหมุนหน้าจอ Activity จะตาย และได้คืนชีพเมื่อตอนหมุนหน้าจอแล้ว แต่เรายังไม่ทำการ Save State ไงครับ ตัวแปรเลยไม่ถูกคืนค่าที่ควรจะเป็น ดังนั้นเรามาดูวิธีกันเลยครับ

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

รูปแบบที่เราต้องเขียนคือ outState.putString(“key”, value); โดยที่เรายังสามารถ put ตัวแปรประเภทอื่นๆ ได้เช่นกัน เช่น Int, Boolean และที่เป็นตัวแปรประเภท Primitive Data Type

เมื่อทำการ Save ลง State แล้ว เราจะต้องคืนค่าให้ตัวแปรเราโดยการใช้ method ที่ชื่อว่า onRestoreInstanceState ครับ

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

ทีนี้ลอง Run แล้วทำตามเดิม จะพบว่าต่อให้เราหมุนจอกี่ครั้ง ตัวแปรก็จะคืนค่าได้อย่างถูกต้องเสมอครับ

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

if (savedInstanceState == null) {
//Fragment
}

ยังจำตอนที่เราหมุนหน้าจอได้ใช่ไหมครับ เมื่อมีการหมุนหน้าจอ Activity จะทำการ Save State ไว้ ทำให้ตัวแปร savedInstanceState ไม่เป็น null นั้นเองครับ พูดอย่างนี้คงไม่เห็นภาพ เรามาทดสอบกันเลยครับ โดยให้เขียนคำสั่งนี้ลงใน onCreate ของ Activity

Log.d("MainActivity", "onCreate: " + (savedInstanceState == null? "Null" : "not Null"));

จากนั้นลองทำการ Run และหมุนหน้าจอ และให้ดูที่ LogCat

D/MainActivity: onCreate: Null
D/MainActivity: onCreate: not Null

จะพบว่าตอนที่ Activity ถูกเปิดขึ้นมา จะยังเป็น Null อยู่ แต่เมื่อหมุนหน้าจอจะพบว่าไม่ Null แล้ว

ทำไมไม่ Null ละ

ก็เพราะว่า Activity จะทำการ Save State ก่อนที่จะตายนั้นเองครับ ทำให้ตัวแปร savedInstanceState ไม่เป็น Null เมื่อหมุนหน้าจอ ตามที่ผมเคยเกริ่นไว้ข้างต้นของบทความว่า ทำไมมี bundle ที่ชื่อว่า savedInstanceState ใน onCreate

ต่อมาก็ส่งสัยอีก ทำไมต้องแปะ Fragment โดยที่ต้องเช็คว่า savedInstanceState เท่ากับ Null ไหม ก็เพราะว่าป้องกันการแปะซ้อน Fragment ซ้อนทับกันเรื่อยๆ นั้นเอง เมื่อมีการหมุนหน้าจอไงครับ เพราะเมื่อหมุนหน้าจอ Activity ตายหรือถูกทำลายก็จะวิ่งตาม Life Cycle

ใครยังไม่ชัวร์เรื่อง Life Cycle ของ Activity, Fragment แนะนำทำความเข้าใจนะครับ :D

เพราะทุกๆ ครั้งที่หมุนหน้าจอ จะต้องผ่าน onCreate ทำให้ไปเรียกคำสั่งแปะ Fragment ทุกๆ ครั้งนั้นเองครับ ดังนั้นเราเลยต้องใส่คำสั่งเช็คตัวแปร savedInstanceState เท่ากับ Null หรือเปล่านั้นเอง

if (savedInstanceState == null) {
getSupportFragmentManager().beginTransaction()
.add(R.id.container, HelloFragment.newInstance())
.commit();
}

เพียงเท่านี้เราก็จะได้คำสั่งแปะ Fragment อย่างถูกต้องแล้วครับ

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

เพิ่มเติม: อ้อในส่วนของ View เองก็สามารถ Save กับ Restore State ของตัวเองได้ด้วย เช่น TextView ก็ใส่ android:freezesText=”true” ใน tag TextView

เอาละครับเหล่าผู้ว่างมากทั้งหลายผมก็ขอจบบทความไว้เพียงเท่านี้นะครับ สำหรับโค้ดทั้งหมดลิงค์นี้ได้เลยครับ ไว้เจอกันใหม่คราวหน้าครับ สวัสดีครับ :)

https://github.com/naijab/state_example

Nattapon Pondongnok 【 naijab.com 】

🚀 Backend Developer and Fullstack Developer ⥤ “ just a lonely man likes coding “

More from Nattapon Pondongnok 【 naijab.com 】