Android Kotlin Fundamentals 05.1: Viewmodel وViewmodelMan

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

شاشة العنوان

شاشة اللعبة

شاشة النتائج

مقدمة

في هذا الدرس التطبيقي حول الترميز، ستتعرّف على أحد "مكوّنات Android للبنية" ViewModel:

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

ما يجب معرفته

  • كيفية إنشاء تطبيقات Android الأساسية في Kotlin.
  • كيفية استخدام الرسم البياني للتنقل لتنفيذ التنقل في تطبيقك
  • كيفية إضافة رمز للتنقّل بين وجهات تطبيقك وتمرير البيانات بين وجهات التنقّل
  • آلية عمل النشاط ومراحل النشاط المجزأة.
  • كيفية إضافة معلومات التسجيل إلى أحد التطبيقات وقراءة السجلّات باستخدام Logcat في "استوديو Android".

ما ستتعرَّف عليه

  • كيفية استخدام بنية تطبيق Android الموصى بها.
  • طريقة استخدام صفوف Lifecycle وViewModel وViewModelFactory في تطبيقك.
  • كيفية الاحتفاظ ببيانات واجهة المستخدم من خلال تغييرات ضبط الجهاز.
  • نمط تصميم طريقة المصنع وكيفية استخدامه.
  • كيفية إنشاء عنصر ViewModel باستخدام الواجهة ViewModelProvider.Factory

الإجراءات التي ستنفذّها

  • أضِف ViewModel إلى التطبيق لحفظ بيانات التطبيق حتى تظل البيانات نشطة.
  • استخدِم ViewModelFactory ونمط التصميم على الإعدادات الأصلية لإنشاء عنصر ViewModel باستخدام معلّمات دالة الإنشاء.

في الدرس التطبيقي حول الترميز، يمكنك تطوير تطبيق GuessTheWord الذي يبدأ برمز التفعيل. GuessTheWord هي لعبة من فئة charades ثنائية اللاعبين، حيث يتعاون اللاعبون لتحقيق أعلى نتيجة ممكنة.

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

لتشغيل اللعبة، يفتح المشغِّل الأول التطبيق على الجهاز ويرى كلمة، على سبيل المثال "غيتار&"& كما يظهر في لقطة الشاشة أدناه.

يمارس اللاعب الأول الكلمة، مع الحرص على عدم قول الكلمة نفسها في الواقع.

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

في هذه المَهمّة، يمكنك تنزيل تطبيق المبتدئين وتشغيله وفحص الرمز.

الخطوة 1: تنفيذ الخطوات الأولى

  1. نزِّل رمز GuessThe Word Starter وافتح المشروع في "استوديو Android".
  2. شغِّل التطبيق على جهاز يعمل بنظام التشغيل Android أو محاكي.
  3. انقر على الأزرار. لاحظ أن الزر تخط يعرض الكلمة التالية ويقلل النتيجة بمقدار كلمة واحدة، ويعرض الزر حسنًا الكلمة التالية ويزيد النتيجة بمقدار كلمة رئيسية واحدة. لم يتم تنفيذ زر إنهاء اللعبة، لذا لن يحدث أي شيء عند النقر عليها.

الخطوة 2: إجراء جولة تفصيلية حول الرمز

  1. في Android Studio، استكشِف الرمز للاطّلاع على آلية عمل التطبيق.
  2. احرص على الاطّلاع على الملفات الموضّحة أدناه، وهي مهمة بشكل خاص.

MainActivity.kt

يحتوي هذا الملف على رمز تلقائي تم إنشاؤه بواسطة النموذج فقط.

res/Layout/main_activity.xml

يحتوي هذا الملف على التنسيق الرئيسي للتطبيق. يستضيف NavHostFragment الأجزاء الأخرى أثناء تنقّل المستخدم في التطبيق.

أجزاء واجهة المستخدم

