إنتاج حالة واجهة المستخدم

نادرًا ما تكون واجهات المستخدم الحديثة ثابتة. تتغيّر حالة واجهة المستخدم عندما يتفاعل المستخدم معها أو عندما يحتاج التطبيق إلى عرض بيانات جديدة.

تحدّد هذه المستندات إرشادات لإنتاج حالة واجهة المستخدم وإدارتها. تهدف هذه المستندات إلى مساعدتك في فهم ما يلي:

  • واجهات برمجة التطبيقات التي يجب استخدامها لإنتاج حالة واجهة المستخدم: يعتمد ذلك على طبيعة مصادر تغيير الحالة المتوفّرة في عناصر الاحتفاظ بالحالة، وفقًا لمبادئ تدفق البيانات أحادي الاتجاه. يعتمد هذا على طبيعة مصادر تغيير الحالة المتاحة في حاملي الحالة لديك، باتباع مبادئ تدفق البيانات أحادي الاتجاه.
  • كيفية تحديد نطاق إنتاج حالة واجهة المستخدم ليكون واعيًا بموارد النظام.
  • بشكل أساسي، إنتاج الحالة هو تطبيق هذه التغييرات بشكل تدريجي على حالة واجهة المستخدم.

تكون الحالة موجودة دائمًا، وتتغيّر نتيجةً للأحداث. يتم تلخيص الاختلافات بين الأحداث والحالة في الجدول أدناه: الأحداث

الحالة الحالة
موجودة دائمًا مدخلات إنتاج الحالة
ناتج إنتاج الحالة ناتج الحالة
تستهلكها واجهة المستخدم عبارة **الحالة هي؛ الأحداث تحدث** هي عبارة تذكّرية رائعة تلخّص ما سبق.

يساعد الرسم البياني أدناه في تصوُّر التغييرات التي تطرأ على الحالة عند وقوع الأحداث في مخطط زمني. يساعد المخطّط البياني أدناه في تصور التغييرات التي تطرأ على الحالة مع وقوع الأحداث في المخطط الزمني. ![](/static/images/topic/architecture/ui-layer/events-vs-state.png) **الشكل 1** : الأحداث تؤدي إلى تغيير الحالة

الأحداث في مقابل الولاية
الشكل 1: تتسبب الأحداث في تغيير الحالة

**المستخدمون** : أثناء تفاعلهم مع واجهة مستخدم التطبيق

  • مصادر أخرى لتغيير الحالة: واجهات برمجة التطبيقات التي تعرض بيانات التطبيق من طبقات واجهة المستخدم أو النطاق أو البيانات، مثل أحداث مهلة Snackbar أو حالات الاستخدام أو المستودعات على التوالي
  • مصادر أخرى لتغيير الحالة: واجهات برمجة التطبيقات التي تقدم بيانات التطبيق من واجهة المستخدم، أو طبقات المجال أو البيانات مثل أحداث مهلة شريط الوجبات الخفيفة، أو حالات الاستخدام، أو المستودعات، على التوالي.

يمكن اعتبار إنتاج الحالة في تطبيقات Android خط أنابيب معالجة يتألف من ما يلي:

يمكن اعتبار إنتاج الحالة في تطبيقات Android بمثابة خط أنابيب معالجة يتألف مما يلي:

مسار الإنتاج في الولاية
الشكل 2: مسار إنتاج الحالة

هناك واجهتا برمجة تطبيقات رئيسيتان تُستخدمان في إنتاج الحالة، وذلك حسب مرحلة خط الأنابيب التي تستخدمها:

مرحلة خط الأنابيب

مرحلة مسار التعلّم الإدخال
استخدِم واجهات برمجة التطبيقات غير المتزامنة، مثل الإجراءات البرمجية المتزامنة وFlows، لتنفيذ العمل خارج سلسلة التعليمات البرمجية لواجهة المستخدم من أجل الحفاظ على سلاسة واجهة المستخدم. استخدِم واجهات برمجة التطبيقات غير المتزامنة، مثل Coroutines وFlows، لتنفيذ المهام خارج سلسلة واجهة المستخدم للحفاظ على واجهة المستخدم خالية من الإيقاف المؤقت لعرض واجهة المستخدم.
استخدِم واجهات برمجة التطبيقات لعناصر الاحتفاظ بالبيانات القابلة للمراقبة، مثل Compose State أو StateFlow، لإبطال واجهة المستخدم وإعادة عرضها عند تغيير الحالة. استخدِم واجهات برمجة تطبيقات حاملي البيانات القابلة للتتبّع مثل Compose State أو StateFlow لـ إلغاء صلاحية واجهة المستخدم وإعادة عرضها عند تغيير الحالة. تضمن حامِلات البيانات القابلة للمراقبة أن واجهة المستخدم لديها دائمًا حالة واجهة مستخدم لعرضها على الشاشة.

يؤثر اختيار واجهة برمجة التطبيقات غير المتزامنة للمدخلات بشكل أكبر على طبيعة مسار إنتاج الحالة من اختيار واجهة برمجة التطبيقات القابلة للملاحظة للمخرجات. تجميع خط أنابيب إنتاج الحالة

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

كل خط أنابيب لإنتاج الحالة هو مزيج من المدخلات والمخرجات ويجب أن يكون على النحو التالي: يُعدّ كل خط أنابيب لإنتاج الحالة مزيجًا من المدخلات والمخرجات ويجب أن يكون على النحو التالي:

  • مراحل النشاط: في الحالة التي لا تكون فيها واجهة المستخدم مرئية أو نشطة، يجب ألا يستهلك مسار إنتاج الحالة أي موارد ما لم يكن ذلك مطلوبًا بشكل صريح.
  • سهولة الاستهلاك: يجب أن تكون واجهة المستخدم قادرة على عرض حالة واجهة المستخدم التي تم إنتاجها بسهولة. في Jetpack Compose، يُعد استهلاك الحالة أمرًا أساسيًا لواجهة المستخدم، لأن الدوال المركّبة يمكنها التحديث بناءً على تغييرات الحالة.

توفّر المدخلات في خط أنابيب إنتاج الحالة مصادر تغيير الحالة من خلال ما يلي:

المدخلات في مسار الإنتاج توفر مصادر تغيير الحالة من خلال ما يلي:

  • العمليات أحادية اللقطة التي يمكن أن تكون متزامنة أو غير متزامنة—على سبيل المثال، استدعاءات دوال suspend
  • كل ما سبقFlows
  • تتناول الأقسام التالية كيفية تجميع خط أنابيب لإنتاج الحالة لكل من المدخلات أعلاه.

تغطي الأقسام التالية كيفية تجميع مسار إنتاج للحالة لكل من المدخلات المذكورة أعلاه.

يمكنك إدارة الحالة باستخدام عناصر الاحتفاظ بالبيانات القابلة للمراقبة.

إدارة الحالة باستخدام حاملي البيانات القابلة للملاحظة. استخدِم واجهة برمجة التطبيقات mutableStateOf، خاصةً عند العمل مع واجهات برمجة تطبيقات النصوص في Compose. لإدارة حالة أكثر تعقيدًا أو عند الدمج مع مكونات معمارية أخرى، استخدِم واجهة برمجة التطبيقات MutableStateFlow. على سبيل المثال، ضع في اعتبارك تعديلات الحالة في تطبيق بسيط لرمي النرد. يؤدي كل رمية نرد من المستخدم إلى استدعاء طريقة `Random.nextInt` المتزامنة، ويتم تسجيل النتيجة في حالة واجهة المستخدم.

Compose StateRandom.nextInt

حالة Compose

@Stable
interface DiceUiState {
    val firstDieValue: Int?
    val secondDieValue: Int?
    val numberOfRolls: Int?
}

private class MutableDiceUiState: DiceUiState {
    override var firstDieValue: Int? by mutableStateOf(null)
    override var secondDieValue: Int? by mutableStateOf(null)
    override var numberOfRolls: Int by mutableStateOf(0)
}

class DiceRollViewModel : ViewModel() {

    private val _uiState = MutableDiceUiState()
    val uiState: DiceUiState = _uiState

    // Called from the UI
    fun rollDice() {
        _uiState.firstDieValue = Random.nextInt(from = 1, until = 7)
        _uiState.secondDieValue = Random.nextInt(from = 1, until = 7)
        _uiState.numberOfRolls = _uiState.numberOfRolls + 1
    }
}

تغيير حالة واجهة المستخدم من خلال طلبات غير متزامنة

data class DiceUiState(
    val firstDieValue: Int? = null,
    val secondDieValue: Int? = null,
    val numberOfRolls: Int = 0,
)

class DiceRollViewModel : ViewModel() {

    private val _uiState = MutableStateFlow(DiceUiState())
    val uiState: StateFlow<DiceUiState> = _uiState.asStateFlow()

    // Called from the UI
    fun rollDice() {
        _uiState.update { currentState ->
            currentState.copy(
            firstDieValue = Random.nextInt(from = 1, until = 7),
            secondDieValue = Random.nextInt(from = 1, until = 7),
            numberOfRolls = currentState.numberOfRolls + 1,
            )
        }
    }
}

بالنسبة إلى تغييرات الحالة التي تتطلّب نتيجة غير متزامنة، شغِّل إجراءً برمجية متزامنة في `CoroutineScope` المناسب.

بالنسبة لتغييرات الحالة التي تتطلب نتيجة غير متزامنة، قم بتشغيل كوروتين في CoroutineScope المناسب. بعد ذلك، يسجِّل عنصر الاحتفاظ بالحالة نتيجة استدعاء طريقة الإيقاف المؤقت في واجهة برمجة التطبيقات القابلة للمراقبة المستخدَمة لعرض حالة واجهة المستخدم.CoroutineScope يكتب عنصر الاحتفاظ بالحالة بعد ذلك نتيجة طلب إجراء التعليق في واجهة برمجة التطبيقات القابلة للملاحظة المستخدمة لعرض حالة واجهة المستخدم.

على سبيل المثال، ضع في اعتبارك AddEditTaskViewModel في نموذج بنية التطبيق. Compose StatesaveTaskupdate

حالة Compose

@Stable
interface AddEditTaskUiState {
    val title: String
    val description: String
    val isTaskCompleted: Boolean
    val isLoading: Boolean
    val userMessage: String?
    val isTaskSaved: Boolean
}

private class MutableAddEditTaskUiState : AddEditTaskUiState() {
    override var title: String by mutableStateOf("")
    override var description: String by mutableStateOf("")
    override var isTaskCompleted: Boolean by mutableStateOf(false)
    override var isLoading: Boolean by mutableStateOf(false)
    override var userMessage: String? by mutableStateOf<String?>(null)
    override var isTaskSaved: Boolean by mutableStateOf(false)
}

class AddEditTaskViewModel(...) : ViewModel() {

   private val _uiState = MutableAddEditTaskUiState()
   val uiState: AddEditTaskUiState = _uiState

   private fun createNewTask() {
        viewModelScope.launch {
            val newTask = Task(uiState.value.title, uiState.value.description)
            try {
                tasksRepository.saveTask(newTask)
                // Write data into the UI state.
                _uiState.isTaskSaved = true
            }
            catch(cancellationException: CancellationException) {
                throw cancellationException
            }
            catch(exception: Exception) {
                _uiState.userMessage = getErrorMessage(exception))
            }
        }
    }
}

**ملاحظة:** يتم تشغيل الإجراءات البرمجية المتزامنة التي يتم تشغيلها في `viewModelScope` من `ViewModel` في AAC حتى اكتمالها، سواء كان ذلك بشكل استثنائي أو غير ذلك.

data class AddEditTaskUiState(
    val title: String = "",
    val description: String = "",
    val isTaskCompleted: Boolean = false,
    val isLoading: Boolean = false,
    val userMessage: String? = null,
    val isTaskSaved: Boolean = false
)

class AddEditTaskViewModel(...) : ViewModel() {

   private val _uiState = MutableStateFlow(AddEditTaskUiState())
   val uiState: StateFlow<AddEditTaskUiState> = _uiState.asStateFlow()

   private fun createNewTask() {
        viewModelScope.launch {
            val newTask = Task(uiState.value.title, uiState.value.description)
            try {
                tasksRepository.saveTask(newTask)
                // Write data into the UI state.
                _uiState.update {
                    it.copy(isTaskSaved = true)
                }
            }
            catch(cancellationException: CancellationException) {
                throw cancellationException
            }
            catch(exception: Exception) {
                _uiState.update {
                    it.copy(userMessage = getErrorMessage(exception))
                }
            }
        }
    }
}

من الأفضل تشغيل الإجراءات البرمجية المتزامنة على برنامج الإرسال الرئيسي لإنتاج حالة واجهة المستخدم، أي خارج كتلة `withContext` في مقتطفات الرموز البرمجية أدناه.

من الأفضل تشغيل Coroutines على المرسل الرئيسي لإنتاج حالة واجهة المستخدم - أي خارج الكتلة withContext في مقتطفات التعليمات البرمجية أدناه. استخدِم طريقة `withContext` لتشغيل الإجراءات البرمجية المتزامنة في سياق متزامن مختلف.

  • استخدِم طريقة withContext لتشغيل Coroutines في سياق متزامن مختلف.
  • عند استخدام MutableStateFlow، استخدِم طريقة update كالمعتاد.
  • عند استخدام Compose State، استخدِم Snapshot.withMutableSnapshot لضمان التحديثات الذرية للحالة في السياق المتزامن.

For example, assume that in the DiceRollViewModel snippet below, SlowRandom.nextInt is a computationally intensive suspend function that needs to be called from a CPU-bound Coroutine.

حالة Compose

class DiceRollViewModel(
    private val defaultDispatcher: CoroutineScope = Dispatchers.Default
) : ViewModel() {

    private val _uiState = MutableDiceUiState()
    val uiState: DiceUiState = _uiState

  // Called from the UI
  fun rollDice() {
        viewModelScope.launch() {
            // Other Coroutines that may be called from the current context
            
            withContext(defaultDispatcher) {
                Snapshot.withMutableSnapshot {
                    _uiState.firstDieValue = SlowRandom.nextInt(from = 1, until = 7)
                    _uiState.secondDieValue = SlowRandom.nextInt(from = 1, until = 7)
                    _uiState.numberOfRolls = _uiState.numberOfRolls + 1
                }
            }
        }
    }
}

**ملاحظة:** إذا كان يجب استدعاء جميع الإجراءات البرمجية المتزامنة التي تم تشغيلها من سياق مختلف، يمكنك استدعاء `viewModelScope.launch(defaultDispatcher){ }` مباشرةً.**تحذير:** يمكن أن يؤدي تعديل حالة Compose من سلسلة تعليمات برمجية غير تابعة لواجهة المستخدم بدون استخدام `Snapshot.withMutableSnapshot{ }` إلى حدوث حالات غير متسقة في الحالة المنتَجة.

class DiceRollViewModel(
    private val defaultDispatcher: CoroutineScope = Dispatchers.Default
) : ViewModel() {

    private val _uiState = MutableStateFlow(DiceUiState())
    val uiState: StateFlow<DiceUiState> = _uiState.asStateFlow()

  // Called from the UI
  fun rollDice() {
        viewModelScope.launch() {
            // Other Coroutines that may be called from the current context
            
            withContext(defaultDispatcher) {
                _uiState.update { currentState ->
                    currentState.copy(
                        firstDieValue = SlowRandom.nextInt(from = 1, until = 7),
                        secondDieValue = SlowRandom.nextInt(from = 1, until = 7),
                        numberOfRolls = currentState.numberOfRolls + 1,
                    )
                }
            }
        }
    }
}

