تطوير أندرويد الحديث على Kotlin. الجزء 2

مرحبا يا هبر! أقدم لكم ترجمة المقال " تطوير أندرويد الحديث مع Kotlin (الجزء 2) " للملادين Rakonjac.

ملاحظة هذه المقالة هي ترجمة لسلسلة المقالات من Mladen Rakonjac ، تاريخ المقالة: 09/23/2017. جيثب بدأت في قراءة الجزء الأول من SemperPeritus ، اكتشفت أنه لم تتم ترجمة الجزء الآخر لسبب ما. لذلك ، أوجه انتباهكم إلى الجزء الثاني. تحولت المقالة إلى أن تكون ضخمة.

الصورة

"من الصعب جدًا العثور على مشروع واحد يغطي كل ما هو جديد في التطوير لنظام Android في Android Studio 3.0 ، لذلك قررت كتابته."

في هذه المقالة ، سنقوم بتحليل ما يلي:

  1. Android Studio 3 beta 1 Part 1
  2. Kotlin لغة البرمجة الجزء 1
  3. بناء خيارات الجزء 1
  4. قيد القيد الجزء 1
  5. مكتبة ربط البيانات الجزء 1
  6. MVVM Architecture + Repository + Android Manager Wrappers Pattern
  7. RxJava2 وكيف يساعدنا في العمارة الجزء 3
  8. خنجر 2.11 ، ما هو حقن التبعية ، لماذا يجب عليك استخدام هذا الجزء 4
  9. التحديثية (مع Rx Java2)
  10. غرفة (مع Rx Java2)

MVVM Architecture + Repository + Android Manager Wrappers Pattern


بضع كلمات عن العمارة في عالم أندرويد


لبعض الوقت ، لم يستخدم مطورو Android أي هندسة في مشاريعهم. على مدى السنوات الثلاث الماضية ، ظهر الكثير من الضجيج حولها في مجتمع مطوري نظام Android. لقد مر وقت نشاط الله ونشرت Google مستودع مخططات أندرويد للهندسة المعمارية ، مع العديد من الأمثلة والتعليمات على مختلف الأساليب المعمارية. أخيرًا ، في Google IO '17 ، قاموا بتقديم Android Architecture Components ، وهي مجموعة من المكتبات المصممة لمساعدتنا في إنشاء تعليمات برمجية أنظف وتحسين التطبيقات. يقول مكون يمكنك استخدام كل منهم ، أو واحد منهم فقط. ومع ذلك ، وجدت لهم جميعا مفيدة حقا. كذلك في النص وفي الأجزاء التالية سوف نستخدمها. أولاً ، سأواجه المشكلة في الكود ، وبعد ذلك سأعيد استخدام هذه المكونات والمكتبات لمعرفة المشكلات التي من المفترض حلها.

هناك نموذجان رئيسيان يشتركان في رمز واجهة المستخدم الرسومية:

  • MVP
  • MVVM

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

ما هو نمط MVVM؟


MVVM هو نمط معماري يتوسع كنموذج عرض ViewModel. أعتقد أن هذا الاسم يربك المطورين. إذا كنت الشخص الذي أتى باسمه ، فسأطلق عليه View-ViewModel-Model ، لأن ViewModel في الوسط ، ويربط طريقة العرض والطراز .

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

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

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

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

مثال رمز


الآن سأكتب مثالا بسيطا يوضح كيف يعمل هذا.

للبدء ، دعنا ننشئ نموذجًا بسيطًا يعرض خطًا:

RepoModel.kt
class RepoModel { fun refreshData() : String { return "Some new data" } } 


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

RepoModel.kt
 class RepoModel { fun refreshData(onDataReadyCallback: OnDataReadyCallback) { Handler().postDelayed({ onDataReadyCallback.onDataReady("new data") },2000) } } interface OnDataReadyCallback { fun onDataReady(data : String) } 


أنا خلقت واجهة OnDataReadyCallback مع أسلوب onDataReady . والآن refreshData طريقة refreshData (تنفذ) OnDataReadyCallback . لمحاكاة الانتظار ، استخدم Handler . مرة واحدة onDataReady ثانيتين ، سيتم استدعاء الأسلوب onDataReady على الفئات التي تطبق واجهة OnDataReadyCallback .

لنقم بإنشاء ViewModel :

MainViewModel.kt
 class MainViewModel { var repoModel: RepoModel = RepoModel() var text: String = "" var isLoading: Boolean = false } 


كما ترى ، هناك مثيل لـ RepoModel ، text ، سيتم عرضه ، ومتغير isLoading يقوم بتخزين الحالة الحالية. لنقم بإنشاء طريقة refresh مسؤولة عن استرداد البيانات:

MainViewModel.kt
 class MainViewModel { ... val onDataReadyCallback = object : OnDataReadyCallback { override fun onDataReady(data: String) { isLoading.set(false) text.set(data) } } fun refresh(){ isLoading.set(true) repoModel.refreshData(onDataReadyCallback) } } 


يستدعي أسلوب refresh refreshData على RepoModel ، والذي يأخذ تطبيق OnDataReadyCallback في الوسائط. حسنا ، ولكن ما هو object ؟ عندما تريد تطبيق واجهة أو ترث فئة تمديد دون فئة فرعية ، ستستخدم إعلان كائن . وإذا كنت تريد استخدام هذا كصف مجهول؟ في هذه الحالة ، تستخدم تعبير الكائن :

MainViewModel.kt
 class MainViewModel { var repoModel: RepoModel = RepoModel() var text: String = "" var isLoading: Boolean = false fun refresh() { repoModel.refreshData( object : OnDataReadyCallback { override fun onDataReady(data: String) { text = data }) } } 


عندما نسمي refresh ، يجب أن نغير طريقة العرض إلى حالة التحميل وعندما تصل البيانات ، اضبط isLoading إلى false .

يجب علينا أيضًا استبدال text بـ
 ObservableField<String> 
و isLoading on
 ObservableField<Boolean> 
. يمثل ObservableField فئة من مكتبة ربط البيانات يمكننا استخدامها بدلاً من إنشاء كائن يمكن ملاحظته ، وهو يلف الكائن الذي نريد مراقبته.

MainViewModel.kt
 class MainViewModel { var repoModel: RepoModel = RepoModel() val text = ObservableField<String>() val isLoading = ObservableField<Boolean>() fun refresh(){ isLoading.set(true) repoModel.refreshData(object : OnDataReadyCallback { override fun onDataReady(data: String) { isLoading.set(false) text.set(data) } }) } } 


لاحظ أنني أستخدم val بدلاً من var ، لأننا سنقوم فقط بتغيير القيمة في الحقل ، ولكن ليس الحقل نفسه. وإذا كنت ترغب في التهيئة ، فاستخدم ما يلي:

initobserv.kt
 val text = ObservableField("old data") val isLoading = ObservableField(false) 



دعنا نغير التصميم الخاص بنا حتى يتمكن من ملاحظة النص والتحميل . للبدء ، قم بربط MainViewModel بدلاً من السجل :

activity_main.xml
 <data> <variable name="viewModel" type="me.mladenrakonjac.modernandroidapp.MainViewModel" /> </data> 


ثم:

  • تغيير TextView لمراقبة النص من MainViewModel
  • أضف ProgressBar ، والذي سيكون مرئيًا فقط إذا كان التحميل حقيقيًا
  • إضافة زر ، والتي عند النقر عليها ، ستستدعي طريقة التحديث من MainViewModel ولن تكون قابلة للنقر إلا إذا تم تحميل false

main_activity.xml
 ... <TextView android:id="@+id/repository_name" android:text="@{viewModel.text}" ... /> ... <ProgressBar android:id="@+id/loading" android:visibility="@{viewModel.isLoading ? View.VISIBLE : View.GONE}" ... /> <Button android:id="@+id/refresh_button" android:onClick="@{() -> viewModel.refresh()}" android:clickable="@{viewModel.isLoading ? false : true}" /> ... 


إذا قمت View.VISIBLE and View.GONE cannot be used if View is not imported . حسنًا ، دعنا نستورد:

main_activity.xml
 <data> <import type="android.view.View"/> <variable name="viewModel" type="me.fleka.modernandroidapp.MainViewModel" /> </data> 


حسنا ، فعلت مع التخطيط. الآن الانتهاء من الربط. كما قلت ، يجب أن يكون لدى View مثيل لـ ViewModel :

MainActivity.kt
 class MainActivity : AppCompatActivity() { lateinit var binding: ActivityMainBinding var mainViewModel = MainViewModel() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = DataBindingUtil.setContentView(this, R.layout.activity_main) binding.viewModel = mainViewModel binding.executePendingBindings() } } 