يتألف رمز التفعيل من ثلاثة أجزاء في ثلاث حِزم مختلفة ضمن حزمة com.example.android.guesstheword.screens:

  • title/TitleFragment لشاشة العنوان
  • game/GameFragment على شاشة اللعبة
  • score/ScoreFragment لشاشة النتائج

screen/title/TitleFragment.kt

جزء العنوان هو أول شاشة يتم عرضها عند تشغيل التطبيق. تم ضبط معالج النقر على الزر تشغيل للانتقال إلى شاشة اللعبة.

شاشات/لعبة/GameFragment.kt

هذا هو الجزء الرئيسي، الذي يحدث فيه معظم محتوى اللعبة:

  • يتم تحديد المتغيرات للكلمة الحالية والنتيجة الحالية.
  • wordList المُحدَّدة داخل طريقة resetList() هي نموذج لقائمة الكلمات التي سيتم استخدامها في اللعبة.
  • الطريقة onSkip() هي معالج النقر على الزر تخطّي. ويؤدي ذلك إلى خفض النتيجة بمقدار 1، ثم عرض الكلمة التالية باستخدام طريقة nextWord().
  • الطريقة onCorrect() هي معالج النقر على الزر حسنًا. يتم تطبيق هذه الطريقة بالطريقة نفسها التي تتّبعها طريقة onSkip(). الفرق الوحيد هو أن هذه الطريقة تضيف 1 إلى النتيجة بدلاً من طرحها.

screen/score/ScoreFragment.kt

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

res/Navigation/main_Navigation.xml.

يوضّح الرسم البياني للتنقل كيفية ربط الأجزاء من خلال التنقّل:

  • ويمكن أن ينتقل المستخدم إلى جزء اللعبة من جزء العنوان.
  • ومن جزء اللعبة، يمكن للمستخدم الانتقال إلى جزء النتيجة.
  • ومن جزء النتيجة، يمكن للمستخدم الانتقال مرة أخرى إلى جزء اللعبة.

في هذه المهمة، ستجد مشاكل في تطبيق GuessTheWord مبتدئ.

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

مشاكل في التطبيق:

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

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

بنية التطبيق

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

يتبع تطبيق GuessTheWord مبدأ تصميم فصل المخاوف وينقسم إلى فئات، وتتعامل كل فئة مع المخاوف المنفصلة. في هذا الدرس التطبيقي حول الترميز الأول للدرس، تتضمن الصفوف التي تعمل معها وحدة تحكُّم في واجهة المستخدم وViewModel وViewModelFactory.

وحدة تحكُّم واجهة المستخدم

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

في رمز GuessTheWord للمبتدئين، تكون وحدات التحكّم في واجهة المستخدم هي الأجزاء الثلاثة: GameFragment وScoreFragment, وTitleFragment. وفقًا لمبدأ التصميم والفصل عن المخاوف والمشاكل، فإن GameFragment مسؤول فقط عن رسم عناصر اللعبة والشاشة عندما ينقر المستخدم على الأزرار فقط. عندما ينقر المستخدم على زر، يتم تمرير هذه المعلومات إلى GameViewModel.

نموذج العرض

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

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

عرض النماذج

ينشئ ViewModelFactory مثيلات ViewModel، سواء كانت تحتوي على معلمات إنشاء أو بدونها.

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

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

في هذه المهمة، يمكنك إضافة أول ViewModel إلى تطبيقك، وGameViewModel لـ GameFragment. ويمكنك أيضًا التعرّف على معنى ViewModel عندما يكون واعيًا بمراحل الحياة.

الخطوة 1: إضافة فئة GameViewmodel

  1. افتح ملف build.gradle(module:app). داخل الكتلة dependencies، أضِف تبعية Gradle إلى ViewModel، و

    إذا كنت تستخدم أحدث إصدار من المكتبة، يجب أن يجمع تطبيق الحل كما هو متوقع. وإذا لم يحدث ذلك، جرِّب حل المشكلة، أو ارجع إلى الإصدار الموضّح أدناه.
