จะยกสถานะที่ไหน

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

แนวทางปฏิบัติแนะนำ

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

บรรพบุรุษร่วมที่ต่ำที่สุดอาจอยู่นอก Composition ก็ได้ เช่น เมื่อยกสถานะขึ้นใน ViewModel เนื่องจากมีตรรกะทางธุรกิจเข้ามาเกี่ยวข้อง

หน้านี้จะอธิบายแนวทางปฏิบัติแนะนำนี้โดยละเอียดและข้อควรระวังที่ควรทราบ

ประเภทของสถานะ UI และตรรกะ UI

ด้านล่างนี้คือคำจำกัดความของประเภทสถานะและตรรกะ UI ที่ใช้ในเอกสารนี้

สถานะ UI

สถานะ UI คือพร็อพเพอร์ตี้ที่ อธิบาย UI โดยมี 2 ประเภทดังนี้

  • สถานะ UI ของหน้าจอ คือ สิ่งที่ คุณต้องแสดงบนหน้าจอ เช่น คลาส NewsUiState อาจมีบทความข่าวและข้อมูลอื่นๆ ที่จำเป็นต่อการแสดงผล UI สถานะนี้มักจะเชื่อมโยงกับเลเยอร์อื่นๆ ในลำดับชั้นเนื่องจากมีข้อมูลแอป
  • สถานะองค์ประกอบ UI หมายถึงพร็อพเพอร์ตี้ที่อยู่ในองค์ประกอบ UI ซึ่งส่งผลต่อวิธีแสดงผล องค์ประกอบ UI อาจแสดงหรือซ่อน และอาจมีแบบอักษร ขนาดแบบอักษร หรือสีแบบอักษรที่เฉพาะเจาะจง ใน Jetpack Compose สถานะจะอยู่นอก Composable และคุณยังสามารถยกสถานะออกจากบริเวณใกล้เคียงของ Composable ไปยังฟังก์ชัน Composable ที่เรียกใช้หรือตัวเก็บสถานะได้ด้วย ตัวอย่างของสถานะนี้คือ ScaffoldState สำหรับ Scaffold Composable

ตรรกะ

ตรรกะในแอปพลิเคชันอาจเป็นตรรกะทางธุรกิจหรือตรรกะ UI ก็ได้

  • ตรรกะทางธุรกิจ คือการนำข้อกำหนดของผลิตภัณฑ์สำหรับข้อมูลแอปไปใช้ เช่น การเพิ่มบทความลงในบุ๊กมาร์กในแอปอ่านข่าวเมื่อผู้ใช้แตะปุ่ม โดยปกติแล้วตรรกะในการบันทึกบุ๊กมาร์กลงในไฟล์หรือฐานข้อมูลจะอยู่ในเลเยอร์โดเมนหรือเลเยอร์ข้อมูล ตัวยึดสถานะมักจะมอบหมายตรรกะนี้ไปยังเลเยอร์เหล่านั้นโดยการเรียกใช้เมธอดที่เลเยอร์เหล่านั้นแสดง
  • ตรรกะ UI เกี่ยวข้องกับ วิธี แสดงสถานะ UI บนหน้าจอ เช่น การรับคำแนะนำที่เหมาะสมสำหรับแถบค้นหาเมื่อผู้ใช้เลือกหมวดหมู่ การเลื่อนไปยังรายการที่เฉพาะเจาะจงในรายการ หรือตรรกะการนำทางไปยังหน้าจอที่เฉพาะเจาะจงเมื่อผู้ใช้คลิกปุ่ม

ตรรกะ UI

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

ด้านล่างนี้คือคำอธิบายของโซลูชันทั้ง 2 รายการและคำอธิบายเกี่ยวกับเวลาที่จะใช้โซลูชันใด

Composable เป็นเจ้าของสถานะ

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

ไม่จำเป็นต้องยกสถานะขึ้น

การยกสถานะขึ้นไม่จำเป็นเสมอไป คุณสามารถเก็บสถานะไว้ภายใน Composable ได้เมื่อไม่มี Composable อื่นที่ต้องควบคุมสถานะนั้น ในข้อมูลโค้ดนี้ มี Composable ที่ขยายและยุบเมื่อแตะ

