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

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

Fragment จะมี Empty Constructor เสมอ

เพราะ Fragment เป็น Component ตัวหนึ่งที่มีโอกาสโดนระบบแอนดรอยด์เคลียร์ทิ้งชั่วคราวเพื่อคืน Memory ได้เช่นเดียวกับ Activity จึงทำให้ระบบแอนดรอยด์บังคับว่า Constuctor ของ Fragment จะต้องมี Empty Constructor อยู่ด้วย

class HomeFragment : Fragment() { ... }

ถ้านักพัฒนาสร้าง Fragment แบบไม่มี Empty Constructor อยู่ด้วย ตอนสร้าง Instance ขึ้นมาจะทำงานได้ปกติ แต่แอปจะพังทันทีในตอนที่ Recreate Fragment ตัวนั้นขึ้นมาใหม่

Fragment สามารถทำ Overload Constructor โดยมี Empty Constructor ได้เช่นกัน แต่ส่วนใหญ่จะไม่ทำแบบนั้นกัน และจะใช้วิธีในหัวข้อถัดไปแทน

ส่งข้อมูลเริ่มต้นให้ Fragment ด้วย Bundle แทนการสร้าง Constructor Parameter

ในบางครั้งนักพัฒนาอาจจะต้องการส่งข้อมูลเริ่มต้นเพื่อให้ Fragment เริ่มทำงานตามที่ต้องการได้ ซึ่งการส่งข้อมูลแบบนี้จะใช้วิธีส่งผ่าน Argument ของ Fragment ในรูปของ Bundle

val fragment = HomeFragment().apply {
    arguments = Bundle().apply {
        putString("productId", /* ... */)
        putInt("amount", /* ... */)
    }
}
เหมือนกับตอนที่แนบข้อมูลใน Intent เพื่อส่งให้ Activity เลยใช่มั้ยล่ะ

และ Fragment ก็จะดึงข้อมูลจาก Argument ดังกล่าวเพื่อนำไปใช้งานต่อ

class HomeFragment : Fragment() { 
	private val productId: String? by lazy { arguments?.getString("productId") }
    private val amount: Int? by lazy { arguments?.getInt("amount") }
    /* ... */
}

การส่งข้อมูลด้วยวิธีนี้จะมีความต่อการทำงานของ Fragment ในตอนที่ Fragment ถูกชทำลายเพื่อคืน Memory ให้กับระบบแอนดรอยด์ชั่วคราว เพราะข้อมูลใน Argument ของ Fragment จะถูกเก็บไว้ในระบบแอนดรอยด์และคืนให้ตอนที่ Recreate Fragment ทำให้ข้อมูลดังกล่าวไม่มีทางหาย ในขณะที่การสร้าง Constructor Parameter จะเจอปัญหาว่าข้อมูลจะหายไปในระหว่างนั้น

ใช้ Function ใน Companion Object เพื่อให้ง่ายต่อการสร้าง Fragment

จากตัวอย่างก่อนหน้าจะเห็นว่าการแนบข้อมูลเริ่มต้นจะส่งผ่าน Arguments ของ Fragment ที่มีโค้ดประมาณหนึ่ง จึงนิยมสร้าง Function ไว้ใน Companion Object ของ Fragment เพื่อรวมคำสั่งในการสร้าง Fragment แทน

class HomeFragment : Fragment() {
    /* ... */
    companion object {
        fun newInstance(productId: String, amount: Int) = HomeFragment().apply {
            arguments = Bundle().apply {
                putString("productId", productId)
                putInt("amount", amount)
            }
        }
    }
}

ทำให้เวลาสร้าง Instance ของ Fragment จากที่ใดก็ตาม จะเรียกผ่านคำสั่ง newInstance ที่มีความกระชับกว่าได้เลย

val productId: String = /* ... */
val amount: Int = /* ... */
val fragment = HomeFragment.newInstance(productId, amount)
ในกรณีที่ใช้ Jetpack Navigation จะกำหนดค่าผ่าน Destination Argument ของ Jetpack Navigation ได้เลย ไม่ต้องสร้าง Function แบบนี้ขึ้นมาเอง

Fragment ก็รองรับ State Changes

Fragment รองรับ State Changes แบบเดียวกับ Activity ไม่ว่าจะเป็น Configuration Changes หรือ Activity Recreation ก็ตาม จึงทำให้ Fragment สามารถกลับมาทำงานต่อจากเดิมได้

ค่าใด ๆ ที่ประกาศไว้ใน Fragment ก็จะต้องจัดการแบบเดียวกับ Activity ด้วยเช่นกัน

และสิ่งที่ควรระวังก็คือ ถ้านักพัฒนาสร้าง Fragment ผ่าน Fragment Manager อย่าเผลอไปสร้างซ้ำในตอนที่เกิด State Changes เด็ดขาด ไม่เช่นนั้นจะได้ Fragment ซ้ำขึ้นมาอีกตัวโดยไม่รู้ตัว

ยกตัวอย่างเช่น ถ้าสร้าง Fragment ผ่าน Fragment Manager ตอน onCreate ของ Activity ก็ให้สร้างเฉพาะตอนที่ Activity ถูกสร้างขึ้นมาครั้งแรกเท่านั้น

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        /* ... */
        if (savedInstanceState == null) {
            supportFragmentManager.commit {
                replace(/* ... */)
                addToBackStack(null)
            }
        }
    }
    /* ... */
}

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

สรุป

เพื่อให้ Fragment รองรับการ Recreate ใหม่ได้ ทำให้ระบบแอนดรอยด์ออกแบบให้ Fragment ต้องมี Empty Constructor ด้วยเสมอ และการส่งข้อมูลในตอนที่สร้าง Instance ของ Fragment ตัวนั้น ก็จะให้ส่งผ่าน Argument ของ Fragment แทน

ทั้งหมดนี้ก็เพื่อให้ Fragment รองรับการทำงานของระบบแอนดรอยด์ในทุกเงื่อนไขได้อย่างถูกต้องนั่นเอง