บันทึกสถานะ UI ใน Compose

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

แอป Android อาจสูญเสียสถานะ UI เนื่องจากการสร้างกิจกรรมหรือกระบวนการใหม่ การสูญเสียสถานะนี้อาจเกิดขึ้นเนื่องจากเหตุการณ์ต่อไปนี้

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

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

ตรรกะ UI

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

ในข้อมูลโค้ดต่อไปนี้ ระบบจะใช้ rememberSaveable เพื่อจัดเก็บสถานะองค์ประกอบ UI บูลีนรายการเดียว

@Composable
fun ChatBubble(
    message: Message
) {
    var showDetails by rememberSaveable { mutableStateOf(false) }

    ClickableText(
        text = AnnotatedString(message.content),
        onClick = { showDetails = !showDetails }
    )

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

รูปที่ 1 บับเบิลข้อความแชทจะขยายและยุบเมื่อแตะ

showDetails เป็นตัวแปรบูลีนที่จัดเก็บข้อมูลว่าแชทบับเบิลยุบหรือขยายอยู่

rememberSaveable จะจัดเก็บสถานะองค์ประกอบ UI ไว้ใน Bundle ผ่านกลไกสถานะอินสแตนซ์ที่บันทึกไว้

โดยสามารถจัดเก็บประเภทพื้นฐานลงใน Bundle โดยอัตโนมัติ หากสถานะของคุณอยู่ในประเภทที่ไม่ใช่พื้นฐาน เช่น คลาสข้อมูล คุณสามารถใช้กลไกการจัดเก็บที่แตกต่างกัน เช่น การใช้คำอธิบายประกอบ Parcelize, การใช้ Compose API เช่น listSaver และ mapSaver หรือการใช้คลาส Saver ที่กำหนดเองซึ่งขยายคลาสรันไทม์ Compose Saver ดูข้อมูลเพิ่มเติมเกี่ยวกับวิธีการเหล่านี้ได้ในเอกสารประกอบวิธีจัดเก็บสถานะ

ในข้อมูลโค้ดต่อไปนี้ API ของ Compose rememberLazyListState จะจัดเก็บ LazyListState ซึ่งประกอบด้วยสถานะการเลื่อนของ LazyColumn หรือ LazyRow โดยใช้ rememberSaveable โดยจะใช้ LazyListState.Saver ซึ่งเป็นโปรแกรมบันทึกที่กำหนดเองที่สามารถจัดเก็บและกู้คืนสถานะการเลื่อนได้ หลังจากสร้างกิจกรรมหรือกระบวนการใหม่ (เช่น หลังจากเปลี่ยนการกำหนดค่า เช่น การเปลี่ยนการวางแนวอุปกรณ์) ระบบจะคงสถานะการเลื่อนไว้

@Composable
fun rememberLazyListState(
    initialFirstVisibleItemIndex: Int = 0,
    initialFirstVisibleItemScrollOffset: Int = 0
): LazyListState {
    return rememberSaveable(saver = LazyListState.Saver) {
        LazyListState(
            initialFirstVisibleItemIndex, initialFirstVisibleItemScrollOffset
        )
    }
}

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

rememberSaveable ใช้ Bundle เพื่อจัดเก็บสถานะ UI ซึ่งแชร์โดย API อื่นๆ ที่เขียนลงในนั้นด้วย เช่น การเรียก onSaveInstanceState() ในกิจกรรมของคุณ อย่างไรก็ตาม ขนาดของ Bundle นี้มีจำกัด และการจัดเก็บออบเจ็กต์ขนาดใหญ่อาจทำให้เกิดข้อยกเว้น TransactionTooLarge ในรันไทม์ ซึ่งอาจเป็นปัญหาอย่างยิ่งในแอป Activity แอปเดียวที่มีการใช้ Bundle เดียวกันทั่วทั้งแอป

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

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

ตัวเลือกการออกแบบเหล่านี้ขึ้นอยู่กับ Use Case ที่เฉพาะเจาะจงสำหรับแอปของคุณและลักษณะการทำงานที่ผู้ใช้คาดหวังจากแอป

ยืนยันการคืนค่าสถานะ

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

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

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

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

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

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

SavedStateHandle ยังใช้กลไก Bundle เพื่อจัดเก็บสถานะ UI ด้วย ดังนั้นคุณจึงควรใช้เพื่อจัดเก็บสถานะองค์ประกอบ UI แบบง่ายเท่านั้น

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

API ของ SavedStateHandle

SavedStateHandle มี API ต่างๆ เพื่อจัดเก็บสถานะองค์ประกอบ UI โดยที่โดดเด่นที่สุดคือ

เขียน State saveable()
StateFlow getStateFlow()

เขียน State

ใช้ API saveable ของ SavedStateHandle เพื่ออ่านและเขียนสถานะองค์ประกอบ UI เป็น MutableState เพื่อให้คงอยู่ตลอดกิจกรรมและการสร้างกระบวนการใหม่โดยใช้การตั้งค่าโค้ดเพียงเล็กน้อย

saveable API รองรับประเภทดั้งเดิมตั้งแต่แกะกล่องและรับพารามิเตอร์ stateSaver เพื่อใช้ Saver ที่กำหนดเอง เช่นเดียวกับ rememberSaveable()

ในข้อมูลโค้ดต่อไปนี้ message จะจัดเก็บข้อมูลที่ผู้ใช้ป้อนลงใน TextField

class ConversationViewModel(
    savedStateHandle: SavedStateHandle
) : ViewModel() {

    var message by savedStateHandle.saveable(stateSaver = TextFieldValue.Saver) {
        mutableStateOf(TextFieldValue(""))
    }
        private set

    fun update(newMessage: TextFieldValue) {
        message = newMessage
    }

    /*...*/
}

val viewModel = ConversationViewModel(SavedStateHandle())

@Composable
fun UserInput(/*...*/) {
    TextField(
        value = viewModel.message,
        onValueChange = { viewModel.update(it) }
    )
}

ดูข้อมูลเพิ่มเติมเกี่ยวกับการใช้ saveable API ได้ในเอกสารประกอบของ SavedStateHandle

StateFlow

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

ในข้อมูลโค้ดต่อไปนี้ savedFilterType คือตัวแปร StateFlow ที่จัดเก็บประเภทตัวกรองที่ใช้กับรายการช่องแชทในแอปแชท

private const val CHANNEL_FILTER_SAVED_STATE_KEY = "ChannelFilterKey"

class ChannelViewModel(
    channelsRepository: ChannelsRepository,
    private val savedStateHandle: SavedStateHandle
) : ViewModel() {

    private val savedFilterType: StateFlow<ChannelsFilterType> = savedStateHandle.getStateFlow(
        key = CHANNEL_FILTER_SAVED_STATE_KEY, initialValue = ChannelsFilterType.ALL_CHANNELS
    )

    private val filteredChannels: Flow<List<Channel>> =
        combine(channelsRepository.getAll(), savedFilterType) { channels, type ->
            filter(channels, type)
        }.onStart { emit(emptyList()) }

    fun setFiltering(requestType: ChannelsFilterType) {
        savedStateHandle[CHANNEL_FILTER_SAVED_STATE_KEY] = requestType
    }

    /*...*/
}

enum class ChannelsFilterType {
    ALL_CHANNELS, RECENT_CHANNELS, ARCHIVED_CHANNELS
}

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

ดูข้อมูลเพิ่มเติมเกี่ยวกับ getStateFlow() API ได้ในเอกสารประกอบของ SavedStateHandle

สรุป

ตารางต่อไปนี้สรุป API ที่กล่าวถึงในส่วนนี้ และกรณีที่ควรใช้แต่ละ API เพื่อบันทึกสถานะ UI

กิจกรรม ตรรกะ UI ตรรกะทางธุรกิจในViewModel
การเปลี่ยนแปลงการกำหนดค่า rememberSaveable อัตโนมัติ
การสิ้นสุดการประมวลผลที่ระบบเริ่มดำเนินการ rememberSaveable SavedStateHandle

API ที่จะใช้ขึ้นอยู่กับตำแหน่งที่เก็บสถานะและตรรกะที่ต้องใช้ สำหรับสถานะที่ใช้ในตรรกะ UI ให้ใช้ rememberSaveable สำหรับสถานะที่ใช้ในตรรกะทางธุรกิจ หากคุณเก็บไว้ใน ViewModel ให้บันทึกโดยใช้ SavedStateHandle

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

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