บทความสั้นและปฏิบัติจริงนี้จะพูดถึงการจัดการ UI events ใน Jetpack Compose
ในระบบเก่าใช้ OnClickListeners และ interfaces อื่นๆ ใน Compose ใช้ประโยชน์จาก Sealed Classes, Function Types และ Lambda Expressions ของ Kotlin เต็มที่
หากไม่รู้ composable คืออะไร อ่านบทความพื้นฐานก่อน
สร้างโมเดล UI Events ด้วย Sealed Class
ก่อนอื่น ต้องเข้าใจ UI Events และการสร้างโมเดลด้วย Sealed Classes
เคยอธิบายกระบวนการนี้สำหรับ Java และ Kotlin (ระบบ View เก่า) ไปแล้ว จะทำให้สั้นที่สุด
กระบวนการ
สำหรับแต่ละหน้าจอหรือ sub-screen ถามตัวเอง: ผู้ใช้โต้ตอบกับมันได้อย่างไรบ้าง?
ตัวอย่างจากแอปแรกของผมที่สร้างด้วย Compose ทั้งหมด Graph Sudoku:

Sealed class สำหรับ UI interactions ของหน้าจอนี้:
sealed class ActiveGameEvent { data class OnInput(val input: Int) : ActiveGameEvent() data class OnTileFocused(val x: Int, val y: Int) : ActiveGameEvent() object OnNewGameClicked : ActiveGameEvent() object OnStart : ActiveGameEvent() object OnStop : ActiveGameEvent() }อธิบายสั้น:
- OnInput: ผู้ใช้แตะปุ่ม 입력 (0,1,2,3,4)
- OnTileFocused: ผู้ใช้เลือกไทล์ (ไฮไลท์สีอัมเบอร์)
- OnNewGameClicked: ชื่อบอกเลย
- OnStart และ OnStop: lifecycle events ที่ composables ไม่สนใจ แต่ใช้ใน Activity ที่เป็น Container
หลังมี sealed class แล้ว จัดการ events หลากหลายได้ด้วย event handler function เดียว บางครั้งอาจควรมี multiple handler functions ขึ้นอยู่กับ requirements ของโปรเจกต์
เชื่อมต่อ Software Architecture
สิ่งที่จัดการ events เป็นอะไรก็ได้ บางคนคิด MVVM เป็น gold standard แต่คนมากขึ้นตระหนักว่า ไม่มี architecture เดียวที่เหมาะกับทุกสถานการณ์
สำหรับ Android + Compose วิธีของผมคือ minimalist approach ที่มีในแต่ละ feature (screen):
- (Presentation) Logic class เป็น event handler
- ViewModel เก็บ data สำหรับ render View
- Activity เป็น Container (ไม่ใช่ god object)
- Composables สร้าง View

ไม่ว่าจะใช้ ViewModel, Fragment, หรือ Activity เป็น event handler ทั้งหมดตั้งค่าได้เหมือนกัน: Function Types!
ใน class ที่เลือก ตั้งค่า event handler function รับ sealed class เป็น argument:
class ActiveGameLogic( private val container: ActiveGameContainer?, private val viewModel: ActiveGameViewModel, private val gameRepo: IGameRepository, private val statsRepo: IStatisticsRepository, dispatcher: DispatcherProvider ) : BaseLogic<ActiveGameEvent>(dispatcher), CoroutineScope { //... override fun onEvent(event: ActiveGameEvent) { when (event) { is ActiveGameEvent.OnInput -> onInput( event.input, viewModel.timerState ) ActiveGameEvent.OnNewGameClicked -> onNewGameClicked() ActiveGameEvent.OnStart -> onStart() ActiveGameEvent.OnStop -> onStop() is ActiveGameEvent.OnTileFocused -> onTileFocused(event.x, event.y) } } //... }วิธีนี้ organized และทดสอบทุก Unit ใน class ไม่มี library ได้ผ่าน single entry point
แต่ยังไม่จบ ต้องการ reference ของ event handler function onEvent ไปยัง Composables ใช้ function reference:
class ActiveGameActivity : AppCompatActivity(), ActiveGameContainer { private lateinit var logic: ActiveGameLogic override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val viewModel = ActiveGameViewModel() setContent { ActiveGameScreen( onEventHandler = logic::onEvent, viewModel ) } logic = buildActiveGameLogic(this, viewModel, applicationContext) } //... }บางคนถามทำไมใช้ Activity คำตอบรายละเอียดใน livestream Q&A
สรุป: Fragments ดูไร้ประโยชน์กับ Compose ใน architecture ของผม (ไม่ใช้ Jetpack Navigation) และไม่มีปัญหาใช้ Activities เป็น feature-specific container แค่หลีกเลี่ยง god activities
วิธีอ้างอิง function ใน Kotlin: ให้ class/interface name (หรือข้ามถ้าเป็น Top-Level function) ตามด้วย สอง colon และ ชื่อฟังก์ชันโดยไม่มี arguments หรือ brackets:
onEventHandler = logic::onEventแทนที่ onClickListener ด้วย Jetpack Compose onClick Modifier
พร้อมแล้ว มาดูใน composable Root composable ต้องมี event handler function เป็น parameter:
@Composable fun ActiveGameScreen( onEventHandler: (ActiveGameEvent) -> Unit, viewModel: ActiveGameViewModel ) { //... }Function type syntax อาจ tricky แต่เข้าใจว่า นี่คือ reference ไปยังฟังก์ชัน ไม่ต่างจาก reference ไปยัง class มากนัก
เช่นกัน ไม่ควรสร้าง giant composables:
- แยก UI เป็น smallest reasonable parts
- ห่อใน composable function
- Composable ที่มี UI interaction ต้องได้รับ reference ของ event handler function
Composable สำหรับปุ่มป้อนข้อมูล Sudoku ที่ได้รับ event handler โดย reference:
@Composable fun SudokuInputButton( onEventHandler: (ActiveGameEvent) -> Unit, number: Int ) { Button( onClick = { onEventHandler.invoke(ActiveGameEvent.OnInput(number)) }, modifier = Modifier .requiredSize(56.dp) .padding(2.dp) ) { Text( text = number.toString(), style = inputButton.copy(color = MaterialTheme.colors.onPrimary), modifier = Modifier.fillMaxSize() ) } }เพื่อส่ง event ไป logic class ใช้ฟังก์ชัน invoke ซึ่งรับ arguments ตาม function type definition (รับ ActiveGameEvent ในกรณีนี้)
ตอนนี้คุณพร้อมจัดการ UI interaction events ใน Kotlin (Compose หรือไม่ก็ตาม) โดยใช้ประโยชน์จากภาษาโปรแกรมมิ่งทันสมัยและสวยงามนี้
ถ้าชอบบทความนี้ แชร์บนโซเชียลมีเดีย และดูทรัพยากรด้านล่างเพื่อสนับสนุนนักเขียนและ content creator อิสระ
Social
หาผมได้ที่ Instagram และ Twitter
บทช่วยสอนและคอร์สของผม
https://youtube.com/wiseass https://www.freecodecamp.org/news/author/ryan-michael-kay/ https://skl.sh/35IdKsj (introduction to Android with Android Studio)