ผลข้างเคียงใน Compose

ผลข้างเคียงคือการเปลี่ยนแปลงสถานะของแอปที่เกิดขึ้นนอก ขอบเขตของฟังก์ชันที่ประกอบกันได้ เนื่องจากวงจรของ Composable และพร็อพเพอร์ตี้ต่างๆ เช่น การประกอบใหม่ที่คาดเดาไม่ได้ การดำเนินการประกอบใหม่ของ Composable ในลำดับที่แตกต่างกัน หรือการประกอบใหม่ที่ทิ้งได้ Composable ควรไม่มีผลข้างเคียง

อย่างไรก็ตาม บางครั้งก็จำเป็นต้องมีผลข้างเคียง เช่น เพื่อทริกเกอร์เหตุการณ์แบบครั้งเดียว เช่น การแสดงแถบแสดงข้อความหรือการไปยังหน้าจออื่นเมื่อมีเงื่อนไขสถานะ ที่แน่นอน ควรเรียกใช้การดำเนินการเหล่านี้จากสภาพแวดล้อมที่ควบคุมได้ซึ่งรับรู้ถึงวงจรของ Composable ในหน้านี้ คุณจะได้เรียนรู้เกี่ยวกับ API ผลข้างเคียงต่างๆ ที่ Jetpack Compose มีให้

กรณีการใช้งานสถานะและเอฟเฟกต์

ตามที่กล่าวไว้ในเอกสารประกอบการคิดใน Compose คอมโพสابلไม่ควรมีผลข้างเคียง เมื่อต้องการทำการเปลี่ยนแปลงสถานะของแอป (ตามที่อธิบายไว้ในเอกสารการจัดการสถานะ) คุณควรใช้ Effect API เพื่อให้ระบบดำเนินการกับผลข้างเคียงเหล่านั้นในลักษณะที่คาดการณ์ได้

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

LaunchedEffect: เรียกใช้ฟังก์ชันระงับในขอบเขตของ Composable

หากต้องการดำเนินการตลอดช่วงอายุของ Composable และมีความสามารถในการเรียกใช้ฟังก์ชัน ระงับ ให้ใช้ Composable LaunchedEffect เมื่อ LaunchedEffect เข้าสู่ Composition จะเปิด โครูทีนที่มีบล็อกโค้ดที่ส่งผ่านเป็นพารามิเตอร์ ระบบจะยกเลิกโครูทีนหาก LaunchedEffect ออกจากองค์ประกอบ หาก LaunchedEffect มีการ ประกอบใหม่ด้วยคีย์ที่แตกต่างกัน (ดูส่วนการรีสตาร์ท เอฟเฟกต์ด้านล่าง) ระบบจะ ยกเลิกโครูทีนที่มีอยู่และเปิดใช้ฟังก์ชันระงับใหม่ในโครูทีนใหม่

เช่น นี่คือภาพเคลื่อนไหวที่เพิ่มค่าอัลฟ่าโดยมี การหน่วงเวลาที่กำหนดค่าได้

// Allow the pulse rate to be configured, so it can be sped up if the user is running
// out of time
var pulseRateMs by remember { mutableLongStateOf(3000L) }
val alpha = remember { Animatable(1f) }
LaunchedEffect(pulseRateMs) { // Restart the effect when the pulse rate changes
    while (isActive) {
        delay(pulseRateMs) // Pulse the alpha every pulseRateMs to alert the user
        alpha.animateTo(0f)
        alpha.animateTo(1f)
    }
}

ในโค้ดด้านบน ภาพเคลื่อนไหวใช้ฟังก์ชันระงับ delay เพื่อรอตามระยะเวลาที่กำหนด จากนั้นจะเคลื่อนไหวอัลฟ่า เป็น 0 และกลับมาอีกครั้งโดยใช้ animateTo ซึ่งจะทำซ้ำตลอดอายุการใช้งานของ Composable

rememberCoroutineScope: รับขอบเขตที่รับรู้การจัดองค์ประกอบเพื่อเปิดใช้โครูทีนภายนอก Composable

