ข้อควรพิจารณาอื่นๆ

แม้ว่าการย้ายข้อมูลจาก View ไปยัง Compose จะเกี่ยวข้องกับ UI โดยเฉพาะ แต่ก็มีหลายสิ่งที่ต้องคำนึงถึงเพื่อให้การย้ายข้อมูลเป็นไปอย่างปลอดภัยและค่อยๆ เป็นค่อยๆ ไป หน้านี้มีข้อควรพิจารณาบางประการขณะย้ายข้อมูลแอปแบบ View-based ไปยัง Compose

การย้ายข้อมูลธีมของแอป

Material Design เป็นระบบการออกแบบที่แนะนำสำหรับการกำหนดธีมแอป Android

สำหรับแอปแบบ View-based จะมี 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 3) หากระบบการออกแบบของแอปพร้อมที่จะทำเช่นนั้น โดยมีคู่มือการย้ายข้อมูลสำหรับทั้ง View และ Compose ดังนี้

เมื่อสร้างหน้าจอใหม่ใน Compose ไม่ว่าคุณจะใช้ Material Design เวอร์ชันใดก็ตาม ให้ตรวจสอบว่าได้ใช้ MaterialTheme ก่อนที่จะใช้ Composables ใดๆ ที่แสดงผล UI จากไลบรารี Compose Material คอมโพเนนต์ Material (Button, Text ฯลฯ) ต้องใช้ MaterialTheme และลักษณะการทำงานของคอมโพเนนต์จะไม่ได้กำหนดไว้หากไม่มี MaterialTheme

ตัวอย่าง Jetpack Compose ทั้งหมด ใช้ธีม Compose ที่กำหนดเองซึ่งสร้างขึ้นจาก MaterialTheme

ดูข้อมูลเพิ่มเติมได้ที่ ระบบการออกแบบใน Compose และ การย้ายข้อมูลธีม XML ไปยัง Compose

หากคุณใช้คอมโพเนนต์การนำทางในแอป โปรดดู ข้อมูลเพิ่มเติมที่การนำทางด้วย Compose - ความสามารถในการทำงานร่วมกันและ การย้ายข้อมูล Jetpack Navigation ไปยัง Navigation Compose

ทดสอบ UI แบบผสม Compose/View

หลังจากย้ายข้อมูลบางส่วนของแอปไปยัง 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 จาก Composables ใดก็ได้โดย เรียกใช้ฟังก์ชัน viewModel() ตามที่อธิบายไว้ใน Compose และไลบรารีอื่นๆ

เมื่อใช้ Compose โปรดระมัดระวังในการใช้ ViewModel ประเภทเดียวกันใน Composables ต่างๆ เนื่องจากองค์ประกอบ ViewModel เป็นไปตามขอบเขตวงจรการทำงานของ View ขอบเขตจะเป็นกิจกรรมโฮสต์, Fragment หรือกราฟการนำทางหากใช้ไลบรารีการนำทาง

ตัวอย่างเช่น หาก Composables โฮสต์อยู่ในกิจกรรม viewModel() จะแสดงผลอินสแตนซ์เดียวกันเสมอ ซึ่งจะล้างข้อมูลเมื่อกิจกรรมสิ้นสุดลงเท่านั้น ในตัวอย่างต่อไปนี้ ผู้ใช้คนเดียวกัน ("user1") ได้รับการทักทาย 2 ครั้งเนื่องจากมีการใช้ GreetingViewModel อินสแตนซ์เดียวกันซ้ำใน Composables ทั้งหมดภายใต้กิจกรรมโฮสต์ ระบบจะใช้อินสแตนซ์ ViewModel แรกที่สร้างขึ้นซ้ำใน Composables อื่นๆ

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 ด้วย Composables ที่เป็นปลายทางในกราฟการนำทางจึงมีอินสแตนซ์ 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 เป็นแหล่งข้อมูลที่เชื่อถือได้

ใช้ Composables SideEffect เพื่อเผยแพร่สถานะ Compose ไปยังโค้ดที่ไม่ใช่ Compose ในกรณีนี้ แหล่งข้อมูลที่เชื่อถือได้จะเก็บไว้ใน Composables ซึ่งจะส่งการอัปเดตสถานะ

ตัวอย่างเช่น ไลบรารีการวิเคราะห์อาจอนุญาตให้คุณแบ่งกลุ่มฐานผู้ใช้โดยแนบข้อมูลเมตาที่กำหนดเอง (พร็อพเพอร์ตี้ผู้ใช้ ในตัวอย่างนี้) กับเหตุการณ์การวิเคราะห์ทั้งหมดที่ตามมา หากต้องการสื่อสารประเภทผู้ใช้ของผู้ใช้ปัจจุบันกับไลบรารีข้อมูลวิเคราะห์ ให้ใช้ 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 เป็นแหล่งข้อมูลที่เชื่อถือได้

หากระบบ View เป็นเจ้าของสถานะและแชร์สถานะกับ Compose เราขอแนะนำให้คุณห่อหุ้มสถานะไว้ในออบเจ็กต์ mutableStateOf เพื่อให้ Compose ใช้ได้อย่างปลอดภัย หากใช้วิธีนี้ ฟังก์ชัน Composables จะง่ายขึ้นเนื่องจากไม่มีแหล่งข้อมูลที่เชื่อถือได้อีกต่อไป แต่ระบบ View ต้องอัปเดตสถานะที่เปลี่ยนแปลงได้และ View ที่ใช้สถานะนั้น

ในตัวอย่างต่อไปนี้ CustomViewGroup มี TextView และ ComposeView ที่มี Composables 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-based

ใน Compose องค์ประกอบ UI ที่แชร์จะกลายเป็น Composables ที่นำกลับมาใช้ใหม่ได้ทั่วทั้งแอป ไม่ว่าองค์ประกอบนั้นจะจัดรูปแบบโดยใช้ XML หรือเป็น View ที่กำหนดเอง ตัวอย่างเช่น คุณจะสร้าง Composables CallToActionButton สำหรับคอมโพเนนต์การกระตุ้นให้ดำเนินการที่กำหนดเอง Button

หากต้องการใช้ Composables ในหน้าจอแบบ View-based ให้สร้าง Wrapper View ที่กำหนดเองซึ่งขยายจาก AbstractComposeView ใน Composables Content ที่ลบล้าง ให้วาง Composables ที่คุณสร้างขึ้นซึ่งห่อหุ้มไว้ในธีม 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)
        }
    }
}

โปรดทราบว่าพารามิเตอร์ Composables จะกลายเป็นตัวแปรที่เปลี่ยนแปลงได้ภายใน View ที่กำหนดเอง ซึ่งจะทำให้ View CallToActionViewButton ที่กำหนดเองสามารถขยายและใช้งานได้เหมือนกับ View แบบเดิม ดูตัวอย่างการผูก 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 ช่วยให้แสดง Composables ที่แตกต่างกันโดยสิ้นเชิงได้ง่ายๆ โดยใช้ตรรกะแบบมีเงื่อนไขใน Kotlin ดังนี้

@Composable
fun MyComposable(showCautionIcon: Boolean) {
    if (showCautionIcon) {
        CautionIcon(/* ... */)
    }
}

CautionIcon ไม่จำเป็นต้องทราบหรือสนใจว่าเหตุใดจึงมีการแสดงผล และไม่มีแนวคิดเรื่อง visibility โดยไอคอนจะอยู่ใน Composition หรือไม่เท่านั้น

การแยกการจัดการสถานะและตรรกะการนำเสนออย่างชัดเจนช่วยให้คุณเปลี่ยนวิธีแสดงเนื้อหาได้อย่างอิสระมากขึ้นเมื่อแปลงสถานะเป็น UI นอกจากนี้ การยกสถานะเมื่อจำเป็นยังทำให้ Composables นำกลับมาใช้ใหม่ได้มากขึ้น เนื่องจากความเป็นเจ้าของสถานะมีความยืดหยุ่นมากขึ้น

ส่งเสริมคอมโพเนนต์ที่ห่อหุ้มและนำกลับมาใช้ใหม่ได้

องค์ประกอบ View มักจะทราบว่าองค์ประกอบนั้นอยู่ตรงไหน เช่น ภายใน Activity, Dialog, Fragment หรือที่ใดที่หนึ่งภายในลำดับชั้น View อื่น เนื่องจากมักจะขยายจากไฟล์เลย์เอาต์แบบคงที่ โครงสร้างโดยรวมของ View จึงมีแนวโน้มที่จะแข็งทื่อมาก ซึ่งส่งผลให้เกิดการผูกที่แน่นแฟ้นยิ่งขึ้น และทำให้การเปลี่ยนแปลงหรือการนำ View กลับมาใช้ใหม่ทำได้ยากขึ้น

ตัวอย่างเช่น View ที่กำหนดเองอาจสันนิษฐานว่ามี View ย่อยประเภทหนึ่งที่มีรหัสหนึ่ง และเปลี่ยนพร็อพเพอร์ตี้โดยตรงเพื่อตอบสนองต่อการดำเนินการบางอย่าง ซึ่งจะผูกองค์ประกอบ View เหล่านั้นเข้าด้วยกันอย่างแน่นหนา View ที่กำหนดเองอาจหยุดทำงานหรือเกิดข้อผิดพลาดหากไม่พบ View ย่อย และ View ย่อยอาจนำกลับมาใช้ใหม่ไม่ได้หากไม่มี View หลักที่กำหนดเอง

