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

บทความในซีรีย์เดียวกัน

ถึงจะเป็น Fragment แต่ก็ต้องจัดการกับ UI State ด้วยเหมือนกัน

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

จึงเป็นเรื่องปกติที่ Fragment จะมี UI State เป็นของตัวเอง และนักพัฒนาก็ควรจะคอย Save และ Restore UI State ใน Fragment ให้ถูกต้องด้วยเช่นกัน

แล้วขั้นตอนของ Fragment ต่างกับ Activity ยังไง?

เมื่อลองย้อนกลับไปดู Cycle ของ Activity ก็จะมีลักษณะแบบนี้

โดยการ Save จะเกิดขึ้นใน onSaveInstanceState และ Restore จะเกิดขึ้นใน onRestoreInstanceState

แต่เนื่องจาก Fragment นั้นไม่ได้มี Lifecycle เหมือนกับ Activity ทั้งหมด และมีลำดับของ Lifecycle ที่จะสอดคล้องกับ Activity ที่ถือ Fragment ตัวนั้นไว้ จึงทำให้ Cycle ของ Fragment เวลาที่นักพัฒนาต้องการ Save และ Restore ให้กับ UI State กลายเป็นแบบนี้แทน

ซึ่งบน Fragment จะมีแค่ onSaveInstanceState แต่ไม่มี onRestoreInstanceState ให้ใช้เหมือน Activity

แล้วจะต้อง Restore UI State ตอนไหนล่ะ?

เนื่องจาก Fragment ถูกออกแบบมาให้ Attach/Detach บน Activity ตัวไหนก็ได้ เพื่อให้สิ่งที่อยู่ใน Fragment สามารถ Reuse ได้ตลอดเวลา (เป็นเหตุผลที่ทำให้ Fragment มี Lifecycle ที่แตกต่างกับ Activity และขึ้นอยู่กับ Lifecycle ของ Activity ด้วย) จึงทำให้การ Restore UI State บน Fragment ไม่ได้มีจังหวะที่ตายตัวแบบ Activity

ด้วย Lifecycle ที่ต่างกันจึงทำให้ Bundle ที่ส่งเข้ามาใน Fragment นั้นมีมากถึง 3 ช่วงด้วยกัน

// HomeFragment.kt
class HomeFragment: Fragment() {
    override fun onCreate(savedInstanceState: Bundle?) {
        /* ... */
    }

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
        return /* ... */
    }

    override fun onActivityCreated(savedInstanceState: Bundle?) {
        /* ... */
    }
}
  • onCreate(...) : ตอนที่ Instance ของ Fragment ถูกสร้างขึ้นมา แต่ยังไม่ได้สร้าง View ที่จะใช้กับ Fragment ตัวนั้นๆ
  • onCreateView(...) : ตอนที่ Fragment ทำการสร้าง View เพื่อใช้ใน Fragment ตัวนั้นๆ
  • onActivityCreated(...) : ตอนที่ Fragment ถูกนำไปแปะอยู่บน Activity ที่ต้องการเรียกใช้งาน

สำหรับการ Restore UI State หรือการคืนค่าให้กับ Fragment ควรทำใน onCreateView(...)

จริงๆแล้วนักพัฒนาสามารถ Restore UI State ใน onCreate หรือ onActivityCreated ก็ได้เหมือนกัน แต่เพื่อให้สอดคล้องกับการสร้าง View ใน Fragment การ Restore ใน onCreateView(...)จะถือว่าเป็นช่วงที่เหมาะสมที่สุด

ตัวอย่างการ Save และ Restore บน Fragment

สมมติว่าเจ้าของบล็อกมี Instance ที่เป็น UI State อยู่ใน Fragment แบบนี้

// HomeFragment.kt
class HomeFragment: Fragment() {
    private var name: String? = null
    /* ... */
}

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

หลังจากนี้จะใช้คำว่า Config Changes กับ App Process เพื่อความกระชับของคำ
// HomeFragment.kt
class HomeFragment : Fragment() {
    private var name: String? = null

    companion object {
        private const val EXTRA_NAME = "extra_name"
    }

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
        /* ... */
        savedInstanceState?.run {
            name = getString(EXTRA_NAME)
        }
    }

    override fun onSaveInstanceState(outState: Bundle) {
        /* ... */
        outState.putString(EXTRA_NAME, name)
    }
    /* ... */
}

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

