تطوير Android / المتوسط: الحالة والبيانات والميزات الشاملة / الدرس 7
ViewModel وcoroutines المهيكلة وFlow
أنشئ مسار حالة شاشة تبقى ملكيته وإلغاؤه ومعالجة أخطائه مفهومة أثناء التحديثات غير المتزامنة.
في هذه الصفحة
أهداف الدرس
- اكشف حالة شاشة ثابتة الاستخدام من ViewModel.
- ميّز بين التعليق وتوزيع التنفيذ والجمع والعمل الدائم.
امنح حالة الأعمال مالكاً
يمتلك ViewModel حالة الشاشة وعملياتها لمالك دورة حياة محدد. يصمد أمام تغييرات الإعداد لذلك المالك ويُمسح عند إزالته نهائياً. ينبغي ألا يحتفظ بمرجع نشاط أو دالة تركيبية أو View. يوفر المستودع عمليات البيانات ويحولها ViewModel إلى معلومات قابلة للعرض. اكشف StateFlow للقراءة فقط وأبقِ التعديل خاصاً. يجعل ذلك الحالات الممكنة ظاهرة ويمنع عنصراً في الواجهة من تغيير حالة مشتركة دون عملية مقصودة.
حدّد نطاق العمل واحفظ الإلغاء
تعمل coroutine داخل نطاق يتحكم إلغاؤه في أبنائه. يفيد viewModelScope لعمليات الشاشة التي ينبغي أن تعيش أطول من تركيب معين وتتوقف عند مسح ViewModel. تستطيع دالة suspend التعليق دون حجب المستدعي، لكن الكلمة لا تنقل الشيفرة الحاجبة إلى خيط آخر. اجعل عمليات المستودع آمنة للخيط الرئيسي باستخدام موزعات مناسبة حول العمل الحاجب. التقط الإخفاقات المتوقعة بدقة وأعد رمي CancellationException عندما يلزم حد واسع لالتقاط الاستثناءات؛ فالإلغاء تدفق تحكم وليس خطأ خادم يعرض للمستخدم.
data class ShelfState(val titles: List<String> = emptyList(), val failed: Boolean = false)
interface ShelfRepository { suspend fun loadTitles(): List<String> }
class ShelfViewModel(private val repository: ShelfRepository) : ViewModel() {
private val _state = MutableStateFlow(ShelfState())
val state: StateFlow<ShelfState> = _state.asStateFlow()
fun refresh() {
viewModelScope.launch {
try { _state.value = ShelfState(repository.loadTitles()) }
catch (e: CancellationException) { throw e }
catch (e: IOException) { _state.update { it.copy(failed = true) } }
}
}
}
استورد ViewModel وviewModelScope من lifecycle وواجهات kotlinx.coroutines وflow وjava.io.IOException. وفّر المستودع عبر مصنع ViewModel أو حقن الاعتماديات. يحفظ المثال المحتوى السابق عند خطأ إدخال وإخراج؛ وينبغي للحالة الفعلية أيضاً تمثيل التحميل وسياسة تداخل التحديثات.
افصل التدفقات عن الحالة الحالية
يبدأ Flow البارد منتجه عادة مع كل جمع. أما StateFlow فنشط ويحمل قيمة حالية، فيتلقى الجامع الجديد أحدث حالة. يستطيع stateIn مشاركة تدفق علوي في نطاق مختار بسياسة بدء صريحة. افهم أن وقف الجمع قد يوقف العمل العلوي وفق سياسة أثناء الاشتراك. لا تفترض أن كل إصدار حدث يجب تسليمه مرة واحدة؛ فقد تختزل الحالة إلى أحدث قيمة. يحتاج أمر مثل تأكيد شراء عقد أعمال دائماً يتجاوز تدفق واجهة عابراً.
راقب وفق دورة حياة الشاشة
اجمع StateFlow بمحوّل Compose الذي يراعي دورة حياة Android بدلاً من تشغيل جامع غير محدود داخل الجسم. تصبح القيمة المجمعة حالة Compose وتغذي شاشة بلا حالة داخلية. ضع التهيئة المفردة في مالك الحالة أو أثر بمفتاح مقصود بدلاً من استدعاء refresh مع كل عرض. عند تغيير البحث سريعاً تستطيع عوامل مثل debounce وflatMapLatest إلغاء عمليات البحث القديمة، لكن الإلغاء لا يعوض اتساق الخادم ولا يمنع كتابة قبلها الخادم بالفعل.
@Composable
fun ShelfRoute(viewModel: ShelfViewModel) {
val state by viewModel.state.collectAsStateWithLifecycle()
ShelfScreen(
titles = state.titles,
showError = state.failed,
onRetry = viewModel::refresh
)
}
أضف اعتماد lifecycle-runtime-compose متوافقاً واستورد collectAsStateWithLifecycle ومفوّض getValue في Compose. تمثل ShelfScreen عارض التطبيق بلا حالة داخلية. لا تبدأ الوجهة تحديثاً تلقائياً؛ يقرر مالكها سياسة التحميل الأولي.
حدّد معنى تداخل العمليات
قد ينتهي تحديثان بعكس ترتيب بدئهما. اختر إلغاء الطلب السابق أو تسلسل الطلبات أو تجاهل النتائج القديمة برقم جيل. تمنع update الذرية في StateFlow سباق القراءة والتعديل والكتابة المحلي لكنها لا تحل ترتيب الطلبات وحدها. احقن الموزعات والاعتماديات ليتحكم الاختبار بالوقت والنتائج. وصنّف العمل بحسب دوامه: العملية المؤكدة التي يجب استمرارها بعد خروج العملية تحتاج تخزيناً دائماً ومجدولاً مناسباً، لا coroutine عامة أطول عمراً اختُرعت للهروب من إلغاء دورة الحياة.
الأخطاء الشائعة وإصلاحها
ما زالت دالة suspend تجمد التمرير
- لماذا يحدث؟
- يعمل عمل حاجب على الموزع الرئيسي.
- كيف تصلحه؟
- انقل التنفيذ الحاجب إلى موزع مناسب محقون.
- كيف تتجنب تكراره؟
- راجع أمان الخيط الرئيسي عند حدود المستودع.
تظهر عمليات البحث الملغاة كإخفاقات
- لماذا يحدث؟
- ابتلع التقاط واسع CancellationException.
- كيف تصلحه؟
- أعد رمي الإلغاء وحوّل الإخفاقات ذات الصلة فقط.
- كيف تتجنب تكراره؟
- اختبر الإلغاء مستقلاً عن أخطاء الشبكة.
تستبدل نتائج قديمة نتائج بحث أحدث
- لماذا يحدث؟
- لا تملك الطلبات المتزامنة سياسة ترتيب.
- كيف تصلحه؟
- ألغِ القراءات المتقادمة أو ارفض الأجيال القديمة.
- كيف تتجنب تكراره؟
- اختبر ترتيب انتهاء معكوساً عمداً.
اختبر فهمك
1. يعيد مستودع Flow بارداً ينفذ استعلاماً، ويشترك جامعان. اشرح سلوك المنتج وقرار المشاركة.
اعرض الحل المشروح
قد يبدأ كل جمع منتجه الخاص. إذا احتاجت الشاشة حالة حالية مشتركة فاستخدم stateIn في ViewModel بسياسة بدء مقصودة وحالة أولية. طابق النطاق مع ملكية الشاشة واختبر اختفاء المشتركين وعودتهم.
2. يؤكد مستخدم رفعاً ويغلق التطبيق فوراً. هل يكفي viewModelScope؟ صمّم نقل الملكية.
اعرض الحل المشروح
لا. احفظ العملية وهويتها الثابتة قبل إعلان إدراجها ثم جدوِل عملاً دائماً مناسباً. تراقب الواجهة الحالة من التخزين. قد تبدأ coroutine في ViewModel هذا التسليم، لكن مهمتها في الذاكرة ليست السجل الدائم ولا تضمن التقدم بعد وفاة العملية.
أهم ما تعلمته
- التعليق وتبديل الخيط قراران مختلفان.
- تحتاج تدفقات الحالة نطاقاً ودلالات إعادة بدء صريحة.
- يتطلب الإلغاء والترتيب والدوام سياسات منفصلة.