‫Android Kotlin Fundamentals 06.2: الكوروتينات وRoom

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

مقدمة

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

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

ما يجب معرفته

يجب أن تكون على دراية بما يلي:

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

أهداف الدورة التعليمية

  • طريقة عمل سلاسل المحادثات في Android
  • كيفية استخدام الروتينات الفرعية في Kotlin لنقل عمليات قاعدة البيانات بعيدًا عن سلسلة التعليمات الرئيسية
  • كيفية عرض البيانات المنسَّقة في TextView

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

  • توسيع تطبيق TrackMySleepQuality لجمع البيانات وتخزينها وعرضها في قاعدة البيانات ومنها
  • استخدِم إجراءات روتينية متزامنة لتنفيذ عمليات قاعدة البيانات التي تستغرق وقتًا طويلاً في الخلفية.
  • استخدِم LiveData لتفعيل التنقّل وعرض شريط المعلومات.
  • استخدِم LiveData لتفعيل الأزرار وإيقافها.

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

يحتوي التطبيق على شاشتَين، يتم تمثيلهما بلقطات، كما هو موضّح في الشكل أدناه.

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

الشاشة الثانية، المعروضة على اليسار، مخصّصة لاختيار تقييم لجودة النوم. في التطبيق، يتم تمثيل التقييم رقميًا. لأغراض التطوير، يعرض التطبيق رموز الوجوه وما يعادلها من أرقام.

يكون تدفّق المستخدم على النحو التالي:

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

يستخدم هذا التطبيق بنية مبسطة، كما هو موضّح أدناه في سياق البنية الكاملة. يستخدم التطبيق المكوّنات التالية فقط:

  • وحدة التحكّم في واجهة المستخدم
  • عرض النموذج وLiveData
  • قاعدة بيانات Room

في هذه المهمة، ستستخدم TextView لعرض البيانات المنسَّقة لتتبُّع النوم. (هذه ليست الواجهة النهائية. ستتعرّف على طريقة أفضل في درس برمجة آخر.)

يمكنك مواصلة العمل على تطبيق TrackMySleepQuality الذي أنشأته في الدرس التطبيقي السابق أو تنزيل تطبيق البداية لهذا الدرس التطبيقي.

الخطوة 1: تنزيل تطبيق البداية وتشغيله

  1. نزِّل تطبيق TrackMySleepQuality-Coroutines-Starter من GitHub.
  2. أنشئ التطبيق وشغِّله. يعرض التطبيق واجهة المستخدم للجزء SleepTrackerFragment، ولكن بدون بيانات. لا تستجيب الأزرار للنقرات.

الخطوة 2: فحص الرمز

رمز البداية لهذا الدرس التطبيقي حول الترميز هو نفسه رمز الحلّ للدرس التطبيقي حول الترميز 6.1 إنشاء قاعدة بيانات Room.

  1. افتح الملف res/layout/activity_main.xml. يحتوي هذا التصميم على الجزء nav_host_fragment. لاحظ أيضًا العلامة <merge>.

    يمكن استخدام العلامة merge لإزالة التنسيقات المكرّرة عند تضمين التنسيقات، ويُنصح باستخدامها. مثال على تخطيط مكرّر: ConstraintLayout > LinearLayout > TextView، حيث قد يتمكّن النظام من إزالة LinearLayout. يمكن أن يؤدي هذا النوع من التحسين إلى تبسيط بنية العرض وتحسين أداء التطبيق.
  2. في مجلد navigation، افتح الملف navigation.xml. يمكنك الاطّلاع على جزأين وإجراءات التنقّل التي تربط بينهما.
  3. في مجلد التصميم، انقر مرّتين على جزء "متتبّع النوم" للاطّلاع على تصميم XML الخاص به. يُرجى ملاحظة ما يلي:
  • يتم تضمين بيانات التنسيق في العنصر <layout> لتفعيل ربط البيانات.
  • يتم ترتيب ConstraintLayout وطرق العرض الأخرى داخل العنصر <layout>.
  • يتضمّن الملف علامة العنصر النائب <data>.

