SideEffect คือฟังก์ชัน composable ใน Jetpack Compose ที่ดำเนินการบล็อกโค้ดที่ส่งผ่านในทุกครั้งที่มีการเรียกองค์ประกอบใหม่ (recomposition) ที่สำเร็จ แตกต่างจาก LaunchedEffect และ DisposableEffect ตรงที่ SideEffect ไม่ได้ผูกอยู่กับคีย์และไม่มีบล็อกการทำความสะอาด — มันเพียงแค่ซิงโครไนซ์สถานะ Compose กับระบบภายนอกหลังจากการเรนเดอร์แต่ละครั้ง สิ่งนี้ทำให้เหมาะสำหรับการอัปเดตฟังก์ชัน callback การซิงโครไนซ์กับ ViewPager และการส่งข้อมูลไปยัง Analytics SDK ตาม Android Developers Documentation (2025) ระบุว่า SideEffect จะถูกดำเนินการหลังจากที่ Compose ยืนยันการเรียกองค์ประกอบใหม่ที่สำเร็จแล้วเท่านั้น และจะไม่ถูกดำเนินการหากข้ามการเรียกองค์ประกอบใหม่
ประเด็นสำคัญ
SideEffect เป็น API เอฟเฟกต์ข้างเคียงที่ง่ายที่สุดใน Jetpack Compose มันดำเนินการบล็อกโค้ดในการเรียกองค์ประกอบใหม่ที่สำเร็จแต่ละครั้งของคอมโพเนนต์ composable คำว่า “สำเร็จ” เป็นสิ่งสำคัญ: ถ้า Compose ตัดสินใจว่าไม่จำเป็นต้องเรียกองค์ประกอบใหม่ (เช่น พารามิเตอร์อินพุตทั้งหมดไม่เปลี่ยนแปลงและผลลัพธ์จะเหมือนเดิม) SideEffect จะไม่ถูกดำเนินการ สิ่งนี้รับประกันว่าบล็อกการซิงโครไนซ์จะถูกเรียกเฉพาะเมื่อ UI เปลี่ยนแปลงจริง
กรณีการใช้งานหลักของ SideEffect คือการซิงโครไนซ์สถานะ Compose กับออบเจ็กต์ที่ไม่ได้เป็นส่วนหนึ่งของทรี Compose ตัวอย่างทั่วไปได้แก่: การอัปเดตฟังก์ชัน callback ในระบบ Legacy View การส่งผ่านสถานะปัจจุบันไปยัง ViewPager การส่งเหตุการณ์ไปยัง SDK วิเคราะห์เมื่อข้อมูลที่แสดงเปลี่ยนแปลง และการซิงโครไนซ์กับ SDK แผนที่ที่คาดหวังการอัปเดตในรูปแบบภายนอก
ตาม Android Developer Blog (2025) ระบุว่า SideEffect มักใช้ร่วมกับ remember: remember เก็บรักษาออบเจ็กต์ (เช่น callback) และ SideEffect อัปเดตมันเมื่อใดก็ตามที่การพึ่งพาเปลี่ยนแปลง รูปแบบนี้สำคัญเป็นพิเศษสำหรับไลบรารีที่ยอมรับออบเจ็กต์ listener และไม่สร้างใหม่เมื่ออัปเดต — หากไม่มี SideEffect listener จะเก็บรีเฟอเรนซ์ที่ล้าสมัยไปยังสถานะปัจจุบัน
@Composable
fun MapScreen(zoomLevel: Int, markers: List<Marker>) {
val mapView = remember { MapView(LocalContext.current) }
SideEffect {
mapView.setZoom(zoomLevel)
mapView.updateMarkers(markers)
}
AndroidView(factory = { mapView })
}
เพื่อเข้าใจ SideEffect คุณต้องเข้าใจเฟสการดำเนินการของ Jetpack Compose Compose ผ่านสามเฟสสำหรับแต่ละเฟรม: Composition (แสดงอะไร), Layout (แสดงที่ไหน), Drawing (แสดงอย่างไร) SideEffect จะถูกดำเนินการที่ส่วนท้ายของเฟส Composition — หลังจากฟังก์ชัน composable ทั้งหมดทำงานแล้ว แต่ก่อนเฟส Layout สิ่งนี้รับประกันว่า SideEffect จะเห็นสถานะสุดท้ายของตัวแปรทั้งหมดหลังจากการเรียกองค์ประกอบใหม่
ตำแหน่งในวงจรชีวิตนี้ให้ข้อได้เปรียบที่สำคัญ: SideEffect ไม่สามารถทำให้เกิดการเรียกองค์ประกอบใหม่ไม่สิ้นสุด แม้ว่าสถานะจะถูกเปลี่ยนแปลงภายในมันก็ตาม เนื่องจากมันถูกดำเนินการหลังจาก composition การเปลี่ยนแปลงที่ทำภายใน SideEffect จะถูกนำไปใช้ในเฟรมถัดไปเท่านั้น — สิ่งนี้ป้องกันรอบทั่วไปของการเปลี่ยนแปลงสถานะภายในเนื้อหาของฟังก์ชัน composable (เมื่อ setState ภายใน composition กระตุ้น composition ใหม่ก่อนที่ composition ปัจจุบันจะเสร็จสิ้น)
คุณสมบัติอีกอย่างคือ SideEffect ไม่ได้รับการปรับให้เหมาะสมด้วยคีย์ มันถูกดำเนินการในการเรียกองค์ประกอบใหม่ทุกครั้งโดยไม่คำนึงถึงว่าสถานะเฉพาะใดเปลี่ยนแปลงไป หากคุณต้องการการควบคุมที่แม่นยำยิ่งขึ้น (ดำเนินการเฉพาะเมื่อพารามิเตอร์เฉพาะเปลี่ยนแปลง) ให้ใช้ LaunchedEffect พร้อมคีย์หรือห่อ SideEffect ในการตรวจสอบการเปลี่ยนแปลงผ่าน remember
// SideEffect optimized with remember
var currentZoom by remember { mutableIntStateOf(zoomLevel) }
SideEffect {
if (currentZoom != zoomLevel) {
map.animateToZoom(zoomLevel)
currentZoom = zoomLevel
}
}
// Without this check SideEffect would call animateToZoom
// on every recomposition, even if zoomLevel didn't change
สถานการณ์ปฏิบัติที่พบบ่อยที่สุดสำหรับ SideEffect คือการอัปเดตฟังก์ชัน callback ที่จับภาพสถานะปัจจุบัน ใน Jetpack Compose สิ่งนี้เรียกว่า “การจัดการวงจรชีวิต callback” ปัญหาคือนิพจน์ lambda ใน Kotlin จับภาพตัวแปรโดยรีเฟอเรนซ์ และถ้า callback ถูกสร้างขึ้นด้วยค่าหนึ่งและต่อมาตัวแปรเปลี่ยนไป — callback จะยังคงใช้ค่าเก่าต่อไป
พิจารณาตัวอย่าง: Google Maps SDK สำหรับ Android ยอมรับออบเจ็กต์ OnCameraMoveListener ผ่าน setOnCameraMoveListener() ถ้าคุณส่ง lambda ที่จับภาพ isTrackingEnabled เมื่อ isTrackingEnabled เปลี่ยนแปลง lambda จะไม่ถูกอัปเดต — Maps SDK จะยังคงเรียก callback เก่าด้วยข้อมูลที่ล้าสมัย SideEffect แก้ปัญหานี้: มันตั้งค่า listener ใหม่ในการเรียกองค์ประกอบใหม่ทุกครั้ง รับประกันว่า SDK จะใช้ lambda ปัจจุบันกับสถานะล่าสุดเสมอ
ตาม เอกสาร Maps SDK สำหรับ Android (2025) Google แนะนำรูปแบบนี้เมื่อรวม Maps กับ Jetpack Compose วิธีการที่คล้ายกันใช้สำหรับ WebView, VideoView, TextureView และคอมโพเนนต์ที่ใช้ View อื่นๆ ที่ยอมรับ callback ผ่านเมธอด set SideEffect รับประกันว่า callback จะเป็นปัจจุบันทุกครั้งที่มีการเปลี่ยนแปลงสถานะ
@Composable
fun MapComposable(isTrackingEnabled: Boolean, onMarkerClick: (Marker) -> Unit) {
val mapView = remember { MapView(LocalContext.current) }
SideEffect {
mapView.setOnMarkerClickListener { marker ->
onMarkerClick(marker)
true
}
mapView.isTrafficEnabled = isTrackingEnabled
}
AndroidView(factory = { mapView })
}
อีกสถานการณ์สำคัญสำหรับ SideEffect คือการส่งเหตุการณ์ไปยังระบบวิเคราะห์เมื่อสถานะ UI เปลี่ยนแปลง ตัวอย่างเช่น เมื่อผู้ใช้เปลี่ยนแท็บใน TabLayout ภายในหน้าจอ Compose SideEffect สามารถส่งผ่านสถานะแท็บที่ถูกเลือกปัจจุบันไปยัง Firebase Analytics หรือ AppsFlyer ทุกครั้งที่แท็บที่ถูกเลือกเปลี่ยน (และเกิดการเรียกองค์ประกอบใหม่) SideEffect จะส่งเหตุการณ์ที่เกี่ยวข้อง
ความแตกต่างจากการส่งเหตุการณ์โดยตรงใน onClick หรือ onTabSelected คือ SideEffect ถูกกระตุ้นโดยการเปลี่ยนแปลงสถานะจากแหล่งใดๆ — ไม่เพียงแต่การกระทำของผู้ใช้ แต่ยังรวมถึงการเปลี่ยนแปลงเชิงโปรแกรม การกู้คืนสถานะหลังการหมุนหน้าจอ หรือ Deep Links สิ่งนี้ทำให้ SideEffect เป็นกลไกการซิงโครไนซ์สากลที่ไม่ขึ้นกับแหล่งที่มาของการเปลี่ยนแปลง
ตาม Firebase Best Practices (Google, 2025) การส่งเหตุการณ์วิเคราะห์ผ่าน SideEffect ให้ภาพการเดินทางของผู้ใช้ที่สมบูรณ์มากขึ้น เนื่องจากมันจับภาพการเปลี่ยนแปลงสถานะทั้งหมด รวมถึงที่เกิดขึ้นโดยไม่มีการกระทำโดยตรงของผู้ใช้ อย่างไรก็ตาม สิ่งสำคัญคืออย่ามากเกินไป: ทุกเหตุการณ์วิเคราะห์คือคำขอเครือข่าย ดังนั้นสำหรับสถานะที่เปลี่ยนแปลงบ่อย (ตำแหน่งเลื่อน, พิกัดนิ้ว) SideEffect ไม่เหมาะสม — ใช้ debounce หรือส่งเหตุการณ์เฉพาะเมื่อมีการเปลี่ยนแปลงที่มีนัยสำคัญ
@Composable
fun ProductScreen(selectedTab: ProductTab, productId: String) {
val firebaseAnalytics = remember { FirebaseAnalytics.getInstance(LocalContext.current) }
SideEffect {
val params = Bundle().apply {
putString(FirebaseAnalytics.Param.CONTENT_TYPE, selectedTab.name)
putString(FirebaseAnalytics.Param.ITEM_ID, productId)
}
firebaseAnalytics.logEvent( FirebaseAnalytics.Event.VIEW_ITEM, params)
}
// UI with TabRow and selected tab
}
การเลือกระหว่าง SideEffect และ LaunchedEffect ขึ้นอยู่กับสองปัจจัย: จำเป็นต้องดำเนินการแบบอะซิงโครนัสหรือไม่และจำเป็นต้องควบคุมแบบคีย์หรือไม่ SideEffect เป็นแบบซิงโครนัสและดำเนินการในการเรียกองค์ประกอบใหม่ทุกครั้ง LaunchedEffect เป็นแบบอะซิงโครนัส (coroutine) และดำเนินการเฉพาะเมื่อคีย์เปลี่ยนแปลง ไม่ใช่ในการเรียกองค์ประกอบใหม่ทุกครั้ง
| คุณลักษณะ | SideEffect | LaunchedEffect |
|---|---|---|
| การดำเนินการ | ในการเรียกองค์ประกอบใหม่ทุกครั้ง | เมื่อคีย์เปลี่ยนแปลง |
| อะซิงโครนัส | ซิงโครนัส | Coroutine |
| คีย์ | ไม่มี | มี (vararg) |
| การทำความสะอาด | ไม่มี | ยกเลิก coroutine อัตโนมัติ |
| การใช้งานทั่วไป | Callback, วิเคราะห์, ซิงโครไนซ์มุมมอง | โหลด, สมัครสมาชิก Flow, ตัวจับเวลา |
ในทางปฏิบัติ 70% ของกรณีการใช้งานเอฟเฟกต์ข้างเคียงถูกครอบคลุมโดย LaunchedEffect (การดำเนินการแบบอะซิงโครนัส, การโหลดข้อมูล), 20% โดย DisposableEffect (ทรัพยากรที่มีการทำความสะอาด) และเพียง 10% โดย SideEffect (การซิงโครไนซ์ callback) SideEffect เป็นเครื่องมือเฉพาะสำหรับช่วงงานที่จำกัด แต่ในงานเหล่านั้นมันเป็นสิ่งที่ขาดไม่ได้
ข้อผิดพลาดหลักคือ การเปลี่ยนแปลงสถานะ Compose ภายใน SideEffect แม้ว่า SideEffect จะไม่ทำให้เกิดลูปไม่สิ้นสุดโดยตรง (เนื่องจากมันถูกดำเนินการหลังจากเฟส composition) มันสามารถกระตุ้นการเรียกองค์ประกอบใหม่ที่มากเกินไป ถ้าสถานะถูกเปลี่ยนแปลงภายใน SideEffect (mutableStateOf) มันจะกระตุ้นการเรียกองค์ประกอบใหม่ในเฟรมถัดไป ซึ่งจะดำเนินการ SideEffect อีกครั้ง — และต่อเนื่องไปจนกว่าจะคงที่ สิ่งนี้ไม่ใช่ลูปไม่สิ้นสุด แต่เป็นงานที่ไม่จำเป็นสำหรับเฟรมเวิร์ก
ข้อผิดพลาดที่สองคือ การคำนวณหนักภายใน SideEffect เนื่องจาก SideEffect ถูกเรียกในการเรียกองค์ประกอบใหม่ทุกครั้ง และการเรียกองค์ประกอบใหม่อาจเกิดขึ้นหลายสิบครั้งต่อวินาที (ระหว่างแอนิเมชัน, การเลื่อน) โค้ดหนักใดๆ ภายใน SideEffect จะทำให้เฟรมดรอป ย้ายการดำเนินการหนักออกจาก composition — ไปยัง coroutine (LaunchedEffect) หรือคำนวณผ่าน derivedStateOf / remember
ข้อผิดพลาดที่สามคือ การพยายามใช้ SideEffect สำหรับโค้ดอะซิงโครนัส SideEffect ไม่ใช่ฟังก์ชัน suspend ดังนั้น delay(), await(), collect() และการดำเนินการ coroutine อื่นๆ ภายในมันจะไม่คอมไพล์ ถ้าคุณต้องการดำเนินการแบบอะซิงโครนัสหลังการเรียกองค์ประกอบใหม่ ให้ใช้ snapshotFlow { ... } ร่วมกับ LaunchedEffect หรือเริ่ม coroutine ผ่าน rememberCoroutineScope
คำถามที่พบบ่อย
ใช่ SideEffect ถูกดำเนินการในทุก composition ที่สำเร็จ รวมถึงครั้งแรก — เมื่อคอมโพเนนต์ปรากฏบนหน้าจอเป็นครั้งแรก สิ่งนี้แตกต่างจาก LaunchedEffect(Unit) ซึ่งถูกดำเนินการครั้งเดียวในการ composition แรก แต่ไม่ถูกดำเนินการในการเรียกองค์ประกอบใหม่ครั้งต่อไป (ถ้าคีย์ไม่เปลี่ยนแปลง)
ไม่ SideEffect ถูกดำเนินการหลังจากเฟส composition — การเปลี่ยนแปลงที่ทำภายในมันจะถูกนำไปใช้ในเฟรมถัดไปเท่านั้น ซึ่งป้องกันลูป อย่างไรก็ตาม การเปลี่ยนแปลงสถานะบ่อยครั้งภายใน SideEffect อาจทำให้เกิดการเรียกองค์ประกอบใหม่เป็นลูกโซ่ ลดประสิทธิภาพ เปลี่ยนแปลงสถานะภายใน SideEffect เฉพาะเมื่อจำเป็นจริงๆ
SideEffect ถูกดำเนินการแบบซิงโครนัสในการเรียกองค์ประกอบใหม่ทุกครั้ง snapshotFlow สร้าง Flow จากสถานะ Compose และสามารถใช้กับ collectLatest ใน LaunchedEffect สำหรับการจัดการการเปลี่ยนแปลงแบบรีแอกทีฟ snapshotFlow เหมาะสำหรับกรณีที่คุณต้องตอบสนองต่อการเปลี่ยนแปลงด้วย debounce, filter หรือ distinctUntilChanged — ซึ่งเป็นไปไม่ได้ใน SideEffect แบบซิงโครนัส
ใช้ Android Studio Compose Modifier Debugger หรือเพิ่มการบันทึกด้วยชื่อคอมโพเนนต์และความถี่การเรียก ถ้า SideEffect ถูกดำเนินการบ่อยกว่าที่คาดไว้ ให้ตรวจสอบว่าสถานะของคอมโพเนนต์แม่เปลี่ยนแปลงโดยไม่จำเป็นหรือไม่ การปรับเหมาะสม: แยกส่วนที่เสถียรของ UI ออกเป็นฟังก์ชัน composable แยกต่างหากด้วยคำอธิบายประกอบ unstable เพื่อลดจำนวนการเรียกองค์ประกอบใหม่
ใช่ สามารถใช้ในคอมโพเนนต์เดียวกันเพื่อวัตถุประสงค์ที่แตกต่างกัน DisposableEffect รับผิดชอบการตั้งค่าและการทำความสะอาดทรัพยากร (ครั้งเดียว) ในขณะที่ SideEffect จัดการการซิงโครไนซ์สถานะปัจจุบันกับทรัพยากรนั้นในการเรียกองค์ประกอบใหม่ทุกครั้ง ตัวอย่างทั่วไป: DisposableEffect ลงทะเบียน callback ผ่าน API และ SideEffect อัปเดตข้อมูลที่จับภาพใน callback นั้นทุกครั้งที่มีการเปลี่ยนแปลง
สรุป
เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร
IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ
อ่านเพิ่มเติม