‫Android Kotlin Fundamentals 05.1: ViewModel وViewModelFactory

هذا الدرس التطبيقي حول الترميز هو جزء من دورة "أساسيات 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 باستخدام مَعلمات الدالة الإنشائية.

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

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

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

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

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

في هذه المهمة، ستنزّل تطبيقًا تجريبيًا وتشغّله وتفحص الرمز البرمجي.

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

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

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

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

MainActivity.kt

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

res/layout/main_activity.xml

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

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

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

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

screens/title/TitleFragment.kt

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

screens/game/GameFragment.kt

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

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

screens/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 إجراء عمليات حسابية وتحويلات بسيطة على البيانات لإعدادها ليتم عرضها من خلال وحدة التحكّم في واجهة المستخدم. في هذا التصميم، يتولّى ViewModel عملية اتخاذ القرار.

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

ViewModelFactory

ينشئ 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، سيتم إنشاء عنصر جديد في كل مرة تتم فيها إعادة إنشاء الجزء. بدلاً من ذلك، أنشِئ مثيل ViewModel باستخدام ViewModelProvider.

طريقة عمل ViewModelProvider:

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

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

  1. في فئة GameFragment، ابدأ متغيّر viewModel. ضَع هذا الرمز داخل onCreateView()، بعد تعريف متغير الربط. استخدِم طريقة ViewModelProviders.of()، ومرِّر سياق GameFragment المرتبط وفئة GameViewModel.
  2. أضِف عبارة تسجيل إلى سجلّ الأنشطة لتسجيل استدعاء الطريقة ViewModelProviders.of()، وذلك فوق عملية تهيئة الكائن ViewModel.
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 ونقل بيانات واجهة المستخدم الخاصة بقطعة اللعبة إلى ViewModel:
    أصبحت جميع البيانات التي تحتاجها القطعة لعرضها هي ViewModel. عندما يخضع التطبيق لتغيير في الإعدادات، يظل ViewModel نشطًا ويتم الاحتفاظ بالبيانات.

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

الخطوة 1: نقل حقول البيانات ومعالجة البيانات إلى ViewModel

انقل حقول البيانات والطُرق التالية من 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() للانتقال إلى شاشة النتائج في التطبيق. مرِّر النتيجة كوسيطة باستخدام Safe Args.
/**
* 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": GuessTheWord

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

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

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

ViewModel

من الأمثلة على وحدات التحكّم في واجهة المستخدم ScoreFragment التي أنشأتها في هذا الدرس العملي.

أحد الأمثلة على ViewModel هو ScoreViewModel الذي أنشأته في هذا الدرس العملي.

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

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

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

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

يتم إتلافها وإعادة إنشائها عند كل تغيير في الإعدادات.

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

تحتوي على طرق عرض.

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

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

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

دورة Udacity التدريبية:

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

غير ذلك:

يسرد هذا القسم مهامًا منزلية محتملة للطلاب الذين يعملون على هذا الدرس التطبيقي العملي كجزء من دورة تدريبية يقودها مدرّب. على المعلّم تنفيذ ما يلي:

  • حدِّد واجبًا منزليًا إذا لزم الأمر.
  • توضيح كيفية إرسال الواجبات المنزلية للطلاب
  • وضع درجات للواجبات المنزلية

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

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

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

السؤال 1

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

  • ViewModel
  • LiveData
  • Fragment
  • Activity

السؤال 2

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

  • True
  • خطأ

السؤال 3

متى يتم إتلاف ViewModel؟

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

السؤال 4

ما هو الغرض من واجهة ViewModelFactory؟

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

ابدأ الدرس التالي: ‫5.2: LiveData ووسائل مراقبة LiveData

للحصول على روابط تؤدي إلى دروس تطبيقية أخرى في هذه الدورة التدريبية، اطّلِع على الصفحة المقصودة الخاصة بالدروس التطبيقية حول أساسيات Android Kotlin.