แม้ว่าการย้ายข้อมูลจาก Views ไปยัง Compose จะเกี่ยวข้องกับ UI เท่านั้น แต่ก็มีหลายสิ่งที่ต้องพิจารณาเพื่อทำการย้ายข้อมูลที่ปลอดภัยและเพิ่มขึ้นทีละน้อย หน้านี้มีข้อควรพิจารณาบางประการขณะย้ายข้อมูลแอปแบบวิวเบสไปยัง Compose
การย้ายธีมของแอป
Material Design เป็นระบบการออกแบบที่แนะนำสำหรับการกำหนดธีมแอป Android
สำหรับแอปที่อิงตามมุมมอง 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 ดังนี้
- Material 1 ถึง Material 2 ใน View
- Material 2 เป็น Material 3 ใน Views
- Material 2 เป็น Material 3 ใน Compose
เมื่อสร้างหน้าจอใหม่ใน Compose ไม่ว่าคุณจะใช้ Material Design เวอร์ชันใดก็ตาม โปรดตรวจสอบว่าคุณใช้ MaterialTheme ก่อนคอมโพสิเบิลใดๆ ที่ปล่อย UI จากไลบรารี Compose Material คอมโพเนนต์ Material (Button, Text ฯลฯ) ขึ้นอยู่กับ MaterialTheme ที่มีอยู่ และจะไม่มีการกำหนดลักษณะการทำงานหากไม่มีคอมโพเนนต์ดังกล่าว
ตัวอย่าง Jetpack Compose ทั้งหมดใช้ธีม Compose ที่กำหนดเองซึ่งสร้างขึ้นบน MaterialTheme
ดูข้อมูลเพิ่มเติมได้ที่ระบบการออกแบบใน Compose และการย้ายธีม XML ไปยัง Compose
การนำทาง
หากคุณใช้คอมโพเนนต์การนำทางในแอป โปรดดูข้อมูลเพิ่มเติมที่หัวข้อไปยังส่วนต่างๆ จาก Compose ในแอปที่อิงตาม Fragment และย้ายข้อมูลการนำทาง Jetpack ไปยัง Navigation Compose
ทดสอบ UI ของ Compose/Views แบบผสม
หลังจากย้ายข้อมูลบางส่วนของแอปไปยัง Compose แล้ว การทดสอบเป็นสิ่งสำคัญเพื่อให้แน่ใจว่าคุณไม่ได้ทำให้ส่วนใดเสียหาย
เมื่อกิจกรรมหรือ Fragment ใช้ Compose คุณต้องใช้ createAndroidComposeRule แทนการใช้ ActivityScenarioRule createAndroidComposeRule ผสานรวมActivityScenarioRuleกับ ComposeTestRule ที่ช่วยให้คุณทดสอบ Compose และดูโค้ดได้พร้อมกัน
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 เดียวกันใน Composable ที่แตกต่างกัน เนื่องจากองค์ประกอบ ViewModel เป็นไปตามขอบเขตวงจรของ View ขอบเขตจะเป็นกิจกรรมโฮสต์, Fragment หรือกราฟการนําทางหากใช้ไลบรารีการนําทาง
เช่น หากคอมโพสิเบิลโฮสต์อยู่ในกิจกรรม viewModel() จะแสดงผลอินสแตนซ์เดียวกันเสมอซึ่งจะล้างเมื่อกิจกรรมเสร็จสิ้นเท่านั้น
ในตัวอย่างต่อไปนี้ ผู้ใช้คนเดียวกัน ("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 ด้วย คอมโพสิเบิลที่เป็นปลายทางในกราฟการนําทางจึงมีอินสแตนซ์ของ ViewModel ที่แตกต่างกัน
ในกรณีนี้ ViewModel จะกำหนดขอบเขตตามวงจรของปลายทาง และระบบจะล้างข้อมูลเมื่อนำปลายทางออกจาก Back Stack ในตัวอย่างต่อไปนี้ เมื่อผู้ใช้ไปยังหน้าจอโปรไฟล์ ระบบจะสร้างอินสแตนซ์ใหม่ของ 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 ซึ่งจะส่งการอัปเดตสถานะ
ตัวอย่างเช่น ไลบรารีการวิเคราะห์อาจอนุญาตให้คุณแบ่งกลุ่มประชากรผู้ใช้โดยแนบข้อมูลเมตาที่กำหนดเอง (พร็อพเพอร์ตี้ผู้ใช้ในตัวอย่างนี้) กับเหตุการณ์การวิเคราะห์ที่ตามมาทั้งหมด หากต้องการสื่อสารประเภทผู้ใช้ของผู้ใช้ปัจจุบันไปยังไลบรารี 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 }
ดูข้อมูลเพิ่มเติมได้ที่ผลข้างเคียงใน Compose
ดูระบบเป็นแหล่งข้อมูลที่เชื่อถือได้
หากระบบ View เป็นเจ้าของสถานะและแชร์สถานะนั้นกับ Compose เราขอแนะนำให้คุณรวมสถานะไว้ในออบเจ็กต์ mutableStateOf เพื่อให้ปลอดภัยต่อเธรดสำหรับ Compose หากใช้แนวทางนี้ ฟังก์ชันที่ประกอบกันได้จะเรียบง่ายขึ้นเนื่องจากไม่มีแหล่งข้อมูลที่ถูกต้องอีกต่อไป แต่ระบบมุมมองจำเป็นต้องอัปเดตสถานะที่เปลี่ยนแปลงได้และมุมมองที่ใช้สถานะนั้น
ในตัวอย่างต่อไปนี้ 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 ให้สร้าง Wrapper ของมุมมองที่กำหนดเองซึ่งขยายจาก AbstractComposeView ใน Content Composable ที่ลบล้าง ให้วาง 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) } } }
โปรดสังเกตว่าพารามิเตอร์ Composable จะกลายเป็นตัวแปรที่เปลี่ยนแปลงได้ภายในมุมมองที่กำหนดเอง ซึ่งจะทำให้มุมมอง CallToActionViewButton ที่กำหนดเองสามารถขยายและใช้งานได้เหมือนกับมุมมองแบบดั้งเดิม ดูตัวอย่างการดำเนินการนี้ด้วย View Binding ด้านล่าง
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 หรือไม่อยู่ก็ได้
การแยกการจัดการสถานะและตรรกะการนำเสนออย่างชัดเจนช่วยให้คุณเปลี่ยนวิธีแสดงเนื้อหาเป็นการแปลงสถานะเป็น UI ได้อย่างอิสระมากขึ้น การยกสถานะเมื่อจำเป็นยังช่วยให้คอมโพสิเบิลนำกลับมาใช้ใหม่ได้มากขึ้น เนื่องจากเจ้าของสถานะมีความยืดหยุ่นมากกว่า
ส่งเสริมคอมโพเนนต์ที่ห่อหุ้มและนำกลับมาใช้ใหม่ได้
องค์ประกอบ View มักจะมีแนวคิดเกี่ยวกับตำแหน่งที่อยู่ของตนเอง เช่น ภายใน Activity, Dialog, Fragment หรือที่ใดที่หนึ่งภายในลำดับชั้น View อื่น เนื่องจากมักจะมีการขยายจากไฟล์เลย์เอาต์แบบคงที่ โครงสร้างโดยรวมของ View จึงมักจะเข้มงวดมาก ซึ่งจะส่งผลให้เกิดการเชื่อมต่อแบบแน่น และทำให้ View เปลี่ยนแปลงหรือนำกลับมาใช้ใหม่ได้ยากขึ้น
ตัวอย่างเช่น View ที่กำหนดเองอาจถือว่ามีมุมมองย่อยของประเภทหนึ่งๆ ที่มีรหัสหนึ่งๆ และเปลี่ยนพร็อพเพอร์ตี้โดยตรงเพื่อตอบสนองต่อการดำเนินการบางอย่าง ซึ่งจะเชื่อมโยงองค์ประกอบ View เหล่านั้นเข้าด้วยกันอย่างแน่นหนา โดย View ที่กำหนดเองอาจขัดข้องหรือเสียหายหากไม่พบองค์ประกอบย่อย และองค์ประกอบย่อยอาจนำมาใช้ซ้ำไม่ได้หากไม่มีองค์ประกอบหลัก View ที่กำหนดเอง
ปัญหานี้จะเกิดขึ้นน้อยลงใน Compose ที่มี Composable ที่นำมาใช้ซ้ำได้ คอมโพสิเบิลหลักสามารถระบุสถานะและ Callback ได้อย่างง่ายดาย คุณจึงเขียนคอมโพสิเบิลที่นำกลับมาใช้ใหม่ได้โดยไม่ต้องทราบตำแหน่งที่แน่นอนที่จะใช้
@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จะฝังอยู่ลึกเพียงใด เด็กๆ เหล่านั้นอาจกำลังสร้างภาพเคลื่อนไหวเพื่อแสดงการเปลี่ยนแปลง สลับเนื้อหา หรือส่งต่อเนื้อหาให้เด็กคนอื่นๆ
รูปแบบนี้เรียกว่า Inversion of Control ซึ่งคุณสามารถอ่านเพิ่มเติมได้ในเอกสารประกอบของ CompositionLocal
การจัดการการเปลี่ยนแปลงขนาดหน้าจอ
การมีทรัพยากรที่แตกต่างกันสำหรับขนาดหน้าต่างที่แตกต่างกันเป็นวิธีหลักวิธีหนึ่งในการสร้างเลย์เอาต์ View ที่ปรับเปลี่ยนตามอุปกรณ์ แม้ว่าทรัพยากรที่ผ่านการรับรองจะยังคงเป็นตัวเลือกสำหรับการตัดสินใจเกี่ยวกับเลย์เอาต์ระดับหน้าจอ แต่ Compose ช่วยให้เปลี่ยนเลย์เอาต์ทั้งหมดในโค้ดด้วยตรรกะแบบมีเงื่อนไขปกติได้ง่ายขึ้นมาก ดูข้อมูลเพิ่มเติมได้ที่ใช้คลาสขนาดหน้าต่าง
นอกจากนี้ โปรดดูรองรับขนาดการแสดงผลต่างๆ เพื่อดูข้อมูลเกี่ยวกับเทคนิคที่ Compose มีให้เพื่อสร้าง UI ที่ปรับเปลี่ยนตามอุปกรณ์
การเลื่อนที่ฝังไว้ด้วย Views
ดูข้อมูลเพิ่มเติมเกี่ยวกับวิธีเปิดใช้การทำงานร่วมกันของการเลื่อนที่ฝังอยู่ระหว่างองค์ประกอบ View ที่เลื่อนได้และ Composable ที่เลื่อนได้ซึ่งฝังอยู่ทั้ง 2 ทิศทางได้ที่การทำงานร่วมกันของการเลื่อนที่ฝังอยู่
เขียนใน RecyclerView
คอมโพสิเบิลใน RecyclerView มีประสิทธิภาพตั้งแต่ RecyclerView เวอร์ชัน 1.3.0-alpha02 ตรวจสอบว่าคุณใช้ RecyclerView เวอร์ชัน 1.3.0-alpha02 เป็นอย่างน้อยเพื่อดูประโยชน์เหล่านั้น
การทำงานร่วมกันของ WindowInsets กับมุมมอง
คุณอาจต้องลบล้างส่วนแทรกเริ่มต้นเมื่อหน้าจอมีทั้งโค้ด Views และ Compose ในลำดับชั้นเดียวกัน ในกรณีนี้ คุณต้องระบุอย่างชัดเจนว่า องค์ประกอบใดควรใช้ส่วนแทรก และองค์ประกอบใดควรละเว้น
ตัวอย่างเช่น หากเลย์เอาต์นอกสุดเป็นเลย์เอาต์ Android View คุณควรใช้ส่วนแทรกในระบบ View และไม่ต้องสนใจส่วนแทรกเหล่านั้นสำหรับ Compose
หรือหากเลย์เอาต์นอกสุดเป็น Composable คุณควรใช้ส่วนแทรกใน Compose และเพิ่มระยะห่างจากขอบให้กับ Composable AndroidView ตามความเหมาะสม
โดยค่าเริ่มต้น ComposeView แต่ละรายการจะใช้ส่วนแทรกทั้งหมดที่ระดับการใช้ WindowInsetsCompat หากต้องการเปลี่ยนลักษณะการทำงานเริ่มต้นนี้ ให้ตั้งค่า ComposeView.consumeWindowInsets เป็น false
ดูข้อมูลเพิ่มเติมได้ที่เอกสารประกอบเกี่ยวกับ WindowInsets ใน Compose
แนะนำสำหรับคุณ
- หมายเหตุ: ข้อความลิงก์จะแสดงเมื่อ JavaScript ปิดอยู่
- แสดงอีโมจิ
- Material Design 2 ใน Compose
- ส่วนที่เว้นไว้ในหน้าต่างใน Compose