بالنسبة إلى مصادر تغيير الحالة التي تنتج قيمًا متعدّدة بمرور الوقت في البث، فإنّ تجميع نواتج جميع المصادر في وحدة متماسكة هو نهج بسيط لإنتاج الحالة.

عند استخدام Kotlin Flows، يمكنك تحقيق ذلك باستخدام دالة combine.

عند استخدام Kotlin Flows، يمكنك تحقيق ذلك باستخدام الدالة combine. **ملاحظة:** يمكنك استخدام عامل التشغيل `stateIn` لتحويل `Flow` المجمّع إلى `StateFlow` كواجهة برمجة تطبيقات قابلة للمراقبة لحالة واجهة المستخدم. مثال على ذلك يمكن رؤيته في نموذج"الآن في Android" في الـ InterestsViewModel:

class InterestsViewModel(
    authorsRepository: AuthorsRepository,
    topicsRepository: TopicsRepository
) : ViewModel() {

    val uiState = combine(
        authorsRepository.getAuthorsStream(),
        topicsRepository.getTopicsStream(),
    ) { availableAuthors, availableTopics ->
        InterestsUiState.Interests(
            authors = availableAuthors,
            topics = availableTopics
        )
    }
        .stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(5_000),
            initialValue = InterestsUiState.Loading
    )
}

استخدِم stateIn إذا كان يجب أن يكون خط الأنابيب نشطًا فقط عندما تكون واجهة المستخدم مرئية أثناء جمع البث بطريقة تراعي دورة الحياة.StateFlows

  • استخدِم SharingStarted.WhileSubscribed إذا كان يجب أن يكون خط الأنابيب نشطًا فقط عندما تكون واجهة المستخدم مرئية أثناء جمع التدفق بطريقة تراعي مراحل النشاط.
  • استخدِم SharingStarted.Lazily إذا كان المسار بحاجة إلى أن يكون نشطًا طالما أن المستخدم قد يعود إلى واجهة المستخدم، أي أن واجهة المستخدم في حزمة الخلفية أو في علامة تبويب أخرى خارج الشاشة.

في الحالات التي لا ينطبق فيها تجميع مصادر الحالة المستندة إلى البث، توفر واجهات برمجة تطبيقات البث مثل Kotlin Flows مجموعة غنية من التحويلات مثل الدمج، والتسوية، وما إلى ذلك للمساعدة في معالجة البث في حالة واجهة المستخدم.

في حال كان خط أنابيب إنتاج الحالة يعتمد على كل من عمليات الاستدعاء التي يتم تنفيذها مرة واحدة والبث كمصادر لتغيير الحالة، فإنّ البث هو القيد المحدد.

في الحالة التي يعتمد فيها مسار الإنتاج على كل من الاستدعاءات التي يتم تنفيذها مرة واحدة والبث كمصادر لتغيير الحالة، يكون البث هو الشرط المحدد. باستخدام التدفقات، يعني ذلك عادةً إنشاء واحد أو أكثر من مثيلات `MutableStateFlow` الخاصة بالدعم لتوزيع تغييرات الحالة.

باستخدام التدفقات، يعني هذا عادةً إنشاء مثيل واحد أو أكثر من مثيلات الدعم الخاصة MutableStateFlow لنشر تغييرات الحالة. يمكنك أيضًا إنشاء تدفقات لقطات من حالة Compose.

ضع في اعتبارك TaskDetailViewModel من مستودع architecture-samples. تعتمد حالة واجهة المستخدم على دفق للمهمة الحالية (_task) و مصدر لمرة واحدة (_isTaskDeleted) يتم تحديثه عند حذف المهمة. Compose State

حالة Compose

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

    private var _isTaskDeleted by mutableStateOf(false)
    private val _task = tasksRepository.getTaskStream(taskId)

    val uiState: StateFlow<TaskDetailUiState> = combine(
        snapshotFlow { _isTaskDeleted },
        _task
    ) { isTaskDeleted, taskAsync ->
        TaskDetailUiState(
            task = taskAsync.data,
            isTaskDeleted = isTaskDeleted
        )
    }
        // Convert the result to the appropriate observable API for the UI
        .stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(5_000),
            initialValue = TaskDetailUiState()
        )

    fun deleteTask() = viewModelScope.launch {
        tasksRepository.deleteTask(taskId)
        _isTaskDeleted = true
    }
}