เนื่องจาก LaunchedEffect เป็นฟังก์ชันที่ใช้ร่วมกันได้ จึงใช้ได้เฉพาะภายในฟังก์ชันที่ใช้ร่วมกันได้อื่นๆ หากต้องการเปิดใช้โครูทีนภายนอก Composable แต่กำหนดขอบเขตเพื่อให้ระบบยกเลิกโดยอัตโนมัติเมื่อออกจาก Composition ให้ใช้ rememberCoroutineScope นอกจากนี้ ให้ใช้ rememberCoroutineScope เมื่อใดก็ตามที่คุณต้องการควบคุมวงจรของ หนึ่งหรือหลายโครูทีนด้วยตนเอง เช่น ยกเลิกภาพเคลื่อนไหวเมื่อเกิด เหตุการณ์ของผู้ใช้

rememberCoroutineScope คือฟังก์ชันที่ประกอบกันได้ซึ่งแสดงผล CoroutineScope ที่เชื่อมโยงกับจุดของการคอมโพสที่เรียกใช้ ระบบจะยกเลิกขอบเขตเมื่อการเรียกใช้ออกจาก Composition

จากตัวอย่างก่อนหน้า คุณสามารถใช้โค้ดนี้เพื่อแสดง Snackbar เมื่อผู้ใช้แตะ Button ได้

@Composable
fun MoviesScreen(snackbarHostState: SnackbarHostState) {

    // Creates a CoroutineScope bound to the MoviesScreen's lifecycle
    val scope = rememberCoroutineScope()

    Scaffold(
        snackbarHost = {
            SnackbarHost(hostState = snackbarHostState)
        }
    ) { contentPadding ->
        Column(Modifier.padding(contentPadding)) {
            Button(
                onClick = {
                    // Create a new coroutine in the event handler to show a snackbar
                    scope.launch {
                        snackbarHostState.showSnackbar("Something happened!")
                    }
                }
            ) {
                Text("Press me")
            }
        }
    }
}

rememberUpdatedState: อ้างอิงค่าในเอฟเฟกต์ที่ไม่ควรรีสตาร์ทหากค่ามีการเปลี่ยนแปลง

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

ตัวอย่างเช่น สมมติว่าแอปของคุณมี LandingScreen ที่หายไปหลังจากผ่านไปสักระยะ หนึ่ง แม้ว่าจะมีการสร้าง LandingScreen ใหม่ แต่เอฟเฟกต์ที่รอสักครู่ และแจ้งว่าเวลาผ่านไปแล้วไม่ควรเริ่มต้นใหม่

@Composable
fun LandingScreen(onTimeout: () -> Unit) {

    // This will always refer to the latest onTimeout function that
    // LandingScreen was recomposed with
    val currentOnTimeout by rememberUpdatedState(onTimeout)

    // Create an effect that matches the lifecycle of LandingScreen.
    // If LandingScreen recomposes, the delay shouldn't start again.
    LaunchedEffect(true) {
        delay(SplashWaitTimeMillis)
        currentOnTimeout()
    }

    /* Landing screen content */
}

หากต้องการสร้างเอฟเฟกต์ที่ตรงกับวงจรการใช้งานของเว็บไซต์ที่เรียกใช้ ระบบจะส่งค่าคงที่ที่ไม่เปลี่ยนแปลง เช่น Unit หรือ true เป็นพารามิเตอร์ ในโค้ดด้านบน เราใช้ LaunchedEffect(true) หากต้องการให้ onTimeout Lambda มีค่าล่าสุดที่ LandingScreen สร้างขึ้นใหม่onTimeout เสมอ คุณจะต้องห่อด้วยฟังก์ชัน rememberUpdatedState ควรใช้ State, currentOnTimeout ที่ส่งคืนในโค้ดในเอฟเฟกต์

DisposableEffect: เอฟเฟกต์ที่ต้องมีการล้างข้อมูล

