แม้ว่าการย้ายข้อมูลจาก 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 ดังนี้
- สื่อการเรียนการสอน 1 ถึงสื่อการเรียนการสอน 2 ในมุมมอง
- Material 2 เป็น Material 3 ใน Views
- Material 2 เป็น Material 3 ใน Compose
เมื่อสร้างหน้าจอใหม่ใน Compose ไม่ว่าคุณจะใช้ Material
Design เวอร์ชันใดก็ตาม ให้ตรวจสอบว่าคุณได้ใช้ MaterialTheme ก่อน Composable ใดๆ ที่ปล่อย UI จากไลบรารี Compose Material คอมโพเนนต์ Material (Button, Text ฯลฯ) ขึ้นอยู่กับMaterialTheme ที่มีอยู่
และจะไม่มีการกำหนดลักษณะการทำงานหากไม่มีคอมโพเนนต์ดังกล่าว
ตัวอย่าง Jetpack Compose ทั้งหมด
ใช้ธีม Compose ที่กำหนดเองซึ่งสร้างขึ้นบน MaterialTheme
ดูข้อมูลเพิ่มเติมได้ที่ระบบการออกแบบใน Compose และการย้ายข้อมูลธีม XML ไปยัง Compose
การนำทาง
หากคุณใช้คอมโพเนนต์การนำทางในแอป โปรดดูข้อมูลเพิ่มเติมที่ ไปยังส่วนต่างๆ จาก Compose ในแอปที่ใช้ Fragment และ ย้ายข้อมูลการนำทาง 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 ใน Compose
หากใช้ไลบรารี Architecture Components
ViewModel คุณจะเข้าถึง
ViewModel ได้จาก Composable ใดก็ได้โดย
เรียกใช้ฟังก์ชัน
viewModel()
ตามที่อธิบายไว้ใน Compose และไลบรารีอื่นๆ
เมื่อใช้ Compose โปรดระมัดระวังในการใช้ประเภท ViewModel เดียวกันใน
Composables ต่างๆ เนื่องจากองค์ประกอบ 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 เป็นแหล่งข้อมูลที่เชื่อถือได้
ใช้ Composable
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 ที่มี Composable 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 หรือเป็นมุมมองที่กำหนดเองก็ตาม เช่น คุณจะสร้าง Composable CallToActionButton สำหรับคอมโพเนนต์คำกระตุ้นให้ดำเนินการ (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: โดยจะอยู่ในองค์ประกอบหรือไม่อยู่
ก็ได้
การแยกการจัดการสถานะและตรรกะการนำเสนออย่างชัดเจนจะช่วยให้คุณเปลี่ยนวิธีแสดงเนื้อหาเป็น 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 ในลำดับชั้นเดียวกัน ในกรณีนี้ คุณต้องระบุอย่างชัดเจนว่า คอมโพเนนต์ใดควรใช้ Inset และคอมโพเนนต์ใดควรละเว้น
เช่น หากเลย์เอาต์ชั้นนอกสุดเป็นเลย์เอาต์ Android View คุณควร
ใช้ Inset ในระบบ View และไม่สนใจ Inset สำหรับ Compose
หรือหากเลย์เอาต์ชั้นนอกสุดเป็น Composable คุณควรใช้
ระยะขอบใน Compose และเว้นที่ว่างสำหรับ Composable AndroidView ตามนั้น
โดยค่าเริ่มต้น ComposeView แต่ละรายการจะใช้ขอบทั้งหมดที่ระดับการใช้ WindowInsetsCompat หากต้องการเปลี่ยนลักษณะการทำงานเริ่มต้นนี้ ให้ตั้งค่า
ComposeView.consumeWindowInsets
เป็น false
อ่านข้อมูลเพิ่มเติมได้ในเอกสารประกอบเกี่ยวกับ WindowInsets ใน Compose
แนะนำสำหรับคุณ
- หมายเหตุ: ข้อความลิงก์จะแสดงเมื่อ JavaScript ปิดอยู่
- แสดงอีโมจิ
- Material Design 2 ใน Compose
- ส่วนที่เว้นไว้ในหน้าต่างใน Compose