//ViewModel
implementation 'androidx.lifecycle:lifecycle-extensions:2.0.0'
  1. في المجلد screens/game/، أنشِئ فئة Kotlin جديدة باسم GameViewModel.
  2. جعل الصف GameViewModel يعمل على تمديد الصف التجريدي ViewModel.
  3. لمساعدتك في فهم كيفية فهم ViewModel لمراحل النشاط بشكل أفضل، يمكنك إضافة حظر init باستخدام عبارة log.
class GameViewModel : ViewModel() {
   init {
       Log.i("GameViewModel", "GameViewModel created!")
   }
}

الخطوة 2: تجاوز onCleared() وإضافة تسجيل

يتم تلف ViewModel عند فصل الجزء المرتبط أو عند انتهاء النشاط. قبل تدمير ViewModel مباشرة، يتم استدعاء استدعاء onCleared() لتنظيف الموارد.

  1. في الصف GameViewModel، يجب إلغاء الطريقة onCleared().
  2. يمكنك إضافة بيان سجل داخل onCleared() لتتبّع مراحل نشاط GameViewModel.
override fun onCleared() {
   super.onCleared()
   Log.i("GameViewModel", "GameViewModel destroyed!")
}

الخطوة 3: ربط GameViewmodel بجزء اللعبة

يجب ربط ViewModel بوحدة تحكّم في واجهة المستخدم. ولربط الاثنين، يمكنك إنشاء مرجع إلى ViewModel داخل وحدة التحكُّم في واجهة المستخدم.

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

  1. في الصف GameFragment، أضِف حقلاً من النوع GameViewModel في المستوى العلوي على أنه متغيّر الفئة.
private lateinit var viewModel: GameViewModel

الخطوة 4: إعداد طريقة العرض

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

آلية عمل تطبيق ViewModelProvider:

  • تعرض ViewModelProvider القيمة ViewModel الحالية إذا كانت موجودة، أو تنشئ رمزًا جديدًا إذا لم تكن موجودة.
  • تنشئ السمة ViewModelProvider مثيل ViewModel مرتبطًا بالنطاق المحدّد (نشاط أو جزء).
  • يتم الاحتفاظ بـ ViewModel التي تم إنشاؤها طالما كان النطاق نشطًا. على سبيل المثال، إذا كان النطاق عبارة عن جزء، يتم الاحتفاظ بـ ViewModel حتى يتم فصل الجزء.

عليك إعداد ViewModel، باستخدام طريقة ViewModelProviders.of() لإنشاء ViewModelProvider:

  1. في الصف GameFragment، عليك إعداد المتغيّر viewModel. ضع هذا الرمز داخل onCreateView()، بعد تعريف متغير الربط. استخدِم الطريقة ViewModelProviders.of()، وأجِب السياق GameFragment المرتبط والصف GameViewModel.
  2. فوق إعداد الكائن ViewModel، عليك إضافة عبارة سجلّ لتسجيل استدعاء طريقة ViewModelProviders.of().
Log.i("GameFragment", "Called ViewModelProviders.of")
viewModel = ViewModelProviders.of(this).get(GameViewModel::class.java)
  1. شغِّل التطبيق. في "استوديو Android"، افتح الجزء Logcat واختَر الفلتر Game. انقر على زر تشغيل على جهازك أو المحاكي. يتم فتح شاشة اللعبة.

    كما هو موضّح في Logcat، فإن طريقة onCreateView() في GameFragment تتطلب الطريقة ViewModelProviders.of() لإنشاء GameViewModel. تظهر بيانات التسجيل التي أضفتها إلى GameFragment وGameViewModel في Logcat.

  1. فعِّل إعداد "التدوير التلقائي" على جهازك أو المحاكي واغيِّر اتجاه الشاشة بضع مرات. يتم محو GameFragment وإعادة إنشائها في كل مرة، لذلك يتم استدعاء ViewModelProviders.of() في كل مرة. ولكن يتم إنشاء GameViewModel مرة واحدة فقط، ولا تتم إعادة إنشائه أو محوه لكل مكالمة.
