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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

على سبيل المثال، ضع في اعتبارك تعديلات الحالة في تطبيق بسيط لرمي النرد. يؤدي كل رمية نرد من المستخدم إلى استدعاء طريقة 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,
            )
        }
    }
}

Compose State

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

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

بالنسبة إلى تغييرات الحالة التي تتطلب نتيجة غير متزامنة، شغِّل نمط "كوروتين" في 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))
                }
            }
        }
    }
}

Compose State

@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 تتطلب الكثير من العمليات الحسابية ويجب استدعاؤها من نمط "كوروتين" مرتبط بوحدة المعالجة المركزية.

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

Compose State

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 Flows، يمكنك تحقيق ذلك باستخدام دالة combine. يمكنك الاطّلاع على مثال على ذلك في "الآن في 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 الاحتياطية الخاصة لنشر تغييرات الحالة. يمكنك أيضًا إنشاء تدفقات اللقطات من حالة Compose.

ضع في اعتبارك TaskDetailViewModel من مستودع architecture-samples أدناه:

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

Compose State

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 أو Compose State
واجهات برمجة التطبيقات للبث المشاهدات StateFlow أو LiveData
واجهات برمجة التطبيقات للبث إنشاء 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 التالية إنتاج الحالة في طبقة واجهة المستخدم. يمكنك استكشافها للاطّلاع على هذه الإرشادات عمليًا: