Task และ Back Stack ตอนที่ 2 - Back Stack

อย่างที่เรารู้กันว่าบนแอนดรอยด์ได้ออกแบบให้ Activity มีได้มากกว่าหนึ่งตัว และจะแสดงอยู่บนหน้าจอได้เพียงแค่ Activity เดียวเท่านั้น (ในบทความนี้จะยังไม่พูดถึงกรณืของ Multi-window) และ Activity ที่โดนแทนที่ด้วย Activity ตัวอื่น ก็จะถูกเก็บไว้ในสิ่งที่เรียกว่า Back Stack

โดย Back Stack ถูกสร้างขึ้นมาเพื่อเก็บลำดับการทำงานของ Activity ในรูปแบบของ Stack แบบ Last In, First Out

บทความในชุดนี้

หลักการทำงานของ Back Stack

เพื่อให้เห็นภาพมากขึ้น สมมติว่าแอปมี Activity อยู่ทั้งหมด 3 ตัวคือ Home, List และ Detail ซึ่งทั้งหมดเป็น Activity คนละตัวกัน

0:00
/

ถ้าดูภาพประกอบข้างบน ตอนที่ List เข้ามาแสดงบนหน้าจอ จะทำให้ Home ถูกย้ายเข้าไปอยู่ใน Back Stack แทน (จะเรียกว่า Push ก็ได้) และเช่นเดียวกับ List ในตอนที่ถูก Detail เข้ามาแสดงแทนที่

เวลาที่ผู้ใช้กดปุ่ม Back, เรียกคำสั่ง onBackPressed หรือ finish() ก็จะทำให้ Activity ตัวล่าสุดถูกทำลายทิ้ง แล้ว Back Stack ก็จะคืน Activity ตัวล่าสุด (Pop) กลับขึ้นมาและทำงานต่อจากเดิม

และเมื่อ Activity ที่แสดงผลบนหน้าจอถูกทำลายไปทีละตัวเรื่อย ๆ จนไม่เหลือ Activity ใด ๆ ใน Back Stack แล้ว ก็จะเป็นการออกจากแอป แล้วเข้าสู่หน้า Home Screen นั่นเอง

โดยเราจะเรียก Back Stack ของ Activity กันแบบสั้น ๆ กันว่า "Activity Stack"

สำหรับ Activity ที่มี Fragment อยู่ข้างใน

ในกรณีที่ Activity มี Fragment อยู่ด้วย ก็จะมี Back Stack สำหรับ Fragment ด้วยเช่นกัน  โดยจะเรียกว่า Fragment Stack และจะมี Fragment Manager เป็นคนคอยจัดการให้

Activity Stack กับ Fragment Stack มีหน้าที่เหมือนกัน แค่จัดการ Activity กับ Fragment แยกกัน

โดย Fragment Stack จะเริ่มทำงานก่อน จนไม่เหลือ Fragment ข้างใน จากนั้น Activity Stack ถึงจะเริ่มทำงาน

0:00
/

ในกรณีที่ Activity มี Fragment อยู่ การทำงานก็จะต่างไปจากเดิมเล็กน้อย โดยที่ Fragment Stack จะเริ่มทำงานก่อน จนไม่เหลือ Fragment ข้างใน จากนั้น Activity Stack ถึงจะเริ่มทำงาน

Fragment Stack จะมีผลกับการกดปุ่ม Back, การใช้คำสั่งใน Fragment Manager, และคำสั่ง onBackPressed เท่านั้น ยกเว้นคำสั่ง finish ที่เป็นคำสั่งของ Activity โดยตรง

และอย่าลืมว่า Activity แต่ละตัวมี Fragment Manager ของใครของมัน นั่นหมายความว่าการทำงานของ Fragment Stack ก็จะแยกจากกันด้วย

ถ้าไม่ต้องการให้ Activity บางตัวอยู่ใน Back Stack

โดยปกติแล้วเวลานักพัฒนาสร้าง Activity ขึ้นมาใหม่ Activity นั้นก็จะถูกเพิ่มเข้าไปใน Back Stack ให้ทันทีโดยไม่ต้องทำอะไร

แต่ถ้าไม่ต้องการให้ Activity บางตัวอยู่ใน Back Stack จะต้องกำหนด XML Attribute ที่ชื่อว่า android:noHistory="true" ให้กับ Activity นั้น ๆ ใน Android Manifest

