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

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

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

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

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

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

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

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

สถานะ UI

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

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

เชิงตรรกะ

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

ตรรกะสร้างสถานะ UI
รูปที่ 2 ตรรกะในฐานะผู้ผลิตสถานะ 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 แสดงให้เห็นว่าปลายทางการนำทางที่แสดงฟังก์ชันหลักของแอป 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 จะยังคงอยู่แม้จะมีการเปลี่ยนแปลงการกำหนดค่า
  • การผสานรวมกับ Navigation
    • แคชการนำทางจะแคช 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 ของ Composable ที่สร้างด้วยฟังก์ชัน remembered มักจะมอบหมายให้ rememberSaveable เพื่อรักษาสถานะใน Activity การสร้างใหม่ ตัวอย่างฟังก์ชันดังกล่าว ได้แก่ rememberScaffoldState() และ rememberLazyListState()
  • มีการอ้างอิงถึงแหล่งข้อมูลที่กำหนดขอบเขต UI: แหล่งข้อมูล เช่น API และทรัพยากรวงจร สามารถอ้างอิงและอ่านได้อย่างปลอดภัย เนื่องจากตัวเก็บสถานะตรรกะของ UI มีวงจรเดียวกันกับ UI
  • ใช้ซ้ำได้ใน UI หลายรายการ: อินสแตนซ์ต่างๆ ของตัวเก็บสถานะตรรกะ UI เดียวกันอาจนำไปใช้ซ้ำในส่วนต่างๆ ของแอปได้ เช่น ตัวเก็บสถานะสำหรับการจัดการข้อมูลจากผู้ใช้สำหรับกลุ่มชิปอาจใช้ในหน้าค้นหาสำหรับชิปตัวกรอง และยังใช้กับช่อง "ถึง" สำหรับผู้รับอีเมลได้ด้วย

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

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

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