أخيرًا ، يمكننا الركض


يمكنك أن ترى أن البيانات القديمة يتم استبدالها ببيانات جديدة .

كان هذا مثالا بسيطا على MVVM.

ولكن هناك مشكلة واحدة ، دعونا ندير الشاشة


البيانات القديمة محل البيانات الجديدة . كيف هذا ممكن؟ ألق نظرة على دورة حياة النشاط:

دورة حياة النشاط
الصورة

عند تشغيل الهاتف ، تم إنشاء مثيل جديد onCreate() وتم استدعاء طريقة onCreate() . ألق نظرة على نشاطنا:

MainActivity.kt
 class MainActivity : AppCompatActivity() { lateinit var binding: ActivityMainBinding var mainViewModel = MainViewModel() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = DataBindingUtil.setContentView(this, R.layout.activity_main) binding.viewModel = mainViewModel binding.executePendingBindings() } } 


كما ترون ، عند إنشاء مثيل نشاط ، تم إنشاء مثيل MainViewModel أيضًا. هل هو جيد إذا كان لدينا بطريقة ما نفس مثيل MainViewModel لكل نشاط معاد صوغه؟

مقدمة لمكونات دورة الحياة


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

دعونا نرث MainViewModel من ViewModel من مكونات مدركة لدورة الحياة. نحتاج أولاً إلى إضافة مكتبة المكونات التي تدرك دورة الحياة إلى ملف build.gradle الخاص بنا:

بناء
 dependencies { ... implementation "android.arch.lifecycle:runtime:1.0.0-alpha9" implementation "android.arch.lifecycle:extensions:1.0.0-alpha9" kapt "android.arch.lifecycle:compiler:1.0.0-alpha9" 


جعل MainViewModel وراثة ViewModel :

MainViewModel.kt
 package me.mladenrakonjac.modernandroidapp import android.arch.lifecycle.ViewModel class MainViewModel : ViewModel() { ... } 


ستبدو طريقة onCreate () الخاصة بـ MainActivity لدينا كما يلي:

MainActivity.kt
 class MainActivity : AppCompatActivity() { lateinit var binding: ActivityMainBinding override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = DataBindingUtil.setContentView(this, R.layout.activity_main) binding.viewModel = ViewModelProviders.of(this).get(MainViewModel::class.java) binding.executePendingBindings() } } 


لاحظ أننا لم ننشئ مثيلًا جديدًا لبرنامج MainViewModel . سوف نحصل عليه باستخدام ViewModelProviders . ViewModelProviders هي فئة أدوات مساعدة لديها طريقة للحصول على ViewModelProvider . كل شيء عن النطاق . إذا اتصلت بـ ViewModelProviders.of (هذا) في أحد الأنشطة ، فسيظل LiveModel الخاص بك طالما كان هذا النشاط على قيد الحياة (حتى يتم إتلافه دون إعادة إنشائه). لذلك ، إذا قمت باستدعاء هذا في جزء منه ، فسيعيش LiveModel الخاص بك بينما يكون الجزء قيد الحياة ، إلخ. ألقِ نظرة على المخطط:

نطاق دورة الحياة
الصورة

يعد ViewModelProvider مسؤولاً عن إنشاء مثيل جديد في المكالمة الأولى أو إرجاع المكالمة القديمة إذا تمت إعادة إنشاء نشاطك أو شظيتك.

لا تخلط مع

 MainViewModel::class.java 

في Kotlin ، إذا كنت تتبع

 MainViewModel::class 

هذا سيعيدك إلى KClass ، وهي ليست نفس الفئة من Java. لذلك إذا كتبنا . java ، ثم وفقًا للوثائق ، يكون:
سيعود مثيل Java Class المطابق لمثيل KClass هذا
دعونا نرى ما يحدث عند تدوير الشاشة


لدينا نفس البيانات كما كان قبل دوران الشاشة.

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

RepoModel.kt
 class RepoModel { fun refreshData(onDataReadyCallback: OnDataReadyCallback) { Handler().postDelayed({ onDataReadyCallback.onDataReady("new data") },2000) } fun getRepositories(onRepositoryReadyCallback: OnRepositoryReadyCallback) { var arrayList = ArrayList<Repository>() arrayList.add(Repository("First", "Owner 1", 100 , false)) arrayList.add(Repository("Second", "Owner 2", 30 , true)) arrayList.add(Repository("Third", "Owner 3", 430 , false)) Handler().postDelayed({ onRepositoryReadyCallback.onDataReady(arrayList) },2000) } } interface OnDataReadyCallback { fun onDataReady(data : String) } interface OnRepositoryReadyCallback { fun onDataReady(data : ArrayList<Repository>) } 


نحتاج أيضًا إلى وجود طريقة في MainViewModel تستدعي getRepositories من RepoModel :

MainViewModel.kt
 class MainViewModel : ViewModel() { ... var repositories = ArrayList<Repository>() fun refresh(){ ... } fun loadRepositories(){ isLoading.set(true) repoModel.getRepositories(object : OnRepositoryReadyCallback{ override fun onDataReady(data: ArrayList<Repository>) { isLoading.set(false) repositories = data } }) } } 


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

  • قم بإنشاء تخطيط rv_item_repository.xml
  • أضف RecyclerView إلى تخطيط activity_main.xml
  • إنشاء RepositoryRecyclerViewAdapter
  • تثبيت محول في إعادة التدوير

لإنشاء rv_item_repository.xml استخدمت مكتبة CardView ، لذلك نحن بحاجة إلى إضافتها إلى build.gradle (التطبيق):

 implementation 'com.android.support:cardview-v7:26.0.1' 

إليك ما يبدو عليه:

rv_item_repository.xml
 <?xml version="1.0" encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools"> <data> <import type="android.view.View" /> <variable name="repository" type="me.mladenrakonjac.modernandroidapp.uimodels.Repository" /> </data> <android.support.v7.widget.CardView android:layout_width="match_parent" android:layout_height="96dp" android:layout_margin="8dp"> <android.support.constraint.ConstraintLayout android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:id="@+id/repository_name" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginEnd="16dp" android:layout_marginStart="16dp" android:text="@{repository.repositoryName}" android:textSize="20sp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintHorizontal_bias="0.0" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintVertical_bias="0.083" tools:text="Modern Android App" /> <TextView android:id="@+id/repository_has_issues" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginEnd="16dp" android:layout_marginStart="16dp" android:layout_marginTop="8dp" android:text="@string/has_issues" android:textStyle="bold" android:visibility="@{repository.hasIssues ? View.VISIBLE : View.GONE}" app:layout_constraintBottom_toBottomOf="@+id/repository_name" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="1.0" app:layout_constraintStart_toEndOf="@+id/repository_name" app:layout_constraintTop_toTopOf="@+id/repository_name" app:layout_constraintVertical_bias="1.0" /> <TextView android:id="@+id/repository_owner" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginBottom="8dp" android:layout_marginEnd="16dp" android:layout_marginStart="16dp" android:text="@{repository.repositoryOwner}" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/repository_name" app:layout_constraintVertical_bias="0.0" tools:text="Mladen Rakonjac" /> <TextView android:id="@+id/number_of_starts" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginBottom="8dp" android:layout_marginEnd="16dp" android:layout_marginStart="16dp" android:layout_marginTop="8dp" android:text="@{String.valueOf(repository.numberOfStars)}" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="1" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/repository_owner" app:layout_constraintVertical_bias="0.0" tools:text="0 stars" /> </android.support.constraint.ConstraintLayout> </android.support.v7.widget.CardView> </layout> 


الخطوة التالية هي إضافة RecyclerView إلى activity_main.xml . قبل القيام بذلك ، تأكد من إضافة مكتبة RecyclerView:

 implementation 'com.android.support:recyclerview-v7:26.0.1' 