@Composable
fun ChatBubble(
    message: Message
) {
    var showDetails by rememberSaveable { mutableStateOf(false) } // Define the UI element expanded state

    Text(
        text = AnnotatedString(message.content),
        modifier = Modifier.clickable {
            showDetails = !showDetails // Apply UI logic
        }
    )

    if (showDetails) {
        Text(message.timestamp)
    }
}

ตัวแปร showDetails คือสถานะภายในขององค์ประกอบ UI นี้ โดยจะอ่านและแก้ไขใน Composable นี้เท่านั้น และตรรกะที่ใช้กับตัวแปรนี้ก็มีความซับซ้อนไม่มาก ดังนั้นการยกสถานะขึ้นในกรณีนี้จึงไม่เป็นประโยชน์มากนัก คุณจึงเก็บสถานะไว้ภายในได้ การทำเช่นนี้จะทำให้ Composable นี้เป็นเจ้าของและแหล่งข้อมูลที่เชื่อถือได้แหล่งเดียวของสถานะที่ขยาย

การยกสถานะขึ้นภายใน Composable

หากต้องการแชร์สถานะองค์ประกอบ UI กับ Composable อื่นๆ และใช้ตรรกะ UI กับสถานะนั้นในที่ต่างๆ คุณสามารถยกสถานะขึ้นไปในระดับที่สูงขึ้นในลำดับชั้น UI ซึ่งยังทำให้ Composable ของคุณนำกลับมาใช้ซ้ำได้มากขึ้นและทดสอบได้ง่ายขึ้นด้วย

ตัวอย่างต่อไปนี้เป็นแอปแชทที่ใช้ฟังก์ชันการทำงาน 2 อย่าง

  • ปุ่ม JumpToBottom จะเลื่อนรายการข้อความไปที่ด้านล่าง ปุ่มนี้จะใช้ตรรกะ UI กับสถานะรายการ
  • รายการ MessagesList จะเลื่อนไปที่ด้านล่างหลังจากที่ผู้ใช้ส่งข้อความใหม่ UserInput จะใช้ตรรกะ UI กับสถานะรายการ
แอปแชทที่มีปุ่ม JumpToBottom และเลื่อนไปที่ด้านล่างเมื่อมีข้อความใหม่
รูปที่ 1 แอปแชทที่มีปุ่ม JumpToBottom และเลื่อนไปที่ด้านล่างเมื่อมีข้อความใหม่

ลำดับชั้น Composable มีดังนี้

แผนผัง Chat ที่ประกอบได้
รูปที่ 2 แผนผัง Composable ของแชท

สถานะ LazyColumn จะถูกยกขึ้นไปยังหน้าจอการสนทนาเพื่อให้แอปใช้ ตรรกะ UI และอ่านสถานะจาก Composable ทั้งหมดที่ต้องใช้สถานะนั้นได้

การยกสถานะ LazyColumn จาก LazyColumn ไปยัง ConversationScreen
รูปที่ 3 การยกสถานะ LazyColumn จาก LazyColumn ไปยัง ConversationScreen

ดังนั้น Composable สุดท้ายจึงมีลักษณะดังนี้

แชทแบบ Composable Tree ที่มี LazyListState ยกขึ้นไปที่ ConversationScreen
รูปที่ 4 แผนผัง Composable ของแชทที่มี LazyListState ยกขึ้นไปยัง ConversationScreen

โค้ดมีลักษณะดังนี้

@Composable
private fun ConversationScreen(/*...*/) {
    val scope = rememberCoroutineScope()

    val lazyListState = rememberLazyListState() // State hoisted to the ConversationScreen

    MessagesList(messages, lazyListState) // Reuse same state in MessageList

    UserInput(
        onMessageSent = { // Apply UI logic to lazyListState
            scope.launch {
                lazyListState.scrollToItem(0)
            }
        },
    )
}

@Composable
private fun MessagesList(
    messages: List<Message>,
    lazyListState: LazyListState = rememberLazyListState() // LazyListState has a default value
) {

    LazyColumn(
        state = lazyListState // Pass hoisted state to LazyColumn
    ) {
        items(messages, key = { message -> message.id }) { item ->
            Message(/*...*/)
        }
    }

    val scope = rememberCoroutineScope()

    JumpToBottom(onClicked = {
        scope.launch {
            lazyListState.scrollToItem(0) // UI logic being applied to lazyListState
        }
    })
}

