طبقة واجهة المستخدم (طرق العرض)

المفاهيم والتنفيذ في Jetpack Compose

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

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

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

بعد تحديد حالة واجهة المستخدم وتحديد كيفية إدارة إنتاج هذه الحالة، تتمثّل الخطوة التالية في عرض الحالة المنتَجة على واجهة المستخدم. بما أنّك تستخدم UDF لإدارة إنتاج الحالة، يمكنك اعتبار الحالة المنتَجة بمثابة بث، أي سيتم إنتاج إصدارات متعدّدة من الحالة بمرور الوقت. نتيجةً لذلك، عليك عرض حالة واجهة المستخدِم في حاوية بيانات قابلة للمراقبة، مثل LiveData أو StateFlow. والسبب في ذلك هو أنّ واجهة المستخدم يمكنها الاستجابة لأي تغييرات يتم إجراؤها في الحالة بدون الحاجة إلى جلب البيانات يدويًا مباشرةً من ViewModel. تتميّز هذه الأنواع أيضًا بأنّها تحتفظ دائمًا بأحدث إصدار من حالة واجهة المستخدم المخزّنة مؤقتًا، وهو أمر مفيد لاستعادة الحالة بسرعة بعد تغييرات الإعدادات.

class NewsViewModel(...) : ViewModel() {

    val uiState: StateFlow<NewsUiState> = 
}

إحدى الطرق الشائعة لإنشاء مصدر UiState هي عرض مصدر قابل للتغيير كعرض غير قابل للتغيير من ViewModel، مثل عرض MutableStateFlow<UiState> كـ StateFlow<UiState>.

class NewsViewModel(...) : ViewModel() {

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

    ...

}

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

class NewsViewModel(
    private val repository: NewsRepository,
    ...
) : ViewModel() {

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

    private var fetchJob: Job? = null

    fun fetchArticles(category: String) {
        fetchJob?.cancel()
        fetchJob = viewModelScope.launch {
            try {
                val newsItems = repository.newsItemsForCategory(category)
                _uiState.update {
                    it.copy(newsItems = newsItems)
                }
            } catch (ioe: IOException) {
                // Handle the error and notify the UI when appropriate.
                _uiState.update {
                    val messages = getMessagesFromThrowable(ioe)
                    it.copy(userMessages = messages)
                }
            }
        }
    }
}

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

عند استخدام حاويات البيانات القابلة للمراقبة في واجهة المستخدم، احرص على مراعاة عمر واجهة المستخدم. هذا مهم لأنّه يجب ألا تراقب واجهة المستخدم حالة واجهة المستخدم عندما لا يتم عرض طريقة العرض للمستخدم. لمزيد من المعلومات حول هذا الموضوع، يُرجى الاطّلاع على مشاركة المدونة هذه. عند استخدام LiveData، تتولّى LifecycleOwner ضِمنيًا معالجة المشاكل المتعلّقة بدورة الحياة. عند استخدام التدفقات، من الأفضل التعامل مع ذلك باستخدام نطاق الروتين المشترك المناسب وواجهة برمجة التطبيقات repeatOnLifecycle:

class NewsActivity : AppCompatActivity() {

    private val viewModel: NewsViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        ...

        lifecycleScope.launch {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                viewModel.uiState.collect {
                    // Update UI elements
                }
            }
        }
    }
}

عرض العمليات قيد التقدّم

إحدى الطرق البسيطة لتمثيل حالات التحميل في فئة UiState هي استخدام حقل منطقي:

data class NewsUiState(
    val isFetchingArticles: Boolean = false,
    ...
)

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

class NewsActivity : AppCompatActivity() {

    private val viewModel: NewsViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        ...

        lifecycleScope.launch {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                // Bind the visibility of the progressBar to the state
                // of isFetchingArticles.
                viewModel.uiState
                    .map { it.isFetchingArticles }
                    .distinctUntilChanged()
                    .collect { progressBar.isVisible = it }
            }
        }
    }
}

الصور المتحركة

لتوفير انتقالات سلسة بين عناصر التنقّل في المستوى الأعلى، قد تحتاج إلى الانتظار إلى أن يتم تحميل البيانات في الشاشة الثانية قبل بدء الحركة. يوفّر إطار عمل العرض في Android نقاط ربط لتأخير عمليات الانتقال بين وجهات الأجزاء باستخدام واجهتَي برمجة التطبيقات postponeEnterTransition() وstartPostponedEnterTransition(). توفّر واجهات برمجة التطبيقات هذه طريقة لضمان أنّ عناصر واجهة المستخدم على الشاشة الثانية (عادةً ما تكون صورة يتم جلبها من الشبكة) جاهزة للعرض قبل أن تحرّك واجهة المستخدم عملية الانتقال إلى تلك الشاشة.