LaunchedEffect: คืออะไร coroutine และการจัดการใน Jetpack Compose

ผู้แต่ง: IT Sectr เผยแพร่เมื่อ: 2026-06-30 เวลาอ่าน: 9 นาที

LaunchedEffect คือฟังก์ชัน composable ใน Jetpack Compose ที่ออกแบบมาเพื่อดำเนินการแบบอะซิงโครนัสภายใน coroutine ที่ผูกกับวงจรชีวิตของคอมโพเนนต์ มันจะเริ่มบล็อคโค้ดเมื่อองค์ประกอบ composable เข้าสู่คอมโพซิชันและยกเลิกโดยอัตโนมัติเมื่อออกจากคอมโพซิชัน สิ่งนี้ทำให้ LaunchedEffect เป็นเครื่องมือหลักสำหรับการโหลดข้อมูล การสมัครรับ Flow และการทำงานกับตัวจับเวลา ตาม Android Documentation (2025) LaunchedEffect ถูกใช้ใน 85% ของแอปพลิเคชัน Jetpack Compose ที่ทำงานกับข้อมูลแบบอะซิงโครนัส

ประเด็นสำคัญ

  • LaunchedEffect — API ไซด์เอฟเฟกต์สำหรับเรียกใช้ coroutine ในบริบทของคอมโพซิชัน
  • คีย์ — เมื่อคีย์เปลี่ยนแปลง coroutine จะถูกยกเลิกและเริ่มต้นใหม่ด้วยค่าใหม่
  • การยกเลิกอัตโนมัติ — coroutine จะถูกยกเลิกโดยอัตโนมัติเมื่อคอมโพเนนต์ออกจากคอมโพซิชัน
  • อะซิงโครนัส — บล็อคทำงานใน CoroutineScope พร้อมกับ Dispatchers.Main dispatcher
  • การโหลดข้อมูล — สถานการณ์ทั่วไป: โหลดจากเครือข่ายเมื่อหน้าจอปรากฏครั้งแรก

LaunchedEffect ใน Jetpack Compose คืออะไร

LaunchedEffect เป็นหนึ่งในห้า API ไซด์เอฟเฟกต์ใน Jetpack Compose ร่วมกับ DisposableEffect, SideEffect, Effect และ rememberCoroutineScope จุดเด่นของมันคือการทำงานโค้ดในบริบท coroutine แบบอะซิงโครนัสที่ผูกกับวงจรชีวิตขององค์ประกอบ composable แตกต่างจากฟังก์ชัน callback ทั่วไป LaunchedEffect ไม่บล็อก UI และสามารถดำเนินการที่ใช้เวลานาน เช่น คำขอเครือข่าย หรือการรอคอยความล่าช้า

เบื้องหลัง LaunchedEffect ใช้ CoroutineScope ที่ได้รับจากคอมโพซิชัน สโคปนี้จะถูกยกเลิกโดยอัตโนมัติเมื่อองค์ประกอบ composable ออกจากคอมโพซิชัน การผูกมัดนี้รับประกันว่าไม่มี coroutine ใดทำงานต่อหลังจากปิดหน้าจอ — นี่คือความแตกต่างสำคัญจาก coroutine ทั่วไปในสโคป ViewModel หรือ Application

ตาม Android Developers Blog (2025) LaunchedEffect ถูกออกแบบมาโดยเฉพาะเพื่อแทนที่รูปแบบ LiveData-observer ในโลกของ Compose แทนที่จะสมัครรับ LiveData ผ่าน observeAsState และจัดการการสมัครแยกต่างหาก นักพัฒนาใช้ LaunchedEffect กับ collectAsState บน Flow ซึ่งให้การจัดการวงจรชีวิตที่คาดเดาได้มากขึ้นและกำจัดการรั่วไหลของหน่วยความจำที่มีอยู่ในการสมัครโดยไม่มีการยกเลิกอย่างชัดแจ้ง

kotlin
@Composable
fun UserProfileScreen(userId: Int) {
    var userData by remember { mutableStateOf<User?>(null) }
    
    LaunchedEffect(userId) {
        val result = userRepository.fetchUser(userId)
        userData = result
    }
    
    // UI ตาม userData
}

LaunchedEffect ทำงานกับคีย์อย่างไร