LazyListState จะถูกยกขึ้นไปสูงเท่าที่จำเป็นสำหรับตรรกะ UI ที่ต้องใช้ เนื่องจากมีการเริ่มต้นในฟังก์ชันที่ประกอบกันได้ ระบบจึงจัดเก็บไว้ใน Composition ตามวงจรของ Composition

โปรดทราบว่า lazyListState กำหนดไว้ในเมธอด MessagesList โดยมีค่าเริ่มต้นเป็น rememberLazyListState() ซึ่งเป็นรูปแบบที่ใช้กันทั่วไปใน Compose และทำให้ Composable นำกลับมาใช้ซ้ำได้มากขึ้นและมีความยืดหยุ่นมากขึ้น จากนั้นคุณจะใช้ Composable ในส่วนต่างๆ ของแอปที่ไม่จำเป็นต้องควบคุมสถานะได้ ซึ่งมักเกิดขึ้นขณะทดสอบหรือแสดงตัวอย่าง Composable และนี่คือวิธีที่ LazyColumn กำหนดสถานะของตัวเอง

บรรพบุรุษร่วมที่ต่ำที่สุดของ LazyListState คือ ConversationScreen
รูปที่ 5 บรรพบุรุษร่วมที่ต่ำที่สุดสำหรับ LazyListState คือ ConversationScreen

คลาสตัวยึดสถานะธรรมดาเป็นเจ้าของสถานะ

เมื่อ Composable มีตรรกะ UI ที่ซับซ้อนซึ่งเกี่ยวข้องกับฟิลด์สถานะ อย่างน้อย 1 รายการขององค์ประกอบ UI องค์ประกอบนั้นควรมอบหมายความรับผิดชอบดังกล่าวให้กับตัวยึดสถานะ เช่น คลาสตัวยึดสถานะธรรมดา ซึ่งจะทำให้ตรรกะของ Composable ทดสอบได้ง่ายขึ้นและลดความซับซ้อนลง แนวทางนี้สนับสนุน หลักการแยกความกังวลออกจากกัน: Composable มีหน้าที่ แสดงองค์ประกอบ UI และตัวยึดสถานะมีตรรกะ UI และสถานะ องค์ประกอบ UI

คลาสตัวเก็บสถานะธรรมดามีฟังก์ชันที่สะดวกสำหรับผู้เรียกใช้ฟังก์ชันที่ประกอบกันได้ เพื่อให้ผู้เรียกใช้ไม่ต้องเขียนตรรกะนี้เอง

ระบบจะสร้างและจดจำคลาสธรรมดาเหล่านี้ใน Composition เนื่องจากคลาสเหล่านี้ เป็นไปตามวงจรการทำงานของ Composable จึงใช้ประเภทที่ไลบรารี Compose มีให้ได้ เช่น rememberNavController() หรือ rememberLazyListState()

ตัวอย่างของคลาสนี้คือ LazyListState ตัวยึดสถานะธรรมดา คลาส ซึ่งใช้ใน Compose เพื่อควบคุมความซับซ้อน UI ของ LazyColumn หรือ LazyRow

// LazyListState.kt

@Stable
class LazyListState constructor(
    firstVisibleItemIndex: Int = 0,
    firstVisibleItemScrollOffset: Int = 0
) : ScrollableState {
    /**
     *   The holder class for the current scroll position.
     */
    private val scrollPosition = LazyListScrollPosition(
        firstVisibleItemIndex, firstVisibleItemScrollOffset
    )

    suspend fun scrollToItem(/*...*/) { /*...*/ }

    override suspend fun scroll() { /*...*/ }

    suspend fun animateScrollToItem() { /*...*/ }
}

LazyListState ห่อหุ้มสถานะของ LazyColumn โดยจัดเก็บ scrollPosition สำหรับองค์ประกอบ UI นี้ นอกจากนี้ยังแสดงเมธอดเพื่อแก้ไขตำแหน่งการเลื่อน เช่น การเลื่อนไปยังรายการที่ระบุ

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

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

ตรรกะทางธุรกิจ