activity_main.xml
 <?xml version="1.0" encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools"> <data> <import type="android.view.View"/> <variable name="viewModel" type="me.fleka.modernandroidapp.MainViewModel" /> </data> <android.support.constraint.ConstraintLayout android:layout_width="match_parent" android:layout_height="match_parent" tools:context="me.fleka.modernandroidapp.MainActivity"> <ProgressBar android:id="@+id/loading" android:layout_width="48dp" android:layout_height="48dp" android:indeterminate="true" android:visibility="@{viewModel.isLoading ? View.VISIBLE : View.GONE}" app:layout_constraintBottom_toTopOf="@+id/refresh_button" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> <android.support.v7.widget.RecyclerView android:id="@+id/repository_rv" android:layout_width="0dp" android:layout_height="0dp" android:indeterminate="true" android:visibility="@{viewModel.isLoading ? View.GONE : View.VISIBLE}" app:layout_constraintBottom_toTopOf="@+id/refresh_button" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" tools:listitem="@layout/rv_item_repository" /> <Button android:id="@+id/refresh_button" android:layout_width="160dp" android:layout_height="40dp" android:layout_marginBottom="8dp" android:layout_marginEnd="8dp" android:layout_marginStart="8dp" android:layout_marginTop="8dp" android:onClick="@{() -> viewModel.loadRepositories()}" android:clickable="@{viewModel.isLoading ? false : true}" android:text="Refresh" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintVertical_bias="1.0" /> </android.support.constraint.ConstraintLayout> </layout> 



لاحظ أننا أزلنا بعض عناصر TextView والآن يقوم الزر بتشغيل loadRepositories بدلاً من التحديث :

button.xml
 <Button android:id="@+id/refresh_button" android:onClick="@{() -> viewModel.loadRepositories()}" ... /> 


دعنا نزيل طريقة التحديث من MainViewModel و refreshData من RepoModel باعتبارها غير ضرورية.

أنت الآن بحاجة إلى إنشاء محول لـ RecyclerView:

RepositoryRecyclerViewAdapter.kt
 class RepositoryRecyclerViewAdapter(private var items: ArrayList<Repository>, private var listener: OnItemClickListener) : RecyclerView.Adapter<RepositoryRecyclerViewAdapter.ViewHolder>() { override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): ViewHolder { val layoutInflater = LayoutInflater.from(parent?.context) val binding = RvItemRepositoryBinding.inflate(layoutInflater, parent, false) return ViewHolder(binding) } override fun onBindViewHolder(holder: ViewHolder, position: Int) = holder.bind(items[position], listener) override fun getItemCount(): Int = items.size interface OnItemClickListener { fun onItemClick(position: Int) } class ViewHolder(private var binding: RvItemRepositoryBinding) : RecyclerView.ViewHolder(binding.root) { fun bind(repo: Repository, listener: OnItemClickListener?) { binding.repository = repo if (listener != null) { binding.root.setOnClickListener({ _ -> listener.onItemClick(layoutPosition) }) } binding.executePendingBindings() } } } 


لاحظ أن ViewHolder يأخذ نسخة من النوع RvItemRepositoryBinding ، بدلاً من View ، حتى نتمكن من تطبيق ربط البيانات في ViewHolder لكل عنصر. لا تشعر بالحرج من وظيفة سطر واحد (على الانترنت):

 override fun onBindViewHolder(holder: ViewHolder, position: Int) = holder.bind(items[position], listener) 

هذا مجرد إدخال قصير لـ:

 override fun onBindViewHolder(holder: ViewHolder, position: Int){ return holder.bind(items[position], listener) } 

والعناصر [موقف] هو تنفيذ لمشغل الفهرس. انها مماثلة ل items.get (الموقف) .

خط آخر قد يربكك:

 binding.root.setOnClickListener({ _ -> listener.onItemClick(layoutPosition) }) 

يمكنك استبدال المعلمة بـ _ إذا كنت لا تستخدمها. لطيف ، هاه؟

لقد أنشأنا المحول ، لكننا لم نطبقه بعد على recyclerView في MainActivity :