يوفر التطبيق الأولي أيضًا الأبعاد والألوان والأنماط لواجهة المستخدم. يحتوي التطبيق على قاعدة بيانات Room ورمز DAO وكيان SleepNight. إذا لم تكمل تجربة البرمجة السابقة، احرص على استكشاف هذه الجوانب من الرمز بنفسك.

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

الخطوة 1: إضافة SleepTrackerViewModel

  1. في حزمة sleeptracker، افتح SleepTrackerViewModel.kt.
  2. افحص فئة SleepTrackerViewModel، التي يتم توفيرها لك في تطبيق البداية ويتم عرضها أيضًا أدناه. يُرجى العِلم أنّ الفئة توسّع AndroidViewModel(). هذه الفئة هي نفسها الفئة ViewModel، ولكنّها تأخذ سياق التطبيق كمَعلمة وتتيحه كسمة. ستحتاج إلى ذلك لاحقًا.
class SleepTrackerViewModel(
       val database: SleepDatabaseDao,
       application: Application) : AndroidViewModel(application) {
}

الخطوة 2: إضافة SleepTrackerViewModelFactory

  1. في حزمة sleeptracker، افتح SleepTrackerViewModelFactory.kt.
  2. افحص الرمز البرمجي المتوفّر لك للمصنع، والذي يظهر أدناه:
class SleepTrackerViewModelFactory(
       private val dataSource: SleepDatabaseDao,
       private val application: Application) : ViewModelProvider.Factory {
   @Suppress("unchecked_cast")
   override fun <T : ViewModel?> create(modelClass: Class<T>): T {
       if (modelClass.isAssignableFrom(SleepTrackerViewModel::class.java)) {
           return SleepTrackerViewModel(dataSource, application) as T
       }
       throw IllegalArgumentException("Unknown ViewModel class")
   }
}

يُرجى مراعاة ما يلي:

  • تتلقّى السمة SleepTrackerViewModelFactory الوسيطة نفسها التي تتلقّاها السمة ViewModel وتوسّع نطاق ViewModelProvider.Factory.
  • داخل المصنع، يتجاوز الرمز create()، الذي يقبل أي نوع من أنواع الفئات كمعلَمة ويعرض ViewModel.
  • في نص create()، يتحقّق الرمز من توفّر فئة SleepTrackerViewModel، وإذا كانت متوفّرة، يعرض مثالاً عليها. وفي حال عدم توفّرها، يعرض الرمز خطأ.

الخطوة 3: تعديل SleepTrackerFragment

  1. في SleepTrackerFragment، احصل على مرجع لسياق التطبيق. ضَع المرجع في onCreateView()، أسفل binding. تحتاج إلى مرجع للتطبيق الذي يتم إرفاق هذه الفئة المجزّأة به، وذلك لتمريره إلى موفّر مصنع عرض النماذج.

    تُطلق الدالة requireNotNull في Kotlin الخطأ IllegalArgumentException إذا كانت القيمة هي null.
val application = requireNotNull(this.activity).application
  1. تحتاج إلى مرجع إلى مصدر البيانات من خلال مرجع إلى DAO. في onCreateView()، قبل return، حدِّد dataSource. للحصول على مرجع إلى DAO الخاص بقاعدة البيانات، استخدِم SleepDatabase.getInstance(application).sleepDatabaseDao.
val dataSource = SleepDatabase.getInstance(application).sleepDatabaseDao
  1. في onCreateView()، قبل return، أنشئ نسخة من viewModelFactory. يجب إدخال dataSource وapplication.
val viewModelFactory = SleepTrackerViewModelFactory(dataSource, application)
  1. بعد إنشاء المصنع، احصل على مرجع إلى SleepTrackerViewModel. تشير المَعلمة SleepTrackerViewModel::class.java إلى فئة Java في وقت التشغيل لهذا العنصر.
val sleepTrackerViewModel =
       ViewModelProviders.of(
               this, viewModelFactory).get(SleepTrackerViewModel::class.java)
  1. يجب أن تبدو التعليمات البرمجية المكتملة على النحو التالي:
// Create an instance of the ViewModel Factory.
val dataSource = SleepDatabase.getInstance(application).sleepDatabaseDao
val viewModelFactory = SleepTrackerViewModelFactory(dataSource, application)

// Get a reference to the ViewModel associated with this fragment.
val sleepTrackerViewModel =
       ViewModelProviders.of(
               this, viewModelFactory).get(SleepTrackerViewModel::class.java)

إليك طريقة onCreateView() حتى الآن:

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

        // Get a reference to the binding object and inflate the fragment views.
        val binding: FragmentSleepTrackerBinding = DataBindingUtil.inflate(
                inflater, R.layout.fragment_sleep_tracker, container, false)

        val application = requireNotNull(this.activity).application

        val dataSource = SleepDatabase.getInstance(application).sleepDatabaseDao

        val viewModelFactory = SleepTrackerViewModelFactory(dataSource, application)

        val sleepTrackerViewModel =
                ViewModelProviders.of(
                        this, viewModelFactory).get(SleepTrackerViewModel::class.java)

        return binding.root
    }

الخطوة 4: إضافة ربط البيانات لنموذج العرض

بعد إعداد ViewModel الأساسي، عليك إكمال عملية إعداد ربط البيانات في SleepTrackerFragment لربط ViewModel بواجهة المستخدم.


في ملف التنسيق fragment_sleep_tracker.xml:

  1. داخل الحظر <data>، أنشئ <variable> يشير إلى الفئة SleepTrackerViewModel.
<data>
   <variable
       name="sleepTrackerViewModel"
       type="com.example.android.trackmysleepquality.sleeptracker.SleepTrackerViewModel" />
</data>

في SleepTrackerFragment:

  1. اضبط النشاط الحالي كمالك دورة حياة عملية الربط. أضِف هذا الرمز داخل طريقة onCreateView()، قبل عبارة return:
binding.setLifecycleOwner(this)
  1. عيِّن متغير ربط sleepTrackerViewModel إلى sleepTrackerViewModel. ضَع هذا الرمز داخل onCreateView()، أسفل الرمز الذي ينشئ SleepTrackerViewModel:
binding.sleepTrackerViewModel = sleepTrackerViewModel
  1. من المحتمل أن يظهر لك خطأ، لأنّه عليك إعادة إنشاء عنصر الربط. نظِّف المشروع وأعِد إنشاءه للتخلّص من الخطأ.
  2. أخيرًا، كما هو الحال دائمًا، تأكَّد من أنّ الرمز البرمجي يتم إنشاؤه وتشغيله بدون أخطاء.

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

تتضمّن الروتينات المشتركة الخصائص التالية:

  • الروتينات الفرعية متزامنة ولا تحظر التنفيذ.
  • تستخدِم إجراءات Coroutines دوال تعليق لجعل الرمز غير المتزامن تسلسليًا.

الروتينات الفرعية غير متزامنة.

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

على سبيل المثال، لنفترض أنّ لديك سؤالاً يتطلّب بحثًا، وطلبت من زميل العثور على الإجابة. ثم يغادرون ويعملون على حلّها، وكأنّهم يعملون "بشكل غير متزامن" و"في سلسلة محادثات منفصلة". يمكنك مواصلة العمل على مهام أخرى لا تعتمد على الإجابة، إلى أن يعود زميلك ويخبرك بها.

الروتينات الفرعية لا تحظر التنفيذ.

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

تستخدم الروتينات الفرعية دوال تعليق لجعل الرمز غير المتزامن تسلسليًا.

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

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

لا تحدّد الكلمة الرئيسية suspend سلسلة التعليمات البرمجية التي يتم تنفيذها. قد يتم تنفيذ دالة تعليق في سلسلة محادثات في الخلفية أو في سلسلة المحادثات الرئيسية.

لاستخدام الروتينات الفرعية في Kotlin، تحتاج إلى ثلاثة عناصر:

  • وظيفة
  • مرسِل
  • نطاق