I/GameFragment: Called ViewModelProviders.of
I/GameViewModel: GameViewModel created!
I/GameFragment: Called ViewModelProviders.of
I/GameFragment: Called ViewModelProviders.of
I/GameFragment: Called ViewModelProviders.of
  1. اخرج من اللعبة أو انتقِل من جزء اللعبة. تم تدمير GameFragment. سيتم أيضًا محو GameViewModel المرتبطة واستدعاء onCleared().
I/GameFragment: Called ViewModelProviders.of
I/GameViewModel: GameViewModel created!
I/GameFragment: Called ViewModelProviders.of
I/GameFragment: Called ViewModelProviders.of
I/GameFragment: Called ViewModelProviders.of
I/GameViewModel: GameViewModel destroyed!

يظل ViewModel ساريًا على تغييرات الضبط، لذا فهو مكان جيد للبيانات التي تحتاج إلى تجاوز تغييرات الضبط:

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

للمقارنة، في ما يلي كيفية معالجة بيانات واجهة مستخدم GameFragment في تطبيق إجراء التفعيل قبل إضافة ViewModel، وبعد إضافة ViewModel:

  • قبل إضافة ViewModel:
    عندما يخضع التطبيق لتغيير في الضبط، مثل تدوير الشاشة، يتم إتلاف جزء اللعبة وإعادة إنشائه. يتم فقدان البيانات.
  • بعد إضافة ViewModel ونقل بيانات واجهة مستخدم اللعبة المجزأة's إلى ViewModel:
    أصبحت جميع البيانات التي يحتاج إليها الجزء للعرض الآن ViewModel. عند خضوع التطبيق لعملية تغيير في الإعداد، سيتم الإبقاء على ViewModel، ويتم الاحتفاظ بالبيانات.

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

الخطوة 1: نقل حقول البيانات ومعالجة البيانات إلى مشاهدة النموذج

نقل حقول البيانات التالية وطرقها من GameFragment إلى GameViewModel:

  1. نقل حقول البيانات word وscore وwordList تأكَّد من أن word وscore ليسا private.

    لا تنقل المتغير المتغير المُلزِم، GameFragmentBinding لأنه يحتوي على مراجع إلى الملفات الشخصية. يُستخدَم هذا المتغير في تضخيم التنسيق وإعداد مستمعي النقرات وعرض البيانات على الشاشة - وهي مسؤوليات الكسر.
  2. انقل طريقتَي resetList() وnextWord(). تحدِّد هذه الطرق الكلمات التي يجب عرضها على الشاشة.
  3. من داخل طريقة onCreateView()، انقل الطريقة التي تم استدعاءها إلى resetList() وnextWord() إلى كتلة init من GameViewModel.

    يجب أن تكون هذه الطرق في الكتلة init، لأنّه يجب إعادة ضبط قائمة الكلمات عند إنشاء ViewModel، وليس في كل مرة يتم فيها إنشاء الجزء. يمكنك حذف عبارة السجلّ في جزء init من GameFragment.

تحتوي معالجات النقرة onSkip() وonCorrect() في GameFragment على رمز لمعالجة البيانات وتعديل واجهة المستخدم. ويجب أن يظل الرمز المخصّص لتحديث واجهة المستخدم في الجزء، ولكن يجب نقل رمز معالجة البيانات إلى ViewModel.

وفي الوقت الحالي، يمكنك وضع الطرق نفسها في كلتا الطريقتين:

  1. انسخ طريقة onSkip() وonCorrect() من GameFragment إلى GameViewModel.
  2. في GameViewModel، تأكَّد من أنّ onSkip() وonCorrect() طريقةً غير private، لأنك ستشير إلى هاتين الطريقتين من الكسر.