MainActivity.kt
 class MainActivity : AppCompatActivity(), RepositoryRecyclerViewAdapter.OnItemClickListener { lateinit var binding: ActivityMainBinding override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = DataBindingUtil.setContentView(this, R.layout.activity_main) val viewModel = ViewModelProviders.of(this).get(MainViewModel::class.java) binding.viewModel = viewModel binding.executePendingBindings() binding.repositoryRv.layoutManager = LinearLayoutManager(this) binding.repositoryRv.adapter = RepositoryRecyclerViewAdapter(viewModel.repositories, this) } override fun onItemClick(position: Int) { TODO("not implemented") //To change body of created functions use File | Settings | File Templates. } } 


قم بتشغيل التطبيق


هذا غريب. ماذا حدث؟

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

لذلك ، كما يجب أن تقوم MainViewModel بإخطار MainActivity بالعناصر الجديدة ، هل يمكننا استدعاء notifyDataSetChanged ؟

لا يمكننا ذلك.

هذا أمر مهم حقًا ، يجب ألا يعرف MainViewModel عن MainActivity على الإطلاق .

MainActivity هو الشخص الذي لديه مثيل MainViewModel ، لذلك يجب أن يستمع للتغييرات ويخطر محول بالتغييرات.

ولكن كيف نفعل ذلك؟

يمكننا مراقبة المستودعات ، لذا بعد تغيير البيانات ، يمكننا تغيير محولنا.

ما هو الخطأ في هذا القرار؟

لنلقِ نظرة على الحالة التالية:

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

حسنًا ، قرارنا ليس جيدًا بما فيه الكفاية.

مقدمة ل LiveData


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

ننفذ في MainViewModel :

MainViewModel.kt
 class MainViewModel : ViewModel() { var repoModel: RepoModel = RepoModel() val text = ObservableField("old data") val isLoading = ObservableField(false) var repositories = MutableLiveData<ArrayList<Repository>>() fun loadRepositories() { isLoading.set(true) repoModel.getRepositories(object : OnRepositoryReadyCallback { override fun onDataReady(data: ArrayList<Repository>) { isLoading.set(false) repositories.value = data } }) } } 


وبدء مراقبة النشاط الرئيسي:

MainActivity.kt
 class MainActivity : LifecycleActivity(), RepositoryRecyclerViewAdapter.OnItemClickListener { private lateinit var binding: ActivityMainBinding private val repositoryRecyclerViewAdapter = RepositoryRecyclerViewAdapter(arrayListOf(), this) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = DataBindingUtil.setContentView(this, R.layout.activity_main) val viewModel = ViewModelProviders.of(this).get(MainViewModel::class.java) binding.viewModel = viewModel binding.executePendingBindings() binding.repositoryRv.layoutManager = LinearLayoutManager(this) binding.repositoryRv.adapter = repositoryRecyclerViewAdapter viewModel.repositories.observe(this, Observer<ArrayList<Repository>> { it?.let{ repositoryRecyclerViewAdapter.replaceData(it)} }) } override fun onItemClick(position: Int) { TODO("not implemented") //To change body of created functions use File | Settings | File Templates. } } 


ماذا تعني الكلمة؟ إذا كانت الوظيفة تحتوي على معلمة واحدة فقط ، فيمكن الحصول على الوصول إلى هذه المعلمة باستخدام الكلمة الأساسية لها. لذلك ، لنفترض أن لدينا تعبير lambda لمضاعفة 2:

 ((a) -> 2 * a) 

يمكن استبداله على النحو التالي:

 (it * 2) 

إذا قمت بتشغيل التطبيق الآن ، يمكنك التأكد من أن كل شيء يعمل


...

لماذا أفضل MVVM على MVP؟



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

...

نمط المستودع


مخطط
الصورة

كما قلت سابقًا ، النموذج هو مجرد اسم مجردة للطبقة حيث نقوم بإعداد البيانات. وعادة ما تحتوي على مستودعات وفئات البيانات. كل فئة كيان (بيانات) لها فئة مستودع متوافقة. على سبيل المثال ، إذا كان لدينا فئتا المستخدم والنشر ، فيجب أن يكون لدينا أيضًا UserRepository و PostRepository . جميع البيانات تأتي من هناك. يجب ألا ندعو مطلقًا مثيل التفضيلات المشتركة أو قاعدة البيانات من View أو ViewModel.

لذلك يمكننا إعادة تسمية RepoModel الخاص بنا إلى GitRepoRepository ، حيث سيأتي GitRepo من مستودع Github وستأتي المستودع من نمط السجل.

RepoRepositories.kt
 class GitRepoRepository { fun getGitRepositories(onRepositoryReadyCallback: OnRepositoryReadyCallback) { var arrayList = ArrayList<Repository>() arrayList.add(Repository("First", "Owner 1", 100, false)) arrayList.add(Repository("Second", "Owner 2", 30, true)) arrayList.add(Repository("Third", "Owner 3", 430, false)) Handler().postDelayed({ onRepositoryReadyCallback.onDataReady(arrayList) }, 2000) } } interface OnRepositoryReadyCallback { fun onDataReady(data: ArrayList<Repository>) } 


حسنًا ، تحصل MainViewModel على قائمة مستودعات Github من GitRepoRepsitories ، لكن من أين تحصل على مستودعات GitRepoRepositories ؟

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

  • الشيء الوحيد الذي يحتاج المستودع إلى معرفته هو وصول البيانات عن بُعد أو محليًا. ليست هناك حاجة لمعرفة كيفية الحصول على هذه البيانات عن بعد أو المحلية.
  • viewmodel الوحيد المطلوب هو البيانات
  • الشيء الوحيد الذي يجب على العرض القيام به هو إظهار هذه البيانات.

عندما بدأت للتو في تطوير نظام Android ، كنت أتساءل كيف تعمل التطبيقات في وضع عدم الاتصال وكيف تعمل مزامنة البيانات. بنية تطبيقية جيدة تتيح لنا القيام بذلك بسهولة. على سبيل المثال ، عندما يتم استدعاء loadRepositories في ViewModel إذا كان هناك اتصال بالإنترنت ، يمكن لـ GitRepoRepositories استلام البيانات من مصدر بيانات بعيد وحفظه في مصدر بيانات محلي. عندما يكون الهاتف غير متصل ، يمكن لـ GitRepoRepository استلام البيانات من التخزين المحلي. لذلك ، يجب أن يكون لدى المستودعات مثيلات RemoteDataSource و LocalDataSource ومعالجة المنطق حيث يجب أن تأتي هذه البيانات.

إضافة مصدر بيانات محلي :

GitRepoLocalDataSource.kt
 class GitRepoLocalDataSource { fun getRepositories(onRepositoryReadyCallback: OnRepoLocalReadyCallback) { var arrayList = ArrayList<Repository>() arrayList.add(Repository("First From Local", "Owner 1", 100, false)) arrayList.add(Repository("Second From Local", "Owner 2", 30, true)) arrayList.add(Repository("Third From Local", "Owner 3", 430, false)) Handler().postDelayed({ onRepositoryReadyCallback.onLocalDataReady(arrayList) }, 2000) } fun saveRepositories(arrayList: ArrayList<Repository>){ //todo save repositories in DB } } interface OnRepoLocalReadyCallback { fun onLocalDataReady(data: ArrayList<Repository>) } 


لدينا هنا طريقتان: الأولى ، التي تُرجع بيانات محلية مزيفة ، والثانية ، لتخزين البيانات الوهمية.

إضافة مصدر بيانات بعيد :

GitRepoRemoteDataSource.kt
 class GitRepoRemoteDataSource { fun getRepositories(onRepositoryReadyCallback: OnRepoRemoteReadyCallback) { var arrayList = ArrayList<Repository>() arrayList.add(Repository("First from remote", "Owner 1", 100, false)) arrayList.add(Repository("Second from remote", "Owner 2", 30, true)) arrayList.add(Repository("Third from remote", "Owner 3", 430, false)) Handler().postDelayed({ onRepositoryReadyCallback.onRemoteDataReady(arrayList) }, 2000) } } interface OnRepoRemoteReadyCallback { fun onRemoteDataReady(data: ArrayList<Repository>) } 


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

الآن يمكننا إضافة بعض المنطق إلى مستودعنا:

GitRepoRepository.kt
 class GitRepoRepository { val localDataSource = GitRepoLocalDataSource() val remoteDataSource = GitRepoRemoteDataSource() fun getRepositories(onRepositoryReadyCallback: OnRepositoryReadyCallback) { remoteDataSource.getRepositories( object : OnRepoRemoteReadyCallback { override fun onDataReady(data: ArrayList<Repository>) { localDataSource.saveRepositories(data) onRepositoryReadyCallback.onDataReady(data) } }) } } interface OnRepositoryReadyCallback { fun onDataReady(data: ArrayList<Repository>) } 


وبالتالي ، تقاسم المصادر ، ونحن بسهولة حفظ البيانات محليا.

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

...

غلاف مدير أندرويد


ماذا لو كنت تريد التحقق من اتصالك بالإنترنت في GitRepoRepository لمعرفة من أين تطلب البيانات من؟ قلنا بالفعل أنه لا يجب وضع أي كود يتعلق بنظام Android في ViewModel و Model ، فكيف نتعامل مع هذه المشكلة؟

دعنا نكتب غلاف للاتصال بالإنترنت:

NetManager.kt (ينطبق حل مشابه على المديرين الآخرين ، على سبيل المثال ، على NfcManager)
 class NetManager(private var applicationContext: Context) { private var status: Boolean? = false val isConnectedToInternet: Boolean? get() { val conManager = applicationContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager val ni = conManager.activeNetworkInfo return ni != null && ni.isConnected } } 


لن يعمل هذا الرمز إلا إذا أضفنا إذنًا للبيان:

 <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" /> 

ولكن كيف لإنشاء مثيل في مستودع، إذا لم يكن لدينا سياق ( السياق ويمكننا طلبها في المنشئ:

GitRepoRepository.kt
 class GitRepoRepository (context: Context){ val localDataSource = GitRepoLocalDataSource() val remoteDataSource = GitRepoRemoteDataSource() val netManager = NetManager(context) fun getRepositories(onRepositoryReadyCallback: OnRepositoryReadyCallback) { remoteDataSource.getRepositories(object : OnRepoRemoteReadyCallback { override fun onDataReady(data: ArrayList<Repository>) { localDataSource.saveRepositories(data) onRepositoryReadyCallback.onDataReady(data) } }) } } interface OnRepositoryReadyCallback { fun onDataReady(data: ArrayList<Repository>) } 


أنشأنا قبل مثيل GitRepoRepository الجديد في ViewModel. كيف يمكننا الآن الحصول على NetManager في ViewModel عندما نحتاج إلى سياق لـ NetManager ؟ يمكنك استخدام AndroidViewModel من مكتبة مكونات Lifecycle ، التي لها سياق . هذا هو سياق التطبيق ، وليس النشاط.

MainViewModel.kt
 class MainViewModel : AndroidViewModel { constructor(application: Application) : super(application) var gitRepoRepository: GitRepoRepository = GitRepoRepository(NetManager(getApplication())) val text = ObservableField("old data") val isLoading = ObservableField(false) var repositories = MutableLiveData<ArrayList<Repository>>() fun loadRepositories() { isLoading.set(true) gitRepoRepository.getRepositories(object : OnRepositoryReadyCallback { override fun onDataReady(data: ArrayList<Repository>) { isLoading.set(false) repositories.value = data } }) } } 


في هذا الخط

 constructor(application: Application) : super(application) 

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

ملاحظة: يمكننا التخلص من سطر واحد إذا فعلنا:

 class MainViewModel(application: Application) : AndroidViewModel(application) { ... } 

والآن بعد أن أصبح لدينا مثيل NetManager في GitRepoRepository ، يمكننا التحقق من اتصال الإنترنت:

GitRepoRepository.kt
 class GitRepoRepository(val netManager: NetManager) { val localDataSource = GitRepoLocalDataSource() val remoteDataSource = GitRepoRemoteDataSource() fun getRepositories(onRepositoryReadyCallback: OnRepositoryReadyCallback) { netManager.isConnectedToInternet?.let { if (it) { remoteDataSource.getRepositories(object : OnRepoRemoteReadyCallback { override fun onRemoteDataReady(data: ArrayList<Repository>) { localDataSource.saveRepositories(data) onRepositoryReadyCallback.onDataReady(data) } }) } else { localDataSource.getRepositories(object : OnRepoLocalReadyCallback { override fun onLocalDataReady(data: ArrayList<Repository>) { onRepositoryReadyCallback.onDataReady(data) } }) } } } } interface OnRepositoryReadyCallback { fun onDataReady(data: ArrayList<Repository>) } 


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

ملاحظة على Kotlin : مشغل تسمح بالتحقق من لاغية وإرجاع القيمة في تكنولوجيا المعلومات .

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

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

PS I غيرت رأيي حول مخطط ( المخططون ). قررت تغطية هذا في المقالات التالية.

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


All Articles