ผู้ถือสถานะและสถานะ UI

คู่มือเลเยอร์ UI อธิบายโฟลว์ข้อมูลแบบทิศทางเดียว (UDF) เป็นวิธีการสร้างและจัดการสถานะ UI สำหรับเลเยอร์ UI

ข้อมูลจะไหลจากชั้นข้อมูลไปยัง UI ในทิศทางเดียว
รูปที่ 1 การรับส่งข้อมูลทางเดียว

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

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

  • ทําความเข้าใจประเภทสถานะ UI ที่มีอยู่ในเลเยอร์ UI
  • ทําความเข้าใจประเภทของตรรกะที่ทํางานในสถานะ UI เหล่านั้นในเลเยอร์ UI
  • ทราบวิธีเลือกการติดตั้งใช้งานที่เหมาะสมของตัวยึดสถานะ เช่น ViewModel หรือคลาส

องค์ประกอบของไปป์ไลน์การสร้างสถานะ UI

สถานะ UI และตรรกะที่สร้างสถานะดังกล่าวจะกำหนดเลเยอร์ UI

สถานะ UI

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

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

เชิงตรรกะ

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

ตรรกะสร้างสถานะ UI
รูปที่ 2 Logic เป็นผู้สร้างสถานะ UI

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

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

วงจรของ Android รวมถึงประเภทสถานะและตรรกะ UI

เลเยอร์ UI มี 2 ส่วน ได้แก่ ส่วนที่ขึ้นอยู่กับวงจร UI และส่วนที่ไม่ขึ้นอยู่กับวงจร UI การแยกนี้จะกำหนดแหล่งข้อมูลที่แต่ละส่วนใช้ได้ จึงต้องใช้สถานะและตรรกะของ UI ประเภทต่างๆ

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

สรุปข้อมูลข้างต้นได้ดังตารางด้านล่าง

วงจร UI ที่เป็นอิสระ ขึ้นอยู่กับวงจร UI
ตรรกะทางธุรกิจ ตรรกะ UI
สถานะ UI ของหน้าจอ

ไปป์ไลน์การสร้างสถานะ UI

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

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

  • สถานะ UI ที่สร้างและจัดการโดย UI เอง เช่น ตัวนับพื้นฐานแบบง่ายที่ใช้ซ้ำได้

    @Composable
    fun Counter() {
        // The UI state is managed by the UI itself
        var count by remember { mutableStateOf(0) }
        Row {
            Button(onClick = { ++count }) {
                Text(text = "Increment")
            }
            Button(onClick = { --count }) {
                Text(text = "Decrement")
            }
        }
    }
    
  • ตรรกะ UI → UI เช่น การแสดงหรือซ่อนปุ่มที่อนุญาตให้ผู้ใช้ ข้ามไปยังด้านบนของรายการ

    @Composable
    fun ContactsList(contacts: List<Contact>) {
        val listState = rememberLazyListState()
        val isAtTopOfList by remember {
            derivedStateOf {
                listState.firstVisibleItemIndex < 3
            }
        }
    
        // Create the LazyColumn with the lazyListState
        ...
    
        // Show or hide the button (UI logic) based on the list scroll position
        AnimatedVisibility(visible = !isAtTopOfList) {
            ScrollToTopButton()
        }
    }
    
  • ตรรกะทางธุรกิจ → UI องค์ประกอบ UI ที่แสดงรูปภาพของผู้ใช้ปัจจุบันบนหน้าจอ

    @Composable
    fun UserProfileScreen(viewModel: UserProfileViewModel = hiltViewModel()) {
        // Read screen UI state from the business logic state holder
        val uiState by viewModel.uiState.collectAsStateWithLifecycle()
    
        // Call on the UserAvatar Composable to display the photo
        UserAvatar(picture = uiState.profilePicture)
    }
    
  • ตรรกะทางธุรกิจ → ตรรกะ UI → UI องค์ประกอบ UI ที่เลื่อนเพื่อแสดง ข้อมูลที่ถูกต้องบนหน้าจอสำหรับสถานะ UI ที่กำหนด

    @Composable
    fun ContactsList(viewModel: ContactsViewModel = hiltViewModel()) {
        // Read screen UI state from the business logic state holder
        val uiState by viewModel.uiState.collectAsStateWithLifecycle()
        val contacts = uiState.contacts
        val deepLinkedContact = uiState.deepLinkedContact
    
        val listState = rememberLazyListState()
    
        // Create the LazyColumn with the lazyListState
        ...
    
        // Perform UI logic that depends on information from business logic
        if (deepLinkedContact != null && contacts.isNotEmpty()) {
            LaunchedEffect(listState, deepLinkedContact, contacts) {
                val deepLinkedContactIndex = contacts.indexOf(deepLinkedContact)
                if (deepLinkedContactIndex >= 0) {
                  // Scroll to deep linked item
                  listState.animateScrollToItem(deepLinkedContactIndex)
                }
            }
        }
    }
    

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

