تطوير Android / الأساسي: أسس Android وCompose / الدرس 2
قيم Kotlin وقابلية null ونماذج المجال
نمذج عناصر القراءة بشروط واضحة كي تتخذ الواجهة قرارات آمنة دون تخمين معنى البيانات المفقودة.
في هذه الصفحة
أهداف الدرس
- استخدم القيم الثابتة وأصناف البيانات والأنواع القابلة لـ null بقصد.
- حوّل المجموعات مع الحفاظ على الهوية وقواعد التحقق.
اختر قيماً تعبّر عن عقد
تميّز Kotlin بين val الذي لا يعاد إسناده وvar الذي يقبل ذلك. لا يجعل val الذي يحمل قائمة قابلة للتعديل القائمة ثابتة؛ فالمرجع والكائن مسؤوليتان مختلفتان. فضّل قيماً للمجال تُستخدم بصورة ثابتة واستبدلها بتحويلات صريحة. عرّف الوحدات والمديات الصحيحة قرب النموذج بدلاً من توزيع الفحوص على الأزرار. معرّف ثابت وعنوان غير فارغ وعدد صفحات غير سالب لعنصر القراءة أوضح من خريطة نصوص اعتباطية يتعين على المستدعين تذكر مفاتيحها.
عامل الإدخال المفقود كحالة ممثلة
قد تكون String? مساوية لـ null، لذلك يجب تحديد أثر الغياب على السلوك. تمرر الاستدعاءات الآمنة الغياب، ويوفر عامل Elvis قيمة بديلة، ويرفض الرجوع المبكر الإدخال غير الصحيح. تحمل هذه الخيارات معاني مختلفة في المنتج. لا ينبغي أن يتحول العنوان المفقود بصمت إلى سجل ناجح اسمه Unknown إلا إذا كانت تلك قاعدة موثقة. حلّل النصوص الخارجية باستخدام toIntOrNull وافحص العدد الناتج. يزيل عامل التأكيد بعدم null فحص المترجم لكنه لا يثبت وجود قيمة أثناء التشغيل.
data class ReadingItem(val id: String, val title: String, val pages: Int)
fun parseItem(id: String, title: String?, pages: String): ReadingItem? {
val cleanTitle = title?.trim()?.takeIf { it.isNotEmpty() } ?: return null
val count = pages.toIntOrNull()?.takeIf { it >= 0 } ?: return null
return ReadingItem(id, cleanTitle, count)
}
هذا مقتطف Kotlin خالص لا يحتاج واجهات Android. تعني النتيجة null فشل التحقق؛ وينبغي للنموذج الحقيقي ربط الحقل المخفق بخطأ محدد بدلاً من توحيد كل الأخطاء. يفترض هنا أن المستدعي الموثوق يوفر المعرّف.
افصل الهوية عن موضع العرض
يولد data class عمليات مقارنة ونسخ مفيدة انطلاقاً من خصائص المنشئ. تقارن المساواة القيم، بينما يحدد معرّف قاعدة البيانات استمرار السجل عبر التحديثات. لا يحقق فهرس القائمة أياً منهما بثبات لأن التصفية والترتيب ينقلان الصفوف. حدّث العنصر الموافق لمعرّفه الثابت، وأنتج قائمة جديدة ودع الواجهة تعرضها. النسخ بـ copy سطحي؛ فإذا احتوت خاصية على مجموعة قابلة للتعديل فقد تشير النسختان إلى المجموعة نفسها. فضّل العناصر الثابتة وتجنب كشف التفاصيل القابلة للتعديل عبر واجهة المستودع.
data class ShelfItem(val id: String, val title: String, val finished: Boolean)
fun finish(items: List<ShelfItem>, id: String): List<ShelfItem> =
items.map { item ->
if (item.id == id) item.copy(finished = true) else item
}
يحافظ التحويل على الترتيب وجميع المعرّفات الموجودة. إذا لم يطابق أي عنصر تبقى القيم كما هي. يجب على المستدعي نشر القائمة المعادة؛ فتجاهلها لا يحدث الحالة القائمة. حُذفت الاستيرادات ونقطة دخول التطبيق عمداً.
اجعل النتائج صريحة
استخدم sealed interface عندما تملك العملية نتائج مهمة متعددة مثل Loading وReady وFailed. يمكن لتعبير when أن يفرض معالجة كل حالة معروفة، فيقلل تركيبات القيم المنطقية غير المترابطة. تناسب الاستثناءات الإخفاقات الاستثنائية، بينما تستحق نتائج التحقق المتوقعة غالباً قيماً عادية. اجعل الدوال صغيرة بما يكفي لوصفها بجملة واحدة وتجنب إخفاء التعديل داخل تحويلات المجموعات. صياغة lambda موجزة، لكن تسمية النتائج الوسيطة قد تجعل مراجعة التحقق والإلغاء ومعالجة الأخطاء أسهل من سلسلة طويلة من العوامل المعقدة.
طبّق قواعد Kotlin على حدود Android
تمثل إضافات Intent واستجابات JSON وصفوف قاعدة البيانات والحالة المحفوظة حدوداً قد تكون القيم عندها مفقودة أو قديمة أو مشوهة. تحقق عند الدخول وحوّلها إلى نوع مجال مرة واحدة. تحتاج أنواع Java المنصية إلى عناية خاصة لأن قابليتها لـ null قد لا تكون معبّراً عنها بالكامل في Kotlin. لا تستخدم lateinit لمجرد تفادي تحديد الملكية؛ فالوصول إليها قبل التهيئة يثير استثناء. يعبّر حقن المنشئ أو خاصية قابلة لـ null بدورة حياة واضحة غالباً عن العلاقة بصدق أكبر ويسهّل الاختبار المحلي.
الأخطاء الشائعة وإصلاحها
ظهور NullPointerException بعد استعمال !!
- لماذا يحدث؟
- افترض وجود قيمة خارجية دون التحقق منها.
- كيف تصلحه؟
- عالج الغياب بنتيجة تحقق صريحة أو رجوع مبكر.
- كيف تتجنب تكراره؟
- احتفظ بالأنواع القابلة لـ null عند الحدود واختبر الحقول المفقودة.
تتغير قيمة يفترض ثباتها في موضع آخر
- لماذا يحدث؟
- يشير val إلى مجموعة قابلة للتعديل ومشتركة.
- كيف تصلحه؟
- توقف عن كشف التعديل وأعد عناصر منسوخة تُستخدم بثبات.
- كيف تتجنب تكراره؟
- راجع قابلية تعديل الكائن منفصلة عن إعادة إسناد المتغير.
يتغير الصف الخطأ بعد الترتيب
- لماذا يحدث؟
- استُخدم فهرس العرض كهوية دائمة.
- كيف تصلحه؟
- مرّر معرّف العنصر الثابت إلى عملية التحديث.
- كيف تتجنب تكراره؟
- اختبر التحديث بعد التصفية والإضافة وإعادة الترتيب.
اختبر فهمك
1. تتبّع parseItem مع عنوان من مسافات وصفحات بقيمة -3، ثم مع عنوان صحيح وصفحات بقيمة 12.
اعرض الحل المشروح
يعيد الاستدعاء الأول null لأن إزالة المسافات تفرغ العنوان؛ وحتى مع عنوان تُرفض الصفحات السالبة. يعيد الثاني ReadingItem بعنوان منقح واثنتي عشرة صفحة. لا يثير أي مسار استثناء بسبب نص عددي عادي مشوه لأن التحليل يستخدم toIntOrNull.
2. تملك شاشة القيم isLoading وhasError وhasData. اشرح تركيبة غير منطقية وأعد تصميم العقد.
اعرض الحل المشروح
قد تصبح الثلاثة true دون قصد فيغدو العرض ملتبساً. استبدلها بحالة واجهة مغلقة عندما تكون الحالات متنافية. إذا كان المحتوى القديم قد يتعايش مع خطأ تحديث، فنمذج ذلك صراحة كمحتوى مع حالة تحديث بدلاً من افتراض تنافي كل الأوضاع.
أهم ما تعلمته
- تصف قابلية null عقداً وليست إزعاجاً يجب إخفاؤه.
- تصمد الهوية الثابتة أمام التحويلات وإعادة ترتيب الشاشة.
- تمنع أنواع النتائج الصريحة حالات الواجهة المتناقضة.