المهمة: المهمة هي أي شيء يمكن إلغاؤه. لكل روتين فرعي مهمة، ويمكنك استخدام المهمة لإلغاء الروتين الفرعي. يمكن ترتيب المهام في تسلسلات هرمية للعناصر الرئيسية والفرعية. يؤدي إلغاء مهمة رئيسية إلى إلغاء جميع المهام الثانوية التابعة لها على الفور، ما يوفّر عليك عناء إلغاء كل روتين فرعي يدويًا.

أداة الإرسال: ترسل أداة الإرسال إجراءات روتينية مشتركة لتنفيذها على سلاسل محادثات مختلفة. على سبيل المثال، ينفّذ Dispatcher.Main المهام في سلسلة التعليمات الرئيسية، بينما ينقل Dispatcher.IO مهام الحظر المتعلقة بوحدات الإدخال والإخراج إلى مجموعة مشتركة من سلاسل التعليمات.

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

أن يكون المستخدم قادرًا على التفاعل مع بيانات النوم بالطرق التالية:

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

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

الخطوة 1: إعداد إجراءات روتينية لعمليات قاعدة البيانات

عند النقر على الزر بدء في تطبيق "أداة تتبُّع النوم"، عليك استدعاء دالة في SleepTrackerViewModel لإنشاء مثيل جديد من SleepNight وتخزين المثيل في قاعدة البيانات.

يؤدي النقر على أي من الأزرار إلى تشغيل عملية في قاعدة البيانات، مثل إنشاء SleepNight أو تعديله. لهذا السبب ولأسباب أخرى، يمكنك استخدام الروتينات المشتركة لتنفيذ معالجات النقرات لأزرار التطبيق.

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

    يتم تحديد $coroutine_version في ملف build.gradle الخاص بالمشروع على النحو التالي: coroutine_version = '1.0.0'.
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutine_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutine_version"
  1. افتح ملف SleepTrackerViewModel.
  2. في نص الفئة، حدِّد viewModelJob وعيِّن له مثيلاً من Job. تتيح لك هذه السمة viewModelJob إلغاء جميع الروتينات الفرعية التي بدأها نموذج العرض هذا عندما لا يعود نموذج العرض مستخدَمًا ويتم إيقافه. بهذه الطريقة، لن ينتهي بك الأمر إلى استخدام إجراءات فرعية ليس لديها مكان للعودة إليه.
private var viewModelJob = Job()
  1. في نهاية نص الفئة، يمكنك إلغاء onCleared() وإلغاء جميع الروتينات الفرعية. عندما يتم تدمير ViewModel، يتم استدعاء onCleared().
override fun onCleared() {
   super.onCleared()
   viewModelJob.cancel()
}
  1. أسفل تعريف viewModelJob مباشرةً، حدِّد uiScope للروتينات المشتركة. يحدّد النطاق مؤشر الترابط الذي سيتم تشغيل الروتين الفرعي عليه، ويجب أن يكون النطاق على دراية بالمهمة أيضًا. للحصول على نطاق، اطلب مثيلاً من CoroutineScope، ومرِّر أداة إرسال ومهمة.

يعني استخدام Dispatchers.Main أنّ الروتينات الفرعية التي يتم تشغيلها في uiScope ستعمل على سلسلة التعليمات الرئيسية. ويكون ذلك منطقيًا بالنسبة إلى العديد من الروتينات المشتركة التي يبدأها ViewModel، لأنّه بعد أن تنفّذ هذه الروتينات المشتركة بعض عمليات المعالجة، تؤدي إلى تعديل واجهة المستخدم.

private val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)
  1. أسفل تعريف uiScope، حدِّد متغيّرًا باسم tonight لتخزين الليلة الحالية. اجعل المتغير MutableLiveData، لأنّك بحاجة إلى أن تتمكّن من مراقبة البيانات وتغييرها.
private var tonight = MutableLiveData<SleepNight?>()
  1. لتهيئة المتغيّر tonight في أقرب وقت ممكن، أنشئ كتلة init أسفل تعريف tonight واستدعِ initializeTonight(). يمكنك تحديد initializeTonight() في الخطوة التالية.