**ملاحظة:** يتم تحويل `State` في Compose إلى تدفق باستخدام واجهة برمجة التطبيقات `snapshotFlow`.

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

    private val _isTaskDeleted = MutableStateFlow(false)
    private val _task = tasksRepository.getTaskStream(taskId)

    val uiState: StateFlow<TaskDetailUiState> = combine(
        _isTaskDeleted,
        _task
    ) { isTaskDeleted, taskAsync ->
        TaskDetailUiState(
            task = taskAsync.data,
            isTaskDeleted = isTaskDeleted
        )
    }
        // Convert the result to the appropriate observable API for the UI
        .stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(5_000),
            initialValue = TaskDetailUiState()
        )

    fun deleteTask() = viewModelScope.launch {
        tasksRepository.deleteTask(taskId)
        _isTaskDeleted.update { true }
    }
}

يعتمد اختيار واجهة برمجة تطبيقات الناتج لحالة واجهة المستخدم وطبيعة عرضها إلى حد كبير على واجهة برمجة التطبيقات التي يستخدمها تطبيقك لعرض واجهة المستخدم، مثل Compose.

‫Jetpack Compose هي مجموعة الأدوات الحديثة المقترَحة لإنشاء واجهة مستخدم محلية. Jetpack Compose هي مجموعة الأدوات الحديثة الموصى بها لإنشاء واجهة مستخدم أصلية. قراءة الحالة بطريقة تراعي دورة الحياة

يلخص الجدول التالي واجهات برمجة التطبيقات التي يجب استخدامها لمسار إنتاج الحالة عند استخدام Jetpack Compose:

الناتج واجهات برمجة التطبيقات التي يتم تنفيذها مرة واحدة
`StateFlow` أو `State` في Compose StateFlow أو Compose State
واجهات برمجة التطبيقات التي يتم تنفيذها مرة واحدة وواجهات برمجة تطبيقات البث StateFlow
تهيئة خط أنابيب إنتاج الحالة StateFlow

تتضمّن تهيئة خطوط أنابيب إنتاج الحالة ضبط الشروط الأولية لتشغيل خط الأنابيب.

يتضمن إعداد مسارات إنتاج الحالة تحديد الشروط الأولية لتشغيل المسار. عند الإمكان، يمكنك تهيئة خط أنابيب إنتاج الحالة بشكل غير مباشر للحفاظ على موارد النظام.id

عند الإمكان، قم بتهيئة مسار إنتاج الحالة بشكل كسول للحفاظ على موارد النظام. تسمح واجهات برمجة التطبيقات `Flow` بذلك باستخدام الوسيطة `started` في طريقة `stateIn`. تسمح واجهات برمجة التطبيقات Flow بذلك باستخدام الوسيطة started في الطريقة stateIn. في الحالات التي لا ينطبق فيها ذلك، حدد دالة متساوية القوى initialize لبدء مسار إنتاج الحالة بشكل صريح كما هو موضح في المقتطف التالي:

class MyViewModel : ViewModel() {

    private var initializeCalled = false

    // This function is idempotent provided it is only called from the UI thread.
    @MainThread
    fun initialize() {
        if(initializeCalled) return
        initializeCalled = true

        viewModelScope.launch {
            // seed the state production pipeline
        }
    }
}
initViewModelIllegalStateException

توضّح نماذج Google التالية إنتاج الحالة في طبقة واجهة المستخدم.

توضح نماذج Google التالية إنتاج الحالة في طبقة واجهة المستخدم. مراجع إضافية

لمزيد من المعلومات عن حالة واجهة المستخدم، يُرجى الاطّلاع على المراجع الإضافية التالية:

الوثائق

الحالة وJetpack Compose

إنتاج حالة واجهة المستخدم (طريقة العرض)