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

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

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

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

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

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

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

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

สถานะ UI

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

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

ตรรกะ

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

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

ตรรกะ UI

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

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

คอมโพสได้เป็นเจ้าของสถานะ

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

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

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

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

การยกสถานะขึ้นภายในคอมโพสได้

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

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

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

ลำดับชั้นของคอมโพสได้มีดังนี้

แผนผังที่ประกอบได้ของ Chat
รูปที่ 2 แผนผังคอมโพสได้ของแชท

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

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

ดังนั้นคอมโพสได้สุดท้ายจึงมีลักษณะดังนี้

แชทแบบ Composable Tree โดยมี LazyListState ที่ยกขึ้นไปที่ ConversationScreen
รูปที่ 4 แผนผังคอมโพสได้ของแชทที่มี 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 ที่ต้องใช้ เนื่องจากมีการเริ่มต้นในฟังก์ชันที่ประกอบกันได้ ระบบจึงจัดเก็บไว้ในการจัดองค์ประกอบตามวงจรการทำงาน

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

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

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

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

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

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

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

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

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

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

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

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

เมื่อยกสถานะ UI ขึ้นใน ViewModel คุณจะย้ายสถานะนั้นออกจากการจัดองค์ประกอบ

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

ระบบจะไม่จัดเก็บ ViewModel เป็นส่วนหนึ่งของการจัดองค์ประกอบ โดยเฟรมเวิร์กจะเป็นผู้จัดหา 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) { /* ... */ }
}

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

ต่อไปนี้เป็นตัวอย่างของ ViewModel ที่ใช้ในคอมโพสได้ระดับหน้าจอ ในตัวอย่างนี้ คอมโพสได้ 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 ย่อย ซึ่งอาจทำให้เกิดการโอเวอร์โหลดลายเซ็นของฟังก์ชันคอมโพสได้ด้วย

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

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

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

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

สถานะองค์ประกอบ 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 ที่ไม่ได้จำกัดขอบเขตไว้ที่การจัดองค์ประกอบ

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

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

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

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

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

วิดีโอ