ใช้สถานะ Compose นอก UI

State<T> และ MutableState<T> ของ Jetpack Compose เป็นตัวยึดค่าอเนกประสงค์ที่ระบบสแนปชอตของ Compose รองรับ แม้ว่าโดยทั่วไปแล้วจะใช้เพื่อขับเคลื่อนการจัดองค์ประกอบ UI ใหม่ แต่ระบบสแนปชอตเองไม่ได้เชื่อมโยงกับการแสดงผล UI

ไลบรารี AppState ซึ่งประกอบด้วย Transform และ StateStore ช่วยให้คุณเก็บ ดึงข้อมูล สังเกต และคง State ของ Compose ไว้ใน ViewModel, ผู้จัดการ, ที่เก็บ และบริการที่ทำงานอยู่เบื้องหลังได้

เหตุใดจึงควรใช้สถานะ Compose นอกเหนือจาก UI

เมื่อคุณอ่าน Compose State ภายในบล็อกที่สังเกตสถานะ เช่น UI Composable, transform หรือ derivedStateOf รันไทม์ของ Compose จะให้ประโยชน์หลัก 2 ประการในทุกเลเยอร์ของสถาปัตยกรรม ดังนี้

  • การติดตามทรัพยากร Dependency แบบอัตโนมัติและแบบไดนามิก: การอ่าน .value ของสถานะ (หรือการใช้การมอบสิทธิ์พร็อพเพอร์ตี้ by) จะลงทะเบียนสถานะนั้นเป็นทรัพยากร Dependency ของบล็อกโดยอัตโนมัติ เนื่องจากการติดตามเป็นแบบไดนามิก ระบบจึงกำหนดทรัพยากร Dependency ขณะรันไทม์ตามเส้นทางโค้ดที่ดำเนินการ หากไม่มีการใช้ Branch แบบมีเงื่อนไข (เช่น หลังจากการตรวจสอบ if หรือ return ก่อนกำหนด) ระบบจะไม่อ่านสถานะภายใน Branch นั้น และการเปลี่ยนแปลงสถานะจะไม่ทริกเกอร์การคำนวณใหม่โดยไม่จำเป็น
  • ความสอดคล้องและความละเอียด: คุณสามารถประกาศพร็อพเพอร์ตี้ MutableState ที่ละเอียดและแยกกันได้ และรวมการเขียนพร็อพเพอร์ตี้หลายรายการไว้ใน Snapshot.withMutableSnapshot การอัปเดตระดับกลางจะแยกออกจากสแนปชอตที่เปลี่ยนแปลงได้และนำไปใช้แบบอะตอมเมื่อบล็อกเสร็จสมบูรณ์ ดังนั้นผู้สังเกตการณ์ที่อ่านภายในสแนปชอตจะไม่เห็นสถานะระดับกลางที่ไม่สอดคล้องกัน และจะคำนวณใหม่เฉพาะเมื่อพร็อพเพอร์ตี้ที่อ่านมีการเปลี่ยนแปลงเท่านั้น คุณไม่จำเป็นต้องเลือกระหว่าง StateFlow<UiState> แบบโมโนลิธิก รายการเดียว (ซึ่งจะทำให้ตัวรวบรวมทุกตัวใช้งานไม่ได้เมื่อมีการเปลี่ยนแปลงฟิลด์ ใดๆ) กับพร็อพเพอร์ตี้ StateFlow ที่แยกต่างหาก (ซึ่งไม่สามารถอัปเดต พร้อมกันในธุรกรรมแบบอะตอมรายการเดียว) ซึ่งต่างจาก StateFlow

เลือก State API ที่เหมาะสมในสถาปัตยกรรมของคุณ

เลือกวิธีเก็บและคงสถานะ State ของ Compose โดยอิงตามระยะเวลาที่สถานะหนึ่งๆ ควรคงอยู่และคอมโพเนนต์ที่ต้องเข้าถึงสถานะนั้น

หมวดหมู่รัฐ อายุการใช้งานและขอบเขต ตัวอย่าง API ของ Compose State ที่แนะนำ
สถานะ Live จัดเก็บไว้ในหน่วยความจำเสมอ โดยจะค้นหาใหม่เมื่อเริ่มต้นระบบและไม่คงอยู่หลังจากการสิ้นสุดการประมวลผล สถานะการเชื่อมต่อเครือข่าย สถานะการเชื่อมต่อบลูทูธ สถานะ UI ชั่วคราว เช่น การเลือกรายการ
  • mutableStateOf ในViewModel หรือบัญชีดูแลจัดการ @Singleton ที่กำหนดขอบเขต DI
  • transform เพื่อรับสถานะ
  • listener เพื่อสังเกตสถานะ
