استخدام حالة Compose خارج واجهة المستخدم

‫State<T> وMutableState<T> في Jetpack Compose هما حاويتان للقيم ذات الأغراض العامة، وتعتمدان على نظام اللقطات في Compose. على الرغم من أنّها تُستخدم عادةً لإعادة تركيب واجهة المستخدم، إلا أنّ نظام اللقطات نفسه غير مرتبط بعرض واجهة المستخدم.

باستخدام مكتبات AppState، وهي Transform وStateStore، يمكنك الاحتفاظ بحالة State في Compose واستخلاصها ومراقبتها ومواصلة استخدامها في ViewModels والمدراء والمستودعات والخدمات التي تُشغَّل في الخلفية.

لماذا يجب استخدام Compose State خارج واجهة المستخدم؟

عند قراءة Compose State داخل كتلة مراقبة الحالة، مثل واجهة مستخدم قابلة للإنشاء أو transform أو derivedStateOf، يوفّر وقت تشغيل Compose ميزتَين رئيسيتَين في أي طبقة من البنية:

  • التتبُّع التلقائي والديناميكي للتبعية: تؤدي قراءة .value الحالة (أو استخدام تفويض السمة by) إلى تسجيل هذه الحالة تلقائيًا على أنّها تبعية للكتلة. بما أنّ التتبُّع ديناميكي، يتم تحديد التبعيات في وقت التشغيل من خلال المسارات البرمجية التي يتم تنفيذها. إذا لم يتم استخدام فرع شرطي (على سبيل المثال، بعد التحقّق من if أو return المبكر)، لا تتم قراءة الحالات داخل هذا الفرع ولا تؤدي التغييرات التي تطرأ عليها إلى إعادة إجراء عمليات حسابية غير ضرورية.
  • الاتساق والدقة: يمكنك تحديد سمات MutableState منفصلة ودقيقة، وتجميع عمليات الكتابة المتعددة السمات في Snapshot.withMutableSnapshot. يتم عزل التعديلات المؤقتة في اللقطة القابلة للتغيير ويتم تطبيقها بشكل ذري عند اكتمال الكتلة، وبالتالي لا يرى المراقبون الذين يقرأون داخل اللقطة حالة مؤقتة غير متسقة ويعيدون الحساب فقط عندما تتغير الخصائص المحددة التي يقرأونها. على عكس StateFlow، ليس عليك الاختيار بين StateFlow<UiState> واحد متكامل (يبطل كل أداة تجميع عند تغيير أي حقل) وخصائص StateFlow منفصلة (لا يمكن تحديثها معًا في معاملة واحدة).

اختيار واجهة برمجة التطبيقات المناسبة لإدارة الحالة في بنية تطبيقك

يمكنك اختيار كيفية الاحتفاظ ببيانات Compose State ومواصلة استخدامها استنادًا إلى المدة التي يجب أن تظل فيها الحالة نشطة والمكوّنات التي تحتاج إلى الوصول إليها:

فئة الولاية الفترة منذ الإنشاء والنطاق أمثلة واجهات برمجة التطبيقات المقترَحة في Compose State
الحالة المباشرة يتم تخزينها دائمًا في الذاكرة، ويتم الاستعلام عنها من جديد عند بدء التشغيل ولا يتم الاحتفاظ بها بعد إيقاف العملية نهائيًا. حالة الاتصال بالشبكة وحالة اتصال البلوتوث وحالة واجهة المستخدم المؤقتة، مثل اختيار العناصر
  • mutableStateOf في حساب إداري ViewModel أو حساب إداري على مستوى "إحصاءات Google"‏ (@Singleton)
  • transform لاستنتاج الحالة
  • listener لمراقبة الحالة
حالة واجهة المستخدم المؤقتة يحتفظ بالبيانات عند تغيير الإعدادات أو إيقاف العملية نهائيًا من قِبل النظام، ولا يتم تخزينها إلى أجل غير مسمى، ويقتصر نطاقها على شاشة أو نشاط واحد. عناصر في واجهة المستخدم الموسَّعة أو المصغَّرة، أو موضع الانتقال، أو طلب البحث على الشاشة، أو مفاتيح تبديل الفلاتر
حالة الجلسة المؤقتة يستمر بعد إيقاف العملية في الخلفية نهائيًا، ولا يتم تخزينه إلى أجل غير مسمى، وتتم مشاركته بين الأنشطة أو الخدمات أو التطبيقات المصغّرة. مؤقّت تركيز نشط أو سلة تسوّق تتطلّب خطوات متعددة لإتمام الدفع أو مسودة نموذج قيد التقدم
  • StateStore encapsulated in a manager or repository
  • المفاتيح التي تستخدم PersistToDataStore وتتم مزامنتها باستخدام syncToDataStore
الحالة الدائمة يتم الاحتفاظ بها على القرص إلى أجل غير مسمى. إعدادات المستخدم المفضّلة (DataStore) وبيانات التطبيق المنظَّمة ومكتبات الوسائط (Room). collectAsState يجمع تدفقات DataStore أو Room داخل transform

استخدام حالة 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 إلى عزل عمليات الكتابة وتنفيذ كلا التغييرين في معاملة لقطة ذرية واحدة، لذلك يرى المراقبون (بما في ذلك transform) كلا التعديلين معًا في مسار إعادة تركيب واحد بدلاً من ملاحظة searchQuery الجديد المقترن بـ selectedTaskIds القديم.
  • إعادة استخدام التدفقات في عمليات إعادة الإنشاء باستخدام remember(key): يضمن التفاف tasksRepository.getTasks(searchQuery) في remember مع searchQuery كمفتاح أنّه عندما يتغيّر selectedTaskIds ويعيد إنشاء كتلة transform، يعيد Compose استخدام قاعدة البيانات الحالية Flow بدلاً من إعادة تشغيل طلب البحث.

مشاركة حالة النظام المباشرة في تطبيقك

بالنسبة إلى حالة النظام المباشرة التي تتم مشاركتها عبر عدة ViewModels أو أنشطة أو خدمات، مثل حالة اتصال الشبكة أو البلوتوث، لا يُنصح بالاحتفاظ بالحالة القديمة على القرص في حال إيقاف العملية نهائيًا. بدلاً من ذلك، يمكنك الاستعلام عن حالة النظام الحالية عند بدء التشغيل، والاحتفاظ بها في سمة mutableStateOf خاصة داخل عنصر فردي على مستوى التطبيق، وعرض 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 مباشرةً من سلسلة عمليات رد الاتصال في الخلفية. إذا كانت دالة الاستدعاء في الخلفية تعدّل عدة سمات MutableState معًا، يمكنك تجميع عمليات الكتابة في Snapshot.withMutableSnapshot ليتم تطبيقها بشكل ذري.

يمكن لأي مكوّن في تطبيقك قراءة networkMonitor.isOnline.value، سواء كعملية تحقّق لمرة واحدة أو داخل دالة مركّبة لواجهة المستخدم أو داخل وحدة تتبُّع الحالة، مثل 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 تلقائيًا بدون إعادة طلب البحث في قاعدة البيانات.