DisposableEffect — เป็นฟังก์ชัน composable ใน Jetpack Compose ที่ออกแบบมาสำหรับการดำเนินการที่ต้องการการเริ่มต้นอย่างชัดเจนและการทำความสะอาดทรัพยากรภายหลัง แตกต่างจาก side-effect API อื่นๆ DisposableEffect มีบล็อก onDispose ที่รับประกันว่าจะทำงานเมื่อส่วนประกอบออกจากคอมโพซิชันหรือเมื่อคีย์เปลี่ยนแปลง ทำให้มันขาดไม่ได้สำหรับการทำงานกับการสมัครแบบเนทีฟ ตัวฟังเซ็นเซอร์และทรัพยากรฮาร์ดแวร์ ตาม Android Developers Documentation (2025), แนะนำให้ใช้ DisposableEffect ในทุกสถานการณ์ที่ต้องการคู่ setup/teardown คล้ายกับ onStart/onStop ในวงจรอายุ Activity
ข้อมูลสำคัญ
DisposableEffect เป็นเครื่องมือสำคัญสำหรับการจัดการทรัพยากรใน Jetpack Compose คุณสมบัติหลักคือการเรียกใช้บล็อก onDispose อย่างรับประกันเมื่อวงจรอายุของส่วนประกอบ composable สิ้นสุดลง พฤติกรรมนี้สำคัญอย่างยิ่งสำหรับการพัฒนา Android ที่การสมัครบริการระบบที่ไม่ได้รับการปิดอย่างถูกต้องอาจนำไปสู่การรั่วไหลของหน่วยความจำและการขัดข้องของแอปพลิเคชัน
แตกต่างจาก LaunchedEffect, ที่ทำงานในบริบทอะซิงโครนัสของ coroutine DisposableEffect ทำงานแบบซิงโครนัส ซึ่งหมายความว่าไม่สามารถเรียกใช้ฟังก์ชัน suspend ภายในได้ การทำงานแบบซิงโครนัสให้ความสามารถในการคาดการณ์: คุณมั่นใจได้ว่าโค้ดการเริ่มต้นจะทำงานก่อนการแสดงผลครั้งแรกและโค้ดการทำความสะอาดจะทำงานก่อนที่ส่วนประกอบจะถูกลบออกจากหน่วยความจำ
ตาม เอกสาร Jetpack Compose (2025), DisposableEffect ควรใช้ในสี่สถานการณ์หลัก: (1) การสมัครบริการระบบ (เซ็นเซอร์, LocationManager) (2) การลงทะเบียน BroadcastReceiver (3) การทำงานกับไลบรารีที่ใช้ callback ที่ไม่รองรับ coroutine (4) การผูกส่วนประกอบ Compose กับระบบ View แบบเก่าผ่าน AndroidView
class SensorManager(private val context: Context) {
fun startListening(callback: (Float) -> Unit) { /* register */ }
fun stopListening() { /* cancel */ }
}
@Composable
fun SensorDisplay() {
val sensorManager = remember { SensorManager(context) }
var value by remember { mutableStateOf(0f) }
DisposableEffect(Unit) {
sensorManager.startListening { value = it }
onDispose { sensorManager.stopListening() }
}
Text("เซ็นเซอร์: $value")
}
กลไกภายใน DisposableEffect ขึ้นอยู่กับเฟสของวงจรอายุคอมโพซิชัน เมื่อส่วนประกอบ composable เข้าสู่คอมโพซิชัน DisposableEffect จะเรียกใช้บล็อกโค้ดที่ส่งมา บล็อกนี้ส่งคืนอ็อบเจกต์ DisposableEffectResult ที่มี lambda onDispose คอมโพซิชันจะบันทึกผลลัพธ์นี้และเรียก onDispose เมื่อส่วนประกอบออกจากคอมโพซิชัน — โดยไม่คำนึงถึงสาเหตุ (การนำทาง การเปลี่ยนแปลงสถานะพ่อแม่ การลบออกจาก LazyColumn)
กลไก คีย์ ใน DisposableEffect ทำงานคล้ายกับ LaunchedEffect: เมื่อคีย์ใดๆ เปลี่ยนแปลง อันดับแรก onDispose จะทำงานสำหรับสถานะเก่า จากนั้นบล็อกการเริ่มต้นจะทำงานอีกครั้งด้วยคีย์ใหม่ ซึ่งช่วยให้สามารถกำหนดค่าทรัพยากรใหม่เมื่อพารามิเตอร์เปลี่ยนแปลง ตัวอย่างเช่น หากคีย์เป็น URL ของซ็อกเก็ต เมื่อเปลี่ยนแปลง ซ็อกเก็ตเก่าจะถูกปิดและเปิดซ็อกเก็ตใหม่
สำคัญ: บล็อก onDispose เป็นองค์ประกอบที่จำเป็นของ DisposableEffect หากไม่เรียก onDispose ภายในบล็อก โค้ดจะไม่คอมไพล์ ข้อกำหนดของคอมไพเลอร์นี้รับประกันว่านักพัฒนาจะไม่ลืมที่จะทำความสะอาดทรัพยากร ซึ่งเป็นสาเหตุทั่วไปของข้อผิดพลาดในการจัดการสมัครด้วยตนเอง
// การใช้งานที่ถูกต้องด้วยคีย์
DisposableEffect(sensorType) {
val sensor = sensorManager.getDefaultSensor(sensorType)
sensorManager.registerListener(listener, sensor, SensorManager.SENSOR_DELAY_NORMAL)
onDispose {
sensorManager.unregisterListener(listener)
}
}
// ทรัพยากรหลายรายการใน DisposableEffect เดียว
DisposableEffect(Unit) {
context.registerReceiver(receiver, intentFilter)
lifecycle.addObserver(observer)
onDispose {
context.unregisterReceiver(receiver)
lifecycle.removeObserver(observer)
}
}
การรั่วไหลของหน่วยความจำในแอปพลิเคชัน Android มักเกิดจากผู้ฟังและการสมัครที่ไม่ได้ลงทะเบียนซึ่งยังคงเก็บการอ้างอิงไปยัง Activity หรือ Context หลังจากที่หน้าจอถูกปิดไปแล้ว DisposableEffect แก้ปัญหานี้ในระดับเฟรมเวิร์ก: หากนักพัฒนาใช้ DisposableEffect ในการลงทะเบียนผู้ฟัง onDispose จะรับประกันว่าการสมัครจะถูกยกเลิกในทุกสถานการณ์การสิ้นสุดของส่วนประกอบ
โดยเฉพาะอย่างยิ่งสำคัญสำหรับ LazyColumn และ LazyGrid, ที่องค์ประกอบถูกสร้างและทำลายอย่างต่อเนื่องเมื่อผู้ใช้เลื่อน หากไม่มี DisposableEffect แต่ละองค์ประกอบที่หายไปจากพื้นที่มองเห็นจะทิ้งการสมัครที่ยังทำงานอยู่ ด้วย DisposableEffect onDispose จะถูกเรียกสำหรับแต่ละองค์ประกอบที่ถูกเอาออก รับประกันว่าทรัพยากรจะถูกปล่อยทันทีหลังจากที่องค์ประกอบออกจากหน้าจอ
ตาม Android Performance Patterns (Google, 2025), การใช้ DisposableEffect สำหรับการสมัครแบบเนทีฟทั้งหมดช่วยลดจำนวนการรั่วไหลของหน่วยความจำในแอปพลิเคชัน Compose ได้ 60–70% เมื่อเทียบกับการจัดการด้วยตนเองผ่าน callback วงจรอายุ ระบบจะติดตามช่วงเวลาที่ออกจากคอมโพซิชันและรับประกันการทำงานของ onDispose แม้ในระหว่างการปิดหน้าจอฉุกเฉิน
| ทรัพยากร | สิ่งที่ DisposableEffect ทำ | ไม่มี DisposableEffect |
|---|---|---|
| BroadcastReceiver | register + onDispose → unregister | ตัวรับยังคงทำงาน |
| SensorManager | registerListener + onDispose → unregister | เซ็นเซอร์ยังคงส่งข้อมูล |
| Observable (ไม่ใช่ Flow) | subscribe + onDispose → unsubscribe | Callback ยังคงเก็บการอ้างอิง |
| TextureView / SurfaceView | setCallback + onDispose → removeCallback | Callback รั่วไหล |
| Socket / Channel | open + onDispose → close | การเชื่อมต่อยังคงเปิดอยู่ |
หนึ่งในตัวอย่างที่ชัดเจนที่สุดของการใช้ DisposableEffect คือการทำงานกับเซ็นเซอร์ของอุปกรณ์ (มาตรวัดความเร่ง ไจโรสโคป แมกนีโตมิเตอร์) เซ็นเซอร์ต้องการการยกเลิกการลงทะเบียนบังคับเมื่อเสร็จสิ้น มิฉะนั้นจะยังคงใช้พลังงานแบตเตอรี่และส่งข้อมูลต่อไปแม้หลังจากปิดหน้าจอแล้ว
ตัวอย่างปฏิบัติ: แอปพลิเคชันวัดมุมเอียง DisposableEffect(Unit) ลงทะเบียนผู้ฟังมาตรวัดความเร่งเมื่อส่วนประกอบปรากฏและยกเลิกการลงทะเบียนใน onDispose ข้อมูลเซ็นเซอร์จะถูกส่งไปยังสถานะผ่าน mutableStateOf ซึ่งจะอัปเดต UI โดยอัตโนมัติ หากหน้าจอเลื่อนใน LazyColumn และองค์ประกอบหายไป onDispose จะทำงานทันที — เซ็นเซอร์หยุดส่งข้อมูลสำหรับองค์ประกอบนั้น
เมื่อเปลี่ยนประเภทเซ็นเซอร์ (เช่น จากมาตรวัดความเร่งเป็นไจโรสโคป) คีย์ sensorType จะเปลี่ยนแปลง onDispose ยกเลิกการสมัครเก่า และบล็อก DisposableEffect ใหม่จะลงทะเบียนเซ็นเซอร์ใหม่ หากไม่มีคีย์ คุณจะต้องตรวจสอบด้วยตนเองว่าเซ็นเซอร์ใดถูกลงทะเบียนไว้ก่อนหน้านี้และเรียก unregisterListener ด้วย listener ที่ถูกต้อง — ซึ่งเสี่ยงต่อข้อผิดพลาด
@Composable
fun SensorReadingScreen(sensorType: Int) {
val context = LocalContext.current
val sensorManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager
var sensorValue by remember { mutableStateOf(0f) }
DisposableEffect(sensorType) {
val sensor = sensorManager.getDefaultSensor(sensorType)
val listener = SensorEventListener { event, _ ->
sensorValue = event.values[0]
}
sensorManager.registerListener(listener, sensor, SensorManager.SENSOR_DELAY_NORMAL)
onDispose {
sensorManager.unregisterListener(listener)
}
}
Text("ค่า: $sensorValue")
}
BroadcastReceiver เป็นตัวอย่างคลาสสิกของ API ที่ต้องการคู่บังคับ register / unregister. ในแอปพลิเคชัน Compose DisposableEffect เหมาะอย่างยิ่งสำหรับการลงทะเบียนตัวรับในช่วงอายุของหน้าจอที่เฉพาะเจาะจง เมื่อเข้าสู่หน้าจอ BroadcastReceiver ที่มี IntentFilter ที่จำเป็นจะถูกลงทะเบียน เมื่อออกจะถูกยกเลิกโดยอัตโนมัติใน onDispose
สถานการณ์ทั่วไปคือ การตรวจสอบสถานะเครือข่าย. DisposableEffect ลงทะเบียนตัวรับสำหรับ ConnectivityManager ที่แจ้งเตือนเกี่ยวกับการเปลี่ยนแปลงการเชื่อมต่อเครือข่าย เมื่อสถานะเปลี่ยน (WiFi / ข้อมูลมือถือ / ไม่มีเครือข่าย) สถานะของ composable จะถูกอัปเดตและ UI จะแสดงตัวบ่งชี้ที่เกี่ยวข้อง เมื่อหน้าจอปิด onDispose จะรับประกันว่าการลงทะเบียนจะถูกยกเลิก — แม้ว่าแอปพลิเคชันจะเข้าสู่พื้นหลัง
สำหรับตัวรับที่ใช้ ContextCompat.registerReceiver และแฟล็ก RECEIVER_EXPORTED / RECEIVER_NOT_EXPORTED (Android 14+) การใช้ DisposableEffect กลายเป็นสิ่งจำเป็นเนื่องจากระบบต้องการการระบุขอบเขตของตัวรับอย่างชัดเจน DisposableEffect รับประกันว่าขอบเขตถูกจำกัดอยู่ที่อายุของหน้าจอ ซึ่งสอดคล้องกับข้อกำหนดด้านความปลอดภัยของ Android เวอร์ชันใหม่
@Composable
fun NetworkStatusBanner() {
val context = LocalContext.current
var isConnected by remember { mutableStateOf(true) }
DisposableEffect(Unit) {
val receiver = BroadcastReceiver { _, _ ->
val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
isConnected = cm.getActiveNetwork() != null
}
IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION).let { filter ->
context.registerReceiver(receiver, filter)
}
onDispose {
context.unregisterReceiver(receiver)
}
}
if (!isConnected) { ... }
}
ข้อผิดพลาดร้ายแรงแรกคือ การขาดการเรียก onDispose. โค้ดภายในบล็อก DisposableEffect ต้องเรียก onDispose มิฉะนั้นจะเกิดข้อผิดพลาดในการคอมไพล์ อย่างไรก็ตาม นักพัฒนาบางครั้งพยายามหลีกเลี่ยงนี้โดยวาง onDispose ในเงื่อนไข: if (condition) { onDispose { ... } } โค้ดดังกล่าวจะคอมไพล์ แต่ onDispose จะไม่ถูกลงทะเบียนหากเงื่อนไขไม่เป็นจริง — ทรัพยากรจะไม่มีวันถูกปล่อย
ข้อผิดพลาดที่สองคือ การใช้ DisposableEffect สำหรับการดำเนินการแบบอะซิงโครนัส. เนื่องจาก DisposableEffect เป็นแบบซิงโครนัส จึงไม่สามารถเขียนการเรียก delay() หรือ await() ภายในได้ หากต้องการการเริ่มต้นแบบอะซิงโครนัสด้วยการทำความสะอา�ะดภายหลัง ให้ใช้การผสมผสานระหว่าง LaunchedEffect (สำหรับโหลดข้อมูล) และ DisposableEffect (สำหรับการตั้งค่า/ทำความสะอาดทรัพยากรเนทีฟ) หรือใช้กลไกแยกกับ rememberCoroutineScope
ข้อผิดพลาดที่สามคือ การสร้างอ็อบเจกต์ใหม่ภายใน DisposableEffect โดยไม่มี remember. หากอ็อบเจกต์ (เซ็นเซอร์ listener receiver) ถูกสร้างภายในเอฟเฟกต์ทุกครั้งที่เรียกและคีย์เปลี่ยนแปลงบ่อยครั้ง จะนำไปสู่การสร้างอ็อบเจกต์มากเกินไปและการเก็บขยะ ควรย้ายการสร้างอ็อบเจกต์ไปยัง remember หรือ remember { ... } นอก DisposableEffect และภายในเอฟเฟกต์ให้ลงทะเบียนและยกเลิกเท่านั้น
คำถามที่พบบ่อย
DisposableEffect ทำงานแบบซิงโครนัสและให้ onDispose สำหรับการทำความสะอาดทรัพยากรอย่างชัดเจน LaunchedEffect ทำงานแบบอะซิงโครนัสใน coroutine และยกเลิกโดยอัตโนมัติเมื่อคีย์เปลี่ยนหรือออกจากคอมโพซิชัน หากทรัพยากรต้องการเรียกเมธอดทำความสะอาด (close unregister dispose) — ให้ใช้ DisposableEffect หากการดำเนินการเป็นฟังก์ชัน suspend — ให้ใช้ LaunchedEffect
ใช่ onDispose จำเป็น — คอมไพเลอร์ Kotlin ต้องการให้เรียกภายในบล็อก DisposableEffect. หากไม่เรียก onDispose โค้ดจะไม่คอมไพล์ สิ่งนี้ทำโดยเจตนาเพื่อป้องกันการลืมของนักพัฒนาและรับประกันว่าทรัพยากรที่เปิดทุกตัวจะถูกปิดอย่างรับประกันเมื่อออกจากคอมโพซิชัน
ใช้ try-catch ภายในบล็อก DisposableEffect หากการลงทะเบียนทรัพยากรอาจโยนข้อยกเว้น (เช่น ไม่พบเซ็นเซอร์) ให้ห่อด้วย try และจัดการข้อผิดพลาดใน UI ผ่านสถานะแยก onDispose ควรถูกเรียกโดยไม่ขึ้นกับความสำเร็จของการเริ่มต้น — วางไว้ในบล็อก finally หรือที่ส่วนท้ายของส่วน try
ไม่แนะนำ สำหรับ Flow ควรใช้ LaunchedEffect กับ collectLatest หรือเมธอด .collectAsState() กับ Lifecycle.repeatOnLifecycle. DisposableEffect ไม่รองรับฟังก์ชัน suspend ดังนั้นการสมัคร Flow ภายในจะต้องเริ่ม coroutine แยกผ่าน CoroutineScope ซึ่งทำให้โค้ดซับซ้อนและเพิ่มความเสี่ยงในการรั่วไหล
ไม่มีข้อจำกัด แต่แนะนำให้จัดกลุ่มทรัพยากรที่เกี่ยวข้องในหนึ่ง DisposableEffect ที่มีหลายการดำเนินการภายในและ onDispose หนึ่งตัว หากทรัพยากรเป็นอิสระ (เช่น เซ็นเซอร์และ BroadcastReceiver) ควรแบ่งเป็น DisposableEffects แยกต่างหากที่มีคีย์ต่างกัน — ซึ่งช่วยให้การแก้ไขโค้ดง่ายขึ้นและป้องกันการสร้างทรัพยากรทั้งหมดใหม่โดยไม่ต้องการเมื่อคีย์หนึ่งเปลี่ยน
สรุป
เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร
IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ
อ่านเพิ่มเติม