หาก Composable และคลาสตัวเก็บสถานะธรรมดามีหน้าที่รับผิดชอบตรรกะ UI และสถานะองค์ประกอบ UI ตัวเก็บสถานะระดับหน้าจอจะมีหน้าที่รับผิดชอบงานต่อไปนี้

  • ให้สิทธิ์เข้าถึงตรรกะทางธุรกิจของแอปพลิเคชัน ซึ่งโดยปกติจะอยู่ในเลเยอร์อื่นๆ ในลำดับชั้น เช่น เลเยอร์ธุรกิจและ เลเยอร์ข้อมูล
  • เตรียมข้อมูลแอปพลิเคชันสำหรับการนำเสนอในหน้าจอที่เฉพาะเจาะจง ซึ่งจะกลายเป็นสถานะ UI ของหน้าจอ

ViewModel เป็นเจ้าของสถานะ

ประโยชน์ของ AAC ViewModel ในการพัฒนา Android ทำให้ ViewModel เหมาะสม สำหรับการให้สิทธิ์เข้าถึงตรรกะทางธุรกิจและการเตรียมข้อมูลแอปพลิเคชัน สำหรับการนำเสนอบนหน้าจอ

เมื่อยกสถานะ UI ขึ้นใน ViewModel คุณจะย้ายสถานะนั้นออกจาก Composition

สถานะที่ยกระดับไปยัง ViewModel จะจัดเก็บอยู่นอก Composition
รูปที่ 6 สถานะที่ยกขึ้นไปยัง ViewModel จะจัดเก็บไว้นอก Composition

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

จากนั้น ViewModel จะเป็นแหล่งข้อมูลที่เชื่อถือได้และบรรพบุรุษร่วมที่ต่ำที่สุด สำหรับสถานะ UI

สถานะ UI ของหน้าจอ

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

ลองดู ConversationViewModel ของแอปแชทและวิธีที่ ViewModel แสดงสถานะ UI ของหน้าจอและเหตุการณ์ต่างๆ เพื่อแก้ไขสถานะนั้น

class ConversationViewModel(
    channelId: String,
    messagesRepository: MessagesRepository
) : ViewModel() {

    val messages = messagesRepository
        .getLatestMessages(channelId)
        .stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(5_000),
            initialValue = emptyList()
        )

    // Business logic
    fun sendMessage(message: Message) { /* ... */ }
}

Composable จะใช้สถานะ UI ของหน้าจอที่ยกขึ้นใน ViewModel คุณควรแทรกอินสแตนซ์ ViewModel ใน Composable ระดับหน้าจอเพื่อให้สิทธิ์เข้าถึงตรรกะทางธุรกิจ

ต่อไปนี้เป็นตัวอย่าง ViewModel ที่ใช้ใน Composable ระดับหน้าจอ ในตัวอย่างนี้ Composable ConversationScreen() จะใช้สถานะ UI ของหน้าจอที่ยกขึ้นใน ViewModel

@Composable
private fun ConversationScreen(
    conversationViewModel: ConversationViewModel = viewModel()
) {

    val messages by conversationViewModel.messages.collectAsStateWithLifecycle()

    ConversationScreen(
        messages = messages,
        onSendMessage = { message: Message -> conversationViewModel.sendMessage(message) }
    )
}

@Composable
private fun ConversationScreen(
    messages: List<Message>,
    onSendMessage: (Message) -> Unit
) {

    MessagesList(messages, onSendMessage)
    /* ... */
}

การส่งต่อพร็อพเพอร์ตี้

"การส่งต่อพร็อพเพอร์ตี้" หมายถึงการส่งข้อมูลผ่านคอมโพเนนต์ย่อยที่ซ้อนกันหลายรายการไปยังตำแหน่งที่จะอ่านข้อมูลนั้น

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

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

การส่งต่อพร็อพเพอร์ตี้เป็นวิธีที่แนะนำมากกว่าการสร้างคลาส Wrapper เพื่อห่อหุ้มสถานะและเหตุการณ์ต่างๆ ไว้ในที่เดียว เนื่องจากวิธีนี้จะลดการมองเห็นความรับผิดชอบของ Composable การไม่ใช้คลาส Wrapper ยังทำให้คุณมีแนวโน้มที่จะส่งเฉพาะพารามิเตอร์ที่ Composable ต้องการ ซึ่งเป็นแนวทางปฏิบัติแนะนำ

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

หากพบปัญหาด้านประสิทธิภาพ คุณอาจเลือกที่จะเลื่อนการอ่านสถานะออกไป ดูข้อมูลเพิ่มเติมได้ในเอกสารประกอบด้านประสิทธิภาพ