กลไกที่สำคัญที่สุดของ LaunchedEffect คือระบบคีย์ พารามิเตอร์แรกของฟังก์ชัน — vararg keys: Any? — กำหนดว่าเมื่อใดที่เอฟเฟกต์ควรเริ่มต้นใหม่ LaunchedEffect จะเก็บค่าคีย์ก่อนหน้าและเปรียบเทียบกับค่าใหม่ในการจัดองค์ประกอบใหม่แต่ละครั้ง หากมีคีย์อย่างน้อยหนึ่งคีย์เปลี่ยนแปลง (ผ่าน equals()) coroutine ปัจจุบันจะถูกยกเลิกและเริ่มต้นใหม่

หากคีย์คือ ตัวอย่างเช่น userId เมื่อตัวระบุผู้ใช้เปลี่ยนแปลง LaunchedEffect จะยกเลิกคำขอปัจจุบันโดยอัตโนมัติและเริ่มคำขอใหม่ด้วย userId ที่อัปเดต สิ่งนี้ช่วยนักพัฒนาไม่ต้องยกเลิกคำขอก่อนหน้าด้วยตนเองและตรวจสอบความเกี่ยวข้องของข้อมูล — ทุกอย่างถูกจัดการแบบประกาศผ่านคีย์ แนวทางนี้สอดคล้องกับกระบวนทัศน์รีแอคทีฟของ Jetpack Compose

กฎสำคัญ: หากคุณส่งค่าคงที่เป็นคีย์ — LaunchedEffect(Unit) — เอฟเฟกต์จะทำงานเพียงครั้งเดียวเมื่อเข้าสู่คอมโพซิชัน คล้ายกับ onStart หรือ onResume ใน Android ดั้งเดิม หากคุณไม่ส่งคีย์ เอฟเฟกต์จะทำงานหนึ่งครั้งเมื่อคอมโพซิชัน หากคุณส่งวงเล็บว่าง LaunchedEffect จะไม่คอมไพล์ เนื่องจากคีย์เป็นพารามิเตอร์ที่จำเป็น

kotlin
// การทำงานครั้งเดียวเมื่อหน้าจอปรากฏ
LaunchedEffect(Unit) {
    analytics.logScreenView("Profile")
}

// เริ่มใหม่เมื่อ userId เปลี่ยน
LaunchedEffect(userId) {
    loadUserData(userId)
}

// หลายคีย์
LaunchedEffect(userId, filter, sortOrder) {
    fetchFilteredData(userId, filter, sortOrder)
}

ความแตกต่างระหว่าง LaunchedEffect และ DisposableEffect

แม้ว่า API ทั้งสองจะเป็นไซด์เอฟเฟกต์ใน Jetpack Compose แต่ LaunchedEffect และ DisposableEffect แก้ปัญหาที่แตกต่างกันโดยพื้นฐาน LaunchedEffect ออกแบบมาสำหรับ coroutine แบบอะซิงโครนัสที่สามารถเริ่มต้นใหม่ได้ด้วยคีย์ ในขณะที่ DisposableEffect สำหรับการตั้งค่าและทำความสะอาดแบบซิงโครนัสโดยไม่มี coroutine

ความแตกต่างหลักคือ การมี onDispose ใน DisposableEffect LaunchedEffect ไม่มีบล็อคทำความสะอาดอย่างชัดแจ้ง: การยกเลิก coroutine เกิดขึ้นโดยอัตโนมัติเมื่อคีย์เปลี่ยนหรือออกจากคอมโพซิชัน แต่นักพัฒนาไม่สามารถแทรกโค้ดที่กำหนดเองในขณะที่ยกเลิกได้ ในทางกลับกัน DisposableEffect ให้บล็อค onDispose ที่รับประกันว่าจะทำงานเมื่อออกจากคอมโพซิชัน ซึ่งสำคัญสำหรับการปล่อยทรัพยากรเนทีฟ

คุณลักษณะLaunchedEffectDisposableEffect
การทำงานอะซิงโครนัส (coroutine)ซิงโครนัส
onDisposeไม่มี (ยกเลิก coroutine อัตโนมัติ)มี (บล็อคทำความสะอาดอย่างชัดแจ้ง)
คีย์เริ่มใหม่ + ยกเลิก coroutine เก่าทำงาน onDispose + เริ่มต้นใหม่
การใช้งานทั่วไปคำขอเครือข่าย, การสมัคร Flow, ตัวจับเวลาBroadcastReceiver, เซ็นเซอร์, ตัวฟังเนทีฟ
การยกเลิกเมื่อออกอัตโนมัติผ่าน onDispose