<!-- AndroidManifest.xml -->
<application>
    <!-- ... -->
    <activity
        ...
        android:noHistory="true" /> 
</application>

หรือจะกำหนด FLAG_ACTIVITY_NO_HISTORY ให้กับ Intent Flag ในตอนที่สร้าง Intent ขึ้นมาก็ได้เช่นกัน

val activity: Activity = /* ... */
val intent = Intent(/* ... */).apply {
    flags = Intent.FLAG_ACTIVITY_NO_HISTORY
}
activity.startActivity(intent)

ถ้าไม่ต้องการให้ Fragment บางตัวอยู่ใน Back Stack ด้วยล่ะ ?

สำหรับ Fragment นั้น นักพัฒนาจะเป็นคนกำหนดผ่านคำสั่งของ Fragment Manager ด้วยตัวเองอยู่แล้ว ยกตัวอย่างเช่น

val fragmentManager: FragmentManager = /* ... */
fragmentManager.commit {
    add(/* ... */)
    addToBackStack(/* ... */)
}

จะเห็นว่าการเพิ่มคำสั่ง addToBackStack เข้าไปจะทำให้ Fragment ตัวนั้นเพิ่มเข้าไปใน Back Stack ด้วย ดังนั้นถ้าไม่ต้องการให้ Fragment ตัวนั้นอยู่ใน Back Stack ก็ให้เอาคำสั่งดังกล่าวออกไปเท่านั้นเอง

แล้วนักพัฒนาสามารถทำอะไรกับ Back Stack ได้บ้าง

ต้องบอกก่อนว่านักพัฒนาไม่สามารถสลับลำดับของ Activity หรือ Fragment ที่อยู่ข้างใน Back Stack ได้เลย

และสิ่งที่นักพัฒนาสามารถกำหนดเองได้ก็จะมีแค่การดึง Activity หรือ Fragment ขึ้นมาแสดงเท่านั้น (Pop) โดยมีเงื่อนไขว่า Activity หรือ Fragment ที่มีลำดับที่สูงกว่าก็จะถูกดึงออกมาและหายไปในทันที (ตาม Concept ของ Stack น่ะแหละ)

โดยการกำหนดค่าที่เกี่ยวกับข้องการดึง Activity ออกจาก Activity Stack จะอธิบายในหัวข้อ Task และ Back Stack - ตอนที่ 5 - Activity Launch Mode [1/2] กับ ตอนที่ 6 - Activity Launch Mode [2/2]

ส่วน Fragment Stack เจ้าของบล็อกเคยอธิบายไปแล้วในบทความจัดการ Fragment Back​ Stack อย่างไรให้เหมาะสม

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

การเพิ่ม Activity เข้าไปใน Activity Stack โดยตรง

โดยปกติแล้ว Activity ที่ถูกสร้างขึ้นมาและแสดงอยู่บนหน้าจอจะถูกเพิ่มเข้าไปใน Activity Stack โดยอัตโนมัติ แต่ในบางครั้งนักพัฒนาอาจจะต้องการเพิ่ม Activity เข้าไปก่อน Activity ที่จะแสดงบนหน้าจอ

เพื่อให้เห็นภาพมากขึ้น ลองย้อนกลับไปดูตัวอย่าง Activity ทั้ง 3 ตัวที่แบ่งเป็น Home, List, และ Detail กันใหม่อีกครั้ง

โดยรอบนี้จะแตกต่างไปจากเดิมตรงที่ผู้ใช้กดที่ Notification แล้วจะให้เปิดหน้า Detail ในทันที แต่อยากให้ผู้ใช้กดปุ่ม Back แล้วกลับไปเห็นหน้า List และ Home ตามลำดับ

0:00
/

เพื่อให้แอปรองรับการทำงานแบบนี้ จะต้องใช้คลาสที่ชื่อว่า TaskStackBuilder เข้ามาช่วยในตอนที่สั่งให้แสดงหน้า Detail เพื่อบอกให้รู้ว่าจะต้องเพิ่ม Activity ไหนเข้าไปใน Activity Stack บ้าง ซึ่งจะมีอยู่ทั้งหมด 2 รูปแบบด้วยกัน

รูปแบบแรกคือการกำหนด Intent ของ Activity ทั้งหมดเรียงตามลำดับให้กับ TaskStackBuilder แบบนี้

