ผลข้างเคียงคือการเปลี่ยนแปลงสถานะของแอปที่เกิดขึ้นภายนอกขอบเขตของฟังก์ชันที่ประกอบกันได้ เนื่องจากวงจรและพร็อพเพอร์ตี้ของ Composable เช่น การจัดองค์ประกอบใหม่ที่ไม่สามารถคาดเดาได้ การดำเนินการจัดองค์ประกอบใหม่ของ Composable ในลำดับต่างๆ หรือการจัดองค์ประกอบใหม่ที่ทิ้งได้ Composable ควรไม่มีผลข้างเคียง
อย่างไรก็ตาม บางครั้งก็จำเป็นต้องมีผลข้างเคียง เช่น เพื่อทริกเกอร์เหตุการณ์แบบครั้งเดียว เช่น การแสดงแถบแสดงข้อความหรือการไปยังหน้าจออื่นเมื่อมีเงื่อนไขสถานะหนึ่งๆ การดำเนินการเหล่านี้ควรเรียกใช้จากสภาพแวดล้อมที่ควบคุมได้ ซึ่งรับรู้ถึงวงจรของ Composable ในหน้านี้ คุณจะได้เรียนรู้เกี่ยวกับ API ผลข้างเคียงต่างๆ ที่ Jetpack Compose มีให้
กรณีการใช้งานสถานะและเอฟเฟกต์
ตามที่กล่าวไว้ในเอกสารประกอบการคิดใน Compose Composable ควรไม่มีผลข้างเคียง เมื่อต้องการทำการเปลี่ยนแปลงสถานะของแอป (ตามที่อธิบายไว้ในเอกสารการจัดการสถานะ) คุณควรใช้ Effect API เพื่อให้ระบบดำเนินการกับผลข้างเคียงเหล่านั้นในลักษณะที่คาดการณ์ได้
เนื่องจากเอฟเฟกต์ต่างๆ ที่เป็นไปได้ในฟีเจอร์ช่วยเขียน จึงอาจมีการใช้เอฟเฟกต์มากเกินไปได้ง่ายๆ ตรวจสอบว่างานที่คุณทำในนั้นเกี่ยวข้องกับ 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 เป็นฟังก์ชันที่ประกอบกันได้ จึงใช้ได้เฉพาะภายในฟังก์ชันที่ประกอบกันได้อื่นๆ หากต้องการเปิดใช้โครูทีนภายนอกฟังก์ชันที่ประกอบกันได้ แต่กำหนดขอบเขตเพื่อให้ระบบยกเลิกโครูทีนโดยอัตโนมัติเมื่อออกจาก Composition ให้ใช้ rememberCoroutineScope นอกจากนี้ ให้ใช้ rememberCoroutineScope ทุกครั้งที่ต้องการควบคุมวงจรของ โครูทีนอย่างน้อย 1 รายการด้วยตนเอง เช่น ยกเลิกภาพเคลื่อนไหวเมื่อเกิด เหตุการณ์ของผู้ใช้
rememberCoroutineScope เป็นฟังก์ชันที่ประกอบกันได้ซึ่งแสดงผล CoroutineScope ที่เชื่อมโยงกับจุดของการจัดองค์ประกอบที่เรียกใช้ ฟังก์ชันนี้จะยกเลิกขอบเขตเมื่อการเรียกใช้สิ้นสุดลง
จากตัวอย่างก่อนหน้า คุณสามารถใช้โค้ดนี้เพื่อแสดง 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ประกอบใหม่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 จะต้องทิ้ง (ล้างข้อมูล) เอฟเฟกต์ปัจจุบัน และรีเซ็ตโดยการเรียกเอฟเฟกต์อีกครั้ง
ตัวอย่างเช่น คุณอาจต้องการส่งเหตุการณ์วิเคราะห์ตาม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
ระบบจะเปิดใช้ Producer เมื่อ 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 การจัดองค์ประกอบใหม่จะเกิดขึ้น ทุกครั้งที่ออบเจ็กต์สถานะที่สังเกตได้หรืออินพุต Composable มีการเปลี่ยนแปลง ออบเจ็กต์สถานะ หรืออินพุตอาจเปลี่ยนแปลงบ่อยกว่าที่ UI จำเป็นต้องอัปเดต ซึ่งทำให้เกิดการจัดองค์ประกอบใหม่ที่ไม่จำเป็น
คุณควรใช้ฟังก์ชัน derivedStateOf
เมื่ออินพุตไปยัง Composable เปลี่ยนแปลงบ่อยกว่าที่คุณต้องการ
เพื่อทำการ Recompose กรณีนี้มักเกิดขึ้นเมื่อมีการเปลี่ยนแปลงบางอย่างบ่อยๆ เช่น
ตำแหน่งการเลื่อน แต่ 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: แปลงสถานะของ Compose เป็น Flow
ใช้ 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 }
เนื่องจากลักษณะการทำงานนี้มีความซับซ้อน ปัญหาจึงอาจเกิดขึ้นหากพารามิเตอร์ ที่ใช้รีสตาร์ทเอฟเฟกต์ไม่ถูกต้อง
- การรีสตาร์ทเอฟเฟกต์น้อยกว่าที่ควรจะเป็นอาจทำให้เกิดข้อบกพร่องในแอป
- การรีสตาร์ทเอฟเฟกต์บ่อยเกินไปอาจไม่มีประสิทธิภาพ
โดยทั่วไปแล้ว ตัวแปรที่เปลี่ยนแปลงได้และเปลี่ยนแปลงไม่ได้ซึ่งใช้ในบล็อกเอฟเฟกต์ของโค้ดควรเพิ่มเป็นพารามิเตอร์ไปยัง 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 เป็นคีย์เอฟเฟกต์เพื่อติดตามวงจรของเว็บไซต์ที่เรียกใช้ได้ มีกรณีการใช้งานที่ถูกต้องสำหรับค่าคงที่นี้ เช่น ตัวอย่าง LaunchedEffect ที่แสดงด้านบน อย่างไรก็ตาม โปรดพิจารณาให้รอบคอบและตรวจสอบว่าคุณต้องการใช้ค่าคงที่นี้จริงๆ
แนะนำสำหรับคุณ
- หมายเหตุ: ข้อความลิงก์จะแสดงเมื่อ JavaScript ปิดอยู่
- สถานะและ Jetpack Compose
- Kotlin สำหรับ Jetpack Compose
- การใช้ View ใน Compose