ข้อมูลจะไหลจากเลเยอร์ที่สร้างข้อมูลไปยัง UI
รูปที่ 3 การใช้ตรรกะในเลเยอร์ UI

ผู้เก็บสถานะและความรับผิดชอบ

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

ซึ่งจะให้ประโยชน์ดังต่อไปนี้

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

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

ประเภทผู้ถือครองสถานะ

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

  • ผู้ถือสถานะตรรกะทางธุรกิจ
  • ตัวเก็บสถานะตรรกะ UI

ส่วนต่อไปนี้จะเจาะลึกประเภทของที่เก็บสถานะ โดยเริ่มจากที่เก็บสถานะตรรกะทางธุรกิจ

ตรรกะทางธุรกิจและผู้ถือสถานะ

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

พร็อพเพอร์ตี้ รายละเอียด
สร้างสถานะ UI ผู้ถือสถานะตรรกะทางธุรกิจมีหน้าที่สร้างสถานะ UI สำหรับ UI ของตน สถานะ UI นี้มักเป็นผลมาจากการประมวลผลเหตุการณ์ของผู้ใช้และการอ่านข้อมูลจากโดเมนและเลเยอร์ข้อมูล
คงไว้ผ่านการสร้างกิจกรรมใหม่ ผู้ถือสถานะตรรกะทางธุรกิจจะคงไปป์ไลน์การประมวลผลสถานะและสถานะของตนไว้ตลอดการActivityสร้างใหม่ ซึ่งจะช่วยมอบประสบการณ์ของผู้ใช้ที่ราบรื่น ในกรณีที่เก็บตัวยึดสถานะไว้ไม่ได้และมีการสร้างใหม่ (โดยปกติหลังจากกระบวนการสิ้นสุด) ตัวยึดสถานะต้องสร้างสถานะล่าสุดขึ้นใหม่ได้อย่างง่ายดายเพื่อให้มั่นใจว่าผู้ใช้จะได้รับประสบการณ์การใช้งานที่สอดคล้องกัน
มีสถานะที่ใช้งานได้นาน โดยมักใช้ที่เก็บสถานะตรรกะทางธุรกิจเพื่อจัดการสถานะสำหรับปลายทางการนำทาง ด้วยเหตุนี้ Fragment จึงมักจะรักษาสถานะไว้เมื่อมีการเปลี่ยนแปลงการนำทางจนกว่าจะนำออกจากกราฟการนำทาง
เป็นเอกลักษณ์เฉพาะของ UI และนำกลับมาใช้ใหม่ไม่ได้ โดยปกติแล้ว ผู้ถือครองสถานะตรรกะทางธุรกิจจะสร้างสถานะสำหรับฟังก์ชันแอปบางอย่าง เช่น TaskEditViewModel หรือ TaskListViewModel ดังนั้นจึงใช้ได้กับฟังก์ชันแอปนั้นๆ เท่านั้น โดยผู้ถือสถานะเดียวกันจะรองรับฟังก์ชันแอปเหล่านี้ในอุปกรณ์รูปแบบต่างๆ ได้ ตัวอย่างเช่น แอปเวอร์ชันอุปกรณ์เคลื่อนที่ ทีวี และแท็บเล็ตอาจใช้ที่เก็บสถานะตรรกะทางธุรกิจเดียวกันซ้ำ

ตัวอย่างเช่น พิจารณาปลายทางการนำทางของผู้เขียนในแอป "Now in Android"

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