إليك رمز صف GameViewModel، بعد إعادة البناء:

class GameViewModel : ViewModel() {
   // The current word
   var word = ""
   // The current score
   var score = 0
   // The list of words - the front of the list is the next word to guess
   private lateinit var wordList: MutableList<String>

   /**
    * Resets the list of words and randomizes the order
    */
   private fun resetList() {
       wordList = mutableListOf(
               "queen",
               "hospital",
               "basketball",
               "cat",
               "change",
               "snail",
               "soup",
               "calendar",
               "sad",
               "desk",
               "guitar",
               "home",
               "railway",
               "zebra",
               "jelly",
               "car",
               "crow",
               "trade",
               "bag",
               "roll",
               "bubble"
       )
       wordList.shuffle()
   }

   init {
       resetList()
       nextWord()
       Log.i("GameViewModel", "GameViewModel created!")
   }
   /**
    * Moves to the next word in the list
    */
   private fun nextWord() {
       if (!wordList.isEmpty()) {
           //Select and remove a word from the list
           word = wordList.removeAt(0)
       }
       updateWordText()
       updateScoreText()
   }
 /** Methods for buttons presses **/
   fun onSkip() {
       if (!wordList.isEmpty()) {
           score--
       }
       nextWord()
   }

   fun onCorrect() {
       if (!wordList.isEmpty()) {
           score++
       }
       nextWord()
   }

   override fun onCleared() {
       super.onCleared()
       Log.i("GameViewModel", "GameViewModel destroyed!")
   }
}

إليك رمز الصف GameFragment، بعد إعادة البناء:

/**
* Fragment where the game is played
*/
class GameFragment : Fragment() {


   private lateinit var binding: GameFragmentBinding


   private lateinit var viewModel: GameViewModel


   override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                             savedInstanceState: Bundle?): View? {

       // Inflate view and obtain an instance of the binding class
       binding = DataBindingUtil.inflate(
               inflater,
               R.layout.game_fragment,
               container,
               false
       )

       Log.i("GameFragment", "Called ViewModelProviders.of")
       viewModel = ViewModelProviders.of(this).get(GameViewModel::class.java)

       binding.correctButton.setOnClickListener { onCorrect() }
       binding.skipButton.setOnClickListener { onSkip() }
       updateScoreText()
       updateWordText()
       return binding.root

   }


   /** Methods for button click handlers **/

   private fun onSkip() {
       if (!wordList.isEmpty()) {
           score--
       }
       nextWord()
   }

   private fun onCorrect() {
       if (!wordList.isEmpty()) {
           score++
       }
       nextWord()
   }


   /** Methods for updating the UI **/

   private fun updateWordText() {
       binding.wordText.text = word
   }

   private fun updateScoreText() {
       binding.scoreText.text = score.toString()
   }
}

الخطوة 2: تعديل المراجع لمعالجات النقرات وحقول البيانات في GameFragment

  1. في GameFragment، عدِّل طريقتَي onSkip() وonCorrect(). أزِل الرمز لتعديل النتيجة واستدعِ طريقتَي onSkip() وonCorrect() المقابلتَين على viewModel.
  2. بما أنك نقلت طريقة nextWord() إلى ViewModel، لم يعد بإمكان جزء اللعبة الوصول إليه.

    في GameFragment، بالطريقة onSkip() وonCorrect()، استبدِل المكالمة بـ nextWord() بupdateScoreText() وupdateWordText(). تعرض هذه الطرق البيانات على الشاشة.
private fun onSkip() {
   viewModel.onSkip()
   updateWordText()
   updateScoreText()
}
private fun onCorrect() {
   viewModel.onCorrect()
   updateScoreText()
   updateWordText()
}
  1. في GameFragment، عليك تعديل المتغيّرَين score وword لاستخدام المتغيّرات GameViewModel، لأن هذه المتغيّرَين متوفّرة الآن في GameViewModel.