สำหรับผลข้างเคียงที่ต้องล้างข้อมูลหลังจากที่คีย์เปลี่ยนแปลงหรือหาก Composable ออกจาก Composition ให้ใช้ DisposableEffect หากคีย์ DisposableEffect เปลี่ยนไป Composable จะต้องทิ้ง (ล้างข้อมูล) เอฟเฟกต์ปัจจุบัน และรีเซ็ตโดยการเรียกใช้เอฟเฟกต์อีกครั้ง

ตัวอย่างเช่น คุณอาจต้องการส่งเหตุการณ์ Analytics โดยอิงตามเหตุการณ์Lifecycle โดยใช้LifecycleObserver หากต้องการฟังเหตุการณ์เหล่านั้นใน Compose ให้ใช้ DisposableEffect เพื่อลงทะเบียนและ ยกเลิกการลงทะเบียน Observer เมื่อจำเป็น

@Composable
fun HomeScreen(
    lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
    onStart: () -> Unit, // Send the 'started' analytics event
    onStop: () -> Unit // Send the 'stopped' analytics event
) {
    // Safely update the current lambdas when a new one is provided
    val currentOnStart by rememberUpdatedState(onStart)
    val currentOnStop by rememberUpdatedState(onStop)

    // If `lifecycleOwner` changes, dispose and reset the effect
    DisposableEffect(lifecycleOwner) {
        // Create an observer that triggers our remembered callbacks
        // for sending analytics events
        val observer = LifecycleEventObserver { _, event ->
            if (event == Lifecycle.Event.ON_START) {
                currentOnStart()
            } else if (event == Lifecycle.Event.ON_STOP) {
                currentOnStop()
            }
        }

        // Add the observer to the lifecycle
        lifecycleOwner.lifecycle.addObserver(observer)

        // When the effect leaves the Composition, remove the observer
        onDispose {
            lifecycleOwner.lifecycle.removeObserver(observer)
        }
    }

    /* Home screen content */
}

ในโค้ดด้านบน เอฟเฟกต์จะเพิ่ม observer ไปยัง lifecycleOwner หาก lifecycleOwner เปลี่ยนแปลง ระบบจะทิ้งเอฟเฟกต์และ รีสตาร์ทด้วย lifecycleOwner ใหม่

DisposableEffect ต้องมีข้อความonDisposeเป็นข้อความสุดท้าย ในบล็อกโค้ด มิฉะนั้น IDE จะแสดงข้อผิดพลาดขณะคอมไพล์

SideEffect: เผยแพร่สถานะ Compose ไปยังโค้ดที่ไม่ใช่ Compose

หากต้องการแชร์สถานะ Compose กับออบเจ็กต์ที่ไม่ได้จัดการโดย Compose ให้ใช้ Composable SideEffect การใช้ SideEffect จะรับประกันว่าเอฟเฟกต์จะทำงานหลังจากการจัดองค์ประกอบใหม่ที่สำเร็จทุกครั้ง ในทางกลับกัน การ ใช้เอฟเฟกต์ก่อนที่จะรับประกันการประกอบใหม่ที่สำเร็จนั้นไม่ถูกต้อง ซึ่งเป็น กรณีที่เขียนเอฟเฟกต์โดยตรงใน Composable

เช่น ไลบรารีการวิเคราะห์อาจช่วยให้คุณแบ่งกลุ่มประชากรผู้ใช้ได้โดยการแนบข้อมูลเมตาที่กำหนดเอง ("พร็อพเพอร์ตี้ผู้ใช้" ในตัวอย่างนี้) กับเหตุการณ์การวิเคราะห์ทั้งหมดในภายหลัง หากต้องการสื่อสารประเภทผู้ใช้ของ ผู้ใช้ปัจจุบันไปยังไลบรารีการวิเคราะห์ ให้ใช้ SideEffect เพื่ออัปเดตค่า

@Composable
fun rememberFirebaseAnalytics(user: User): FirebaseAnalytics {
    val analytics: FirebaseAnalytics = remember {
        FirebaseAnalytics()
    }

    // On every successful composition, update FirebaseAnalytics with
    // the userType from the current User, ensuring that future analytics
    // events have this metadata attached
    SideEffect {
        analytics.setUserProperty("userType", user.userType)
    }
    return analytics
}

