هندسة وتصميم تطبيقات الأندرويد (تجربتي)

هبر ، مرحباً!

اليوم أريد أن أتحدث عن الهندسة التي أتبعها في تطبيقات Android الخاصة بي. أنا أعتبر Clean Architecture أساسًا ، وأستخدم مكونات Android Architecture (ViewModel و LiveData و LiveEvent) + Kotlin Coroutines كأدوات. المرفقة هي رمز مثال وهمي متاح على GitHub .

تنصل


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

المشكلة: لماذا نحتاج إلى العمارة؟


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

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

الغرض من العمارة والتصميم


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

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

بالنسبة لي ، يجب أن تستوفي الهندسة المعمارية المعايير التالية:

  1. واجهة المستخدم بسيطة قدر الإمكان ولها ثلاث وظائف فقط:
  2. تقديم البيانات للمستخدم. تأتي البيانات جاهزة للعرض. هذه هي الوظيفة الرئيسية لواجهة المستخدم. إليك عناصر واجهة المستخدم والرسوم المتحركة والأجزاء وما إلى ذلك.
  3. . ViewModel LiveData.
  4. . framework, . .
  5. - . .


يظهر الرسم التخطيطي للعمارة في الشكل أدناه:

صورة

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

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

تحتوي طبقة منطق التطبيق على نصوص برمجية للتطبيق نفسه. هنا يتم تحديد جميع اتصالات التطبيق ، وقد تم بناء جوهره.

طبقة api ، android هي مجرد تطبيق محدد لتطبيقنا في بيئة Android. من الناحية المثالية ، يمكن تغيير هذه الطبقة إلى أي شيء.

, , — . . 2- . , . . TDD , . Android, API ..

Android-.

صورة

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

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

المخطط العام للتطبيق كما يلي:

صورة

  1. يتم إنشاء بيئة android (نشاط ، أجزاء ، إلخ).
  2. يتم إنشاء ViewModel (واحد أو أكثر).
  3. يقوم ViewModel بإنشاء النصوص البرمجية الضرورية التي يمكن تشغيلها من هذا النموذج. من الأفضل حقن السيناريوهات مع DI.
  4. يقوم المستخدم بعمل.
  5. يرتبط كل مكون من مكونات واجهة المستخدم بأمر يمكن تشغيله.
  6. يتم تشغيل برنامج نصي بالمعلمات الضرورية ، على سبيل المثال ، Login.execute (تسجيل الدخول وكلمة المرور).
  7. DI , . ( api, ). . , , , REST JSON . , , . , . , . - . , . , , . , , .
  8. . ViewModel, UI. LiveData (.9 10).

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

الأوامر

تبدأ واجهة المستخدم بتنفيذ البرنامج النصي باستخدام الأمر. في مشاريعي أستخدم التنفيذ الخاص بي للأوامر ، فهي ليست جزءًا من المكونات المعمارية أو أي شيء آخر. بشكل عام ، يكون تنفيذها بسيطًا ، حيث أنه من خلال فهم أعمق للفكرة ، يمكنك رؤية تنفيذ الأوامر في Reactiveui.netلـ c #. لسوء الحظ ، لا يمكنني وضع كود العمل الخاص بي ، فقط تنفيذ مبسط على سبيل المثال.

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

مثال


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

  1. يجب أن يكون زر "تسجيل الدخول" غير نشط إذا كان اسم المستخدم وكلمة المرور يحتويان على أقل من 4 أحرف.
  2. يجب أن يكون زر "تسجيل الدخول" غير نشط أثناء عملية تسجيل الدخول.
  3. أثناء إجراء تسجيل الدخول ، يجب عرض مؤشر (محمل).
  4. إذا كان تسجيل الدخول ناجحًا ، فيجب عرض رسالة ترحيب.
  5. إذا كان تسجيل الدخول و / أو كلمة المرور غير صحيحين ، فيجب أن تظهر رسالة خطأ أعلى حقل تسجيل الدخول.
  6. إذا تم عرض رسالة خطأ على الشاشة ، فإن أي إدخال لشخصية في حقول تسجيل الدخول أو كلمة المرور سيزيل هذه الرسالة حتى المحاولة التالية.

يمكنك حل هذه المشكلة بطرق مختلفة ، على سبيل المثال ، عن طريق وضع كل شيء في MainActivity.
لكني أتابع دائمًا تنفيذ قاعدتي الرئيسيتين:

  1. منطق الأعمال مستقل عن التفاصيل.
  2. واجهة المستخدم بسيطة قدر الإمكان. يتعامل مع مهمته فقط (يعرض البيانات التي تم نقلها إليه ، كما يبث أوامر من المستخدم).

هذا ما يبدو عليه التطبيق:

صورة

يبدو MainActivity كما يلي:

class MainActivity : AppCompatActivity() {

   private val vm: MainViewModel by viewModel()

   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       setContentView(R.layout.activity_main)

       bindLoginView()
       bindProgressBar()

       observeAuthorization()
       observeRefreshView()
   }

   private fun bindProgressBar() {
       progressBar.bindVisibleWithCommandIsExecuting(this, vm.loginCommand)
   }

   private fun bindLoginView() {
       loginEdit.bindAfterTextChangedWithCommand(vm.loginValidityCommand)
       passwordEdit.bindAfterTextChangedWithCommand(vm.passwordValidityCommand)

       loginButton.bindCommand(this, vm.loginCommand) {
           LoginParameters(loginEdit.text.toString(), passwordEdit.text.toString())
       }
   }

   private fun observeAuthorization() {
       vm.authorizationSuccessLive.observe(this, Observer {
           showAuthorizeSuccessMsg(it?.data)
       })
       vm.authorizationErrorLive.observe(this, Observer {
           showAuthorizeErrorMsg()
       })
   }

   private fun observeRefreshView() {
       vm.refreshLoginViewLive.observe(this, Observer {
           hideAuthorizeErrorMsg()
       })
   }

   private fun showAuthorizeErrorMsg() {
       loginErrorMsg.isInvisible = false
   }

   private fun hideAuthorizeErrorMsg() {
       loginErrorMsg.isInvisible = true
   }

   private fun showAuthorizeSuccessMsg(name : String?) {
       val msg = getString( R.string.success_login, name)
       Toast.makeText(this, msg, Toast.LENGTH_LONG).show()
   }
}


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

رمز هذا المثال مع التعليقات متاح على GitHub ، إذا كنت مهتمًا ، يمكنك تنزيله والتعرف عليه.

هذا كل شيء ، شكرا للمشاهدة!

All Articles