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

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

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

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

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

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

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

หากต้องการทำงานตลอดอายุการใช้งานของ Composable และสามารถเรียกใช้ฟังก์ชันระงับได้ ให้ใช้ Composable LaunchedEffect เมื่อ LaunchedEffect เข้าสู่ Composition ระบบจะเปิดใช้ Coroutine ที่มีบล็อกโค้ดที่ส่งเป็นพารามิเตอร์ ระบบจะยกเลิกโครูทีนหาก 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 เพื่อรอตามระยะเวลาที่กำหนด จากนั้นจะแสดงภาพเคลื่อนไหวของอัลฟาเป็นศูนย์และกลับมาอีกครั้งตามลำดับโดยใช้ animateTo การดำเนินการนี้จะทำซ้ำตลอดอายุการใช้งานของ Composable

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

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

rememberCoroutineScope เป็นฟังก์ชันที่ประกอบกันได้ซึ่งแสดงผล CoroutineScope ที่เชื่อมโยงกับจุดของ Composition ที่มีการเรียกใช้ ขอบเขตจะถูกยกเลิกเมื่อการเรียกออกจาก 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 */
}

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

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

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

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

@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

ตัวอย่างเช่น ไลบรารีการวิเคราะห์อาจอนุญาตให้คุณแบ่งกลุ่มประชากรผู้ใช้โดยแนบข้อมูลเมตาที่กำหนดเอง ("พร็อพเพอร์ตี้ผู้ใช้" ในตัวอย่างนี้) กับเหตุการณ์การวิเคราะห์ที่ตามมาทั้งหมด หากต้องการสื่อสารประเภทผู้ใช้ของผู้ใช้ปัจจุบันไปยังไลบรารี Analytics ให้ใช้ 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 จะเปิด Coroutine ที่กำหนดขอบเขตไว้สำหรับ Composition ซึ่งสามารถพุชค่าไปยัง State ที่แสดงผลได้ ใช้เพื่อแปลงสถานะที่ไม่ใช่ Compose เป็นสถานะ Compose เช่น การนำสถานะที่ขับเคลื่อนด้วยการสมัครใช้บริการภายนอก เช่น Flow, LiveData หรือ RxJava เข้ามาใน Composition

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

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

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

@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 การจัดองค์ประกอบใหม่จะเกิดขึ้นทุกครั้งที่ออบเจ็กต์สถานะที่สังเกตการณ์หรืออินพุตที่ประกอบกันได้มีการเปลี่ยนแปลง ออบเจ็กต์สถานะหรืออินพุตอาจมีการเปลี่ยนแปลงบ่อยกว่าที่ UI จำเป็นต้องอัปเดตจริงๆ ซึ่งทำให้เกิดการจัดองค์ประกอบใหม่โดยไม่จำเป็น

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

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

ข้อมูลโค้ดต่อไปนี้แสดงกรณีการใช้งานที่เหมาะสมสำหรับ 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: แปลง State ของ Compose เป็น Flow

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

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

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

ตามหลักทั่วไปแล้ว ควรเพิ่มตัวแปรที่เปลี่ยนแปลงได้และเปลี่ยนแปลงไม่ได้ซึ่งใช้ในบล็อกโค้ดของเอฟเฟกต์เป็นพารามิเตอร์ให้กับ Composable ของเอฟเฟกต์ นอกจากนี้ คุณยังเพิ่มพารามิเตอร์อื่นๆ เพื่อบังคับให้รีสตาร์ทเอฟเฟกต์ได้ด้วย หากการเปลี่ยนแปลงของตัวแปรไม่ควรทำให้เอฟเฟกต์รีสตาร์ท ตัวแปรควรอยู่ใน 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 เป็นคีย์เอฟเฟกต์เพื่อทำให้เป็นไปตามวงจรของ Call Site ซึ่งมีกรณีการใช้งานที่ถูกต้อง เช่น ตัวอย่าง LaunchedEffect ที่แสดงด้านบน อย่างไรก็ตาม ก่อนที่จะดำเนินการดังกล่าว โปรดคิดให้รอบคอบและตรวจสอบว่านั่นคือสิ่งที่คุณต้องการ