produceState: แปลงสถานะที่ไม่ใช่ Compose เป็นสถานะ Compose

produceState เปิดใช้โครูทีนที่กำหนดขอบเขตไว้ใน Composition ซึ่งสามารถส่งค่าไปยัง State ที่ส่งคืน ใช้เพื่อเปลี่ยนสถานะที่ไม่ใช่ Compose เป็นสถานะ Compose เช่น การนำสถานะที่ขับเคลื่อนด้วยการสมัครใช้บริการภายนอก เช่น Flow, LiveData หรือ RxJava มาไว้ใน Composition

ระบบจะเปิดตัวโปรดิวเซอร์เมื่อ produceState เข้าสู่การเรียบเรียง และจะยกเลิกเมื่อออกจากเรียบเรียง State ที่แสดงผลจะรวมกัน การตั้งค่าเดียวกันจะไม่ทริกเกอร์การจัดองค์ประกอบใหม่

แม้ว่า produceState จะสร้างโครูทีน แต่ก็ยังใช้เพื่อสังเกตแหล่งข้อมูลที่ไม่ระงับได้ด้วย หากต้องการยกเลิกการติดตามแหล่งข้อมูลนั้น ให้ใช้ฟังก์ชัน awaitDispose

ตัวอย่างต่อไปนี้แสดงวิธีใช้ produceState เพื่อโหลดรูปภาพจากเครือข่าย ฟังก์ชันที่ใช้ร่วมกันได้ loadNetworkImage จะแสดงผล State ที่ใช้ในฟังก์ชันที่ใช้ร่วมกันได้อื่นๆ ได้

@Composable
fun loadNetworkImage(
    url: String,
    imageRepository: ImageRepository = ImageRepository()
): State<Result<Image>> {
    // Creates a State<T> with Result.Loading as initial value
    // If either `url` or `imageRepository` changes, the running producer
    // will cancel and will be re-launched with the new inputs.
    return produceState<Result<Image>>(initialValue = Result.Loading, url, imageRepository) {
        // In a coroutine, can make suspend calls
        val image = imageRepository.load(url)

        // Update State with either an Error or Success result.
        // This will trigger a recomposition where this State is read
        value = if (image == null) {
            Result.Error
        } else {
            Result.Success(image)
        }
    }
}

derivedStateOf: แปลงออบเจ็กต์สถานะอย่างน้อย 1 รายการเป็นสถานะอื่น

ใน Compose recomposition จะเกิดขึ้น ทุกครั้งที่ออบเจ็กต์สถานะที่สังเกตได้หรืออินพุตที่ใช้ร่วมกันได้มีการเปลี่ยนแปลง ออบเจ็กต์สถานะ หรืออินพุตอาจเปลี่ยนแปลงบ่อยกว่าที่ UI จำเป็นต้องอัปเดต ซึ่งทำให้เกิดการจัดองค์ประกอบใหม่ที่ไม่จำเป็น

คุณควรใช้ฟังก์ชัน derivedStateOf เมื่ออินพุตไปยัง Composable เปลี่ยนแปลงบ่อยกว่าที่คุณต้องการ ที่จะทำการ Compose ใหม่ กรณีนี้มักเกิดขึ้นเมื่อมีการเปลี่ยนแปลงบางอย่างบ่อยๆ เช่น ตำแหน่งการเลื่อน แต่ Composable จำเป็นต้องตอบสนองต่อการเปลี่ยนแปลงดังกล่าวเมื่อข้าม เกณฑ์ที่กำหนดเท่านั้น derivedStateOf จะสร้างออบเจ็กต์สถานะ Compose ใหม่ที่คุณ สังเกตได้ว่าจะอัปเดตเฉพาะเท่าที่จำเป็นเท่านั้น ด้วยวิธีนี้ จึงทําให้ทำงาน คล้ายกับตัวดำเนินการ distinctUntilChanged() ของ Kotlin Flows

การใช้งานที่ถูกต้อง

