ผลกระทบข้างเคียงคือการเปลี่ยนแปลงสถานะของแอปที่เกิดขึ้นภายนอก ขอบเขตของฟังก์ชัน Composable เนื่องจาก Composables วงจรและคุณสมบัติ เช่น คาดเดาไม่ได้ การจัดองค์ประกอบใหม่ ดำเนินการจัดองค์ประกอบใหม่ของ Composable ในลำดับที่ต่างกัน หรือ การจัดองค์ประกอบใหม่ที่สามารถทิ้งได้ Composable ควรเป็นผลข้างเคียง ฟรี
แต่บางครั้งก็จำเป็นต้องมีผลข้างเคียง เช่น เพื่อทริกเกอร์แบบครั้งเดียว เช่น การแสดงแถบแสดงข้อความหรือไปยังหน้าจออื่น สถานะ การดำเนินการเหล่านี้ควรถูกเรียกจาก สภาพแวดล้อมที่ตระหนักถึงวงจรของ Composable ในหน้านี้ คุณจะได้เรียนรู้เกี่ยวกับ API ของเอฟเฟกต์ต่างๆ ใน Jetpack Compose
กรณีการใช้งานสถานะและผลกระทบ
ดังที่อธิบายไว้ในหัวข้อคิดในการเขียน Composable ควรจะไม่มีเอฟเฟกต์ข้างเคียง เมื่อคุณต้องการทำให้ เปลี่ยนสถานะของแอป (ตามที่อธิบายไว้ในการจัดการ เอกสารสถานะ) คุณควรใช้เอฟเฟกต์ API เพื่อให้ระบบประมวลผลผลข้างเคียงเหล่านั้นในลักษณะที่คาดการณ์ได้
จากโอกาสต่างๆ ใน Compose จะมีผลกระทบต่างกัน ใช้มากเกินไป ตรวจสอบว่างานที่คุณทำในนั้นเกี่ยวข้องกับ UI และ ไม่แยกการรับส่งข้อมูลแบบทิศทางเดียวกันตามที่อธิบายไว้ในสถานะการจัดการ เอกสารประกอบ
LaunchedEffect
: เรียกใช้ฟังก์ชันระงับในขอบเขตของ Composable
เพื่อทำงานตลอดอายุของ Composable และสามารถเรียก
ระงับฟังก์ชันต่างๆ ให้ใช้
LaunchedEffect
Composable เมื่อ LaunchedEffect
เข้าสู่การเรียบเรียงจะเป็นการเปิด
coroutine กับบล็อกโค้ดที่ส่งผ่านเป็นพารามิเตอร์ โครูทีนจะ
ยกเลิกหาก LaunchedEffect
ออกจากการเรียบเรียง หาก LaunchedEffect
คือ
ได้รับการกำหนดใหม่ด้วยแป้นอื่น (โปรดดูการรีสตาร์ท
เอฟเฟกต์ด้านล่าง) โครูทีนที่มีอยู่จะ
"ยกเลิก" และฟังก์ชันการระงับใหม่จะเปิดตัวใน Coroutine ใหม่
ลองดูตัวอย่างภาพเคลื่อนไหวต่อไปนี้ซึ่งจะแสดงค่าอัลฟ่าด้วย การหน่วงเวลาที่กำหนดค่าได้:
// 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 { mutableStateOf(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
: รับขอบเขตที่รับรู้ถึงองค์ประกอบเพื่อเปิดใช้โครูทีนนอก Compos มี
เนื่องจาก LaunchedEffect
เป็นฟังก์ชันที่ประกอบกันได้ จึงใช้ได้เฉพาะใน
ที่ประกอบกันได้ ในการเรียกใช้ Coroutine นอก Composable
แต่กำหนดขอบเขตเพื่อให้ยกเลิกโดยอัตโนมัติเมื่อออกจาก
การเรียบเรียง, ใช้
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
ใหม่แล้ว เอฟเฟกต์ที่รอสักพัก
และแจ้งว่าไม่ควรเริ่มนับเวลาที่ผ่านไปอีกครั้ง ดังนี้
@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 ออกจาก "การเรียบเรียง" ให้ใช้
DisposableEffect
หากคีย์ DisposableEffect
มีการเปลี่ยนแปลง คอมโพเนนต์ Composable จะต้อง ทิ้ง (มี)
การล้างข้อมูล) เอฟเฟกต์ปัจจุบัน แล้วรีเซ็ตโดยเรียกใช้เอฟเฟกต์อีกครั้ง
ตัวอย่างเช่น คุณอาจต้องการส่งเหตุการณ์ Analytics ที่อิงตาม
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
หากต้องการแชร์สถานะการเขียนกับออบเจ็กต์ที่ไม่ได้จัดการด้วยการเขียน ให้ใช้
SideEffect
Composable การใช้ 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
เปิดตัวโครูทีนซึ่งมีขอบเขตอยู่ในการเรียบเรียงที่สามารถพุชค่าไปยังองค์ประกอบ
ส่งคืนแล้ว State
ใช้เพื่อ
แปลงสถานะที่ไม่ใช่ Compose เป็นสถานะ Compose ตัวอย่างเช่น การนำภายนอก
สถานะการสมัครใช้บริการ เช่น Flow
, LiveData
หรือ RxJava
ลงใน
การเรียบเรียง
ผู้สร้างรายนี้เปิดตัวเมื่อ produceState
เข้าสู่การเรียบเรียง และจะเป็น
ยกเลิกเมื่อออกจากการเรียบเรียง State
ที่แสดงผลซ้อนทับกัน
การตั้งค่าเดียวกันจะไม่ทริกเกอร์การจัดองค์ประกอบใหม่
แม้ว่า produceState
จะสร้างโครูทีน แต่ก็สามารถใช้เพื่อสังเกตการณ์ได้
แหล่งข้อมูลที่ไม่ถูกระงับ หากต้องการนำการสมัครใช้บริการออกจากแหล่งที่มาดังกล่าว ให้ใช้
เวลา
awaitDispose
ตัวอย่างต่อไปนี้จะแสดงวิธีใช้ produceState
เพื่อโหลดรูปภาพจาก
เครือข่าย ฟังก์ชัน Composable 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
: แปลงวัตถุสถานะหนึ่งหรือหลายรายการเป็นสถานะอื่น
ในการเขียน การจัดองค์ประกอบใหม่จะเกิดขึ้น ทุกครั้งที่ออบเจ็กต์สถานะที่พบหรืออินพุตที่ประกอบกันได้มีการเปลี่ยนแปลง ออบเจ็กต์สถานะ อาจมีการเปลี่ยนแปลงบ่อยกว่าที่ UI ต้องอัปเดต ซึ่งนำไปสู่การจัดองค์ประกอบใหม่ที่ไม่จำเป็น
คุณควรใช้derivedStateOf
เมื่ออินพุตไปยัง Composable มีการเปลี่ยนแปลงบ่อยกว่าที่คุณต้องการ
ที่จะเขียนใหม่ กรณีนี้มักเกิดขึ้นเมื่อมีการเปลี่ยนแปลงเกิดขึ้นบ่อยครั้ง เช่น
ตำแหน่งการเลื่อน แต่ Composable จำเป็นต้องตอบสนองเฉพาะเมื่อข้ามผ่าน
เกณฑ์บางอย่าง derivedStateOf
สร้างออบเจ็กต์สถานะ Compose ใหม่ซึ่งคุณ
จะเห็นว่ามีอัปเดตได้มากเท่าที่คุณต้องการ โดยวิธีนี้จะทำหน้าที่
คล้ายกับ Kotlin Flows
distinctUntilChanged()
การใช้งานที่ถูกต้อง
ข้อมูลโค้ดต่อไปนี้แสดงกรณีการใช้งาน 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>
วัตถุลงในโฟลว์ที่เย็น snapshotFlow
เรียกใช้การบล็อกเมื่อรวบรวมและปล่อยออกมา
ผลลัพธ์ของออบเจ็กต์ State
อ่านแล้ว เมื่อวัตถุใดวัตถุหนึ่งใน State
อ่านภายในการเปลี่ยนแปลงของบล็อก snapshotFlow
โฟลว์จะแสดงค่าใหม่
ให้กับผู้รวบรวมหากค่าใหม่ไม่เท่ากับ
ค่าที่ปล่อยออกมาก่อนหน้า (ลักษณะการทำงานนี้คล้ายกับ
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
เป็นโฟลว์ที่
จะได้รับประโยชน์จากพลังของโอเปอเรเตอร์ของโฟลว์ได้
กำลังรีสตาร์ทเอฟเฟกต์
เอฟเฟกต์บางอย่างใน 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
เนื่องจากค่าของคีย์ในการเรียบเรียงจะไม่เปลี่ยนแปลง
rememberUpdatedState
หากคุณไม่ส่ง lifecycleOwner
เป็นพารามิเตอร์และ
มีการเปลี่ยนแปลง HomeScreen
จะเขียนใหม่ แต่ไม่ได้กำจัด DisposableEffect
ทิ้ง
และเริ่มใหม่ วิธีนี้ทำให้เกิดปัญหาเนื่องจาก lifecycleOwner
ที่ไม่ถูกต้องคือ
ที่ใช้นับตั้งแต่จุดนั้นเป็นต้นไป
ค่าคงที่ที่เป็นคีย์
คุณสามารถใช้ค่าคงที่ เช่น true
เป็นแป้นเอฟเฟกต์เพื่อ
ให้เป็นไปตามวงจรของเว็บไซต์สำหรับการโทร มีกรณีการใช้งานที่ถูกต้องสำหรับ
เช่นเดียวกับตัวอย่าง LaunchedEffect
ที่แสดงข้างต้น แต่ก่อนที่จะดำเนินการดังกล่าว
คิดให้รอบคอบและตรวจสอบว่านั่นคือสิ่งที่คุณต้องการ
แนะนำสำหรับคุณ
- หมายเหตุ: ข้อความลิงก์จะแสดงเมื่อ JavaScript ปิดอยู่
- State และ Jetpack Compose
- Kotlin สำหรับ Jetpack Compose
- การใช้มุมมองในการเขียน