val context: Context = /* ... */
val contentIntent: PendingIntent? = TaskStackBuilder.create(context)
    .addNextIntent(Intent(context, HomeActivity::class.java))
    .addNextIntent(Intent(context, ListActivity::class.java))
    .addNextIntent(Intent(context, DetailActivity::class.java))
    .getPendingIntent(/* ... */)

และอีกรูปแบบคือการใช้ android:parentActivityName เข้ามาช่วยเพื่อกำหนดว่า Activity ก่อนหน้าเป็น Activity ใด

<!-- AndroidManifest.xml -->
<application>
    <activity android:name=".HomeActivity" />
    <activity
        android:name=".ListActivity"
        android:parentActivityName=".HomeActivity"/>
    <activity
        android:name=".DetailActivity"
        android:parentActivityName=".ListActivity" />
</application>

และเวลากำหนดค่าใน TaskStackBuilder ก็แค่กำหนดค่าของ Detail ด้วยคำสั่ง addParentStack และ addNextIntent

val context: Context = /* ... */
val contentIntent: PendingIntent? = TaskStackBuilder.create(context)
    .addParentStack(DetailActivity::class.java)
    .addNextIntent(Intent(context, DetailActivity::class.java))
    .getPendingIntent(/* ... */)

และที่สำคัญ TaskStackBuilder จะสร้าง PendingIntent เพื่อนำไปใช้งานกับ Notification เท่านั้น นั่นหมายความว่าวิธีนี้จะใช้กับคำสั่งอย่าง startActivity ไม่ได้นั่นเอง

ดักการทำงานของการกดปุ่ม Back

จากการทำงานของ Back Stack จะเห็นว่าการกดปุ่ม Back บนแอนดรอยด์จะส่งผลต่อการทำงานของ Activity Stack และ Fragment Stack โดยตรง จึงทำให้ในบางครั้งนักพัฒนาก็ต้องการดักการทำงานของปุ่ม Back เพื่อทำอะไรบางอย่างให้เสร็จก่อน

และเพื่อให้การทำงานของ Back Stack นั้นถูกต้องตามปกติ นักพัฒนาควรใช้ OnBackPressedDispatcher ที่สามารถจัดการกับโค้ดได้ง่ายและสะดวกกว่าการสร้าง Override Method สำหรับ onBackPressed โดยตรง

// MainActivity.kt
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        /* ... */
        onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
    }

    private val onBackPressedCallback = object : OnBackPressedCallback(true) {
        override fun handleOnBackPressed() {
            // Do something
        }
    }
}

จะเห็นว่าการใช้งาน OnBackPressedDispatcher จะต้องสร้าง OnBackPressedCallback ขึ้นมาเพื่อกำหนดคำสั่งที่ต้องการ และนักพัฒนาสามารถกำหนดได้ว่าจะให้ดักการกดปุ่ม Back ตอนไหน ผ่านคำสั่งที่ชื่อว่า isEnabled

val onBackPressedCallback: OnBackPressedCallback = /* ... */

// Enable back pressed interception
onBackPressedCallback.isEnabled = true

// Disable back pressed interception
onBackPressedCallback.isEnabled = false

และถ้าต้องการดักการกดปุ่ม Back ใน Fragment ก็แนะนำให้ใช้ OnBackPressedDispatcher เช่นกัน โดยเรียกผ่าน requiresActivity() อีกที

// HomeFragment.kt
class HomeFragment : Fragment() {
    override fun onAttach(context: Context) {
        /* ... */
        requireActivity().onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
    }

    private val onBackPressedCallback = object : OnBackPressedCallback(true) {
        override fun handleOnBackPressed() {
            // Do something
        }
    }
    /* ... */
}

สรุป

Back Stack เป็นหนึ่งในการทำงานพื้นฐานของแอปบนแอนดรอยด์ที่คอยจัดการกับ Activity และ Fragment อยู่เบื้องหลังในรูปแบบของ Activity Stack และ Fragment Stack

ดังนั้นการเข้าใจการทำงานของ Back Stack ก็จะช่วยให้นักพัฒนาสามารถรู้และจัดการกับลำดับการทำงานของ Activity Stack และ Fragment Stack ได้อย่างถูกต้อง รวมไปถึงการกำหนดค่าบางอย่างเพื่อรองรับการทำงานที่เฉพาะเจาะจงอย่างกรณีของ Notification ได้อีกด้วย

แหล่งข้อมูลอ้างอิง