ข้อมูลโค้ดต่อไปนี้แสดงกรณีการใช้งานที่เหมาะสมสำหรับ derivedStateOf

@Composable
// When the messages parameter changes, the MessageList
// composable recomposes. derivedStateOf does not
// affect this recomposition.
fun MessageList(messages: List<Message>) {
    Box {
        val listState = rememberLazyListState()

        LazyColumn(state = listState) {
            // ...
        }

        // Show the button if the first visible item is past
        // the first item. We use a remembered derived state to
        // minimize unnecessary compositions
        val showButton by remember {
            derivedStateOf {
                listState.firstVisibleItemIndex > 0
            }
        }

        AnimatedVisibility(visible = showButton) {
            ScrollToTopButton()
        }
    }
}

ในข้อมูลโค้ดนี้ firstVisibleItemIndex จะเปลี่ยนทุกครั้งที่รายการแรกที่มองเห็น มีการเปลี่ยนแปลง เมื่อเลื่อน ค่าจะกลายเป็น 0, 1, 2, 3, 4, 5 ฯลฯ อย่างไรก็ตาม การเขียนใหม่จะเกิดขึ้นก็ต่อเมื่อค่ามากกว่า 0 เท่านั้น ความถี่ในการอัปเดตที่ไม่ตรงกันนี้หมายความว่ากรณีการใช้งานนี้เหมาะสำหรับ derivedStateOf

การใช้งานไม่ถูกต้อง

ข้อผิดพลาดที่พบบ่อยคือการคิดว่าเมื่อรวมออบเจ็กต์สถานะ Compose 2 รายการ คุณควรใช้ derivedStateOf เนื่องจากคุณกำลัง "สร้างสถานะ" อย่างไรก็ตาม การดำเนินการนี้ เป็นเพียงค่าใช้จ่ายเพิ่มเติมและไม่จำเป็น ดังที่แสดงในตัวอย่างโค้ดต่อไปนี้

// DO NOT USE. Incorrect usage of derivedStateOf.
var firstName by remember { mutableStateOf("") }
var lastName by remember { mutableStateOf("") }

val fullNameBad by remember { derivedStateOf { "$firstName $lastName" } } // This is bad!!!
val fullNameCorrect = "$firstName $lastName" // This is correct

ในข้อมูลโค้ดนี้ fullName ต้องอัปเดตบ่อยเท่ากับ firstName และ lastName ดังนั้นจึงไม่มีการจัดองค์ประกอบใหม่มากเกินไป และไม่จำเป็นต้องใช้ derivedStateOf

snapshotFlow: แปลงสถานะของ Compose เป็นโฟลว์

ใช้ snapshotFlow เพื่อแปลงออบเจ็กต์ State<T> เป็น Flow แบบเย็น snapshotFlow จะเรียกใช้บล็อกเมื่อรวบรวมและส่งผลลัพธ์ของออบเจ็กต์ State ที่อ่านในบล็อก เมื่อStateออบเจ็กต์ ใดออบเจ็กต์หนึ่งที่อ่านภายในบล็อก snapshotFlow มีการเปลี่ยนแปลง Flow จะปล่อยค่าใหม่ ไปยังตัวรวบรวมหากค่าใหม่ไม่เท่ากับ ค่าที่ปล่อยก่อนหน้า (ลักษณะการทำงานนี้คล้ายกับของ Flow.distinctUntilChanged)

ตัวอย่างต่อไปนี้แสดงผลข้างเคียงที่บันทึกเมื่อผู้ใช้เลื่อน ผ่านรายการแรกในรายการไปยัง Analytics

val listState = rememberLazyListState()

LazyColumn(state = listState) {
    // ...
}

LaunchedEffect(listState) {
    snapshotFlow { listState.firstVisibleItemIndex }
        .map { index -> index > 0 }
        .distinctUntilChanged()
        .filter { it == true }
        .collect {
            MyAnalyticsService.sendScrolledPastFirstItemEvent()
        }
}

ในโค้ดด้านบน listState.firstVisibleItemIndex จะแปลงเป็น Flow ที่ ใช้ประโยชน์จากพลังของตัวดำเนินการของ Flow ได้

