สำหรับซีรีย์ Fragment ในคราวนี้ก็ขอพูดถึงเรื่องการสร้าง Fragment กันเสียหน่อย เพราะเป็นอีกหนึ่งเรื่องที่นักพัฒนาอาจจะทำผิดได้โดยไม่รู้ตัว ดังนั้นการรู้วิธีที่ถูกต้องไว้ก่อนก็ย่อมสำคัญเหมือนกันเนอะ

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

สมมติว่าเจ้าของบล็อกสร้างคลาส Fragment ขึ้นมาแบบนี้

class HomeFragment : Fragment() {
    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        return inflater.inflate(R.layout.fragment_home, container, false)
    }
    ...
}

เวลาเรียกใช้งานก็สร้าง Instance ของ Fragment ขึ้นมาแล้วใช้ FragmentTransaction เพื่อแปะลงบน ViewGroup แบบนี้

val supportFragmentManager: FragmentManager = ...
supportFragmentManager
    .beginTransaction()
    .replace(R.id.layout_fragment_container, HomeFragment())
    .commit()

แล้วถ้าอยากจะสร้าง Fragment ขึ้นมาพร้อมกับส่งค่าบางอย่างเข้าไปด้วยล่ะ?

ก็เพิ่ม Argument เข้าไปใน Constructor แบบนี้สิ!!

class HomeFragment(private val data: String) : Fragment() {
    ...
}

ซึ่งวิธีนี้เป็นวิธีที่ไม่ควรทำ เพราะ Constructor ของ Fragment ควรใช้ Default Constructor หรือก็คือ Empty Argument นั่นเอง

ดังนั้นเวลาจะโยนค่าบางอย่างเข้าไปในตอนที่สร้าง Fragment จะต้องเอาค่านั้นๆไปเก็บไว้ใน Bundle แล้วกำหนดให้ Fragment ผ่านคำสั่ง argument แบบนี้

val data = "ANY_VALUE"
val bundle = Bundle().apply {
    putString("EXTRA_KEY", data)
}
val fragment = HomeFragment().apply {
    arguments = bundle
}
val supportFragmentManager: FragmentManager = ...
supportFragmentManager
    .beginTransaction()
    .replace(R.id.layout_fragment_container, fragment)
    .commit()

และใน Fragment ก็จะดึงค่ามาใช้งานได้ผ่าน argument

class HomeFragment : Fragment() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val key: String? = arguments?.getString("EXTRA_KEY")
    }
    ...
}

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

class HomeFragment : Fragment() {
    companion object {
        private const val EXTRA_KEY = "extra_key"

        fun newInstance(key: String?): HomeFragment {
            val fragment = HomeFragment()
            val bundle = Bundle()
            bundle.putString(EXTRA_KEY, key)
            fragment.arguments = bundle
            return fragment
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val key: String? = arguments?.getString("EXTRA_KEY")
    }
    ...
}

วิธีแบบนี้จะช่วยลดคำสั่งในปลายทางที่จะเรียกใช้งาน Fragment ให้น้อยลง เพราะเอาความยุ่งเหยิงของคำสั่งมารวมไว้ใน Fragment ตัวนั้นซะเลย (ก็มันเป็นคำสั่งสำหรับ Fragment ตัวนั้นอยู่แล้วนิ)

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

ดังนั้นคำสั่งเวลาเรียกใช้งาน Fragment ในรูปแบบนี้ก็จะดูสบายตาขึ้นเยอะเลย

val data = "ANY_VALUE"
val supportFragmentManager: FragmentManager = ...
supportFragmentManager
    .beginTransaction()
    .replace(R.id.layout_fragment_container, HomeFragment.newInstance(data))
    .commit()

ทำไมต้องสร้าง Fragment ด้วย Default Constructor เท่านั้น?

เนื่องจาก Fragment นั้นมีโอกาสที่จะถูกทำลายได้เสมอเมื่อ Memory ไม่เพียงพอใช้งานตาม Lifecycle ที่ทางทีมแอนดรอยด์ได้กำหนดไว้ ซึ่งช่วงที่ Fragment ตัวนั้นถูกสร้างขึ้นมาใหม่ (Recreate) เพื่อให้ทำงานต่อจากเดิมได้ สิ่งที่เกิดขึ้นคือระบบจะสร้าง Fragment ขึ้นมาจาก Default Constructor เท่านั้น

ดังนั้นต่อให้ผู้ที่หลงเข้ามาอ่านสร้าง Constructor แบบไหนไว้ก็ตาม ก็จะไม่ถูกเรียกเมื่อมีการ Recreate ซึ่งนั่นอาจจะทำให้เกิดปัญหาว่าข้อมูลที่ส่งมาอาจจะมีการสูญหายไปได้ แต่ถ้าเก็บข้อมูลไว้ใน Bundle ข้อมูลเหล่านั้นก็จะคงอยู่ได้ตาม Lifeycle ของ Fragment

และประโยชน์อีกอย่างหนึ่งที่เจ้าของบล็อกมองเห็นก็คือ การเอาข้อมูลเก็บไว้ใน Bundle นั้นจะต้องเป็นข้อมูลที่เป็น Model เท่านั้น จึงช่วยป้องกันไม่ให้นักพัฒนาใช้วิธีผิดๆอย่างการโยน View หรือ Class ต่างๆที่ไม่ใช่ Model เข้ามาให้ Fragment

สรุป

การสร้าง Fragment ควรจะสร้างจาก Default Constructor ที่ทางแอนดรอยด์ได้กำหนดไว้ และการกำหนดข้อมูลตอนที่สร้าง Instance ของ Fragment ก็ให้ยัดไว้ใน Bundle แทน

ส่วนคำสั่งที่ใช้ในการสร้าง Fragment ก็แนะนำว่าให้สร้างเป็น Static Method ไว้ในคลาส Fragment นั้นๆไปเลย ถึงแม้ว่า Fragment ตัวนั้นไม่มีค่าอะไรที่ต้องกำหนดก็ตาม

class HomeFragment : Fragment() {
    companion object {
        fun newInstance(): HomeFragment = HomeFragment()
    }
    ...
}

class DetailFragment : Fragment() {
    companion object {
        private const val EXTRA_INDEX = "extra_index"

        fun newInstance(index: Int): DetailFragment = DetailFragment().apply {
            arguments = Bundle().apply {
                putInt(EXTRA_INDEX, index)
            }
        }
    }
    ...
}

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