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

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

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

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

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

الأحداث الولاية
مؤقتة وغير متوقعة وموجودة لفترة محدودة. موجود دائمًا.
مدخلات الإنتاج الحكومي. مخرجات الدولة.
منتج واجهة المستخدم أو مصادر أخرى. يتم استهلاكها في واجهة المستخدم.

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

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

يمكن أن تأتي الأحداث من:

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

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

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

  • الإدخالات: مصادر تغيير الحالة. ويمكن أن يكون أحد الأقسام التالية:
    • محلي في طبقة واجهة المستخدم: قد تكون هذه الأحداث أحداث مستخدم، مثل إدخال مستخدم لعنوان "مهمة" في تطبيق إدارة مهام، أو واجهات برمجة تطبيقات توفّر الوصول إلى منطق واجهة المستخدم التي تؤدي إلى تغييرات في حالة واجهة المستخدم. على سبيل المثال، يمكنك استدعاء الطريقة open على DrawerState في Jetpack Compose.
    • مصادر خارجية لطبقة واجهة المستخدم: هذه هي المصادر من النطاق أو طبقات البيانات التي تسبب تغييرات في حالة واجهة المستخدم. مثل الأخبار التي انتهت التحميل من NewsRepository أو أحداث أخرى
    • مزيج من كل ما سبق.
  • حكومات الولايات: الأنواع التي تطبّق منطق العمل و/أو منطق واجهة المستخدم على مصادر تغيير الحالة ومعالجة أحداث المستخدم لإنتاج حالة واجهة المستخدم.
  • الإخراج: حالة واجهة المستخدم التي يمكن أن يعرضها التطبيق لتزويد المستخدمين بالمعلومات التي يحتاجون إليها.
مسار الإنتاج الحكومي
الشكل 2: مسار الإنتاج الحكومي

واجهات برمجة التطبيقات للإنتاج بالحالة

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

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

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

اجتماع مجلس إدارة الإنتاج الحكومي

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

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

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

قد توفّر الإدخالات في مسار الإنتاج الحكومي إما مصادر تغيير الدولة عبر:

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

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

واجهات برمجة التطبيقات أحادية اللقطة كمصادر لتغيير الحالة

استخدام واجهة برمجة التطبيقات MutableStateFlow كحاوية حالة قابلة للملاحظة وقابلة للتغيير في تطبيقات Jetpack Compose، يمكنك أيضًا استخدام mutableStateOf، خاصةً عند التعامل مع واجهات برمجة تطبيقات Compose text. توفِّر واجهتا برمجة التطبيقات طرقًا تتيح إجراء تحديثات صغيرة آمنة للقيم التي تستضيفها سواء كانت التحديثات متزامنة أو غير متزامنة أم لا.

على سبيل المثال، يمكنك تجربة تحديثات الحالة في تطبيق رمي النرد بسيط. تستدعي كل رمية من المستخدم طريقة Random.nextInt() المتزامنة، وتتم كتابة النتيجة في حالة واجهة المستخدم.

StateFlow

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,
            )
        }
    }
}

حالة الإنشاء

@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
    }
}

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

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

على سبيل المثال، يمكنك الاطّلاع على AddEditTaskViewModel في عيّنة البنية. عندما تحفظ طريقة التعليق saveTask() مهمة بشكل غير متزامن، تنشر طريقة update في MutableStateFlow تغيير الحالة إلى حالة واجهة المستخدم.

StateFlow

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))
                }
            }
        }
    }
}

حالة الإنشاء

@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))
            }
        }
    }
}

تبديل حالة واجهة المستخدم من سلاسل المحادثات في الخلفية

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

  • استخدِم طريقة withContext لتشغيل الكوروتينات في سياق متزامن مختلف.
  • عند استخدام MutableStateFlow، استخدِم طريقة update كالمعتاد.
  • عند استخدام Compose State (حالة الإنشاء)، استخدِم Snapshot.withMutableSnapshot لضمان إجراء التعديلات البسيطة على "الحالة" في السياق المتزامن.

لنفترض مثلاً في المقتطف DiceRollViewModel أدناه أنّ SlowRandom.nextInt() هي دالة suspend مكثّفة من الناحية الحسابية، ويجب استدعاؤها من خلال Coroutine المرتبط بوحدة المعالجة المركزية (CPU).

StateFlow

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,
                    )
                }
            }
        }
    }
}

حالة الإنشاء

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
                }
            }
        }
    }
}

واجهات برمجة تطبيقات البث كمصادر لتغيير الحالة

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

عند استخدام تدفقات Kotlin، يمكنك تحقيق ذلك باستخدام الدالة combine. يمكن مشاهدة مثال على ذلك في نموذج "الآن في Android" في interestViewModel:

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 لنشر تغييرات الحالة. يمكنك أيضًا إنشاء تدفقات لقطات من حالة الإنشاء.

يمكنك استخدام TaskDetailViewModel من مستودع نماذج البنية أدناه:

StateFlow

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, task ->
        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 }
    }
}

حالة الإنشاء

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, task ->
        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
    }
}

أنواع الإخراج في مسارات الإنتاج الحكومية

يعتمد اختيار واجهة برمجة التطبيقات الناتجة لحالة واجهة المستخدم وطبيعة عرضها إلى حد كبير على واجهة برمجة التطبيقات التي يستخدمها تطبيقك لعرض واجهة المستخدم. في تطبيقات Android، يمكنك اختيار استخدام المشاهدات أو Jetpack Compose. تشمل الاعتبارات هنا ما يلي:

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

إدخال مستهلك نتيجة واحدة
واجهات برمجة التطبيقات لمرة واحدة المشاهدات StateFlow أو LiveData
واجهات برمجة التطبيقات لمرة واحدة إنشاء StateFlow أو إنشاء State
واجهات برمجة تطبيقات Stream المشاهدات StateFlow أو LiveData
واجهات برمجة تطبيقات Stream إنشاء StateFlow
واجهات برمجة التطبيقات أحادية اللقطة والتدفق المشاهدات StateFlow أو LiveData
واجهات برمجة التطبيقات أحادية اللقطة والتدفق إنشاء StateFlow

إعداد مسار الإنتاج في الولاية

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

يجب تهيئة مسار الإنتاج الحكومي بشكل بطيء عندما يكون ذلك ممكنًا للحفاظ على موارد النظام. ومن الناحية العملية، غالبًا ما يعني هذا الانتظار حتى يكون هناك مستهلك للناتج. تتيح واجهات برمجة التطبيقات 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
        }
    }
}

عيّنات

توضح نماذج Google التالية إنتاج الحالة في طبقة واجهة المستخدم. يمكنك الانتقال إلى هذه الصفحة للاطّلاع على هذه الإرشادات عمليًا: