แม้ว่าการย้ายข้อมูลจาก Views ไปยัง Compose จะเกี่ยวข้องกับ UI โดยเฉพาะ แต่ก็มีหลายสิ่งที่คุณต้องคำนึงถึงเพื่อทำการย้ายข้อมูลอย่างปลอดภัยและค่อยเป็นค่อยไป หน้านี้มีข้อควรพิจารณาบางประการขณะย้ายข้อมูลแอปแบบวิวเบสไปยัง Compose
การย้ายข้อมูลธีมของแอป
Material Design เป็นระบบการออกแบบที่แนะนำสำหรับการกำหนดธีมแอป Android
สำหรับแอปที่อิงตาม View จะมี Material 3 เวอร์ชันให้ใช้งาน ได้แก่
- Material Design 1 โดยใช้ไลบรารี AppCompat (เช่น
Theme.AppCompat.*) - Material Design 2 โดยใช้ไลบรารี
MDC-Android (เช่น
Theme.MaterialComponents.*) - Material Design 3 โดยใช้ไลบรารี
MDC-Android (เช่น
Theme.Material3.*)
สำหรับแอป Compose จะมี Material 2 เวอร์ชันให้ใช้งาน ได้แก่
- Material Design 2 โดยใช้ไลบรารี
Compose Material
(เช่น
androidx.compose.material.MaterialTheme) - Material Design 3 โดยใช้ไลบรารี Compose Material 3
(เช่น
androidx.compose.material3.MaterialTheme)
เราขอแนะนำให้ใช้เวอร์ชันล่าสุด (Material 3) หากระบบการออกแบบของแอป พร้อมที่จะทำเช่นนั้น มีคำแนะนำในการย้ายข้อมูลสำหรับทั้ง Views และ Compose ดังนี้
เมื่อสร้างหน้าจอใหม่ใน Compose ไม่ว่าคุณจะใช้ Material
Design เวอร์ชันใดก็ตาม ให้ตรวจสอบว่าคุณได้ใช้ MaterialTheme ก่อน
Composables ที่ปล่อย UI จากไลบรารี Material ของ Compose คอมโพเนนต์ Material (Button, Text ฯลฯ) ขึ้นอยู่กับMaterialTheme ที่มีอยู่
และจะไม่มีการกำหนดลักษณะการทำงานหากไม่มีคอมโพเนนต์ดังกล่าว
ตัวอย่าง Jetpack Compose ทั้งหมด
ใช้ธีม Compose ที่กำหนดเองซึ่งสร้างขึ้นบน MaterialTheme
ดูข้อมูลเพิ่มเติมได้ที่ระบบการออกแบบใน Compose และการย้ายข้อมูลธีม XML ไปยัง Compose
การนำทาง
หากคุณใช้คอมโพเนนต์การนำทางในแอป โปรดดูข้อมูลเพิ่มเติมที่การไปยังส่วนต่างๆ ด้วย Compose - การทำงานร่วมกันและย้ายข้อมูลการนำทางของ Jetpack ไปยังการนำทางด้วย Compose
ทดสอบ UI แบบผสม Compose/Views
หลังจากย้ายข้อมูลบางส่วนของแอปไปยัง Compose แล้ว การทดสอบเป็นสิ่งสำคัญเพื่อให้แน่ใจว่า คุณไม่ได้ทำให้สิ่งใดเสียหาย
เมื่อกิจกรรมหรือ Fragment ใช้ Compose คุณต้องใช้
createAndroidComposeRule
แทนการใช้ ActivityScenarioRule createAndroidComposeRule ผสานรวม
ActivityScenarioRule กับ ComposeTestRule ที่ช่วยให้คุณทดสอบ Compose และ
โค้ด View ได้พร้อมกัน
class MyActivityTest { @Rule @JvmField val composeTestRule = createAndroidComposeRule<MyActivity>() @Test fun testGreeting() { val greeting = InstrumentationRegistry.getInstrumentation() .targetContext.resources.getString(R.string.greeting) composeTestRule.onNodeWithText(greeting).assertIsDisplayed() } }
ดูข้อมูลเพิ่มเติมเกี่ยวกับการทดสอบได้ที่การทดสอบเลย์เอาต์ Compose ดูการทำงานร่วมกับเฟรมเวิร์กการทดสอบ UI ได้ที่การทำงานร่วมกับ Espresso และการทำงานร่วมกับ UiAutomator
การผสานรวม Compose กับสถาปัตยกรรมแอปที่มีอยู่
สถาปัตยกรรมการไหลของข้อมูลแบบทิศทางเดียว (UDF) ทำงานร่วมกับ Compose ได้อย่างราบรื่น หากแอปใช้รูปแบบสถาปัตยกรรมประเภทอื่นแทน เช่น Model View Presenter (MVP) เราขอแนะนำให้คุณ ย้ายข้อมูลส่วนนั้นของ UI ไปยัง UDF ก่อนหรือขณะใช้ Compose
การใช้ ViewModel ในการเขียน
หากใช้ไลบรารี Architecture Components
ViewModel คุณจะเข้าถึง
ViewModel ได้จาก Composable ใดก็ได้โดย
เรียกใช้ฟังก์ชัน
viewModel()
ตามที่อธิบายไว้ใน Compose และไลบรารีอื่นๆ
เมื่อใช้ Compose ให้ระมัดระวังเกี่ยวกับการใช้ประเภท ViewModel เดียวกันใน
Composable ที่แตกต่างกัน เนื่องจากองค์ประกอบ ViewModel จะทำตามขอบเขตวงจรของ View
ขอบเขตจะเป็นกิจกรรมโฮสต์ Fragment หรือกราฟการนำทางหากใช้
ไลบรารีการนำทาง
เช่น หากโฮสต์ Composable ในกิจกรรม viewModel() always
จะแสดงอินสแตนซ์เดียวกันเสมอ ซึ่งจะล้างเมื่อกิจกรรมเสร็จสิ้นเท่านั้น
ในตัวอย่างต่อไปนี้ ผู้ใช้คนเดียวกัน ("user1") จะได้รับการต้อนรับ 2 ครั้งเนื่องจาก
มีการนำอินสแตนซ์ GreetingViewModel เดียวกันกลับมาใช้ซ้ำใน Composable ทั้งหมดภายใต้
กิจกรรมโฮสต์ ระบบจะนำอินสแตนซ์ ViewModel แรกที่สร้างขึ้นมาใช้ซ้ำใน Composable อื่นๆ
class GreetingActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { MaterialTheme { Column { GreetingScreen("user1") GreetingScreen("user2") } } } } } @Composable fun GreetingScreen( userId: String, viewModel: GreetingViewModel = viewModel( factory = GreetingViewModelFactory(userId) ) ) { val messageUser by viewModel.message.observeAsState("") Text(messageUser) } class GreetingViewModel(private val userId: String) : ViewModel() { private val _message = MutableLiveData("Hi $userId") val message: LiveData<String> = _message } class GreetingViewModelFactory(private val userId: String) : ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") override fun <T : ViewModel> create(modelClass: Class<T>): T { return GreetingViewModel(userId) as T } }
เนื่องจากกราฟการนำทางยังกำหนดขอบเขตขององค์ประกอบ ViewModel ด้วย Composable ที่เป็นปลายทางในกราฟการนำทางจึงมีอินสแตนซ์ของ ViewModel ที่แตกต่างกัน
ในกรณีนี้ ViewModel จะกำหนดขอบเขตตามวงจรของปลายทาง และ
จะล้างเมื่อนำปลายทางออกจาก Backstack ในตัวอย่างต่อไปนี้ เมื่อผู้ใช้นําทางไปยังหน้าจอโปรไฟล์ ระบบจะสร้างอินสแตนซ์ใหม่ของ GreetingViewModel
@Composable fun MyApp() { NavHost(rememberNavController(), startDestination = "profile/{userId}") { /* ... */ composable("profile/{userId}") { backStackEntry -> GreetingScreen(backStackEntry.arguments?.getString("userId") ?: "") } } }
แหล่งข้อมูลที่เชื่อถือได้เพียงแหล่งเดียว
เมื่อใช้ Compose ในส่วนหนึ่งของ UI ก็อาจเป็นไปได้ที่ Compose และโค้ดระบบ View จะต้องแชร์ข้อมูล เราขอแนะนำให้คุณ
ห่อหุ้มสถานะที่แชร์นั้นไว้ในอีกคลาสหนึ่งที่ทำตามแนวทางปฏิบัติแนะนำของ UDF
ซึ่งทั้ง 2 แพลตฟอร์มใช้ เช่น ใน ViewModel ที่แสดงสตรีมของ
ข้อมูลที่แชร์เพื่อส่งการอัปเดตข้อมูล
อย่างไรก็ตาม การดำเนินการดังกล่าวอาจเป็นไปไม่ได้เสมอไปหากข้อมูลที่จะแชร์เปลี่ยนแปลงได้หรือเชื่อมโยงกับองค์ประกอบ UI อย่างใกล้ชิด ในกรณีนี้ ระบบหนึ่งต้องเป็นแหล่งข้อมูลที่เชื่อถือได้ และระบบนั้นต้องแชร์การอัปเดตข้อมูลกับอีกระบบหนึ่ง โดยทั่วไปแล้ว แหล่งข้อมูลที่เชื่อถือได้ควรเป็นขององค์ประกอบที่อยู่ใกล้กับรูทของลำดับชั้น UI มากที่สุด
Compose เป็นแหล่งข้อมูลที่เชื่อถือได้
ใช้
SideEffect
ที่ประกอบได้เพื่อเผยแพร่สถานะ Compose ไปยังโค้ดที่ไม่ใช่ Compose ในกรณีนี้ เราจะเก็บ
แหล่งข้อมูลที่เชื่อถือได้ไว้ใน 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 }
ดูข้อมูลเพิ่มเติมได้ที่ผลข้างเคียงใน Compose
ดูระบบเป็นแหล่งข้อมูลที่ถูกต้อง
หากระบบ View เป็นเจ้าของสถานะและแชร์กับ Compose เราขอแนะนำให้
คุณห่อหุ้มสถานะในออบเจ็กต์ mutableStateOf เพื่อให้ Compose ทำงานได้อย่างปลอดภัยในหลายเธรด หากใช้วิธีนี้ ฟังก์ชันที่ประกอบได้จะง่ายขึ้นเนื่องจาก
ไม่มีแหล่งข้อมูลความจริงอีกต่อไป แต่ระบบ View ต้องอัปเดต
สถานะที่เปลี่ยนแปลงได้และ View ที่ใช้สถานะนั้น
ในตัวอย่างต่อไปนี้ CustomViewGroup มี TextView และ
ComposeView ที่มี TextField ที่ใช้ร่วมกันได้อยู่ภายใน TextView ต้องแสดงเนื้อหาที่ผู้ใช้พิมพ์ใน TextField
class CustomViewGroup @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyle: Int = 0 ) : LinearLayout(context, attrs, defStyle) { // Source of truth in the View system as mutableStateOf // to make it thread-safe for Compose private var text by mutableStateOf("") private val textView: TextView init { orientation = VERTICAL textView = TextView(context) val composeView = ComposeView(context).apply { setContent { MaterialTheme { TextField(value = text, onValueChange = { updateState(it) }) } } } addView(textView) addView(composeView) } // Update both the source of truth and the TextView private fun updateState(newValue: String) { text = newValue textView.text = newValue } }
การย้ายข้อมูล UI ที่แชร์
หากค่อยๆ ย้ายข้อมูลไปยัง Compose คุณอาจต้องใช้ UI ที่แชร์ในทั้ง Compose และระบบ View เช่น หากแอปมีคอมโพเนนต์ CallToActionButton ที่กำหนดเอง คุณอาจต้องใช้ทั้งในหน้าจอที่อิงตาม Compose และ View
ใน Compose องค์ประกอบ UI ที่แชร์จะกลายเป็น Composable ที่นำกลับมาใช้ใหม่ได้ทั่วทั้งแอป
ไม่ว่าองค์ประกอบนั้นจะได้รับการจัดรูปแบบโดยใช้ XML หรือเป็นมุมมองที่กำหนดเองก็ตาม
เช่น คุณจะสร้าง CallToActionButtonComposable สำหรับคอมโพเนนต์ข้อความกระตุ้นให้ดำเนินการ (Call-To-Action) ที่กำหนดเองButton
หากต้องการใช้ Composable ในหน้าจอที่อิงตาม View ให้สร้าง View Wrapper ที่กำหนดเองซึ่ง
ขยายจาก AbstractComposeView ใน ContentComposable ที่ลบล้าง
ให้วาง Composable ที่คุณสร้างขึ้นซึ่งห่อหุ้มด้วยธีม Compose ดังที่แสดงใน
ตัวอย่างด้านล่าง
@Composable fun CallToActionButton( text: String, onClick: () -> Unit, modifier: Modifier = Modifier, ) { Button( colors = ButtonDefaults.buttonColors( containerColor = MaterialTheme.colorScheme.secondary ), onClick = onClick, modifier = modifier, ) { Text(text) } } class CallToActionViewButton @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyle: Int = 0 ) : AbstractComposeView(context, attrs, defStyle) { var text by mutableStateOf("") var onClick by mutableStateOf({}) @Composable override fun Content() { YourAppTheme { CallToActionButton(text, onClick) } } }
โปรดทราบว่าพารามิเตอร์ที่ประกอบได้จะกลายเป็นตัวแปรที่เปลี่ยนแปลงได้ภายใน
มุมมองที่กำหนดเอง ซึ่งจะทำให้มุมมอง CallToActionViewButton ที่กำหนดเองสามารถขยายและใช้งานได้
เหมือนกับมุมมองแบบดั้งเดิม ดูตัวอย่างของกรณีนี้ได้ด้วยการเชื่อมโยงมุมมอง
ด้านล่าง
class ViewBindingActivity : ComponentActivity() { private lateinit var binding: ActivityExampleBinding override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityExampleBinding.inflate(layoutInflater) setContentView(binding.root) binding.callToAction.apply { text = getString(R.string.greeting) onClick = { /* Do something */ } } } }
หากคอมโพเนนต์ที่กำหนดเองมีสถานะที่เปลี่ยนแปลงได้ โปรดดูแหล่งที่มาของความจริงของสถานะ
จัดลำดับความสำคัญของการแยกสถานะออกจากงานนำเสนอ
โดยปกติแล้ว View จะมีสถานะ View จะจัดการฟิลด์ที่อธิบายสิ่งที่จะแสดง รวมถึงวิธีแสดง เมื่อแปลง View เป็น Compose ให้แยกข้อมูลที่แสดงผลเพื่อให้ได้โฟลว์ข้อมูลแบบทิศทางเดียว ดังที่อธิบายเพิ่มเติมในการยกระดับสถานะ
เช่น View มีพร็อพเพอร์ตี้ visibility ที่อธิบายว่าพร็อพเพอร์ตี้นั้น
มองเห็นได้ มองไม่เห็น หรือหายไป ซึ่งเป็นคุณสมบัติโดยธรรมชาติของ View แม้ว่าโค้ดส่วนอื่นๆ อาจเปลี่ยนระดับการเข้าถึงของ View แต่มีเพียง View
เท่านั้นที่รู้ว่าระดับการเข้าถึงปัจจุบันของตนเองคืออะไร ตรรกะในการตรวจสอบว่า View สามารถเข้าถึงได้อาจเกิดข้อผิดพลาด และมักจะเชื่อมโยงกับ View
เอง
ในทางตรงกันข้าม Compose ช่วยให้แสดง Composable ที่แตกต่างกันโดยสิ้นเชิงได้ง่ายๆ โดยใช้ตรรกะแบบมีเงื่อนไขใน Kotlin ดังนี้
@Composable fun MyComposable(showCautionIcon: Boolean) { if (showCautionIcon) { CautionIcon(/* ... */) } }
CautionIcon ไม่จำเป็นต้องรู้หรือสนใจว่าเหตุใดจึงมีการแสดงผล
และไม่มีแนวคิดของ visibility: โดยจะอยู่ใน Composition หรือไม่
ก็ได้
การแยกการจัดการสถานะและตรรกะการนำเสนออย่างชัดเจนช่วยให้คุณเปลี่ยนวิธีแสดงเนื้อหาเป็น Conversion ของสถานะเป็น UI ได้อย่างอิสระมากขึ้น การยกสถานะขึ้นเมื่อจำเป็นยังช่วยให้ Composable นำกลับมาใช้ซ้ำได้มากขึ้น เนื่องจากความเป็นเจ้าของสถานะมีความยืดหยุ่นมากขึ้น
โปรโมตคอมโพเนนต์ที่แคปซูลและนำกลับมาใช้ใหม่ได้
องค์ประกอบ View มักจะมีแนวคิดเกี่ยวกับตำแหน่งขององค์ประกอบนั้นๆ ซึ่งอาจอยู่ภายใน Activity, Dialog, Fragment หรือที่ใดที่หนึ่งภายในลำดับชั้น View อื่น เนื่องจากมักจะขยายจากไฟล์เลย์เอาต์แบบคงที่ โครงสร้างโดยรวมของ View จึงมักจะมีความยืดหยุ่นน้อยมาก ซึ่งจะส่งผลให้เกิดการเชื่อมโยงที่แน่นแฟ้นยิ่งขึ้น และทำให้View เปลี่ยนแปลงหรือนำกลับมาใช้ใหม่ได้ยากขึ้น
ตัวอย่างเช่น View อาจถือว่ามีมุมมองย่อยของ
ประเภทหนึ่งๆ ที่มีรหัสหนึ่งๆ และเปลี่ยนพร็อพเพอร์ตี้โดยตรงเพื่อตอบสนองต่อการดำเนินการบางอย่าง
ซึ่งจะเชื่อมโยงองค์ประกอบ View เหล่านั้นเข้าด้วยกันอย่างแน่นแฟ้น กล่าวคือ View ที่กำหนดเองอาจขัดข้องหรือใช้งานไม่ได้หากไม่พบองค์ประกอบย่อย และองค์ประกอบย่อยมักจะนำกลับมาใช้ใหม่ไม่ได้หากไม่มีองค์ประกอบหลัก View ที่กำหนดเอง
ปัญหานี้จะลดลงใน Compose เมื่อใช้ Composable ที่นำกลับมาใช้ใหม่ได้ ผู้ปกครองสามารถ ระบุสถานะและฟังก์ชันเรียกกลับได้อย่างง่ายดาย คุณจึงเขียน Composable ที่นำกลับมาใช้ใหม่ได้ โดยไม่ต้องทราบตำแหน่งที่แน่นอนที่จะใช้
@Composable fun AScreen() { var isEnabled by rememberSaveable { mutableStateOf(false) } Column { ImageWithEnabledOverlay(isEnabled) ControlPanelWithToggle( isEnabled = isEnabled, onEnabledChanged = { isEnabled = it } ) } }
ในตัวอย่างด้านบน ทั้ง 3 ส่วนจะมีการห่อหุ้มมากขึ้นและมีการเชื่อมโยงกันน้อยลง
ImageWithEnabledOverlayเพียงแค่ต้องรู้ว่าisEnabledสถานะปัจจุบันคืออะไร ไม่จำเป็นต้องรู้ว่าControlPanelWithToggleมีอยู่ หรือ แม้กระทั่งวิธีควบคุมControlPanelWithToggleไม่รู้ว่ามีImageWithEnabledOverlayอยู่isEnabledอาจแสดงได้หลายวิธี และControlPanelWithToggleไม่จำเป็นต้องเปลี่ยนแปลงสำหรับองค์ประกอบระดับบนสุด
ImageWithEnabledOverlayหรือControlPanelWithToggleจะซ้อนกันลึกแค่ไหนก็ได้ เด็กๆ อาจสร้างภาพเคลื่อนไหวของการเปลี่ยนแปลง สลับเนื้อหา หรือส่งต่อเนื้อหาให้เด็กคนอื่นๆ
รูปแบบนี้เรียกว่าการผกผันของการควบคุม ซึ่งคุณสามารถอ่านข้อมูลเพิ่มเติมได้ในเอกสารประกอบของ CompositionLocal
การจัดการการเปลี่ยนแปลงขนาดหน้าจอ
การมีแหล่งข้อมูลที่แตกต่างกันสำหรับขนาดหน้าต่างที่แตกต่างกันเป็นวิธีหลักวิธีหนึ่งในการ
สร้างViewเลย์เอาต์ที่ปรับเปลี่ยนตามอุปกรณ์ แม้ว่าทรัพยากรที่มีคุณสมบัติเหมาะสมจะยังคงเป็นตัวเลือก
สำหรับการตัดสินใจเกี่ยวกับเลย์เอาต์ระดับหน้าจอ แต่ Compose ก็ช่วยให้การเปลี่ยน
เลย์เอาต์ทั้งหมดในโค้ดด้วยตรรกะแบบมีเงื่อนไขปกติเป็นเรื่องง่ายขึ้นมาก ดูข้อมูลเพิ่มเติมได้ที่ใช้คลาสขนาดหน้าต่าง
นอกจากนี้ โปรดดูรองรับขนาดการแสดงผลต่างๆ เพื่อดูเทคนิคที่ Compose มีให้ในการสร้าง UI ที่ปรับเปลี่ยนตามอุปกรณ์
การเลื่อนที่ฝังไว้ด้วย View
ดูข้อมูลเพิ่มเติมเกี่ยวกับวิธีเปิดใช้การทำงานร่วมกันของการเลื่อนที่ซ้อนกันระหว่างองค์ประกอบ View ที่เลื่อนได้กับ Composable ที่เลื่อนได้ ซึ่งซ้อนกันทั้ง 2 ทิศทางได้ที่การทำงานร่วมกันของการเลื่อนที่ซ้อนกัน
เขียนใน RecyclerView
Composable ใน RecyclerView มีประสิทธิภาพตั้งแต่ RecyclerView เวอร์ชัน
1.3.0-alpha02 โปรดตรวจสอบว่าคุณใช้ RecyclerView อย่างน้อยเวอร์ชัน 1.3.0-alpha02 เพื่อดูสิทธิประโยชน์เหล่านั้น
WindowInsets การทำงานร่วมกันกับ Views
คุณอาจต้องลบล้างระยะขอบเริ่มต้นเมื่อหน้าจอมีทั้ง View และโค้ด Compose ในลำดับชั้นเดียวกัน ในกรณีนี้ คุณต้องระบุอย่างชัดเจนว่า เลย์เอาต์ใดควรใช้ระยะขอบ และเลย์เอาต์ใดควรละเว้น
เช่น หากเลย์เอาต์ชั้นนอกสุดเป็นเลย์เอาต์ Android View คุณควร
ใช้ Inset ในระบบ View และไม่สนใจ Inset สำหรับ Compose
หรือหากเลย์เอาต์ชั้นนอกสุดเป็น Composable คุณควรใช้
ระยะขอบใน Compose และเว้นที่ว่างสำหรับ Composable AndroidView ตามนั้น
โดยค่าเริ่มต้น ComposeView แต่ละรายการจะใช้ขอบทั้งหมดที่ระดับการใช้งาน WindowInsetsCompat หากต้องการเปลี่ยนลักษณะการทำงานเริ่มต้นนี้ ให้ตั้งค่า
ComposeView.consumeWindowInsets
เป็น false
อ่านข้อมูลเพิ่มเติมได้ในเอกสารประกอบเกี่ยวกับ WindowInsets ใน Compose
แนะนำสำหรับคุณ
- หมายเหตุ: ข้อความลิงก์จะแสดงเมื่อ JavaScript ปิดอยู่
- แสดงอีโมจิ
- Material Design 2 ใน Compose
- ส่วนที่เว้นไว้ในหน้าต่างใน Compose