ปัญหาดังกล่าวเกิดขึ้นน้อยลงใน Compose ที่มี Composables ที่นำกลับมาใช้ใหม่ได้ องค์ประกอบหลักสามารถระบุสถานะและการเรียกกลับได้อย่างง่ายดาย คุณจึงเขียน Composables ที่นำกลับมาใช้ใหม่ได้โดยไม่ต้องทราบตำแหน่งที่แน่นอนที่จะใช้ Composables เหล่านั้น

@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 เป็น 0, 1 หรือมากกว่านั้น และ ControlPanelWithToggle ไม่จำเป็นต้องเปลี่ยนแปลง

  • สำหรับองค์ประกอบหลัก ไม่สำคัญว่า ImageWithEnabledOverlay หรือ ControlPanelWithToggle จะซ้อนกันลึกเพียงใด องค์ประกอบย่อยเหล่านั้นอาจแสดงการเปลี่ยนแปลงแบบเคลื่อนไหว สลับเนื้อหา หรือส่งเนื้อหาไปยังองค์ประกอบย่อยอื่นๆ

รูปแบบนี้เรียกว่า การผกผันของการควบคุม ซึ่งคุณสามารถอ่านเพิ่มเติม ได้ในเอกสารประกอบ CompositionLocal

การจัดการการเปลี่ยนแปลงขนาดหน้าจอ

การมีทรัพยากรที่แตกต่างกันสำหรับขนาดหน้าต่างที่แตกต่างกันเป็นวิธีหลักวิธีหนึ่งในการสร้างเลย์เอาต์ View ที่ปรับเปลี่ยนตามอุปกรณ์ แม้ว่าทรัพยากรที่มีคุณสมบัติยังคงเป็นตัวเลือกสำหรับการตัดสินใจเกี่ยวกับเลย์เอาต์ระดับหน้าจอ แต่ Compose ช่วยให้การเปลี่ยนเลย์เอาต์ทั้งหมดในโค้ดด้วยตรรกะแบบมีเงื่อนไขปกติทำได้ง่ายขึ้นมาก ดูข้อมูลเพิ่มเติมได้ที่ใช้คลาสขนาดหน้าต่าง

นอกจากนี้ โปรดดูหัวข้อ รองรับขนาดการแสดงผลต่างๆ เพื่อดูข้อมูลเกี่ยวกับเทคนิคที่ Compose มีให้เพื่อสร้าง UI แบบปรับอัตโนมัติ

การเลื่อนที่ฝังไว้ด้วย View

ดูข้อมูลเพิ่มเติมเกี่ยวกับวิธีเปิดใช้การทำงานร่วมกันของการเลื่อนที่ฝังไว้ระหว่าง องค์ประกอบ View ที่เลื่อนได้กับ Composables ที่เลื่อนได้ ซึ่งฝังไว้ทั้ง 2 ทิศทาง ได้ที่ การทำงานร่วมกันของการเลื่อนที่ฝังไว้

Compose ใน RecyclerView

Composables ใน RecyclerView มีประสิทธิภาพสูงนับตั้งแต่ RecyclerView เวอร์ชัน 1.3.0-alpha02 โปรดตรวจสอบว่าคุณใช้ RecyclerView เวอร์ชัน 1.3.0-alpha02 ขึ้นไปเพื่อดูข้อดีเหล่านั้น

WindowInsets การทำงานร่วมกันกับ View

คุณอาจต้องลบล้างระยะขอบเริ่มต้นเมื่อหน้าจอมีทั้งโค้ด View และโค้ด Compose อยู่ในลำดับชั้นเดียวกัน ในกรณีนี้ คุณต้องระบุอย่างชัดเจนว่าองค์ประกอบใดควรใช้ระยะขอบ และองค์ประกอบใดควรละเว้นระยะขอบ

ตัวอย่างเช่น หากเลย์เอาต์ชั้นนอกสุดเป็นเลย์เอาต์ Android View คุณควรใช้ระยะขอบในระบบ View และละเว้นระยะขอบสำหรับ Compose หรือหากเลย์เอาต์ชั้นนอกสุดเป็น Composables คุณควรใช้ระยะขอบใน Compose และเพิ่มระยะห่างจากขอบให้กับ Composables AndroidView ตามความเหมาะสม

โดยค่าเริ่มต้น ComposeView แต่ละรายการจะใช้ระยะขอบทั้งหมดที่ระดับการใช้ WindowInsetsCompat หากต้องการเปลี่ยนลักษณะการทำงานเริ่มต้นนี้ ให้ตั้งค่า ComposeView.consumeWindowInsets เป็น false

ดูข้อมูลเพิ่มเติมได้ในเอกสารประกอบ WindowInsets ใน Compose