State เป็นหัวใจสำคัญอย่างหนึ่งของการใช้ Declarative UI อย่าง Jetpack Compose ที่จะคอยบอกว่า Composable แต่ละตัวควรแสดงผลแบบใด และนั่นจึงทำให้ State ที่จะใช้ใน Composable มีอยู่ 2 รูปแบบด้วยกัน คือ Stateless และ Stateful

ชื่อเต็ม ๆ ของ Composable ทั้ง 2 แบบคือ Stateless Composable และ Stateful Composable

บทความในชุดเดียวกัน

โดยทั้งคู่จะมีวิธีสร้างทุกอย่างเหมือนกันทั้งหมด ต่างกันตรงที่การจัดการ 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 ได้อย่างมีประสิทธิภาพ

แหล่งข้อมูลอ้างอิง