private fun updateWordText() {
   binding.wordText.text = viewModel.word
}

private fun updateScoreText() {
   binding.scoreText.text = viewModel.score.toString()
}
  1. في GameViewModel، ضمن طريقة nextWord()، أزِل الاستدعاءات إلى طريقتي updateWordText() وupdateScoreText(). يتم استدعاء هذه الطرق الآن من GameFragment.
  2. أنشِئ التطبيق وتأكَّد من عدم حدوث أي أخطاء. إذا كانت لديك أخطاء، نظِّف المشروع وأعِد إنشائه.
  3. يمكنك تشغيل التطبيق وتشغيل اللعبة من خلال بعض الكلمات. عندما تكون في شاشة اللعبة، عليك تدوير الجهاز. لاحظ أن النتيجة الحالية والكلمة الحالية يتم الاحتفاظ بها بعد تغيير الاتجاه.

رائع يتم الآن تخزين جميع بيانات تطبيقك في ViewModel، لذا يتم الاحتفاظ بها أثناء تغييرات الضبط.

في هذه المهمة، يتم تنفيذ أداة معالجة النقرات على زر إنهاء اللعبة.

  1. في GameFragment، أضِف طريقة باسم onEndGame(). سيتم استدعاء طريقة onEndGame() عندما ينقر المستخدم على زر إنهاء اللعبة.
private fun onEndGame() {
   }
  1. في GameFragment، ضمن طريقة onCreateView()، حدِّد مكان الرمز الذي يضبط أدوات معالجة النقرات من أجل الزرين حسنًا والتخطّي. وأسفل هذين السطرين مباشرةً، اضبط أداة معالجة نقرة على زر إنهاء اللعبة. استخدِم المتغير المُلزِم، binding. داخل المستمع عند النقر، يمكنك استدعاء طريقة onEndGame().
binding.endGameButton.setOnClickListener { onEndGame() }
  1. في GameFragment، أضِف طريقة باسم gameFinished() للانتقال إلى شاشة النتائج في التطبيق. أدخِل النتيجة كوسيطة باستخدام الوسيطات الآمنة.
/**
* Called when the game is finished
*/
private fun gameFinished() {
   Toast.makeText(activity, "Game has just finished", Toast.LENGTH_SHORT).show()
   val action = GameFragmentDirections.actionGameToScore()
   action.score = viewModel.score
   NavHostFragment.findNavController(this).navigate(action)
}
  1. في الطريقة onEndGame()، عليك استدعاء الطريقة gameFinished().
private fun onEndGame() {
   gameFinished()
}
  1. يمكنك تشغيل التطبيق وتشغيل اللعبة والتبديل بين الكلمات. انقر على الزر إنهاء اللعبة. لاحظ أن التطبيق ينتقل إلى شاشة النتائج، ولكن لا يتم عرض النتيجة النهائية. يمكنك حل هذه المشكلة في المهمة التالية.

وعندما ينهي المستخدم اللعبة، لا تعرض ScoreFragment النتيجة. تريد أن يحتفظ ViewModel بالنتيجة ليتم عرضها بواسطة ScoreFragment. سيتم تمرير قيمة النتيجة أثناء إعداد ViewModel باستخدام نمط طريقة المصنع.

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

في هذه المَهمّة، يمكنك إنشاء ViewModel مع مُنشئ معلّمات لجزء النتيجة وطريقة المصنع لإنشاء مثيل ViewModel.

  1. ضمن حزمة score، أنشِئ فئة Kotlin جديدة باسم ScoreViewModel. سيكون هذا الصف ViewModel لجزء النتيجة.
  2. تمديد فئة ScoreViewModel من ViewModel. إضافة معلمة دالة إنشاء للنتيجة النهائية. أضِف كتلة init باستخدام بيان سجل.
  3. في الصف ScoreViewModel، يمكنك إضافة متغيّر باسم score لحفظ النتيجة النهائية.