สถานะ UI ชั่วคราว คงอยู่แม้จะมีการเปลี่ยนแปลงการกำหนดค่าและการสิ้นสุดการประมวลผลที่ระบบเริ่มต้น ไม่ได้จัดเก็บไว้อย่างไม่มีกำหนด โดยกำหนดขอบเขตไว้ที่หน้าจอหรือกิจกรรมเดียว องค์ประกอบ UI ที่ขยายหรือยุบ ตำแหน่งการเลื่อน คำค้นหาในหน้าจอ หรือปุ่มเปิด/ปิดตัวกรอง
สถานะเซสชันชั่วคราว คงอยู่แม้กระบวนการเบื้องหลังจะสิ้นสุดลง แต่ไม่ได้จัดเก็บไว้อย่างไม่มีกำหนด โดยจะแชร์ในกิจกรรม บริการ หรือวิดเจ็ตต่างๆ ตัวจับเวลาโฟกัสที่ทำงานอยู่ รถเข็นชำระเงินแบบหลายขั้นตอน หรือฉบับร่างแบบฟอร์มที่กำลังดำเนินการอยู่
  • StateStore ที่รวมอยู่ในเครื่องมือจัดการหรือที่เก็บข้อมูล
  • คีย์ที่ใช้ PersistToDataStore และซิงค์โดยใช้ syncToDataStore
สถานะถาวร เก็บไว้ในดิสก์แบบไม่มีกำหนด ค่ากำหนดและการตั้งค่าของผู้ใช้ (DataStore), ข้อมูลแอปที่มีโครงสร้าง และคลังสื่อ (Room) รวบรวมโฟลว์ DataStore หรือ Room ภายใน transform โดยใช้ collectAsState

ใช้สถานะ Compose ใน ViewModel

ภายใน ViewModel คุณสามารถประกาศอินพุตหน้าจออิสระเป็นพร็อพเพอร์ตี้ MutableState แบบละเอียด, คงอินพุตเหล่านั้นไว้ตลอดการสิ้นสุดการประมวลผลด้วย SavedStateHandle.saveable, อัปเดตพร็อพเพอร์ตี้หลายรายการแบบอะตอมด้วย Snapshot.withMutableSnapshot และรวมพร็อพเพอร์ตี้เหล่านั้นกับสตรีม Flow ของที่เก็บภายใน transform ได้ ดังนี้

sealed interface TaskListState {
    data object Loading : TaskListState
    data class Success(
        val tasks: List<Task>,
        val selectedTaskIds: Set<String>,
        val isOnline: Boolean = true,
    ) : TaskListState
}

@HiltViewModel
class TaskListViewModel @Inject constructor(
    private val tasksRepository: TasksRepository,
    savedStateHandle: SavedStateHandle,
) : ViewModel() {

    var selectedTaskIds by mutableStateOf(emptySet<String>())
        private set

    var searchQuery by savedStateHandle.saveable { mutableStateOf("") }
        private set

    fun updateSearchQuery(query: String) {
        Snapshot.withMutableSnapshot {
            searchQuery = query
            selectedTaskIds = emptySet()
        }
    }

    val tasksState: State<TaskListState> = transform<TaskListState>(
        initialValue = TaskListState.Loading,
        scope = viewModelScope,
    ) {
        val tasks = remember(searchQuery) {
            tasksRepository.getTasks(searchQuery)
        }.collectAsState(initial = null).value
            ?: return@transform TaskListState.Loading

        TaskListState.Success(
            tasks = tasks,
            selectedTaskIds = selectedTaskIds,
        )
    }
}

รูปแบบสำคัญในViewModelนี้

  • ใช้ SavedStateHandle.saveable สำหรับสถานะที่บันทึกไว้แบบรีแอ็กทีฟ: การมอบสิทธิ์ searchQuery ให้กับ SavedStateHandle.saveable ด้วย mutableStateOf (จาก androidx.lifecycle:lifecycle-viewmodel-compose) จะคงค่าไว้ตลอดการสิ้นสุดการประมวลผลเป็น MutableState ดังนั้น transform จะติดตามการอ่านโดยอัตโนมัติ หลีกเลี่ยง SavedStateHandle.saved เปล่าๆ ซึ่งจะแสดงผลพร็อพเพอร์ตี้ Kotlin ธรรมดาแทนที่จะเป็น State ของ Compose และจะไม่ทริกเกอร์การจัดองค์ประกอบใหม่เมื่อมีการเปลี่ยนแปลง
  • การเขียนหลายพร็อพเพอร์ตี้แบบอะตอมด้วย Snapshot.withMutableSnapshot: การอัปเดต searchQuery และ selectedTaskIds ภายใน Snapshot.withMutableSnapshot จะแยกการเขียนและคอมมิตการเปลี่ยนแปลงทั้ง 2 รายการในธุรกรรมสแนปชอตแบบอะตอมรายการเดียว ดังนั้นผู้สังเกตการณ์ (รวมถึง transform) จะเห็นการอัปเดตทั้ง 2 รายการพร้อมกันในรอบการจัดองค์ประกอบใหม่รอบเดียว แทนที่จะสังเกตเห็น searchQuery ใหม่ที่จับคู่กับ selectedTaskIds ที่ล้าสมัย
  • ใช้ Flow ซ้ำในการจัดองค์ประกอบใหม่ด้วย remember(key): การรวม tasksRepository.getTasks(searchQuery) ไว้ใน remember โดยใช้ searchQuery เป็นคีย์จะช่วยให้มั่นใจได้ว่าเมื่อ selectedTaskIds มีการเปลี่ยนแปลงและจัดองค์ประกอบบล็อก transform ใหม่ Compose จะใช้ Flow ของฐานข้อมูลที่มีอยู่ซ้ำแทนที่จะเริ่มการค้นหาใหม่

แชร์สถานะระบบแบบเรียลไทม์ในแอปของคุณ

สำหรับสถานะระบบแบบเรียลไทม์ที่แชร์ใน ViewModel, กิจกรรม หรือบริการหลายรายการ เช่น สถานะการเชื่อมต่อเครือข่ายหรือบลูทูธ คุณไม่ควรเก็บสถานะที่ล้าสมัยไว้ในดิสก์เมื่อการประมวลผลสิ้นสุดลง แต่ให้ค้นหาสถานะปัจจุบันของระบบเมื่อเริ่มต้นระบบ เก็บไว้ในพร็อพเพอร์ตี้ mutableStateOf ส่วนตัวภายใน Singleton ที่กำหนดขอบเขตแอปพลิเคชัน และแสดง State<T> แบบอ่านอย่างเดียวแทน

interface NetworkMonitor {
    val isOnline: State<Boolean>
}

@Singleton
class LiveNetworkMonitor @Inject constructor(
    @ApplicationContext context: Context,
) : NetworkMonitor {
    private val connectivityManager =
        context.getSystemService(ConnectivityManager::class.java)

    private val _isOnline = mutableStateOf(checkCurrentConnectivity())
    override val isOnline: State<Boolean> = _isOnline

    private val callback = object : ConnectivityManager.NetworkCallback() {
        override fun onCapabilitiesChanged(
            network: Network,
            networkCapabilities: NetworkCapabilities,
        ) {
            _isOnline.value = networkCapabilities.hasCapability(
                NetworkCapabilities.NET_CAPABILITY_INTERNET
            )
        }

        override fun onLost(network: Network) {
            _isOnline.value = false
        }
    }

    init {
        connectivityManager.registerDefaultNetworkCallback(callback)
    }

    private fun checkCurrentConnectivity(): Boolean {
        val network = connectivityManager.activeNetwork ?: return false
        val capabilities =
            connectivityManager.getNetworkCapabilities(network) ?: return false
        return capabilities.hasCapability(
            NetworkCapabilities.NET_CAPABILITY_INTERNET
        )
    }
}

MutableState รองรับการอ่านและการอัปเดตที่สอดคล้องกันจากชุดข้อความใดก็ได้ ดังนั้น NetworkCallback จึงสามารถอัปเดต _isOnline.value ได้โดยตรงจากชุดข้อความ Callback ในเบื้องหลัง หาก Callback ในเบื้องหลังอัปเดตพร็อพเพอร์ตี้ MutableState หลายรายการพร้อมกัน ให้รวมการเขียนไว้ใน Snapshot.withMutableSnapshot เพื่อให้ระบบนำไปใช้แบบอะตอมมิก

คอมโพเนนต์ใดก็ตามในแอปสามารถอ่าน networkMonitor.isOnline.value ได้ ไม่ว่าจะเป็นการตรวจสอบแบบครั้งเดียว ภายใน UI Composable หรือภายในบล็อกการสังเกตสถานะ เช่น listener, derivedStateOf หรือ transform เช่น TaskListViewModel สามารถอ่าน networkMonitor.isOnline.value ภายในบล็อก transform ได้ดังนี้

val tasksState: State<TaskListState> = transform<TaskListState>(
    initialValue = TaskListState.Loading,
    scope = viewModelScope,
) {
    val tasks = remember(searchQuery) {
        tasksRepository.getTasks(searchQuery)
    }.collectAsState(initial = null).value
        ?: return@transform TaskListState.Loading

    TaskListState.Success(
        tasks = tasks,
        selectedTaskIds = selectedTaskIds,
        isOnline = networkMonitor.isOnline.value,
    )
}

เนื่องจาก transform ติดตาม networkMonitor.isOnline.value ควบคู่ไปกับ searchQuery, selectedTaskIds และที่เก็บ Flow การเปลี่ยนแปลงการเชื่อมต่อจะคำนวณ TaskListState.Success ใหม่โดยอัตโนมัติโดยไม่ต้องค้นหาฐานข้อมูลอีกครั้ง