State Hoisting เป็นรูปแบบใน Jetpack Compose ที่สถานะถูกย้ายออกจากฟังก์ชัน Composable ลูกไปยังฟังก์ชันแม่ และลูกจะได้รับข้อมูลผ่านพารามิเตอร์และแจ้งการเปลี่ยนแปลงผ่าน Callback นี่คือการนำหลักการโฟลว์ข้อมูลทิศทางเดียว (UDF) มาใช้ โดยสถานะถูกยกขึ้นและอีเวนต์ถูกส่งลงมา ตามข้อมูลจาก Google Android Developers, 2026 พบว่า State Hoisting ทำให้คอมโพเนนต์สามารถนำกลับมาใช้ใหม่ ทดสอบได้ และคาดเดาได้
ประเด็นสำคัญ
State Hoisting เป็นรูปแบบที่ฟังก์ชัน Composable ไม่ได้เป็นเจ้าของสถานะ แต่ได้รับจากภายนอก แทนที่จะใช้ var ภายในฟังก์ชัน จะใช้พารามิเตอร์สองตัว: ค่าสำหรับแสดงผลและ Callback แลมบ์ดาสำหรับจัดการการเปลี่ยนแปลง ในทางเทคนิค หมายความว่าคอมโพเนนต์ลูกกลายเป็นไร้สถานะ (stateless) ในขณะที่แม่มีสถานะ (stateful)
ตัวอย่าง: คอมโพเนนต์ TextField จาก Material3 ไม่ได้จัดเก็บข้อความที่ป้อนไว้ภายใน โดยรับ value: String และ onValueChange: (String) -> Unit แม่ที่เรียก TextField ประกาศ var value by remember { mutableStateOf("") } และส่ง value และ onValueChange นี่คือ State Hoisting แบบคลาสสิก: TextField เป็นคอมโพเนนต์ธรรมดา (เพียงแสดงและรายงานการป้อนข้อมูล) แม่เป็นคอมโพเนนต์อัจฉริยะ (เป็นเจ้าของสถานะ)
ไร้สถานะเทียบกับมีสถานะ: คอมโพเนนต์ไร้สถานะทดสอบได้ง่ายกว่า — ไม่ขึ้นอยู่กับสถานะภายใน พฤติกรรมของมันถูกกำหนดโดยพารามิเตอร์อินพุตทั้งหมด คอมโพเนนต์มีสถานะสะดวกสำหรับการสร้างต้นแบบอย่างรวดเร็วแต่ยากต่อการนำกลับมาใช้ใหม่: มันผูกติดกับแหล่งข้อมูลเดียวอย่างแน่นหนา State Hoisting ให้คุณเลือก: คอมโพเนนต์ใด ๆ ก็สามารถทำให้ไร้สถานะได้โดยการย้ายสถานะขึ้นไป
UDF (โฟลว์ข้อมูลทิศทางเดียว) เป็นหลักการทางสถาปัตยกรรมที่ข้อมูลเคลื่อนที่ในทิศทางเดียว: จากแหล่งความจริง (ViewModel หรือ Composable แม่) ไปยัง UI และอีเวนต์ไหลในทิศทางตรงกันข้าม State Hoisting คือการนำ UDF ไปใช้ในระดับคอมโพเนนต์แต่ละตัว แทนที่คอมโพเนนต์แต่ละตัวจะตัดสินใจว่าเมื่อไรและอย่างไรที่จะเปลี่ยนสถานะของตัวเอง มันจะแจ้งให้แม่ทราบเกี่ยวกับอีเวนต์ และแม่ตัดสินใจว่าจะเปลี่ยนสถานะอย่างไร
ข้อดีของ UDF: ความสามารถในการคาดเดา — สถานะเปลี่ยนในที่เดียวเท่านั้น กำจัดสภาวะการแข่งขัน; การติดตามได้ — สแตกการเรียกช่วยให้สามารถสร้างห่วงโซ่การเปลี่ยนแปลงขึ้นใหม่ได้; การทดสอบ — ตรรกะที่มีสถานะสามารถแยกออกเป็นคลาสแยกต่างหากและทดสอบโดยไม่มี UI ในโปรเจกต์ขนาดใหญ่ UDF ที่รวมกับ State Hoisting เป็นมาตรฐานโดยพฤตินัย
แหล่งความจริงเดียว (Single Source of Truth) เป็นอีกหลักการหนึ่งที่มาพร้อมกับ UDF แต่ละส่วนของสถานะมีแหล่งที่มาเพียงแหล่งเดียว หากสองคอมโพเนนต์ใช้สถานะเดียวกัน แหล่งที่มาควรถูกแชร์ (ในระดับ ViewModel หรือแม่ร่วม) State Hoisting ทำให้แน่ใจว่าแหล่งที่มาอยู่ด้านบนในลำดับชั้นและไม่เกิดการซ้ำซ้อนของสถานะ
| ทิศทาง | สิ่งที่ถูกส่ง | วิธีนำไปใช้ |
|---|---|---|
| ลง (แม่ → ลูก) | ค่าสำหรับแสดง | พารามิเตอร์ value: T |
| ขึ้น (ลูก → แม่) | อีเวนต์การเปลี่ยนแปลง | พารามิเตอร์ onValueChange: (T) -> Unit |
กฎหลัก: สถานะควรถูกยกขึ้นไปยังระดับต่ำสุดที่เป็นไปได้เพียงพอสำหรับคอมโพเนนต์ทั้งหมดที่ต้องการ หากสถานะถูกใช้ภายในคอมโพเนนต์เดียวเท่านั้น — ให้เก็บไว้เฉพาะที่ หากคอมโพเนนต์สองตัวที่อยู่ติดกันต้องการสถานะเดียวกัน — ยกขึ้นไปยังแม่ร่วม หากสถานะจำเป็นทั่วทั้งหน้าจอ — ยกขึ้นไปยัง ViewModel
กฎการยกขั้นต่ำ ป้องกันความซับซ้อนที่ไม่จำเป็น ไม่มีประโยชน์ที่จะยกสถานะของช่องข้อความไปยัง ViewModel หากใช้ภายในหน้าจอเดียวเท่านั้นและไม่คงอยู่เมื่อ Activity ถูกสร้างใหม่ ใช้ rememberSaveable ในระดับแม่ของหน้าจอ ไม่ใช่ ViewModel สำหรับสถานะ UI ที่ควรรอดจากการหมุนหน้าจอแต่ไม่จำเป็นต้องใช้โดยตรรกะทางธุรกิจ
เมื่อไรที่จะยกไปยัง ViewModel: หากสถานะต้องคงอยู่หลังจากการสร้าง Activity ใหม่ หากจำเป็นต้องใช้โดยหลายหน้าจอ หากการเปลี่ยนสถานะกระตุ้นตรรกะทางธุรกิจ (คำขอเครือข่าย ฐานข้อมูล) State Hoisting ในระดับ ViewModel เป็นรูปแบบมาตรฐานในสถาปัตยกรรม MVVM ที่ชั้น UI เป็นไร้สถานะและ ViewModel มีสถานะ
// ❌ ไม่ดี: คอมโพเนนต์เป็นเจ้าของสถานะของตัวเอง
@Composable
fun BadTextField(label: String) {
var text by remember { mutableStateOf("") }
TextField(value = text, onValueChange = { text = it }, label = { Text(label) })
}
// ✅ ดี: State Hoisting — สถานะอยู่ในแม่
@Composable
fun GoodTextField(value: String, onValueChange: (String) -> Unit, label: String) {
TextField(value = value, onValueChange = onValueChange, label = { Text(label) })
}
// การใช้งาน: แม่เป็นเจ้าของสถานะ
@Composable
fun Form() {
var name by rememberSaveable { mutableStateOf("") }
GoodTextField(value = name, onValueChange = { name = it }, label = "Name")
}
พิจารณาหน้าจอเข้าสู่ระบบที่มีสองช่อง (อีเมล รหัสผ่าน) และปุ่มหนึ่งปุ่ม คอมโพเนนต์ทั้งสามรับสถานะผ่าน State Hoisting: อีเมลและรหัสผ่านถูกจัดการโดยแม่ ปุ่มรับสถานะ enabled เป็นค่า
// State Hoisting ในระดับหน้าจอ
@Composable
fun LoginScreen(viewModel: LoginViewModel) {
val uiState by viewModel.uiState.collectAsState()
Column(modifier = Modifier.padding(16.dp)) {
// ช่องอีเมล — State Hoisting ผ่านแลมบ์ดา
EmailField(
email = uiState.email,
onEmailChange = { viewModel.onEmailChanged(it) }
)
// ช่องรหัสผ่าน — เช่นเดียวกัน
PasswordField(
password = uiState.password,
onPasswordChange = { viewModel.onPasswordChanged(it) }
)
// ปุ่ม — รับเฉพาะ enabled (อ่านอย่างเดียว)
LoginButton(enabled = uiState.isFormValid, onClick = viewModel::login)
}
}
// คอมโพเนนต์ไร้สถานะ: รับอีเมล + Callback
@Composable
fun EmailField(email: String, onEmailChange: (String) -> Unit) {
OutlinedTextField(
value = email,
onValueChange = onEmailChange,
label = { Text("อีเมล") },
singleLine = true
)
}
// คอมโพเนนต์ปุ่มไร้สถานะ
@Composable
fun LoginButton(enabled: Boolean, onClick: () -> Unit) {
Button(onClick = onClick, enabled = enabled) {
Text("เข้าสู่ระบบ")
}
}
EmailField และ PasswordField ไร้สถานะโดยสมบูรณ์ สามารถนำกลับมาใช้ใหม่บนหน้าจอใดก็ได้โดยเชื่อมต่อกับแหล่งข้อมูลใดก็ได้ LoginButton รับ enabled เป็นแบบอ่านอย่างเดียว — นี่เป็นอีกรูปแบบหนึ่งของ State Hoisting ที่สถานะไม่ได้ถูกยกขึ้น (ปุ่มไม่สามารถเปิดใช้งานตัวเองได้) แต่ถูกส่งลงมาแบบพร้อมใช้งาน วิธีการนี้ให้ความยืดหยุ่นสูงสุดด้วยการผูกพันระหว่างคอมโพเนนต์น้อยที่สุด
ไม่ใช่ทุกสถานะที่จำเป็นต้องยกขึ้น สถานะเฉพาะที่ (State ภายใน Composable) มีความชอบธรรมเมื่อ: ข้อมูลจำเป็นภายในคอมโพเนนต์เดียวเท่านั้น ไม่ส่งผลกระทบต่อองค์ประกอบข้างเคียง และไม่ควรคงอยู่หลังจากการประกอบใหม่ของส่วนเฉพาะ ตัวอย่างเช่น สถานะแอนิเมชัน โฟกัสของช่องป้อนข้อมูล ตำแหน่งการเลื่อนปัจจุบัน — การเก็บไว้เฉพาะที่เป็นเรื่องสมเหตุสมผล
เมื่อไรที่ State Hoisting จำเป็น: สถานะถูกใช้โดยคอมโพเนนต์ลูกหลายตัว; การเปลี่ยนแปลงในลูกหนึ่งควรสะท้อนในอีกลูกหนึ่ง; ตรรกะของการเปลี่ยนแปลงสถานะต้องถูกทดสอบแยกจาก UI; สถานะควรคงอยู่หลังจากการสร้าง Activity ใหม่ ในกรณีเหล่านี้ สถานะเฉพาะที่สร้างความซ้ำซ้อนและความไม่สอดคล้องของข้อมูล
วิธีการแบบผสม: เก็บสถานะขั้นต่ำไว้เฉพาะที่ ยกส่วนที่เหลือ กฎของ Compose: “ยกสถานะให้สูงเท่าที่จำเป็นและต่ำเท่าที่เป็นไปได้” ในทางปฏิบัติ หมายถึงเริ่มต้นด้วย remember เฉพาะที่ และยกระดับเมื่อจำเป็นต้องเข้าถึงจากคอมโพเนนต์อื่นเท่านั้น อย่าใช้ State Hoisting เชิงป้องกัน — มันทำให้โค้ดซับซ้อนโดยไม่จำเป็น
คำถามที่พบบ่อย
State Hoisting เป็นรูปแบบระดับคอมโพเนนต์ UI ViewModel เป็นชั้นทางสถาปัตยกรรมสำหรับตรรกะทางธุรกิจ State Hoisting สามารถยกสถานะไปยังระดับ Composable แม่ ระดับหน้าจอ หรือ ViewModel ViewModel คือจุดยกสูงสุดสำหรับสถานะที่ต้องคงอยู่หลังจากการสร้าง Activity ใหม่
คอมโพเนนต์ไร้สถานะถูกทดสอบโดยเพียงแค่ส่งค่า เรียก Composable ด้วยพารามิเตอร์ที่ต้องการและตรวจสอบการแสดงผลผ่าน ComposeTestRule การเปลี่ยนแปลงสถานะถูกทดสอบในระดับแม่หรือ ViewModel — แยกจาก UI ซึ่งช่วยลดความซับซ้อนของการทดสอบได้อย่างมาก: ไม่จำเป็นต้องจำลองการประกอบใหม่ภายในคอมโพเนนต์
ได้ เป็นวิธีปฏิบัติที่พบได้ทั่วไป หากคอมโพเนนต์ต้องการเพียงแสดงข้อมูลโดยไม่มีความสามารถในการแก้ไข — ส่ง State<T> (อ่านอย่างเดียว) คอมโพเนนต์จะติดตามการเปลี่ยนแปลงแต่ไม่สามารถเริ่มต้นการเปลี่ยนแปลงได้ ซึ่งเสริมสร้างการห่อหุ้มและปกป้องข้อมูลจากการกลายพันธุ์ที่ไม่พึงประสงค์
สำหรับการส่งผ่านแบบลึก ใช้ CompositionLocal หรือส่งผ่านพารามิเตอร์ของ Composable แม่ หากสถานะจำเป็นทั่วทั้งหน้าจอ — แยกไปยัง ViewModel และใช้ collectAsState() การส่งผ่าน 5+ ระดับเป็นสัญญาณของสถาปัตยกรรมที่ไม่ถูกต้อง; พิจารณาลำดับชั้นของคอมโพเนนต์ใหม่
State Hoisting อาจเพิ่มจำนวนการประกอบใหม่เล็กน้อยเนื่องจากการเปลี่ยนแปลงในแม่อาจประกอบลูกทั้งหมดใหม่ ใช้ derivedStateOf เพื่อกรองการเปลี่ยนแปลงและ keys ใน LazyColumn สำหรับการอัปเดตตามเป้าหมาย ในสถานการณ์ส่วนใหญ่ โอเวอร์เฮดของ State Hoisting นั้นเล็กน้อยเมื่อเทียบกับประโยชน์ของการบำรุงรักษา
สรุป
เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร
IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ
อ่านเพิ่มเติม