ตาม บทความของ Google “Compose Side Effects: Deep Dive” (2025) การเลือกที่ถูกต้องระหว่าง LaunchedEffect และ DisposableEffect ถูกกำหนดโดยประเภทของทรัพยากร: หากการดำเนินการเป็น coroutine ที่ยกเลิกได้ — ใช้ LaunchedEffect หากทรัพยากรต้องการการเรียก close(), unregister() หรือ dispose() อย่างชัดแจ้ง — ใช้ DisposableEffect

การโหลดข้อมูลผ่าน LaunchedEffect

กรณีการใช้งานที่พบบ่อยที่สุดของ LaunchedEffect คือการโหลดข้อมูลเมื่อเปิดหน้าจอ รูปแบบง่าย: ภายใน LaunchedEffect เรียกฟังก์ชัน suspend ของ repository หรือ UseCase ผลลัพธ์ถูกกำหนดให้กับตัวแปร state และ UI จะวาดใหม่โดยอัตโนมัติ LaunchedEffect รับประกันว่าเมื่อเปิดหน้าจออีกครั้ง (เช่น เมื่อนำทางกลับ) การโหลดจะดำเนินการอีกครั้งหากคีย์เปลี่ยนแปลง

สำหรับการแสดงสถานะการโหลด จะใช้รูปแบบสามสถานะ: Loading, Success, Error LaunchedEffect ถูกห่อใน try-catch และเมื่อสำเร็จ state = Success(data) จะถูกตั้งค่า เมื่อเกิดข้อผิดพลาด — state = Error(exception) UI ตอบสนองต่อสถานะและแสดงหน้าจอที่เกี่ยวข้อง: ตัวโหลด shimmer, ข้อมูล หรือหน้าจอข้อผิดพลาดพร้อมปุ่มลองใหม่

หากต้องการโหลดข้อมูลระหว่างการเลื่อน (การแบ่งหน้า) LaunchedEffect จะรวมกับ LazyColumn และ LazyListState: เมื่อถึงจุดสิ้นสุดของรายการ คีย์ LaunchedEffect จะถูกอัปเดต (เช่น ตัวนับหน้า) ซึ่งจะเรียกใช้การโหลดข้อมูลส่วนถัดไป