class ScoreViewModel(finalScore: Int) : ViewModel() {
   // The final score
   var score = finalScore
   init {
       Log.i("ScoreViewModel", "Final score is $finalScore")
   }
}
  1. ضمن حزمة score، أنشِئ فئة Kotlin أخرى باسم ScoreViewModelFactory. ستكون هذه الفئة مسؤولة عن إنشاء مثيل ScoreViewModel.
  2. تمديد فترة ScoreViewModelFactory للصف من ViewModelProvider.Factory. أضِف معلّم وضع للنتيجة النهائية.
class ScoreViewModelFactory(private val finalScore: Int) : ViewModelProvider.Factory {
}
  1. في ScoreViewModelFactory، يعرض "استوديو Android" خطأ عن عضو مجرّد غير مُطبّق. لحل الخطأ، يمكنك إلغاء طريقة create(). في طريقة create()، اعرض العنصر ScoreViewModel الذي تم إنشاؤه حديثًا.
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
   if (modelClass.isAssignableFrom(ScoreViewModel::class.java)) {
       return ScoreViewModel(finalScore) as T
   }
   throw IllegalArgumentException("Unknown ViewModel class")
}
  1. في ScoreFragment، أنشئ متغيّرات الفئة لـ ScoreViewModel وScoreViewModelFactory.
private lateinit var viewModel: ScoreViewModel
private lateinit var viewModelFactory: ScoreViewModelFactory
  1. في ScoreFragment، داخل onCreateView()، بعد إعداد المتغيّر binding، عليك إعداد viewModelFactory. استخدِم ScoreViewModelFactory. تمرير النتيجة النهائية من حِزمة الوسيطة كمعلّمة إنشاء إلى ScoreViewModelFactory().
viewModelFactory = ScoreViewModelFactory(ScoreFragmentArgs.fromBundle(arguments!!).score)
  1. في onCreateView()، بعد إعداد viewModelFactory، عليك إعداد الكائن viewModel. يجب استدعاء طريقة ViewModelProviders.of()، وتمريرها في سياق جزء النتيجة المرتبط وviewModelFactory. سيؤدي هذا إلى إنشاء الكائن ScoreViewModel باستخدام طريقة المصنع المحدّدة في فئة viewModelFactory..
viewModel = ViewModelProviders.of(this, viewModelFactory)
       .get(ScoreViewModel::class.java)
  1. في طريقة onCreateView()، بعد إعداد viewModel، اضبط نص طريقة العرض scoreText على النتيجة النهائية المحدّدة في ScoreViewModel.
binding.scoreText.text = viewModel.score.toString()
  1. شغِّل تطبيقك وشغِّل اللعبة. انقر على بعض الكلمات أو انقر على إنهاء اللعبة. تجدر الإشارة إلى أن جزء النتيجة يعرض الآن النتيجة النهائية.

  1. اختياري: تحقّق من سجلات ScoreViewModel في Logcat عن طريق الفلترة على ScoreViewModel. ويجب عرض قيمة النتيجة.
2019-02-07 10:50:18.328 com.example.android.guesstheword I/ScoreViewModel: Final score is 15

في هذه المهمة، نفذت ScoreFragment لاستخدام ViewModel. وتعرّفت أيضًا على كيفية إنشاء مُنشئ معلمات تحتوي على معلَمات ViewModel باستخدام واجهة ViewModelFactory.

تهانينا. لقد غيّرت بنية تطبيقك لاستخدام أحد"مكوّنات Android الأساسية"في ViewModel. لقد تم حل مشكلة مراحل نشاط التطبيق، والآن أصبحت بيانات اللعبة قائمة على التغييرات في الضبط. وتعرّفت أيضًا على كيفية إنشاء مُنشئ معلَمات من أجل إنشاء ViewModel باستخدام واجهة ViewModelFactory.