init {
   initializeTonight()
}
  1. أسفل الحظر init، نفِّذ initializeTonight(). في uiScope، شغِّل روتينًا فرعيًا. في الداخل، احصل على قيمة tonight من قاعدة البيانات عن طريق استدعاء getTonightFromDatabase()، ثمّ خصّص القيمة إلى tonight.value. يمكنك تحديد getTonightFromDatabase() في الخطوة التالية.
private fun initializeTonight() {
   uiScope.launch {
       tonight.value = getTonightFromDatabase()
   }
}
  1. تنفيذ getTonightFromDatabase(). عرِّفها كدالة private suspend تعرض قيمة قابلة للتصغير SleepNight، إذا لم يكن هناك SleepNight حالي تم بدؤه. سيؤدي ذلك إلى حدوث خطأ، لأنّ الدالة يجب أن تعرض قيمة.
private suspend fun getTonightFromDatabase(): SleepNight? { }
  1. داخل نص الدالة getTonightFromDatabase()، اعرض النتيجة من روتين فرعي يعمل في سياق Dispatchers.IO. استخدِم أداة توزيع مهام الإدخال/الإخراج، لأنّ الحصول على البيانات من قاعدة البيانات هو عملية إدخال/إخراج ولا علاقة لها بواجهة المستخدم.
  return withContext(Dispatchers.IO) {}
  1. داخل كتلة الإرجاع، اسمح للروتين المشترك بالحصول على الليلة (الأحدث) من قاعدة البيانات. إذا لم يكن وقت البدء ووقت الانتهاء متطابقَين، ما يعني أنّه قد تم إكمال الليلة، عليك عرض القيمة null. بخلاف ذلك، يتم عرض الليل.
       var night = database.getTonight()
       if (night?.endTimeMilli != night?.startTimeMilli) {
           night = null
       }
       night

يجب أن تبدو getTonightFromDatabase() وظيفة التعليق المكتملة على النحو التالي. يجب ألا تظهر أي أخطاء أخرى.

private suspend fun getTonightFromDatabase(): SleepNight? {
   return withContext(Dispatchers.IO) {
       var night = database.getTonight()
       if (night?.endTimeMilli != night?.startTimeMilli) {
           night = null
       }
       night
   }
}

الخطوة 2: إضافة معالج النقر لزر "البدء"

يمكنك الآن تنفيذ onStartTracking()، وهو معالج النقرات للزر بدء. عليك إنشاء SleepNight جديد وإدراجه في قاعدة البيانات وتعيينه إلى tonight. ستبدو بنية onStartTracking() مشابهة إلى حد كبير لبنية initializeTonight().

  1. ابدأ بتعريف الدالة onStartTracking(). يمكنك وضع معالِجات النقرات فوق onCleared() في الملف SleepTrackerViewModel.
fun onStartTracking() {}
  1. داخل onStartTracking()، شغِّل روتينًا فرعيًا في uiScope، لأنّك تحتاج إلى هذه النتيجة للمتابعة وتعديل واجهة المستخدم.
uiScope.launch {}
  1. داخل عملية تشغيل الروتين الفرعي، أنشئ SleepNight جديدًا، والذي يسجّل الوقت الحالي كوقت بدء.
        val newNight = SleepNight()
  1. داخل عملية تشغيل الروتين الفرعي، استدعِ الدالة insert() لإدراج newNight في قاعدة البيانات. سيظهر لك خطأ لأنّك لم تحدّد وظيفة التعليق insert() هذه بعد. (هذه ليست دالة DAO التي تحمل الاسم نفسه).
       insert(newNight)
  1. داخل عملية تشغيل الروتين الفرعي، عدِّل tonight أيضًا.
       tonight.value = getTonightFromDatabase()
  1. أسفل onStartTracking()، عرِّف insert() كدالة private suspend تأخذ SleepNight كمعلَمة.