สถานะองค์ประกอบ UI

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

จากตัวอย่างแอปแชท แอปจะแสดงคำแนะนำผู้ใช้ในการแชทเป็นกลุ่มเมื่อผู้ใช้พิมพ์ @ และคำแนะนำ คำแนะนำเหล่านั้นมาจากเลเยอร์ข้อมูล และตรรกะในการคำนวณรายการคำแนะนำผู้ใช้ถือเป็นตรรกะทางธุรกิจ ฟีเจอร์นี้มีลักษณะดังนี้

ฟีเจอร์ที่แสดงคำแนะนำผู้ใช้ในแชทเป็นกลุ่มเมื่อผู้ใช้พิมพ์ `@` และคำใบ้
รูปที่ 7 ฟีเจอร์ที่แสดงคำแนะนำผู้ใช้ในการแชทเป็นกลุ่มเมื่อผู้ใช้พิมพ์ @ และคำแนะนำ

ViewModel ที่ใช้ฟีเจอร์นี้จะมีลักษณะดังนี้

class ConversationViewModel(/*...*/) : ViewModel() {

    // Hoisted state
    var inputMessage by mutableStateOf("")
        private set

    val suggestions: StateFlow<List<Suggestion>> =
        snapshotFlow { inputMessage }
            .filter { hasSocialHandleHint(it) }
            .mapLatest { getHandle(it) }
            .mapLatest { repository.getSuggestions(it) }
            .stateIn(
                scope = viewModelScope,
                started = SharingStarted.WhileSubscribed(5_000),
                initialValue = emptyList()
            )

    fun updateInput(newInput: String) {
        inputMessage = newInput
    }
}

inputMessage เป็นตัวแปรที่จัดเก็บสถานะ TextField ทุกครั้งที่ผู้ใช้พิมพ์ข้อมูลใหม่ แอปจะเรียกใช้ตรรกะทางธุรกิจเพื่อสร้าง suggestions

suggestions คือสถานะ UI ของหน้าจอและใช้จาก Compose UI โดยการรวบรวม จาก StateFlow

ข้อควรระวัง

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

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

อย่างไรก็ตาม การเรียกใช้เมธอด close() ของ DrawerState โดยใช้ viewModelScope จาก Compose UI จะทำให้เกิดข้อยกเว้นรันไทม์ประเภท IllegalStateException พร้อมข้อความว่า “a MonotonicFrameClock is not available in this CoroutineContext”.

หากต้องการแก้ไขปัญหานี้ ให้ใช้ CoroutineScope ที่จำกัดขอบเขตไว้ที่ Composition ซึ่งจะให้ MonotonicFrameClock ใน CoroutineContext ที่จำเป็นต่อการทำงานของฟังก์ชันระงับ

หากต้องการแก้ไขข้อขัดข้องนี้ ให้เปลี่ยน CoroutineContext ของ Coroutine ใน ViewModel เป็น CoroutineContext ที่จำกัดขอบเขตไว้ที่ Composition ซึ่งอาจมีลักษณะดังนี้

class ConversationViewModel(/*...*/) : ViewModel() {

    val drawerState = DrawerState(initialValue = DrawerValue.Closed)

    private val _drawerContent = MutableStateFlow(DrawerContent.Empty)
    val drawerContent: StateFlow<DrawerContent> = _drawerContent.asStateFlow()

    fun closeDrawer(uiScope: CoroutineScope) {
        viewModelScope.launch {
            withContext(uiScope.coroutineContext) { // Use instead of the default context
                drawerState.close()
            }
            // Fetch drawer content and update state
            _drawerContent.update { content }
        }
    }
}

// in Compose
@Composable
private fun ConversationScreen(
    conversationViewModel: ConversationViewModel = viewModel()
) {
    val scope = rememberCoroutineScope()

    ConversationScreen(onCloseDrawer = { conversationViewModel.closeDrawer(uiScope = scope) })
}

ดูข้อมูลเพิ่มเติม

ดูข้อมูลเพิ่มเติมเกี่ยวกับสถานะและ Jetpack Compose ได้จากแหล่งข้อมูลเพิ่มเติมต่อไปนี้

ตัวอย่าง

Codelab

วิดีโอ