ทำไม Activity และ Fragment ถึงต้องเป็น Empty Constructor
Activity และ Fragment นั้นเป็น Component พื้นฐานที่แทบจะขาดไปไม่ได้สำหรับหลาย ๆ แอปในปัจจุบัน และนักพัฒนาทุกคนก็คุ้นเคยกับโค้ดในลักษณะนี้กันเป็นอย่างดี
// Activity
class MainActivity: Activity() { /* ... */ }
// Fragment
class HomeFragment: Fragment() { /* ... */ }
แต่สงสัยกันมั้ยว่าทำไมเวลาสร้าง Activity หรือ Fragment จะต้องกำหนด Constructor ให้เป็น Empty Constructor เสมอ? เพราะถ้าลองเพิ่ม Constructor Arguments เข้าไปใน Activity ก็จะทำให้แอปพังตอนที่เรียก Activity นั้นในทันที
class MainActivity(val userId: String): AppCompatActivity() { /* ... */ }
FATAL EXCEPTION: main
Process: a.b.c, PID: 17093
...
Caused by: java.lang.InstantiationException: java.lang.Class<a.b.c.MainActivity> has no zero argument constructor
at java.lang.Class.newInstance(Native Method)
at android.app.AppComponentFactory.instantiateActivity(AppComponentFactory.java:95)
at androidx.core.app.CoreComponentFactory.instantiateActivity(CoreComponentFactory.java:45)
at android.app.Instrumentation.newActivity(Instrumentation.java:1273)
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3536)
App Component ทุกตัวต้องสร้างเป็น Empty Constructor เสมอ
ทั้งนี้ก็เพราะว่าแอนดรอยด์ได้ออกแบบให้ App Component (Activity, Service, Broadcast Receiver และ Content Provider) ทำงานโดยใช้ Intent ในการควบคุมทำงานและเก็บข้อมูลไว้ใน Bundle เพื่อให้ระบบแอนดรอยด์จัดการเรื่อง State Management ให้ในตัว
ด้วยเหตุนี้ทำให้เวลาสร้าง App Component จึงจำเป็นต้องสร้างด้วย Empty Constructor เสมอ
แล้ว Fragment ล่ะ ?
เนื่องจาก Fragment ถูกสร้างขึ้นมาเพื่อเสริมและทำงานร่วมกับ Activity จึงทำให้ถูกออกแบบในรูปแบบคล้ายกัน และต้องเป็น Empty Constructor เหมือนกัน
แต่สิ่งที่แตกต่างกันคือ Fragment ไม่ได้ถูกสร้างจากระบบแอนดรอยด์โดยตรงเหมือนกับ App Component (ใช่ครับ Fragment ไม่นับเป็น App Component) จึงถูกจัดการด้วย Fragment Manager แทน โดยให้นักพัฒนาสร้าง Fragment ขึ้นมาเองแล้วโยนให้ Fragment Manager จัดการต่อ
val fragmentManager: FragmentManager = /* ... */
fragmentManager.commit {
val fragment: Fragment = /* ... */
add(fragment, /* ... */)
addToBackStack(/* ... */)
}
เจ้าของบล็อกใช้ Fragment KTX เพื่อช่วยให้คำสั่งสั้นกว่าเดิม
แต่การปล่อยให้นักพัฒนาสร้าง Fragment ขึ้นมาเองก็อาจจะทำให้นักพัฒนาเผลอใส่ Constructor Argument เข้าไปโดยไม่รู้ตัว
class HomeFragment(val userId: String): Fragment() { /* ... */ }
ซึ่งจะทำให้เกิดปัญหาว่า Fragment Manager สามารถสร้าง Fragment ดังกล่าวและทำงานได้เหมือนปกติก็จริง แต่แอปจะพังทันทีในตอนที่เกิด Config Changes หรือ Fragment Recreation
FATAL EXCEPTION: main
Process: a.b.c, PID: 28210
...
Caused by: androidx.fragment.app.Fragment$InstantiationException: Unable to instantiate fragment a.b.c.HomeFragment: could not find Fragment constructor
at androidx.fragment.app.Fragment.instantiate(Fragment.java:657)
at androidx.fragment.app.FragmentContainer.instantiate(FragmentContainer.java:57)
at androidx.fragment.app.FragmentManager$2.instantiate(FragmentManager.java:470)
at androidx.fragment.app.FragmentState.instantiate(FragmentState.java:81)
at androidx.fragment.app.FragmentStateManager.<init>(FragmentStateManager.java:85)
at androidx.fragment.app.FragmentManager.restoreSaveStateInternal(FragmentManager.java:2432)
at androidx.fragment.app.FragmentManager.attachController(FragmentManager.java:2606)
at androidx.fragment.app.FragmentController.attachHost(FragmentController.java:116)
at androidx.fragment.app.FragmentActivity.lambda$init$3$androidx-fragment-app-FragmentActivity(FragmentActivity.java:141)
at androidx.fragment.app.FragmentActivity$$ExternalSyntheticLambda0.onContextAvailable(Unknown Source:2)
at androidx.activity.contextaware.ContextAwareHelper.dispatchOnContextAvailable(ContextAwareHelper.java:99)
at androidx.activity.ComponentActivity.onCreate(ComponentActivity.java:352)
at androidx.fragment.app.FragmentActivity.onCreate(FragmentActivity.java:218)
at a.b.c.MainActivity.onCreate(MainActivity.kt:12)
นั่นก็เพราะว่า Fragment จะถูกสร้างขึ้นมาใหม่อีกครั้งด้วย Empty Constructor ผ่าน Static Method ที่อยู่ใน Fragment โดยอัตโนมัติ
และในขณะเดียวกัน การกำหนด Fragment ไว้ใน android:name
ของ FragmentContainerView ก็จะทำให้แอปพังในทันทีเหมือนกัน ถ้า Fragment ตัวนั้น ๆ มี Constructor Argument อยู่ด้วย
<androidx.fragment.app.FragmentContainerView
android:name="a.b.c.HomeFragment"
... />
แล้วทำไม AppCompatActivity และ Fragment (AndroidX) ถึงมี Constructor Argument ได้ล่ะ?
ใน AppCompatActivity และ Fragment ของ AndroidX เวอร์ชันล่าสุดจะพบว่า นักพัฒนาสามารถกำหนด Layout ID ผ่าน Constructor Argument ได้เลย
// Activity
class MainActivity : AppCompatActivity(R.layout.activity_main) { /* ... */ }
// Fragment
class HomeFragment: Fragment(R.layout.fragment_home) { /* ... */ }
อ้าว ไหนบอกว่า Activity กับ Fragment ต้องเป็น Empty Constructor เท่านั้นล่ะ?
ถ้านักพัฒนาลองเข้าไปดู ComponentActivity ซึ่งเป็น Super Class ของ AppCompatActivity ก็จะพบว่า จริง ๆ แล้วเป็นแค่เพียง Overload Constructor เท่านั้น และยังมี Empty Constructor อยู่ โดยที่ Layout ID จะถูกส่งเข้าไปข้างในเพื่อกำหนดค่าดังกล่าวให้ในภายหลัง
แน่นอนว่า Fragment ก็ใช้วิธีเดียวกัน ดังนั้นในกรณีนี้ Activity และ Fragment ยังคงใช้ Empty Constructor อยู่นั่นเอง
สรุป
ด้วยรูปแบบการทำงานของแอนดรอยด์ App Component ทั้งหมดของแอนดรอยด์จึงต้องเรียกใช้งานและส่งข้อมูลผ่าน Intent แทนการสร้าง Constructor Argument เพราะการจะสร้าง App Component ขึ้นมานั้น จะต้องทำผ่านระบบของแอนดรอยด์เองเท่านั้น
val context: Context = /* ... */
val intent = Intent(context, MainActivity::class.java).apply {
putExtra(/* ... */, /* ... */)
}
startActivity(intent)
และในขณะเดียวกัน Fragment ก็ถูกออกแบบมาในลักษณะเหมือนกัน จึงต้องสร้างเป็น Empty Constructor เช่นเดียวกัน และถ้าต้องการส่งข้อมูลให้ Fragment ดังกล่าวก็ให้ส่งผ่าน arguments
แทน
val fragment: HomeFragment = HomeFragment().apply {
arguments = Bundle().apply {
// Put data here
}
}
อย่าเผลอใส่ Constructor Argument ให้กับ Fragment ล่ะ!!