private suspend fun insert(night: SleepNight) {}
  1. بالنسبة إلى نص الدالة insert()، شغِّل روتينًا فرعيًا في سياق الإدخال/الإخراج وأدرِج الليلة في قاعدة البيانات من خلال استدعاء الدالة insert() من كائن الوصول إلى البيانات (DAO).
   withContext(Dispatchers.IO) {
       database.insert(night)
   }
  1. في ملف التنسيق fragment_sleep_tracker.xml، أضِف معالج النقرات onStartTracking() إلى start_button باستخدام ميزة ربط البيانات السحرية التي أعددتها سابقًا. تنشئ صيغة الدالة @{() -> دالة lambda لا تأخذ أي وسيطات وتستدعي معالج النقرات في sleepTrackerViewModel.
android:onClick="@{() -> sleepTrackerViewModel.onStartTracking()}"
  1. أنشئ تطبيقك وشغِّله. انقر على الزر بدء. يؤدي هذا الإجراء إلى إنشاء بيانات، ولكن لا يمكنك الاطّلاع على أي شيء بعد. عليك إصلاح ذلك الخطأ أولاً.
fun someWorkNeedsToBeDone {
   uiScope.launch {

        suspendFunction()

   }
}

suspend fun suspendFunction() {
   withContext(Dispatchers.IO) {
       longrunningWork()
   }
}

الخطوة 3: عرض البيانات

في SleepTrackerViewModel، يشير المتغيّر nights إلى LiveData لأنّ getAllNights() في DAO يعرض LiveData.

هذه ميزة في Room تتيح تعديل LiveData nights في كل مرة تتغير فيها البيانات في قاعدة البيانات لعرض أحدث البيانات. ولن تحتاج أبدًا إلى ضبط قيمة LiveData بشكلٍ صريح أو تعديلها. Room تعدّل البيانات لتتطابق مع قاعدة البيانات.

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

  1. افتح ملف Util.kt وأزِل التعليق من رمز تعريف formatNights() وعبارات import المرتبطة به. لإزالة التعليق من التعليمات البرمجية في "استوديو Android"، حدِّد كل التعليمات البرمجية التي تحمل العلامة // واضغط على Cmd+/ أو Control+/.
  2. لاحظ أنّ formatNights() تعرض النوع Spanned، وهو سلسلة بتنسيق HTML.
  3. افتح الملف strings.xml. لاحظ استخدام CDATA لتنسيق موارد السلسلة من أجل عرض بيانات النوم.
  4. افتح SleepTrackerViewModel. في الفئة SleepTrackerViewModel، أسفل تعريف uiScope، حدِّد متغيّرًا باسم nights. احصل على جميع الليالي من قاعدة البيانات وخصِّصها للمتغير nights.
private val nights = database.getAllNights()
  1. أسفل تعريف nights مباشرةً، أضِف رمزًا برمجيًا لتحويل nights إلى nightsString. استخدِم الدالة formatNights() من Util.kt.

    . مرِّر nights إلى الدالة map() من الفئة Transformations. للوصول إلى موارد السلسلة، حدِّد دالة الربط على أنّها استدعاء formatNights(). يجب توفير nights وResources.
val nightsString = Transformations.map(nights) { nights ->
   formatNights(nights, application.resources)
}
  1. افتح ملف التنسيق fragment_sleep_tracker.xml. في TextView، ضمن السمة android:text، يمكنك الآن استبدال سلسلة الموارد بمرجع إلى nightsString.
"@{sleepTrackerViewModel.nightsString}"
  1. أعِد إنشاء الرمز وشغِّل التطبيق. من المفترض أن تظهر الآن جميع بيانات نومك مع أوقات البدء.
  2. انقر على الزر بدء بضع مرات أخرى، وستظهر لك المزيد من البيانات.

في الخطوة التالية، عليك تفعيل وظيفة الزر إيقاف.

الخطوة 4: إضافة معالج النقر لزر الإيقاف

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

  1. أضِف onStopTracking() إلى ViewModel. إطلاق روتين فرعي في uiScope إذا لم يتم ضبط وقت الانتهاء بعد، اضبط endTimeMilli على وقت النظام الحالي واستدعِ update() باستخدام بيانات الليل.

    في Kotlin، يحدّد بناء الجملة return@label الدالة التي تعود منها هذه العبارة، من بين عدة دوال متداخلة.
fun onStopTracking() {
   uiScope.launch {
       val oldNight = tonight.value ?: return@launch
       oldNight.endTimeMilli = System.currentTimeMillis()
       update(oldNight)
   }
}
  1. نفِّذ update() باستخدام النمط نفسه الذي استخدمته لتنفيذ insert().
    .
private suspend fun update(night: SleepNight) {
   withContext(Dispatchers.IO) {
       database.update(night)
   }
}
  1. لربط معالج النقرات بواجهة المستخدم، افتح ملف التنسيق fragment_sleep_tracker.xml وأضِف معالج النقرات إلى stop_button.
android:onClick="@{() -> sleepTrackerViewModel.onStopTracking()}"
  1. إنشاء تطبيقك وتشغيله
  2. انقر على بدء، ثم انقر على إيقاف. يظهر لك وقت البدء ووقت الانتهاء وجودة النوم بدون قيمة ومدة النوم.

الخطوة 5: إضافة معالج النقر لزر "محو"

  1. وبالمثل، نفِّذ الترميزَين onClear() وclear().
fun onClear() {
   uiScope.launch {
       clear()
       tonight.value = null
   }
}

suspend fun clear() {
   withContext(Dispatchers.IO) {
       database.clear()
   }
}
  1. لربط معالج النقرات بواجهة المستخدم، افتح fragment_sleep_tracker.xml وأضِف معالج النقرات إلى clear_button.
android:onClick="@{() -> sleepTrackerViewModel.onClear()}"
  1. إنشاء تطبيقك وتشغيله
  2. انقر على محو للتخلّص من جميع البيانات. بعد ذلك، انقر على بدء وإيقاف لإنشاء بيانات جديدة.

مشروع "استوديو Android": TrackMySleepQualityCoroutines

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

لتشغيل روتين فرعي، تحتاج إلى مهمة وموزّع ونطاق:

  • بشكل أساسي، المهمة هي أي شيء يمكن إلغاؤه. لكل روتين فرعي مهمة، ويمكنك استخدام مهمة لإلغاء روتين فرعي.
  • يرسل المرسِل الروتينات الفرعية لتشغيلها على سلاسل محادثات مختلفة. تنفِّذ Dispatcher.Main المهام على سلسلة التعليمات الرئيسية، بينما تُستخدم Dispartcher.IO لتفريغ مهام الحظر المتعلقة بوحدات الإدخال والإخراج إلى مجموعة مشتركة من سلاسل التعليمات.
  • يجمع النطاق المعلومات، بما في ذلك الوظيفة والموزّع، لتحديد السياق الذي يتم فيه تنفيذ الروتين الفرعي. تتتبّع النطاقات الروتينات الفرعية.

لتنفيذ معالجات النقرات التي تؤدي إلى تشغيل عمليات قاعدة البيانات، اتّبِع النمط التالي:

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

استخدِم Transformations خريطة لإنشاء سلسلة من عنصر LiveData في كل مرة يتغيّر فيها العنصر.

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

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

المستندات والمقالات الأخرى:

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

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

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

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

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

السؤال 1

في ما يلي مزايا الروتينات الفرعية:

  • وهي غير حاصرة
  • يتم تشغيلها بشكل غير متزامن.
  • يمكن تشغيلها على سلسلة محادثات غير السلسلة الرئيسية.
  • وهي تساهم دائمًا في تسريع عمليات تشغيل التطبيق.
  • يمكنهم استخدام الاستثناءات.
  • ويمكن كتابتها وقراءتها كرمز خطي.

السؤال 2

ما هي دالة التعليق؟

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

السؤال 3

ما الفرق بين حظر سلسلة محادثات وتعليقها؟ ضَع علامة على جميع الإجابات الصحيحة.

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

الانتقال إلى الدرس التالي: ‫6.3 استخدام LiveData للتحكّم في حالات الأزرار

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