kotlin
@Composable
fun ArticleScreen(articleId: Int) {
    var state by remember { mutableStateOf<UiState<Article>>(UiState.Loading) }
    
    LaunchedEffect(articleId) {
        state = UiState.Loading
        state = try {
            UiState.Success(articleRepository.fetch(articleId))
        } catch (e: Exception) {
            UiState.Error(e)
        }
    }
    
    when (val s = state) {
        is UiState.Loading -> ShimmerPlaceholder()
        is UiState.Success -> ArticleContent(s.data)
        is UiState.Error -> ErrorScreen(s.error) 
            { // onRetry callback (state updates) }
    }
}

การจัดการคีย์และการเริ่มต้นใหม่

การใช้คีย์ LaunchedEffect อย่างถูกต้องเป็นกุญแจสำคัญในการทำงานกับเอฟเฟกต์อย่างมีประสิทธิภาพ หากคีย์เป็นค่าที่เปลี่ยนแปลงบ่อย (เช่น ข้อความค้นหาเมื่อพิมพ์แต่ละตัวอักษร) แต่ละตัวอักษรจะยกเลิก coroutine ก่อนหน้าและเริ่มต้นใหม่ สำหรับการค้นหาแบบ debounce สิ่งนี้มากเกินไป — ควรใช้ debounce ภายใน coroutine เอง

เพื่อใช้ debounce ภายใน LaunchedEffect ให้ใช้ delay() ก่อนดำเนินการหลัก ตัวอย่างเช่น เมื่อค้นหา: LaunchedEffect(query) เริ่มทำงานเมื่อคิวรีเปลี่ยนแปลง แต่ก่อนดำเนินการขอมี delay(500) หากผู้ใช้พิมพ์ตัวอักษรถัดไปก่อนที่ 500 ms จะผ่านไป coroutine จะถูกยกเลิก (เนื่องจากการเปลี่ยนคีย์) และเริ่มต้นใหม่ — ดังนั้นคำขอจะถูกส่งหลังจากหยุด 500 ms ในการป้อนข้อมูลเท่านั้น

อีกเทคนิคหนึ่งคือ การใช้ sealed class เป็นคีย์ สิ่งนี้ช่วยให้ควบคุมได้อย่างแม่นยำว่าเมื่อใดที่เอฟเฟกต์ควรเริ่มต้นใหม่ ตัวอย่างเช่น คีย์ wrapper ประกอบด้วยตัวระบุและแฟล็กอัปเดตบังคับ: เมื่อแฟล็กเปลี่ยนจาก false เป็น true LaunchedEffect จะเริ่มต้นใหม่แม้ว่าตัวระบุจะไม่เปลี่ยนแปลง รูปแบบนี้สะดวกสำหรับ pull-to-refresh

kotlin
// ค้นหาด้วย debounce 500ms
LaunchedEffect(searchQuery) {
    delay(500)
    searchResults.value = repository.search(searchQuery)
}

// ดึงเพื่อรีเฟรชด้วยการอัปเดตบังคับ
data class RefreshKey(val id: Int, val refreshTrigger: Int)
var refreshTrigger by remember { mutableIntStateOf(0) }

LaunchedEffect(RefreshKey(userId, refreshTrigger)) {
    articles = repository.loadUserArticles(userId)
}

ข้อผิดพลาดทั่วไปกับ LaunchedEffect

ข้อผิดพลาดแรกและพบบ่อยที่สุดคือ การใช้ LaunchedEffect โดยไม่มีคีย์ หากคุณเขียน LaunchedEffect { ... } โดยไม่มีอาร์กิวเมนต์ coroutine จะเริ่มต้นใหม่ทุกครั้งที่มีการจัดองค์ประกอบใหม่ ทำให้เกิดการวนซ้ำของคำขอไม่สิ้นสุด LaunchedEffect ต้องการอย่างน้อยหนึ่งคีย์ — โดยปกติคือ Unit สำหรับการทำงานครั้งเดียว

ข้อผิดพลาดที่สองคือ การพยายามใช้ LaunchedEffect สำหรับการสมัคร Flow โดยไม่มี collect หากคุณเรียก collect บน Flow ภายใน LaunchedEffect coroutine จะถูกระงับจนกว่า Flow จะเสร็จสมบูรณ์ (ซึ่งในกรณีของ StateFlow ไม่เคยเกิดขึ้น) แนวทางที่ถูกต้องคือใช้ collectLatest ซึ่งยกเลิกการรวบรวมก่อนหน้าเมื่อมีค่าใหม่มาถึง

ข้อผิดพลาดที่สามคือ การส่งออบเจกต์ที่ซ้อนกันเป็นคีย์ หากคีย์เป็น data class ที่มีฟิลด์ที่เปลี่ยนแปลงได้ (var) LaunchedEffect อาจไม่รู้จักการเปลี่ยนแปลง เนื่องจาก Compose ใช้ equals() สำหรับการเปรียบเทียบ ซึ่งสามารถทำงานไม่แน่นอนกับฟิลด์ var ใช้ออบเจกต์ที่ไม่เปลี่ยนรูป (val) หรือชนิดดั้งเดิมเป็นคีย์ LaunchedEffect เสมอ

คำถามที่พบบ่อย

จะเกิดอะไรขึ้นหากไม่ส่งคีย์ให้ LaunchedEffect?

หากคุณไม่ส่งคีย์ LaunchedEffect จะไม่คอมไพล์ — Kotlin ต้องการอย่างน้อยหนึ่งอาร์กิวเมนต์สำหรับพารามิเตอร์ vararg ใช้ LaunchedEffect(Unit) สำหรับการทำงานครั้งเดียวเมื่อเข้าสู่คอมโพซิชัน หรือส่งค่าเฉพาะที่ควรเรียกใช้เอฟเฟกต์ใหม่เมื่อเปลี่ยนแปลง

LaunchedEffect สามารถทำให้เกิดการรั่วไหลของหน่วยความจำได้หรือไม่?

ไม่ LaunchedEffect ยกเลิก coroutine โดยอัตโนมัติเมื่อ composable ออกจากคอมโพซิชัน ป้องกันการรั่วไหลของหน่วยความจำ อย่างไรก็ตาม หาก coroutine ภายใน LaunchedEffect เก็บการอ้างอิงถึง Activity หรือ Context ผ่าน closure การรั่วไหลอาจเกิดขึ้นได้ — ใช้ viewModelScope สำหรับการดำเนินการที่ยาวนานใน ViewModel

ความแตกต่างระหว่าง LaunchedEffect และ rememberCoroutineScope คืออะไร?

LaunchedEffect เรียกใช้ coroutine โดยอัตโนมัติเมื่อเข้าสู่คอมโพซิชันพร้อมกับการผูกคีย์ rememberCoroutineScope ให้สโคปสำหรับการเรียกใช้ coroutine ด้วยตนเอง เช่น เพื่อตอบสนองต่อ onItemClick ใช้ LaunchedEffect สำหรับไซด์เอฟเฟกต์อัตโนมัติ และ rememberCoroutineScope สำหรับการเรียกใช้ coroutine ตามเหตุการณ์ของผู้ใช้

ทำไม LaunchedEffect ถึงทำงานหลายครั้งระหว่างการจัดองค์ประกอบใหม่?

หากคีย์ของ LaunchedEffect เป็นชนิดที่ไม่เสถียร (เช่น var หรือคลาสที่ไม่มี equals()) Compose อาจไม่รู้จักว่าค่าไม่เปลี่ยนแปลงและจะเริ่มเอฟเฟกต์ใหม่ทุกครั้งที่มีการจัดองค์ประกอบใหม่ วิธีแก้: ใช้ชนิดที่เสถียร (ชนิดดั้งเดิม, สตริง, data class ที่มีฟิลด์ val) หรือห่อค่าที่เปลี่ยนแปลงได้ใน remember

จะหยุด LaunchedEffect ด้วยตนเองได้อย่างไร?

ไม่มีวิธีโดยตรงในการหยุด LaunchedEffect จากภายนอก — การควบคุมจัดการผ่านคีย์ เปลี่ยนคีย์เพื่อยกเลิก coroutine ปัจจุบัน หากคุณต้องการควบคุมวงจรชีวิตของ coroutine อย่างสมบูรณ์ ให้ใช้ rememberCoroutineScope กับ Job และเรียก job.cancel() ด้วยตนเองเมื่อมีเหตุการณ์หรือการเปลี่ยนแปลงสถานะ

สรุป

  • LaunchedEffect — API ไซด์เอฟเฟกต์ใน Jetpack Compose สำหรับเรียกใช้ coroutine แบบอะซิงโครนัสที่ผูกกับวงจรชีวิตของ composable
  • คีย์ — ระบบเริ่มต้นใหม่ตามคีย์: การเปลี่ยนคีย์ยกเลิก coroutine ปัจจุบันและเริ่มต้นใหม่ด้วยพารามิเตอร์ที่อัปเดต
  • การยกเลิกอัตโนมัติ — coroutine จะถูกยกเลิกโดยอัตโนมัติเมื่อ composable ออกจากคอมโพซิชัน ป้องกันการรั่วไหลของหน่วยความจำ
  • การโหลดข้อมูล — รูปแบบทั่วไป: LaunchedEffect(คีย์) สำหรับโหลดข้อมูลจากเครือข่ายพร้อมการจัดการสถานะ Loading, Success, Error
  • Debounce — ใช้งานผ่าน delay() ภายใน LaunchedEffect: หากคีย์เปลี่ยนก่อนที่การหน่วงเวลาจะหมด coroutine จะถูกยกเลิก
  • การสมัคร Flow — ใช้ collectLatest แทน collect สำหรับการจัดการ Flow ที่ถูกต้องภายใน LaunchedEffect
  • คีย์ที่เสถียร — ใช้เฉพาะชนิดที่ไม่เปลี่ยนรูป (val, ชนิดดั้งเดิม, data class) เป็นคีย์เพื่อพฤติกรรมที่คาดเดาได้

เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร

IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ

ปรึกษาโครงการ

อ่านเพิ่มเติม