การรีสตาร์ทเอฟเฟกต์

เอฟเฟกต์บางอย่างใน Compose เช่น LaunchedEffect, produceState หรือ DisposableEffect จะรับอาร์กิวเมนต์หรือคีย์จำนวนหนึ่งที่ใช้เพื่อ ยกเลิกเอฟเฟกต์ที่กำลังทำงานและเริ่มเอฟเฟกต์ใหม่ด้วยคีย์ใหม่

รูปแบบทั่วไปของ API เหล่านี้คือ

EffectName(restartIfThisKeyChanges, orThisKey, orThisKey, ...) { block }

เนื่องจากลักษณะการทำงานนี้มีความซับซ้อน ปัญหาจึงอาจเกิดขึ้นหากพารามิเตอร์ ที่ใช้เพื่อรีสตาร์ทเอฟเฟกต์ไม่ถูกต้อง

  • การรีสตาร์ทเอฟเฟกต์น้อยกว่าที่ควรจะเป็นอาจทำให้เกิดข้อบกพร่องในแอป
  • การรีสตาร์ทเอฟเฟกต์บ่อยเกินไปอาจไม่มีประสิทธิภาพ

โดยหลักการแล้ว ควรเพิ่มตัวแปรที่เปลี่ยนแปลงได้และเปลี่ยนแปลงไม่ได้ซึ่งใช้ในบล็อกเอฟเฟกต์ของโค้ดเป็นพารามิเตอร์ไปยังฟังก์ชันที่ใช้คอมโพสเอฟเฟกต์ นอกเหนือจากพารามิเตอร์เหล่านั้น คุณยังเพิ่มพารามิเตอร์อื่นๆ เพื่อบังคับให้เอฟเฟกต์รีสตาร์ทได้ด้วย หากการเปลี่ยนแปลงตัวแปรไม่ควรทำให้เอฟเฟกต์รีสตาร์ท คุณควรใส่ตัวแปรไว้ใน rememberUpdatedState หากตัวแปรไม่เคยเปลี่ยนแปลงเนื่องจากอยู่ใน remember ที่ไม่มีคีย์ คุณไม่จำเป็นต้องส่งตัวแปรเป็นคีย์ไปยังเอฟเฟกต์

ในDisposableEffectโค้ดที่แสดงด้านบน เอฟเฟกต์จะใช้เป็นพารามิเตอร์ lifecycleOwnerที่ใช้ในบล็อก เนื่องจากหากมีการเปลี่ยนแปลงใดๆ กับพารามิเตอร์เหล่านั้น จะทำให้เอฟเฟกต์รีสตาร์ท

@Composable
fun HomeScreen(
    lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
    onStart: () -> Unit, // Send the 'started' analytics event
    onStop: () -> Unit // Send the 'stopped' analytics event
) {
    // These values never change in Composition
    val currentOnStart by rememberUpdatedState(onStart)
    val currentOnStop by rememberUpdatedState(onStop)

    DisposableEffect(lifecycleOwner) {
        val observer = LifecycleEventObserver { _, event ->
            /* ... */
        }

        lifecycleOwner.lifecycle.addObserver(observer)
        onDispose {
            lifecycleOwner.lifecycle.removeObserver(observer)
        }
    }
}

ไม่จำเป็นต้องใช้ currentOnStart และ currentOnStop เป็นคีย์ DisposableEffect เนื่องจากค่าของคีย์ดังกล่าวจะไม่เปลี่ยนแปลงใน Composition เนื่องจากมีการใช้ rememberUpdatedState หากคุณไม่ส่ง lifecycleOwner เป็นพารามิเตอร์และ มีการเปลี่ยนแปลง HomeScreen จะสร้างใหม่ แต่ DisposableEffect จะไม่ถูกทิ้ง และรีสตาร์ท ซึ่งจะทำให้เกิดปัญหาเนื่องจากระบบจะใช้ lifecycleOwner ที่ไม่ถูกต้อง นับจากนั้นเป็นต้นไป

ค่าคงที่เป็นคีย์

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