State เป็นหัวใจสำคัญอย่างหนึ่งของการใช้ Declarative UI อย่าง Jetpack Compose ที่จะคอยบอกว่า Composable แต่ละตัวควรแสดงผลแบบใด และนั่นจึงทำให้ State ที่จะใช้ใน Composable มีอยู่ 2 รูปแบบด้วยกัน คือ Stateless และ Stateful
ชื่อเต็ม ๆ ของ Composable ทั้ง 2 แบบคือ Stateless Composable และ Stateful Composable
บทความในชุดเดียวกัน
- Stateless & Stateful Composable [Now Reading]
- State Hoisting
- Slot API
- Shape
- UI Preview
- Composition Local
- Adoption Strategy
โดยทั้งคู่จะมีวิธีสร้างทุกอย่างเหมือนกันทั้งหมด ต่างกันตรงที่การจัดการ State ที่ Composable นั้น ๆ เรียกใช้งาน
ยกตัวอย่างเช่น เจ้าของบล็อกต้องการสร้าง Composable ขึ้นมาในลักษณะแบบนี้
ItemOption
Row
Checkbox
Text
Stateless Composable
Composable ใด ๆ ที่ไม่ได้เก็บ State ไว้ในตัวเองและใช้ State Hoisting เพื่อส่ง State ใด ๆ เข้ามาเป็น Argument ของ Composable Function จะถือว่าเป็น Stateless Composable
ยกตัวอย่างเช่น
@Composable
fun ItemOption(
label: String,
checked: Boolean,
onItemClicked: (Boolean) -> Unit,
) {
Row(/* ... */) {
Checkbox(
checked = checked,
onCheckedChange = onItemClicked,
)
Text(
text = label,
/* ... */,
)
}
}
นั่นหมายความว่า Composable ที่เป็น Host ก็จะทำหน้าที่จัดการ State ให้กับ Stateless Composable แทน
@Composable
fun MenuOption() {
var itemChecked by remember { mutableStateOf(false) }
ItemOption(
label = "Option 1",
checked = itemChecked,
onItemClick = { checked ->
itemChecked = checked
// Do something
}
)
}
โดย Stateless Composable จะมีข้อดีตรงที่
- การทำงานไม่ซับซ้อน - เมื่อ State ถูกส่งเข้ามาจากภายนอก ทำให้เราสามารถคาดเดาการแสดงผลของ Composable ตัวนี้ได้ง่าย โดยดูจาก State ที่ส่งเข้ามานั่นแหละ
- ใช้ซ้ำได้ง่าย - เพราะเป็น Composable ที่จบในตัว ไม่ต้องพึ่งพา Dependency ใด ๆ จากภายนอก จึงเรียกใช้งานในหลาย ๆ ที่ได้ง่าย
- เขียนเทสสะดวก - เวลาต้องการ Preview หรือเขียนเทสเพื่อทดสอบการทำงานก็สามารถส่ง State เข้าไปได้ทันที
ถึงแม้ว่าใน Stateless Composable ที่เราสร้างขึ้นมาจะมี Composable พื้นฐานของ Jetpack Compose ที่มีเบื้องหลังเป็น Stateful Composable แต่เราก็จะเรียก Composable ที่เราสร้างขึ้นมาว่าเป็น Stateless Composable อยู่ดี เพราะเราไม่สนใจเบื้องหลังการทำงานของ Composable เหล่านั้น
Stateful Composable
Composable ใด ๆ ที่มี State เป็นของตัวเองและจัดการ State นั้นภายในตัวเอง จะถือว่าเป็น Stateful Composable
ยกตัวอย่างเช่น
@Composable
fun ItemOption(
label: String,
initial: Boolean,
onItemClicked: (Boolean) -> Unit,
) {
var itemChecked by remember { mutableStateOf(initial) }
Row(/* ... */) {
Checkbox(
checked = itemChecked,
onCheckedChange = { checked ->
itemChecked = checked
onItemClicked(checked)
},
)
Text(
text = label,
/* ... */,
)
}
}
ในกรณีนี้จะเห็นว่ามีบางส่วนที่เป็น State Hoisting อยู่ก็จริง แต่มีการใช้ remember
กับ itemChecked
จึงทำให้ Composable ตัวนี้มี State อยู่ข้างในตัวเอง เพียงแค่รับ Argument บางส่วนจากภายนอกได้
ทำให้ Composable ที่เป็น Host ไม่สามารถควบคุม State ทั้งหมดได้โดยตรง ทำได้แค่ส่งค่าบางส่วนเข้าไปและรับค่าที่ Stateless Composable ส่งออกมาเท่านั้น
@Composable
fun MenuOption() {
ItemOption(
label = "Option 1",
initial = false,
onItemClick = { checked ->
// Do something
}
)
}
โดย Stateful Composable จะมีข้อดีตรงที่
- จัดการ State จบในตัว - ทำให้ Composable ที่เป็น Host ไม่ต้องจัดการกับ State ให้
- รองรับการทำงานที่ซับซ้อน - เมื่อเก็บ State ไว้ในตัวเอง จึงทำให้สามารถทำบางอย่างที่ซับซ้อนมาก ๆ ได้
- State เก็บได้นาน - ตราบเท่าที่ Composable จะยังคงอยู่ ถึงแม้ว่าจะเกิด Recomposition ก็ตาม
Stateless หรือ Stateful ดี?
โดยปกติแล้ว Composable ส่วนใหญ่ควรจะสร้างเป็นแบบ Stateless Composable ไว้ก่อนดีกว่า เพราะนำไปใช้งานซ้ำได้ง่าย สามารถ Preview เพื่อแก้ไขหรือเขียนเทสได้สะดวก และถ้าจำเป็นต้องจัดการกับ State จริง ๆ ก็ให้เรียกใช้งานใน Stateful Composable เพื่อทำหน้าที่จัดการกับ State ของ Stateless Composable ตัวนั้นแทน
data class State(/* ... */)
@Composable
fun StatelessComposable(state: State) {
/* ... */
}
// With remember
@Composable
fun RememberStatefulComposable() {
val state: State by remember { /* ... */ }
StatelessComposable(state = state)
}
// With ViewModel
class CustomViewModel : ViewModel() {
private val _state = MutableStateFlow(State())
val state: StateFlow<State> = _uiState.asStateFlow()
}
@Composable
fun ViewModelStatefulComposable() {
val viewModel: CustomViewModel = /* ... */
val state: State by viewModel.state.collectAsState()
StatelessComposable(state = state)
}
จากตัวอย่างจะเห็นว่านักพัฒนาสามารถนำ StatelessComposable
ไปใช้ได้ทั้งใน RememberStatefulComposable
และ ViewModelStatefulComposable
โดยที่ทั้งคู่จัดการกับ State ที่จะส่งให้ StatelessComposable
แตกต่างกัน
และนอกจากนี้ ViewModelStatefulComposable ก็สามารถทำเป็น StatelessComposable ได้เช่นกัน โดยทำ State Hoisting ให้กับ ViewModel แบบนี้แทน
@Composable
fun ViewModelStatelessComposable(
viewModel: CustomViewModel
) {
val state: State by viewModel.state.collectAsState()
StatelessComposable(state = state)
}
สรุป
ไม่ว่าจะเป็น Stateless Composable หรือ Statefule Composable ก็ตาม ทั้งคู่ล้วนข้อดีข้อเสียและจุดประสงค์ในการใช้งานที่แตกต่างกันออกไป ดังนั้นการทำความเข้าใจในแต่ละรูปแบบและเลือกใช้งานให้เหมาะสมกับแต่ละโจทย์จึงเป็นวิธีที่ดีที่สุดที่ทำให้นักพัฒนาใช้งาน Jetpack Compose ได้อย่างมีประสิทธิภาพ