AuthorViewModel ทำหน้าที่เป็นผู้ถือสถานะตรรกะทางธุรกิจและสร้างสถานะ UI ในกรณีนี้ ดังนี้

@HiltViewModel
class AuthorViewModel @Inject constructor(
    savedStateHandle: SavedStateHandle,
    private val authorsRepository: AuthorsRepository,
    newsRepository: NewsRepository
) : ViewModel() {

    val uiState: StateFlow<AuthorScreenUiState> = 

    // Business logic
    fun followAuthor(followed: Boolean) {
      
    }
}

โปรดทราบว่า AuthorViewModel มีแอตทริบิวต์ที่ระบุไว้ก่อนหน้านี้ดังนี้

พร็อพเพอร์ตี้ รายละเอียด
สร้าง AuthorScreenUiState AuthorViewModel จะอ่านข้อมูลจาก AuthorsRepository และ NewsRepository แล้วใช้ข้อมูลดังกล่าวเพื่อสร้าง AuthorScreenUiState นอกจากนี้ยังใช้ตรรกะทางธุรกิจเมื่อผู้ใช้ต้องการติดตามหรือเลิกติดตาม Author โดยการมอบสิทธิ์ให้ AuthorsRepository
มีสิทธิ์เข้าถึง Data Layer ระบบจะส่งอินสแตนซ์ของ AuthorsRepository และ NewsRepository ไปยังอินสแตนซ์ดังกล่าวในตัวสร้าง ซึ่งจะช่วยให้สามารถใช้ตรรกะทางธุรกิจในการติดตาม Author ได้
ทนทานต่อการใช้งานActivity เนื่องจากมีการติดตั้งใช้งานด้วย ViewModel ระบบจึงจะเก็บรักษาไว้เมื่อมีการสร้าง Activity ใหม่โดยเร็ว ในกรณีที่กระบวนการสิ้นสุดลง ระบบจะอ่านออบเจ็กต์ SavedStateHandle เพื่อให้ข้อมูลขั้นต่ำที่จำเป็นในการกู้คืนสถานะ UI จากเลเยอร์ข้อมูล
มีสถานะที่มีอายุยาวนาน ViewModel มีขอบเขตอยู่ที่กราฟการนำทาง ดังนั้นหากไม่ได้นำปลายทางของผู้เขียนออกจากกราฟการนำทาง สถานะ UI ใน uiState StateFlow จะยังคงอยู่ในหน่วยความจำ การใช้ StateFlow ยังช่วยให้การใช้ตรรกะทางธุรกิจที่สร้างสถานะเป็นแบบเลซี่ เนื่องจากจะมีการสร้างสถานะก็ต่อเมื่อมีตัวรวบรวมสถานะ UI เท่านั้น
ไม่ซ้ำกันใน UI AuthorViewModel ใช้ได้กับปลายทางการนำทางของผู้เขียนเท่านั้น และนำไปใช้ซ้ำที่อื่นไม่ได้ หากมีตรรกะทางธุรกิจที่นำกลับมาใช้ซ้ำในปลายทางการนำทาง ตรรกะทางธุรกิจนั้นจะต้องแคปซูลในคอมโพเนนต์ที่มีขอบเขตเป็นเลเยอร์ข้อมูลหรือโดเมน

ViewModel ในฐานะที่เก็บสถานะตรรกะทางธุรกิจ

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

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

