هذا الدرس التطبيقي حول الترميز هو جزء من دورة "أساسيات 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: تنفيذ الخطوات الأولى
- نزِّل رمز بدء تشغيل تطبيق GuessTheWord وافتح المشروع في "استوديو Android".
- تشغيل التطبيق على جهاز يعمل بنظام التشغيل Android أو على محاكي
- انقر على الأزرار. يُرجى العِلم أنّ الزر تخطّي يعرض الكلمة التالية ويخفض النتيجة بمقدار واحد، بينما يعرض الزر فهمت الكلمة التالية ويزيد النتيجة بمقدار واحد. لم يتم تنفيذ الزر إنهاء اللعبة، لذا لا يحدث أي شيء عند النقر عليه.
الخطوة 2: إجراء عملية تفصيلية للرمز
- في "استوديو Android"، استكشِف الرمز البرمجي للتعرّف على طريقة عمل التطبيق.
- احرص على الاطّلاع على الملفات الموضّحة أدناه، فهي مهمة بشكل خاص.
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 المبدئي.
- نفِّذ الرمز الأولي والعب اللعبة من خلال بضع كلمات، وانقر على تخطّي أو حسنًا بعد كل كلمة.
- تعرض شاشة اللعبة الآن كلمة والنتيجة الحالية. غيِّر اتجاه الشاشة من خلال تدوير الجهاز أو المحاكي. يُرجى العِلم أنّه سيتم فقدان النتيجة الحالية.
- أدخِل بضع كلمات أخرى في اللعبة. عندما تظهر شاشة اللعبة مع بعض النتائج، أغلِق التطبيق وأعِد فتحه. لاحظ أنّ اللعبة تبدأ من البداية، لأنّه لم يتم حفظ حالة التطبيق.
- العب اللعبة من خلال بضع كلمات، ثم انقر على الزر إنهاء اللعبة. لاحظ أنّه لن يحدث أي شيء.
المشاكل في التطبيق:
- لا يحفظ التطبيق المبدئي حالة التطبيق ولا يستعيدها أثناء تغييرات الضبط، مثل تغيير اتجاه الجهاز أو إيقاف التطبيق وإعادة تشغيله.
يمكنك حلّ هذه المشكلة باستخدام دالة معاودة الاتصال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
- افتح ملف
build.gradle(module:app)
. داخل الحظرdependencies
، أضِف تبعية Gradle للحزمةViewModel
.
. إذا كنت تستخدم أحدث إصدار من المكتبة، من المفترض أن يتم تجميع تطبيق الحلّ على النحو المتوقّع. إذا لم يكن كذلك، حاوِل حلّ المشكلة أو الرجوع إلى الإصدار الموضّح أدناه.
//ViewModel
implementation 'androidx.lifecycle:lifecycle-extensions:2.0.0'
- في مجلد الحزمة
screens/game/
، أنشئ فئة Kotlin جديدة باسمGameViewModel
. - اجعل الفئة
GameViewModel
توسّع الفئة المجردةViewModel
. - لمساعدتك في فهم كيفية عمل
ViewModel
مع مراعاة مراحل النشاط، أضِف كتلةinit
تتضمّن عبارةlog
.
class GameViewModel : ViewModel() {
init {
Log.i("GameViewModel", "GameViewModel created!")
}
}
الخطوة 2: إلغاء طريقة onCleared() وإضافة تسجيل
يتم إتلاف ViewModel
عند فصل الجزء المرتبط به أو عند انتهاء النشاط. قبل إيقاف ViewModel
مباشرةً، يتم استدعاء ردّ الاتصال onCleared()
لتنظيف الموارد.
- في الفئة
GameViewModel
، ألغِ طريقةonCleared()
. - أضِف عبارة سجلّ داخل
onCleared()
لتتبُّع دورة حياةGameViewModel
.
override fun onCleared() {
super.onCleared()
Log.i("GameViewModel", "GameViewModel destroyed!")
}
الخطوة 3: ربط GameViewModel بالجزء الخاص باللعبة
يجب ربط ViewModel
بأداة تحكّم في واجهة المستخدم. لربط الاثنين، عليك إنشاء مرجع إلى ViewModel
داخل وحدة التحكّم في واجهة المستخدِم.
في هذه الخطوة، يمكنك إنشاء مرجع GameViewModel
داخل وحدة التحكّم في واجهة المستخدم المقابلة، وهي GameFragment
.
- في فئة
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
:
- في فئة
GameFragment
، ابدأ متغيّرviewModel
. ضَع هذا الرمز داخلonCreateView()
، بعد تعريف متغير الربط. استخدِم طريقةViewModelProviders.of()
، ومرِّر سياقGameFragment
المرتبط وفئةGameViewModel
. - أضِف عبارة تسجيل إلى سجلّ الأنشطة لتسجيل استدعاء الطريقة
ViewModelProviders.of()
، وذلك فوق عملية تهيئة الكائنViewModel
.
Log.i("GameFragment", "Called ViewModelProviders.of")
viewModel = ViewModelProviders.of(this).get(GameViewModel::class.java)
- شغِّل التطبيق. في "استوديو Android"، افتح لوحة Logcat وفلتر حسب
Game
. انقر على زر تشغيل على جهازك أو المحاكي. تفتح شاشة اللعبة.
كما هو موضّح في Logcat، تستدعي الطريقةonCreateView()
منGameFragment
الطريقةViewModelProviders.of()
لإنشاءGameViewModel
. تظهر عبارات التسجيل التي أضفتها إلىGameFragment
وGameViewModel
في Logcat.
- فعِّل إعداد التدوير التلقائي على جهازك أو المحاكي وغيِّر اتجاه الشاشة عدة مرات. يتم تدمير
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
- اخرج من اللعبة أو انتقِل خارج جزء اللعبة. تم إتلاف
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
:
- انقل حقول البيانات
word
وscore
وwordList
. تأكَّد من أنّword
وscore
ليساprivate
.
لا تنقل متغيّر الربطGameFragmentBinding
لأنّه يحتوي على مراجع إلى طرق العرض. يُستخدَم هذا المتغيّر لتوسيع التصميم وإعداد أدوات معالجة النقرات وعرض البيانات على الشاشة، وهي مسؤوليات الجزء. - نقل طريقتَي
resetList()
وnextWord()
تحدّد هذه الطرق الكلمة التي ستظهر على الشاشة. - من داخل طريقة
onCreateView()
، انقل استدعاءات الطريقة إلىresetList()
وnextWord()
إلى كتلةinit
منGameViewModel
.
يجب أن تكون هذه الطرق في الحظرinit
، لأنّه يجب إعادة ضبط قائمة الكلمات عند إنشاءViewModel
، وليس في كل مرة يتم فيها إنشاء الجزء. يمكنك حذف عبارة السجلّ في حزمةinit
منGameFragment
.
تحتوي معالجات النقر onSkip()
وonCorrect()
في GameFragment
على رمز لمعالجة البيانات وتعديل واجهة المستخدم. يجب أن يبقى الرمز البرمجي لتعديل واجهة المستخدم في الجزء، ولكن يجب نقل الرمز البرمجي لمعالجة البيانات إلى ViewModel
.
في الوقت الحالي، ضَع الطرق المتطابقة في كلا المكانين:
- انسخ الطريقتَين
onSkip()
وonCorrect()
منGameFragment
إلىGameViewModel
. - في
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
- في
GameFragment
، عدِّل طريقتَيonSkip()
وonCorrect()
. أزِل الرمز لتعديل النتيجة واستخدِم بدلاً من ذلك الطريقتَينonSkip()
وonCorrect()
المناسبتَين فيviewModel
. - بما أنّك نقلت الطريقة
nextWord()
إلىViewModel
، لم يعُد بإمكان جزء اللعبة الوصول إليها.
فيGameFragment
، في الطريقتَينonSkip()
وonCorrect()
، استبدِل استدعاءnextWord()
بـupdateScoreText()
وupdateWordText()
. تعرض هذه الطرق البيانات على الشاشة.
private fun onSkip() {
viewModel.onSkip()
updateWordText()
updateScoreText()
}
private fun onCorrect() {
viewModel.onCorrect()
updateScoreText()
updateWordText()
}
- في
GameFragment
، عدِّل المتغيّرَينscore
وword
لاستخدام المتغيّراتGameViewModel
، لأنّ هذه المتغيّرات أصبحت الآن فيGameViewModel
.
private fun updateWordText() {
binding.wordText.text = viewModel.word
}
private fun updateScoreText() {
binding.scoreText.text = viewModel.score.toString()
}
- في
GameViewModel
، داخل الدالةnextWord()
، أزِل استدعاء الدالتينupdateWordText()
وupdateScoreText()
. يتم الآن استدعاء هذه الطرق منGameFragment
. - أنشئ التطبيق وتأكَّد من عدم حدوث أي أخطاء. إذا ظهرت لك أخطاء، نظِّف المشروع وأعِد إنشاءه.
- شغِّل التطبيق والعب اللعبة من خلال بعض الكلمات. أثناء عرض شاشة اللعبة، أدرِ الجهاز. يُرجى العِلم أنّه يتم الاحتفاظ بالنتيجة الحالية والكلمة الحالية بعد تغيير اتجاه الشاشة.
أحسنت صنعًا. الآن يتم تخزين جميع بيانات تطبيقك في ViewModel
، لذا يتم الاحتفاظ بها أثناء تغييرات الإعدادات.
في هذه المهمة، ستنفّذ أداة معالجة النقرات لزر إنهاء اللعبة.
- في
GameFragment
، أضِف طريقة دفع باسمonEndGame()
. سيتم استدعاء طريقةonEndGame()
عندما ينقر المستخدم على الزر إنهاء اللعبة.
private fun onEndGame() {
}
- في
GameFragment
، داخل طريقةonCreateView()
، ابحث عن الرمز الذي يضبط أدوات معالجة النقرات للزرّين حسنًا وتخطّي. أسفل هذين السطرين مباشرةً، اضبط أداة معالجة نقرات للزر إنهاء اللعبة. استخدِم متغيّر الربطbinding
. داخل أداة معالجة النقرات، استدعِ طريقةonEndGame()
.
binding.endGameButton.setOnClickListener { onEndGame() }
- في
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)
}
- في الطريقة
onEndGame()
، استدعِ الطريقةgameFinished()
.
private fun onEndGame() {
gameFinished()
}
- شغِّل التطبيق واللعبة، وتنقل بين بعض الكلمات. انقر على الزر إنهاء اللعبة . لاحظ أنّ التطبيق ينتقل إلى شاشة النتيجة، ولكن لا يتم عرض النتيجة النهائية. يمكنك حلّ هذه المشكلة في المهمة التالية.
عندما ينهي المستخدم اللعبة، لا تعرض ScoreFragment
النتيجة. يجب أن يكون لديك ViewModel
لعرض النتيجة التي سيتم عرضها من خلال ScoreFragment
. ستمرِّر قيمة النتيجة أثناء عملية تهيئة ViewModel
باستخدام نمط طريقة المصنع.
نمط طريقة المصنع هو نمط تصميم إنشائي يستخدم طرق المصنع لإنشاء الكائنات. طريقة المصنع هي طريقة تعرض مثيلاً من الفئة نفسها.
في هذه المهمة، ستنشئ ViewModel
مع دالة إنشاء ذات وسيطات مسبقة الضبط لجزء النتيجة ودالة مصنع لإنشاء مثيل ViewModel
.
- ضمن حزمة
score
، أنشِئ فئة Kotlin جديدة باسمScoreViewModel
. سيكون هذا الصف هوViewModel
لجزء النتيجة. - وسِّع الفئة
ScoreViewModel
منViewModel.
أضِف مَعلمة دالة إنشائية للنتيجة النهائية. أضِف مربّعinit
مع عبارة سجلّ. - في الفئة
ScoreViewModel
، أضِف متغيّرًا باسمscore
لحفظ النتيجة النهائية.
class ScoreViewModel(finalScore: Int) : ViewModel() {
// The final score
var score = finalScore
init {
Log.i("ScoreViewModel", "Final score is $finalScore")
}
}
- ضمن حزمة
score
، أنشِئ فئة Kotlin أخرى باسمScoreViewModelFactory
. سيكون هذا الصف مسؤولاً عن إنشاء مثيل لكائنScoreViewModel
. - مدِّد الصف
ScoreViewModelFactory
منViewModelProvider.Factory
. أضِف مَعلمة دالة إنشائية للنتيجة النهائية.
class ScoreViewModelFactory(private val finalScore: Int) : ViewModelProvider.Factory {
}
- في
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")
}
- في
ScoreFragment
، أنشئ متغيرات فئة لكل منScoreViewModel
وScoreViewModelFactory
.
private lateinit var viewModel: ScoreViewModel
private lateinit var viewModelFactory: ScoreViewModelFactory
- في
ScoreFragment
، داخلonCreateView()
، بعد إعداد المتغيّرbinding
، عليك إعدادviewModelFactory
. استخدِمScoreViewModelFactory
. مرِّر النتيجة النهائية من حزمة الوسيطات، كمعلَمة للدالة الإنشائية إلىScoreViewModelFactory()
.
viewModelFactory = ScoreViewModelFactory(ScoreFragmentArgs.fromBundle(arguments!!).score)
- في
onCreateView(
)، بعد إعدادviewModelFactory
، عليك إعداد العنصرviewModel
. استدعِ الدالةViewModelProviders.of()
، وأدخِل سياق جزء النتيجة المرتبط وviewModelFactory
. سيؤدي ذلك إلى إنشاء الكائنScoreViewModel
باستخدام طريقة المصنع المحدّدة في الفئةviewModelFactory
.
viewModel = ViewModelProviders.of(this, viewModelFactory)
.get(ScoreViewModel::class.java)
- في طريقة
onCreateView()
، بعد تهيئةviewModel
، اضبط نص العرضscoreText
على النتيجة النهائية المحدّدة فيScoreViewModel
.
binding.scoreText.text = viewModel.score.toString()
- شغِّل تطبيقك والعب اللعبة. تنقَّل بين بعض الكلمات أو جميعها، ثم انقر على إنهاء اللعبة. لاحظ أنّ مقتطف النتيجة يعرض الآن النتيجة النهائية.
- اختياري: راجِع سجلّات
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 |
من الأمثلة على وحدات التحكّم في واجهة المستخدم | أحد الأمثلة على |
لا يحتوي على أي بيانات ليتم عرضها في واجهة المستخدم. | يحتوي على البيانات التي يعرضها عنصر التحكّم في واجهة المستخدم في واجهة المستخدم. |
يحتوي على رمز برمجي لعرض البيانات ورمز برمجي لأحداث المستخدم، مثل أدوات معالجة النقرات. | يحتوي على رمز لمعالجة البيانات. |
يتم إتلافها وإعادة إنشائها عند كل تغيير في الإعدادات. | لا يتم إتلافه إلا عندما يختفي نهائيًا عنصر التحكّم في واجهة المستخدم المرتبط به، أي عندما ينتهي النشاط أو يتم فصل الجزء. |
تحتوي على طرق عرض. | يجب ألا يحتوي أبدًا على مراجع للأنشطة أو الأجزاء أو طرق العرض، لأنّها لا تبقى عند تغيير الإعدادات، بينما يبقى |
يحتوي على مرجع إلى | لا يحتوي على أي إشارة إلى وحدة التحكّم في واجهة المستخدم المرتبطة. |
دورة Udacity التدريبية:
مستندات مطوّري تطبيقات Android:
- نظرة عامة على ViewModel
- التعامل مع دورات الحياة باستخدام المكوّنات التي تراعي دورة الحياة
- دليل حول بنية التطبيق
ViewModelProvider
ViewModelProvider.Factory
غير ذلك:
- نمط التصميم المعماري MVVM (نموذج-عرض-نموذج عرض)
- مبدأ التصميم فصل الاهتمامات (SoC)
- نمط طريقة المصنع
يسرد هذا القسم مهامًا منزلية محتملة للطلاب الذين يعملون على هذا الدرس التطبيقي العملي كجزء من دورة تدريبية يقودها مدرّب. على المعلّم تنفيذ ما يلي:
- حدِّد واجبًا منزليًا إذا لزم الأمر.
- توضيح كيفية إرسال الواجبات المنزلية للطلاب
- وضع درجات للواجبات المنزلية
يمكن للمدرّبين استخدام هذه الاقتراحات بالقدر الذي يريدونه، ويجب ألا يترددوا في تكليف الطلاب بأي واجبات منزلية أخرى يرونها مناسبة.
إذا كنت تعمل على هذا الدرس العملي بنفسك، يمكنك استخدام مهام الواجب المنزلي هذه لاختبار معلوماتك.
الإجابة عن هذه الأسئلة
السؤال 1
لتجنُّب فقدان البيانات أثناء تغيير إعدادات الجهاز، في أي فئة يجب حفظ بيانات التطبيق؟
ViewModel
LiveData
Fragment
Activity
السؤال 2
يجب ألا يحتوي ViewModel
على أي إشارات إلى الرموز أو الأنشطة أو طرق العرض. صواب أم خطأ؟
- True
- خطأ
السؤال 3
متى يتم إتلاف ViewModel
؟
- عندما يتم إيقاف وحدة التحكّم في واجهة المستخدم المرتبطة وإعادة إنشائها أثناء تغيير اتجاه الجهاز
- عند تغيير اتجاه الشاشة
- عند انتهاء وحدة التحكّم في واجهة المستخدم المرتبطة (إذا كانت نشاطًا) أو فصلها (إذا كانت جزءًا).
- عندما يضغط المستخدم على زر الرجوع
السؤال 4
ما هو الغرض من واجهة ViewModelFactory
؟
- إنشاء مثيل لعنصر
ViewModel
- الاحتفاظ بالبيانات عند تغيير اتجاه الشاشة
- إعادة تحميل البيانات المعروضة على الشاشة
- تلقّي إشعارات عند تغيير بيانات التطبيق
ابدأ الدرس التالي:
للحصول على روابط تؤدي إلى دروس تطبيقية أخرى في هذه الدورة التدريبية، اطّلِع على الصفحة المقصودة الخاصة بالدروس التطبيقية حول أساسيات Android Kotlin.