دليل تطبيق هندسة أندرويد

مرحبا يا هبر! أقدم لكم الترجمة المجانية من "دليل بنية التطبيق" من JetPack . أطلب منك ترك جميع التعليقات على الترجمة في التعليقات ، وسيتم حلها. أيضًا ، ستكون تعليقات من استخدموا الهيكل المقدم مع توصيات لاستخدامه مفيدة للجميع.

يغطي هذا الدليل أفضل الممارسات والهندسة المعمارية الموصى بها لبناء تطبيقات قوية. تفترض هذه الصفحة مقدمة أساسية لبرنامج Android Framework. إذا كنت جديدًا في تطوير تطبيقات Android ، فراجع أدلة مطوري البرامج للبدء ومعرفة المزيد عن المفاهيم المذكورة في هذا الدليل. إذا كنت مهتمًا ببنية التطبيق وترغب في التعرف على المواد الموجودة في هذا الدليل من منظور برمجة Kotlin ، تحقق من دورة Udacity ، "تطوير تطبيقات Android مع Kotlin"

تجربة مستخدم تطبيق الجوال


في معظم الحالات ، تحتوي تطبيقات سطح المكتب على نقطة دخول واحدة من سطح المكتب أو المشغل ، ثم تعمل كعملية أحادية متجانسة. تحتوي تطبيقات Android على بنية أكثر تعقيدًا. يحتوي تطبيق Android النموذجي على العديد من مكونات التطبيق ، بما في ذلك الأنشطة ، الأجزاء ، الخدمات ، ContentProviders ، و BroadcastReceivers .

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

على سبيل المثال ، ضع في اعتبارك ما يحدث عند مشاركة صورة في تطبيق الوسائط الاجتماعية المفضل لديك:

  1. يقوم التطبيق بتشغيل القصد من الكاميرا. يقوم Android بتشغيل تطبيق كاميرا لمعالجة الطلب. في الوقت الحالي ، ترك المستخدم التطبيق للشبكات الاجتماعية ، وتجربته كمستخدم لا تشوبها شائبة.
  2. قد يؤدي تطبيق الكاميرا إلى تشغيل نوايا أخرى ، مثل بدء تشغيل منتقي الملفات ، مما قد يؤدي إلى تشغيل تطبيق آخر.
  3. في النهاية ، يعود المستخدم إلى تطبيق الشبكة الاجتماعية ويشارك الصورة.

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

تذكر أن الأجهزة المحمولة محدودة الموارد أيضًا ، لذلك في أي وقت قد يدمر نظام التشغيل بعض عمليات التطبيق من أجل توفير مساحة لعمليات جديدة.

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

المبادئ المعمارية العامة


إذا كان يجب عليك عدم استخدام مكونات التطبيق لتخزين البيانات وحالة التطبيق ، فكيف يجب عليك تطوير التطبيق الخاص بك؟

تقسيم المسؤولية


أهم مبدأ لمتابعة هو تقاسم المسؤوليات . الخطأ الشائع هو عندما تكتب كل التعليمات البرمجية الخاصة بك في النشاط أو الشظية . هذه هي فئات واجهة المستخدم التي يجب أن تحتوي فقط على منطق معالجة تفاعل واجهة المستخدم ونظام التشغيل. من خلال مشاركة المسؤولية قدر الإمكان في هذه الفئات (SRP) ، يمكنك تجنب العديد من المشكلات المرتبطة بدورة حياة التطبيق.

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


مبدأ آخر مهم هو أنه يجب عليك التحكم في واجهة المستخدم الخاصة بك من نموذج ، ويفضل أن يكون ذلك من نموذج دائم. النماذج هي المكونات المسؤولة عن معالجة البيانات للتطبيق. تكون مستقلة عن كائنات عرض ومكونات التطبيق ، وبالتالي ، فهي لا تتأثر دورة حياة التطبيق والمشاكل ذات الصلة.

النموذج الدائم مثالي للأسباب التالية:

  • لن يفقد المستخدمون البيانات إذا كان نظام التشغيل Android يدمر تطبيقك لتحرير الموارد.
  • يستمر تطبيقك في العمل عندما يكون اتصال الشبكة غير مستقر أو غير متوفر.

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

أوصى تطبيق الهندسة المعمارية


يوضح هذا القسم كيفية هيكلة تطبيق باستخدام المكونات المعمارية ، والعمل في سيناريو الاستخدام من طرف إلى طرف .

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

تخيل أننا نقوم بإنشاء واجهة مستخدم تعرض ملف تعريف المستخدم. نستخدم واجهة برمجة تطبيقات خاصة وواجهة برمجة تطبيقات REST لاسترداد بيانات الملف الشخصي.

نظرة عامة


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



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

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

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


تتكون واجهة المستخدم من جزء UserProfileFragment user_profile_layout.xml تخطيط user_profile_layout.xml المطابق.

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

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

نستخدم UserProfileViewModel بناءً على مكون بنية ViewModel لتخزين هذه المعلومات.

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

الآن حددنا الملفات التالية:

  • user_profile.xml : تخطيط واجهة المستخدم المعرفة.
  • UserProfileFragment : الموصوفة وحدة تحكم واجهة المستخدم المسؤولة عن عرض المعلومات للمستخدم.
  • UserProfileViewModel : فئة مسؤولة عن إعداد البيانات لعرضها في UserProfileFragment والاستجابة لتفاعل المستخدم.

تعرض مقتطفات الشفرة التالية المحتويات الأولية لهذه الملفات. (تم حذف ملف التخطيط للبساطة.)

 class UserProfileViewModel : ViewModel() { val userId : String = TODO() val user : User = TODO() } class UserProfileFragment : Fragment() { private val viewModel: UserProfileViewModel by viewModels() override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { return inflater.inflate(R.layout.main_fragment, container, false) } } 

الآن بعد أن أصبح لدينا وحدات الرموز هذه ، كيف يمكننا توصيلها؟ بعد تعيين حقل المستخدم في فئة UserProfileViewModel ، نحتاج إلى طريقة لإعلام واجهة المستخدم.

المذكرة. يتيح SavedStateHandle لـ ViewModel الوصول إلى الحالة المحفوظة والوسيط الخاص بالجزء أو الإجراء المرتبط.

 // UserProfileViewModel class UserProfileViewModel( savedStateHandle: SavedStateHandle ) : ViewModel() { val userId : String = savedStateHandle["uid"] ?: throw IllegalArgumentException("missing user id") val user : User = TODO() } // UserProfileFragment private val viewModel: UserProfileViewModel by viewModels( factoryProducer = { SavedStateVMFactory(this) } ... ) 

الآن نحتاج إلى إعلام قسمنا عند استلام كائن المستخدم. هذا هو المكان الذي يظهر فيه بنية LiveData.

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

المذكرة. إذا كنت تستخدم بالفعل مكتبات مثل RxJava أو Agera ، فيمكنك الاستمرار في استخدامها بدلاً من LiveData. ومع ذلك ، عند استخدام المكتبات والأساليب المماثلة ، تأكد من أنك تتعامل بشكل صحيح مع دورة حياة التطبيق الخاص بك. على وجه الخصوص ، تأكد من تعليق تدفقات البيانات الخاصة بك عند إيقاف LifecycleOwner المقترنة ، وتدمير هذه التدفقات عند إتلاف LifecycleOwner المرتبط. يمكنك أيضًا إضافة android.arch.lifecycle artifact: تدفقات jet لاستخدام LiveData مع مكتبة دفق نفاثة أخرى مثل RxJava2.

لتضمين مكون LiveData في تطبيقنا ، نقوم بتغيير نوع الحقل في UserProfileViewModel إلى LiveData. UserProfileFragment الآن إعلام UserProfileFragment بتحديثات البيانات. بالإضافة إلى ذلك ، نظرًا لأن حقل LiveData يدعم دورة الحياة ، فإنه يقوم تلقائيًا بمسح الروابط عندما لم تعد هناك حاجة إليها.

 class UserProfileViewModel( savedStateHandle: SavedStateHandle ) : ViewModel() { val userId : String = savedStateHandle["uid"] ?: throw IllegalArgumentException("missing user id") val user : LiveData<User> = TODO() } 

الآن نقوم بتعديل UserProfileFragment لمراقبة البيانات في ViewModel ولتحديث واجهة المستخدم وفقًا للتغييرات:

 override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) viewModel.user.observe(viewLifecycleOwner) { //  UI } } 

في كل مرة يتم فيها تحديث بيانات ملف تعريف المستخدم ، يتم استدعاء رد الاتصال onChanged () ويتم تحديث واجهة المستخدم.

إذا كنت معتادًا على المكتبات الأخرى التي تستخدم عمليات الاسترجاعات القابلة للملاحظة ، فقد تكون قد أدركت أننا لم نعيد تعريف طريقة onStop () الخاصة بالجزء لإيقاف مراقبة البيانات. هذه الخطوة اختيارية لـ LiveData لأنها تدعم دورة الحياة ، مما يعني أنها لن تستدعي رد الاتصال onChanged() إذا كانت القطعة في حالة غير نشطة ؛ بمعنى أنه تلقى مكالمة إلى onStart () ، لكنه لم يتلق بعد onStop() ). يزيل LiveData تلقائيًا المراقب عند استدعاء أسلوب onDestroy () على الجزء.

لم نقم بإضافة أي منطق للتعامل مع تغييرات التكوين ، مثل تدوير شاشة الجهاز من قبل المستخدم. UserProfileViewModel استعادة UserProfileViewModel تلقائيًا عند تغيير التكوين ، وبمجرد إنشاء جزء جديد ، يتلقى نفس مثيل ViewModel ، ويتم استدعاء رد الاتصال فورًا باستخدام البيانات الحالية. نظرًا إلى أن كائنات ViewModel مصممة للبقاء على قيد الحياة مع كائنات View المطابقة التي تقوم بتحديثها ، يجب ألا تقوم بتضمين مراجع مباشرة لعرض الكائنات في تطبيق ViewModel الخاص بك. لمزيد من المعلومات حول عمر ViewModel يتوافق مع دورة حياة مكونات واجهة المستخدم ، راجع ViewModel Life Cycle.

استرجاع البيانات


الآن وقد استخدمنا LiveData لتوصيل UserProfileViewModel بـ UserProfileFragment ، كيف يمكننا الحصول على بيانات ملف تعريف المستخدم؟

في هذا المثال ، نفترض أن الواجهة الخلفية الخاصة بنا توفر واجهة برمجة تطبيقات REST. نستخدم مكتبة Retrofit للوصول إلى الواجهة الخلفية لدينا ، على الرغم من أنه يمكنك استخدام مكتبة مختلفة تخدم نفس الغرض.

فيما يلي تعريفنا Webservice التي ترتبط بواجهة الخلفية الخاصة بنا:

 interface Webservice { /** * @GET declares an HTTP GET request * @Path("user") annotation on the userId parameter marks it as a * replacement for the {user} placeholder in the @GET path */ @GET("/users/{user}") fun getUser(@Path("user") userId: String): Call<User> } 

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

بدلاً من ذلك ، يفوض ViewModel عملية استرجاع البيانات إلى وحدة تخزين جديدة.

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

UserRepository فئة UserRepository بنا ، الموضحة في مقتطف الشفرة التالي ، مثيل WebService لاسترداد بيانات المستخدم:

 class UserRepository { private val webservice: Webservice = TODO() // ... fun getUser(userId: String): LiveData<User> { //    .    . val data = MutableLiveData<User>() webservice.getUser(userId).enqueue(object : Callback<User> { override fun onResponse(call: Call<User>, response: Response<User>) { data.value = response.body() } //     . override fun onFailure(call: Call<User>, t: Throwable) { TODO() } }) return data } } 

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

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

إدارة التبعيات بين المكونات

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

لحل هذه المشكلة ، يمكنك استخدام أنماط التصميم التالية:

  • حقن التبعية (DI) . يسمح حقن التبعية للفئات بتحديد تبعياتها دون إنشائها. في وقت التشغيل ، هناك فئة أخرى مسؤولة عن توفير هذه التبعيات. نوصي باستخدام مكتبة Dagger 2 لتطبيق حقن التبعية في تطبيقات Android. يقوم Dagger 2 تلقائيًا بإنشاء كائنات ، متجاوزة شجرة التبعية ، ويوفر ضمانات وقت التحويل للتبعيات.
  • (موقع الخدمة) محدد موقع الخدمة : يوفر قالب محدد موقع الخدمة سجلاً يمكن للفئات من خلالها الحصول على تبعياتها بدلاً من بنائها.

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

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

يستخدم نموذج التطبيق الخاص بنا Dagger 2 لإدارة تبعيات كائن Webservice .

ربط ViewModel والتخزين


الآن نقوم بتعديل UserProfileViewModel بنا لاستخدام كائن UserRepository :

 class UserProfileViewModel @Inject constructor( savedStateHandle: SavedStateHandle, userRepository: UserRepository ) : ViewModel() { val userId : String = savedStateHandle["uid"] ?: throw IllegalArgumentException("missing user id") val user : LiveData<User> = userRepository.getUser(userId) } 

التخزين المؤقت


UserRepository تطبيق UserRepository استدعاء كائن Webservice ، لكن نظرًا لأنه يعتمد على مصدر بيانات واحد فقط ، فإنه ليس مرنًا جدًا.

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

هذا التصميم غير مثالي للأسباب التالية:

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

لمعالجة أوجه القصور هذه ، قمنا بإضافة مصدر بيانات جديد إلى UserRepository ، والذي يقوم بتخزين كائنات User في الذاكرة:

 // Dagger,        . @Singleton class UserRepository @Inject constructor( private val webservice: Webservice, //    .    . private val userCache: UserCache ) { fun getUser(userId: String): LiveData<User> { val cached = userCache.get(userId) if (cached != null) { return cached } val data = MutableLiveData<User>() userCache.put(userId, data) //     ,  ,  . //      . webservice.getUser(userId).enqueue(object : Callback<User> { override fun onResponse(call: Call<User>, response: Response<User>) { data.value = response.body() } //     . override fun onFailure(call: Call<User>, t: Throwable) { TODO() } }) return data } } 

البيانات الثابتة


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

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

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

الطريقة الصحيحة للتعامل مع هذا الموقف هي استخدام نموذج ثابت. تأتي مكتبة البيانات الدائمة للغرفة (DB) في خدمتنا.

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

المذكرة. إذا كان التطبيق الخاص بك يستخدم بالفعل حلًا آخر ، مثل SQLite Object Relational Mapping (ORM) ، فلن تحتاج إلى استبدال الحل الموجود بـ Room. ومع ذلك ، إذا كنت تكتب تطبيقًا جديدًا أو تعيد تنظيم تطبيق موجود ، فننصحك باستخدام Room لحفظ بيانات التطبيق الخاصة بك. وبالتالي ، يمكنك الاستفادة من تجريد المكتبة والتحقق من الاستعلام.

لاستخدام الغرفة ، نحتاج إلى تحديد التصميم المحلي لدينا. أولاً ، نضيف التعليق التوضيحي @Entity إلى فئة نموذج بيانات User والتعليقات التوضيحية @PrimaryKey في حقل id الفصل. تحدد هذه التعليقات التوضيحية User كجدول في قاعدة البيانات الخاصة بنا ، id هو المفتاح الرئيسي للجدول:

 @Entity data class User( @PrimaryKey private val id: String, private val name: String, private val lastName: String ) 

ثم نقوم بإنشاء فئة قاعدة البيانات من خلال تطبيق RoomDatabase للتطبيق لدينا:

 @Database(entities = [User::class], version = 1) abstract class UserDatabase : RoomDatabase() 

لاحظ أن UserDatabase مجردة. توفر مكتبة Room تلقائيًا تنفيذ هذا. راجع وثائق الغرفة للحصول على التفاصيل.

نحتاج الآن إلى طريقة لإدراج بيانات المستخدم في قاعدة البيانات. لهذه المهمة ، نقوم بإنشاء كائن الوصول إلى البيانات (DAO) .

 @Dao interface UserDao { @Insert(onConflict = REPLACE) fun save(user: User) @Query("SELECT * FROM user WHERE id = :userId") fun load(userId: String): LiveData<User> } 

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

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

بعد تحديد فئة UserDao بنا ، UserDao إلى DAO من فئة قاعدة البيانات لدينا:

 @Database(entities = [User::class], version = 1) abstract class UserDatabase : RoomDatabase() { abstract fun userDao(): UserDao } 

الآن يمكننا تغيير UserRepository لدينا لتشمل مصدر بيانات الغرفة:

 //  Dagger,         . @Singleton class UserRepository @Inject constructor( private val webservice: Webservice, //    .    . private val executor: Executor, private val userDao: UserDao ) { fun getUser(userId: String): LiveData<User> { refreshUser(userId) //   LiveData    . return userDao.load(userId) } private fun refreshUser(userId: String) { //    . executor.execute { // ,      . val userExists = userDao.hasUser(FRESH_TIMEOUT) if (!userExists) { //  . val response = webservice.getUser(userId).execute() //    . //   .  LiveData  , //        . userDao.save(response.body()!!) } } } companion object { val FRESH_TIMEOUT = TimeUnit.DAYS.toMillis(1) } } 

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

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

مصدر الحقيقة الوحيد:

عادةً ما تُرجع نقاط نهاية REST API المختلفة نفس البيانات. على سبيل المثال ، إذا كان للواجهة الخلفية لدينا نقطة نهاية أخرى تُرجع قائمة الأصدقاء ، يمكن أن يأتي كائن المستخدم نفسه من نقطتي نهايتين مختلفتين لواجهة برمجة التطبيقات ، وربما حتى باستخدام مستويات مختلفة من التفاصيل. إذا قمنا UserRepositoryبإرجاع الاستجابة من الطلب Webserviceكما هي ، دون التحقق من الاتساق ، فقد تظهر واجهات المستخدم الخاصة بنا معلومات مربكة ، لأن إصدار وشكل البيانات من وحدة التخزين سيعتمد على آخر نقطة نهاية تسمى.

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

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


في بعض حالات الاستخدام ، مثل السحب إلى التحديث ، من المهم أن تُظهر واجهة المستخدم للمستخدم أن هناك عملية شبكة جارية حاليًا. يوصى بفصل إجراء واجهة المستخدم عن البيانات الفعلية ، حيث يمكن تحديث البيانات لأسباب مختلفة. على سبيل المثال ، إذا حصلنا على قائمة الأصدقاء ، فيمكن تحديد المستخدم نفسه مرة أخرى برمجيًا ، مما سيؤدي إلى تحديث LiveData. من وجهة نظر واجهة المستخدم ، فإن حقيقة وجود طلب في الرحلة ليست سوى نقطة بيانات أخرى ، على غرار أي جزء آخر من البيانات في الكائن نفسه User.

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



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

توضح القائمة التالية كيفية اختبار كل وحدة رمز من مثالنا الموسع:

  • واجهة المستخدم والتفاعل : استخدم مجموعة أدوات اختبار واجهة مستخدم Android . أفضل طريقة لإنشاء هذا الاختبار هي استخدام مكتبة Espresso . يمكنك إنشاء جزء وتزويده بالتخطيط UserProfileViewModel. نظرًا لأن الجزء يرتبط فقط UserProfileViewModel، فإن الاستهزاء (تقليد) من هذه الفئة فقط يكفي لاختبار واجهة المستخدم الخاصة بالتطبيق بالكامل.
  • ViewModel: UserProfileViewModel JUnit . , UserRepository .
  • UserRepository: UserRepository JUnit. Webservice UserDao . :

    • -.
    • .
    • , .

  • Webservice , UserDao , .
  • UserDao: DAO . - , . , , , …

    : Room , DAO, JSQL SupportSQLiteOpenHelper . , SQLite SQLite .
  • -: . , -, . , MockWebServer , .
  • : maven . androidx.arch.core : JUnit:

    • InstantTaskExecutorRule: .
    • CountingTaskExecutorRule: . Espresso .



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

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

تجنب تعيين نقاط إدخال التطبيق الخاص بك - مثل الإجراءات والخدمات وأجهزة استقبال البث - كمصادر للبيانات.

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

إنشاء خطوط واضحة للمسؤولية بين وحدات مختلفة من التطبيق الخاص بك.

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

فضح أقل قدر ممكن من كل وحدة.

قاوم الإغراء لإنشاء ملصق "واحد فقط" يكشف عن تفاصيل التنفيذ الداخلي من وحدة واحدة. قد تربح بعض الوقت على المدى القصير ، لكنك ستتكبد دينًا تقنيًا عدة مرات مع تطور قاعدة الكود.

فكر في كيفية جعل كل وحدة قابلة للاختبار بمعزل عن غيرها.

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

ركز على النواة الفريدة لتطبيقك لتبرز من التطبيقات الأخرى.

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

احتفظ بأكبر قدر ممكن من البيانات الجديدة وذات الصلة.

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

قم بتعيين مصدر بيانات واحد كمصدر حقيقي حقيقي.

كلما احتاج تطبيقك إلى الوصول إلى هذا الجزء من البيانات ، يجب أن يأتي دائمًا من مصدر الحقيقة الوحيد هذا.

إضافة: الكشف عن حالة الشبكة


في القسم أعلاه من بنية التطبيق الموصى بها ، تخطينا أخطاء الشبكة وحالات التمهيد لتبسيط مقتطفات الكود.

يوضح هذا القسم كيفية عرض حالة الشبكة باستخدام فئة الموارد ، والتي تضم كل من البيانات وحالتها.

يوفر مقتطف الشفرة التالي تطبيق مثالResource:

 //  ,         . sealed class Resource<T>( val data: T? = null, val message: String? = null ) { class Success<T>(data: T) : Resource<T>(data) class Loading<T>(data: T? = null) : Resource<T>(data) class Error<T>(message: String, data: T? = null) : Resource<T>(data, message) } 

نظرًا لأن تنزيل البيانات من الشبكة عند عرض نسخة من هذه البيانات يعد ممارسة شائعة ، فمن المفيد إنشاء فئة مساعدة يمكن إعادة استخدامها في عدة أماكن. في هذا المثال ، نقوم بإنشاء فصل يحمل الاسم NetworkBoundResource.

يعرض المخطط التالي شجرة القرار لـ NetworkBoundResource:



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

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

. . , .

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

, , , . , , , . `SUCCESS` , .

API, NetworkBoundResource :

 // ResultType:   . // RequestType:   API. abstract class NetworkBoundResource<ResultType, RequestType> { //      API   . @WorkerThread protected abstract fun saveCallResult(item: RequestType) //      ,  ,    //     . @MainThread protected abstract fun shouldFetch(data: ResultType?): Boolean //        . @MainThread protected abstract fun loadFromDb(): LiveData<ResultType> //     API. @MainThread protected abstract fun createCall(): LiveData<ApiResponse<RequestType>> // ,    .   //    ,    . protected open fun onFetchFailed() {} //   LiveData,  , //    . fun asLiveData(): LiveData<ResultType> = TODO() } 

:

  • , ResultType RequestType , , API, , .
  • ويستخدم فئة ApiResponseلطلبات الشبكة. ApiResponseهو غلاف بسيط لفصل Retrofit2.Callيحول الاستجابات إلى الحالات LiveData.

يظهر التنفيذ الكامل للفئة NetworkBoundResourceكجزء من مشروع GitHub android-Architecture-components .

بمجرد إنشائه ، NetworkBoundResourceيمكننا استخدامه لكتابة تطبيقاتنا المرتبطة بالقرص والشبكة Userفي الفصل UserRepository:

 //  Dagger2,         . @Singleton class UserRepository @Inject constructor( private val webservice: Webservice, private val userDao: UserDao ) { fun getUser(userId: String): LiveData<User> { return object : NetworkBoundResource<User, User>() { override fun saveCallResult(item: User) { userDao.save(item) } override fun shouldFetch(data: User?): Boolean { return rateLimiter.canFetch(userId) && (data == null || !isFresh(data)) } override fun loadFromDb(): LiveData<User> { return userDao.load(userId) } override fun createCall(): LiveData<ApiResponse<User>> { return webservice.getUser(userId) } }.asLiveData() } } 

Source: https://habr.com/ru/post/ar456256/


All Articles