ตรรกะ UI และตัวเก็บสถานะ

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

  • สร้างสถานะ UI และจัดการสถานะองค์ประกอบ UI
  • ไม่คงอยู่หลังActivityการสร้างใหม่: ผู้ถือสถานะที่โฮสต์ใน ตรรกะ UI มักจะขึ้นอยู่กับแหล่งข้อมูลจาก UI เอง และ การพยายามเก็บข้อมูลนี้ไว้เมื่อมีการเปลี่ยนแปลงการกำหนดค่ามักจะ ทำให้เกิดหน่วยความจำรั่ว หากผู้ถือสถานะต้องการให้ข้อมูลคงอยู่ เมื่อมีการเปลี่ยนแปลงการกำหนดค่า ก็จะต้องมอบสิทธิ์ให้คอมโพเนนต์อื่น ที่เหมาะกับการอยู่รอดActivityหลังการสร้างใหม่มากกว่า ใน Jetpack Compose เช่น สถานะขององค์ประกอบ UI ที่เขียนได้ซึ่งสร้างด้วยฟังก์ชัน remembered มักจะมอบหมายให้ rememberSaveable เพื่อรักษาสถานะเมื่อมีการสร้าง Activity ใหม่ ตัวอย่างฟังก์ชันดังกล่าว ได้แก่ rememberScaffoldState() และ rememberLazyListState()
  • มีการอ้างอิงถึงแหล่งข้อมูลที่กำหนดขอบเขต UI: แหล่งข้อมูล เช่น API และทรัพยากรของวงจร สามารถอ้างอิงและอ่านได้อย่างปลอดภัย เนื่องจากผู้ถือสถานะตรรกะของ UI มีวงจรเดียวกันกับ UI
  • นำไปใช้ซ้ำใน UI หลายรายการได้: อินสแตนซ์ต่างๆ ของตัวยึดสถานะตรรกะ UI เดียวกันอาจนำไปใช้ซ้ำในส่วนต่างๆ ของแอปได้ เช่น ตัวยึดสถานะสำหรับการจัดการเหตุการณ์อินพุตของผู้ใช้สำหรับกลุ่มชิปอาจใช้ ในหน้าค้นหาสำหรับชิปตัวกรอง และยังใช้กับช่อง "ถึง" สำหรับผู้รับ อีเมลได้ด้วย

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

ตัวอย่างต่อไปนี้ในตัวอย่าง Now in Android แสดงให้เห็นถึงสิ่งที่กล่าวมาข้างต้น

ตอนนี้ Now in Android ใช้ตัวเก็บสถานะคลาสธรรมดาเพื่อจัดการตรรกะ UI
รูปที่ 5 แอปตัวอย่าง Now in Android

ตัวอย่าง Now in Android จะแสดง App Bar ด้านล่างหรือแถบข้างสำหรับไปยังส่วนต่างๆ เพื่อใช้ในการนำทาง ทั้งนี้ขึ้นอยู่กับขนาดหน้าจอของอุปกรณ์ หน้าจอขนาดเล็กจะใช้ แถบแอปด้านล่าง ส่วนหน้าจอขนาดใหญ่จะใช้แถบนำทาง

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

@Stable
class NiaAppState(
    val navController: NavHostController,
    val windowSizeClass: WindowSizeClass
) {

    // UI logic
    val shouldShowBottomBar: Boolean
        get() = windowSizeClass.widthSizeClass == WindowWidthSizeClass.Compact ||
            windowSizeClass.heightSizeClass == WindowHeightSizeClass.Compact

    // UI logic
    val shouldShowNavRail: Boolean
        get() = !shouldShowBottomBar

   // UI State
    val currentDestination: NavDestination?
        @Composable get() = navController
            .currentBackStackEntryAsState().value?.destination

    // UI logic
    fun navigate(destination: NiaNavigationDestination, route: String? = null) { /* ... */ }

     /* ... */
}

ในตัวอย่างก่อนหน้า รายละเอียดต่อไปนี้เกี่ยวกับ NiaAppState เป็นสิ่งที่ควรทราบ

  • ไม่คงอยู่เมื่อมีการActivityสร้างใหม่: NiaAppState จะremembered ใน Composition โดยการสร้างด้วยฟังก์ชันที่ใช้ร่วมกันได้ rememberNiaAppState ตามแบบแผนการตั้งชื่อของ Compose หลังจากสร้าง Activity ใหม่แล้ว อินสแตนซ์ก่อนหน้าจะหายไปและระบบจะสร้างอินสแตนซ์ใหม่ พร้อมส่งผ่านการอ้างอิงทั้งหมด ซึ่งเหมาะกับการกำหนดค่าใหม่ของ Activity ที่สร้างขึ้นใหม่ การอ้างอิงเหล่านี้อาจเป็นรายการใหม่หรือ กู้คืนจากการกำหนดค่าก่อนหน้า เช่น rememberNavController() ใช้ในตัวสร้าง NiaAppState และจะ ส่งต่อให้ rememberSaveable เพื่อรักษาสถานะใน Activity การสร้างใหม่
  • มีการอ้างอิงถึงแหล่งข้อมูลที่กำหนดขอบเขต UI: การอ้างอิงถึง navigationController, Resources และประเภทอื่นๆ ที่กำหนดขอบเขตวงจรที่คล้ายกัน สามารถเก็บไว้ใน NiaAppState ได้อย่างปลอดภัยเนื่องจากมีขอบเขตวงจรเดียวกัน

เลือกระหว่าง ViewModel กับคลาสธรรมดาสำหรับที่เก็บสถานะ

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

โดยสรุปแล้ว แผนภาพต่อไปนี้แสดงตำแหน่งของผู้ถือสถานะใน UI ไปป์ไลน์การผลิตสถานะ

ข้อมูลจะไหลจากเลเยอร์ที่สร้างข้อมูลไปยังเลเยอร์ UI
รูปที่ 6 ตัวเก็บสถานะในไปป์ไลน์การสร้างสถานะ UI ลูกศรหมายถึงการไหลของข้อมูล

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

ตัวเก็บสถานะสามารถรวมกันได้

ผู้ถือสถานะสามารถขึ้นอยู่กับผู้ถือสถานะรายอื่นได้ตราบใดที่ทรัพยากร Dependency มีอายุการใช้งานเท่ากันหรือสั้นกว่า ตัวอย่างเช่น

  • ตัวเก็บสถานะตรรกะ UI สามารถขึ้นอยู่กับตัวเก็บสถานะตรรกะ UI อื่นได้
  • ตัวเก็บสถานะระดับหน้าจอจะขึ้นอยู่กับตัวเก็บสถานะตรรกะ UI ได้

ข้อมูลโค้ดต่อไปนี้แสดงให้เห็นว่า DrawerState ของ Compose ขึ้นอยู่กับ SwipeableState ซึ่งเป็นที่เก็บสถานะภายในอีกรายการหนึ่ง และวิธีที่ที่เก็บสถานะตรรกะ UI ของแอป อาจขึ้นอยู่กับ DrawerState

@Stable
class DrawerState(/* ... */) {
  internal val swipeableState = SwipeableState(/* ... */)
  // ...
}

@Stable
class MyAppState(
  private val drawerState: DrawerState,
  private val navController: NavHostController
) { /* ... */ }

@Composable
fun rememberMyAppState(
  drawerState: DrawerState = rememberDrawerState(DrawerValue.Closed),
  navController: NavHostController = rememberNavController()
): MyAppState = remember(drawerState, navController) {
  MyAppState(drawerState, navController)
}

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

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

class MyScreenViewModel(/* ... */) {
  val uiState: StateFlow<MyScreenUiState> = /* ... */
  fun doSomething() { /* ... */ }
  fun doAnotherThing() { /* ... */ }
  // ...
}

@Stable
class MyScreenState(
  // DO NOT pass a ViewModel instance to a plain state holder class
  // private val viewModel: MyScreenViewModel,

  // Instead, pass only what it needs as a dependency
  private val someState: StateFlow<SomeState>,
  private val doSomething: () -> Unit,

  // Other UI-scoped types
  private val scaffoldState: ScaffoldState
) {
  /* ... */
}

@Composable
fun rememberMyScreenState(
  someState: StateFlow<SomeState>,
  doSomething: () -> Unit,
  scaffoldState: ScaffoldState = rememberScaffoldState()
): MyScreenState = remember(someState, doSomething, scaffoldState) {
  MyScreenState(someState, doSomething, scaffoldState)
}

@Composable
fun MyScreen(
  modifier: Modifier = Modifier,
  viewModel: MyScreenViewModel = viewModel(),
  state: MyScreenState = rememberMyScreenState(
    someState = viewModel.uiState.map { it.toSomeState() },
    doSomething = viewModel::doSomething
  ),
  // ...
) {
  /* ... */
}

แผนภาพต่อไปนี้แสดงการอ้างอิงระหว่าง UI และที่เก็บสถานะต่างๆ ของข้อมูลโค้ดก่อนหน้า

UI ขึ้นอยู่กับทั้งตัวเก็บสถานะตรรกะ UI และตัวเก็บสถานะระดับหน้าจอ
รูปที่ 7 UI ขึ้นอยู่กับตัวเก็บสถานะที่แตกต่างกัน ลูกศรหมายถึงทรัพยากร Dependency

ตัวอย่าง

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