مشروع Android Studio: GuessTheWord

  • تقترح إرشادات بنية تطبيق Android فصل الصفوف التي تتضمن مسؤوليات مختلفة.
  • وحدة تحكُّم واجهة المستخدم هي فئة تستند إلى واجهة المستخدم، مثل Activity أو Fragment. يجب أن تتضمن وحدات التحكم في واجهة المستخدم منطقًا يتعامل مع تفاعلات واجهة المستخدم ونظام التشغيل فقط؛ يجب ألا تحتوي على بيانات لعرضها في واجهة المستخدم. ضع هذه البيانات في ViewModel.
  • تخزّن فئة ViewModel البيانات ذات الصلة بواجهة المستخدم وتديرها. تسمح الفئة ViewModel بالبيانات بالحفاظ على بقاء تغييرات الضبط، مثل تدوير الشاشة.
  • ViewModel هو أحد مكوِّنات بنية Android التي يُنصَح بها.
  • ViewModelProvider.Factory هي واجهة يمكنك استخدامها لإنشاء عنصر ViewModel.

يقارن الجدول التالي وحدات التحكُّم في واجهة المستخدم مع مثيلات ViewModel التي تحتفظ ببياناتها:

وحدة تحكُّم واجهة المستخدم

ViewView

ومن أمثلة وحدات التحكم في واجهة المستخدم ScoreFragment التي أنشأتها في هذا الدرس التطبيقي حول الترميز.

ومن أمثلة ViewModel السمة ScoreViewModel التي أنشأتها في هذا الدرس التطبيقي حول الترميز.

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

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

يحتوي على رمز لعرض البيانات ورمز حدث المستخدم مثل مستمعي النقر.

يحتوي على رمز لمعالجة البيانات.

مسحها وإعادة إنشائها أثناء كل تغيير إعداد.

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

تحتوي على من المشاهدات.

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

يحتوي على مرجع إلى ViewModel المرتبطة.

لا تحتوي على أي مرجع إلى وحدة التحكّم المرتبطة بواجهة المستخدم.

دورة Udacity:

مستندات مطوّر برامج Android:

غير ذلك:

يسرد هذا القسم المهام الدراسية المحتملة للطلاب الذين يعملون من خلال هذا الدرس التطبيقي حول الترميز في إطار دورة تدريبية يُديرها معلِّم. يجب أن ينفِّذ المعلّم ما يلي:

  • يمكنك تخصيص واجب منزلي إذا لزم الأمر.
  • التواصل مع الطلاب بشأن كيفية إرسال الواجبات المنزلية
  • وضع درجات للواجبات المنزلية.

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

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

الإجابة عن هذه الأسئلة

السؤال 1

لتجنب فقدان البيانات أثناء تغيير إعداد الجهاز، عليك حفظ بيانات التطبيق في أي فئة؟

  • ViewModel
  • LiveData
  • Fragment
  • Activity

السؤال 2

يجب ألا تحتوي ViewModel على أي إشارات إلى الأجزاء أو الأنشطة أو الملفات الشخصية. صحيح أم خطأ؟

  • صحيح
  • خطأ

السؤال 3

متى يتم تدمير ViewModel؟

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

السؤال 4

ما هي واجهة ViewModelFactory؟

  • جارٍ إنشاء مثيل لكائن ViewModel.
  • الاحتفاظ بالبيانات أثناء تغييرات الاتجاه
  • إعادة تحميل البيانات التي يتم عرضها على الشاشة.
  • يمكنك تلقّي إشعارات عند تغيير بيانات التطبيق.

بدء الدرس التالي: 5.2: LiveData وLive المراقبون

وللحصول على روابط إلى دروس تطبيقية أخرى حول الترميز في هذه الدورة التدريبية، يُرجى الاطّلاع على الصفحة المقصودة لتطبيق الدروس التطبيقية حول الترميز Kotlin Fundamentals.