ว่าด้วยเรื่อง State Hoisting ใน Jetpack Compose

ใน Jetpack Compose นักพัฒนาสามารถสร้าง Composable ได้ตามใจชอบว่าจะให้ UI มีลักษณอย่างใด รวมถึงจัดการกับ State และ Event ที่อยู่ใน Composable ด้วย และหนึ่งในเทคนิคสำคัญที่จะทำให้นักพัฒนาสามารถสร้าง Composable ได้อย่างมีประสิทธิภาพก็คือการทำ State Hoisting นั่นเอง

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

และการทำ State Hoisting ก็จะทำให้ Composable นั้น ๆ กลายเป็น Stateless Composable ไปโดยปริยาย ซึ่งจะมีข้อดีดังนี้

  • Single Source of Truth - คาดเดาการทำงานที่จะเกิดขึ้นได้ง่าย ลดบั๊กที่เกิดจากการจัดการ State ที่ไม่ครอบคลุม
  • Encapsulated - มีการทำงานที่จบในตัว ไม่สามารถถูกเปลี่ยนแปลงได้ และทำงานตาม State ที่ส่งเข้ามาเท่านั้น
  • Shareable - สามารถใช้ State ร่วมกับ Composable ตัวอื่นได้
  • Interceptable - สามารถแก้ไขหรือหรือจัดการกับ State ก่อนที่จะส่งเข้ามาใน Composable ได้
  • Decoupled - State จะเก็บไว้ที่ไหนก็ได้ ทำให้ Composable ไม่ยึดติดกับ State Holder ใด ๆ จะเก็บไว้ใน Stateful Composable ก็ได้ หรือจะเก็บไว้ใน ViewModel ก็ได้เช่นกัน

โดยการทำ State Hoisting นั้นมีหลักการง่าย ๆ ก็คือการย้าย State และ Event ที่อยู่ภายใน Composable ให้กลายเป็น Argument ที่ส่งเข้ามาใน Composable Function นั้น ๆ แทน

สมมติว่าเจ้าของบล็อกมี Composable แบบนี้

@Composable
fun Topic() {
    val icon: Int = R.drawable.ic_title
    val title: String = "Title"
    Row(
        modifier = Modifier.clickable(
            onClick = {
                // Do something
            }
        )
    ) {
        Icon(painter = painterResource(icon), /* ... */)
        Text(text = title, /* ... */)
    }
}

จะเห็นว่า State และ Event ทั้งหมดอยู่ใน Composable ที่ชื่อว่า Topic ทั้งหมด ทำให้การนำไปใช้งานทำได้ค่อนข้างจำกัด

เมื่อทำ State Hoisting ก็จะกลายเป็นแบบนี้แทน

@Composable
fun Topic(
    @ResId icon: Int,
    title: String,
    onTopicClicked: () -> Unit,
) {
    Row(
        modifier = Modifier.clickable(
            onClick = onTopicClicked
        )
    ) {
        Icon(painter = painterResource(icon), /* ... */)
        Text(text = title, /* ... */)
    }
}

นอกจากนี้นักพัฒนายังสามารถรับ Modifier เข้ามาแบบเดียวกับ State อื่น ๆ เพื่อส่งให้กับ Row ก่อนที่จะเพิ่มคำสั่ง clickable เพิ่มเข้าไปใน Modifier ก็ได้เช่นกัน

@Composable
fun Topic(
    modifier: Modifier = Modifier,
    @ResId icon: Int,
    title: String,
    onTopicClicked: () -> Unit,
) {
    Row(
        modifier = modifier.clickable(
            onClick = onTopicClicked
        )
    ) {
        Icon(painter = painterResource(icon), /* ... */)
        Text(text = title, /* ... */)
    }
}

ด้วยวิธีนี้จึงทำให้ Topic กลายเป็น Stateless Composable ที่สามารถนำไปใช้งานได้ง่าย เอาไปใช้ซ้ำก็ได้

@Composable
fun MainScreen() {
    Topic(
       icon = R.drawable.ic_title,
       title = "Title",
       onTopicClicked = { /* Do something */ }
    )
    /* ... */
}

อีกทั้งยังเปลี่ยนรูปแบบในการแสดงผลผ่าน Modifier ได้ประมาณนึงด้วย เช่น กำหนดสีพื้นหลังที่แตกต่างกันไป

@Composable
fun MainScreen() {
    Topic(
       modifier = Modifier.background(
           color = Color.White, 
           shape = RectangleShape,
       )
       /* ... */
    )
    /* ... */
}

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