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 ล่ะ!!