หรือจะย้ายไปทำใน onViewCreated ก็ได้เหมือนกันนะ เพราะเป็น Override Method ที่จะถูกเรียกต่อจาก onCreateView อีกทีนึง เหมาะกับผู้ที่หลงเข้ามาอ่านที่อยากจะให้คำสั่งใน onCreateView มีแค่คำสั่งเกี่ยวกับการสร้าง View เท่านั้น

// HomeFragment.kt
class HomeFragment : Fragment() {
    private var name: String? = null

    companion object {
        private const val EXTRA_NAME = "extra_name"
    }

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
        /* ... */
        // savedInstanceState?.run {
        //     name = getString(EXTRA_NAME)
        // }
    }
    
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        savedInstanceState?.run {
            name = getString(EXTRA_NAME)
        }
    }

    override fun onSaveInstanceState(outState: Bundle) {
        /* ... */
        outState.putString(EXTRA_NAME, name)
    }
    /* ... */
}

สำหรับ Fragment ที่ถูกทำลายในระหว่างการทำงานของ Activity ได้

ถ้า Fragment ถูกสร้างขึ้นมาเพื่อแสดงบน Activity อยู่ตลอดเวลา ไม่ว่าจะเป็น

  • สร้างใน Layout Resource ด้วย <fragment>
  • สร้างตั้งแต่ตอน onCreate ของ Activity
โดยที่ Lifecycle ของ Fragment เปลี่ยนแปลงไปตาม Lifecycle ของ Activity ตั้งแต่ต้นจนจบ

ก็อาจจะไม่ต้องทำอะไรเพิ่มเติมนอกจากการใช้คำสั่งเพื่อจัดการกับ UI State

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

ด้วยเงื่อนไขดังกล่าว เมื่อ Fragment ที่เคยถูกทำลายกลับขึ้นมาทำงานใหม่อีกครั้ง จะพบปัญหาว่าคำสั่งคืนค่าที่อยู่ใน onCreateView มีโอกาสที่จะคืนค่ากลับมาไม่ได้ในบางครั้ง

ทั้งนี้ก็เพราะว่า Fragment ไม่ได้แสดงอยู่บน Activity จึงไม่จำเป็นต้องสร้าง View ขึ้นมาเลยและตอนที่เกิด Config Changes ก็จะเรียกคำสั่ง onSaveInstanceState และ onDestroy เท่านั้น เมื่อ Fragment ถูกสร้างขึ้นมาใหม่อีกครั้งก็จะเรียกแค่คำสั่ง onCreate ทำให้การคืนค่านั้นยังไม่เกิดขึ้นเพราะคำสั่ง onCreateView จะทำงานตอนที่ Fragment สร้าง View เพื่อแสดงผลบน Activity เท่านั้น

ดังนั้นถ้าเกิด Config Changes ในครั้งที่ 2 ในระหว่างนี้ ก็จะทำให้คำสั่ง onSaveInstanceState ไม่มีผลทันที เพราะว่า UI State ที่เก็บไว้ในตอนแรกสุด ยังไม่ได้คืนกลับไปให้ตัวแปรใน Fragment เลย

ปัญหานี้เกิดขึ้นกับ Fragment ที่ไม่ได้แสดงผลบน Activity และเกิด Config Changes มากกว่า 1 ครั้งขึ้นไป

เพื่อแก้ไขปัญหาดังกล่าว นักพัฒนาจึงต้องเพิ่มคำสั่งsetRetainInstance(true) ไว้ใน onCreate ของ Fragment แบบนี้

// HomeFragment.kt
class HomeFragment : Fragment() {
    override fun onCreate(savedInstanceState: Bundle?) {
        /* ... */
        retainInstance = true
    }
    /* ... */
}

คำสั่งดังกล่าวจะเป็นการบอกให้ระบบแอนดรอยด์เก็บ Instance ของ Fragment ไว้ในตอนที่เกิด Config Changes ให้ด้วย ซึ่งจะทำให้ onDestroy และ onCreate ของ Fragment ไม่ทำงาน เพราะ Fragment ไม่ได้ถูกทำลายนั่นเอง

สรุป

จะเห็นว่าการจัดการกับ UI State บน Fragment นั้นมีความแตกต่างกับ Activity อยู่เล็กน้อย แต่ก็ยังคงมีรูปแบบที่คล้ายๆกัน เพราะมีลำดับการทำงานของ Lifecycle ที่แตกต่างกัน

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