النطاقات الـ Scopes

Mohammad Laifمنذ 4 سنوات

بسم الله الرحمن الرحيم

السلام عليكم ورحمة الله وبركاته

 

في هذا الدرس سنتعرف على النطاقات Scopes بشكل مفصل انواعهم وطرق استخدامهم. وكذلك سنتعرف على مفهوم كلاً من الـ Coroutine Context والـ Job الذي يعتبر فهمهم شئ ضروري لإنشاء النطاقات.

 

ماذا ستقرئ في هذا الدرس

  • ماهو عنصر الـ Job.
  • نقاط يجب عليك معرفتها عند التعامل مع الـ Job.
    •  إنشاء عنصر Job.
    • إنشاء عنصر Deferred.
  • ماهي الـ Coroutine Context.
    • إنشاء Coroutine Context.
  • النطاقات الـ Scopes.
    • فوائد النطاق Scope.
    • انواع النطاقات.
    • إنشاء النطاقات.
      • إنشاء نطاق Scope بإستخدام الواجهة CoroutineScope.
      •  من خلال عمل implementaiton لها.
      • إستخدام نطاق الدالتين التأجيليتين كلاً من الـ coroutineScope و الـ supervisorScope.
  • هيكلة النطاقات.

 

ماهو عنصر الـ Job

عبارة عن عنصر له دورة حياة Life-Cycle تتمثل في ستة مراحل وهي: New و Completing و Cancelling و Cancelled و Completed. وقادر على تمثيل علاقة الوالد والطفل Parent and Child في الهيكلة Hierarchy (إي اننا نستطيع إنشاء Job رئيسي يشمل العديد من الـ Jobs الفرعيه). ومن خلال تميز عنصر الـ Job بالـ Life-Cycle نستطيع مخاطبته للإستطلاع عن الحاله التي هو بها, وكذلك نستطيع الغاء عمل الكروتينات وذلك بإلغاءه. أيضاً يستطيع ارجاع قيمة وذلك بإستخدام النوع المشتق منه والمسمى Deferred (جافا: يتمثل كـ Callable يتوعد Promise بإرجاع قيمة مغلفة في كائن مستقبلي Future).
 

نقاط يجب عليك معرفتها عند التعامل مع الـ Job

  • من خلاله نلغي الكروتينات.
  • نستطيع هيكلته.
  • نستخدم النوع Job عندما لانريد قيمة مرجعة (مثلاً كحفظ بيانات في قاعدة البيانات).
  • نستخدم النوع Deferred عندما نريد قيمة مرجعة (مثلاً كحفظ بيانات في قاعدة البيانات مع ارجاع رقم الـ ID لمكان الحفظ).

 

إنشاء عنصر Job

لإنشاء عنصر من نوع Job نستعين بالدالة Job() كالتالي:

private var myJob: Job = Job()

 

او من خلال اسناد له كروتينه كما في الدرس السابق هكذا:

val jobB: Job = GlobalScope.launch(Dispatchers.Default) {

}

 

إنشاء عنصر Deferred

val myDeferred: Deferred<Int> = GlobalScope.async { return@async 1 }
  • لاحظ تحديد نوع الـ Deferred وذلك بداخل الاقواس <> بنوع Int. وهذا يعني أن المرجع من هذه الكروتينه هو من نوع Int.
  • لاحظ استخدام البناء async وليس launch كما بالمثال السابق, حيث أن الـ async يستطيع ارجاع قيمة (ايضاً له ميزة اخرى سنتعرف عليها فيا الدرس القادم وهي تشغيل الشفرات في وضع متوازي).

 

ماهي الـ Coroutine Context

كمفهوم هي عبارة عن هيكل او كوليكشن من نوع Indexed Set, ويعتبر هذا النوع خليط بين النوعين الـ Map (اخذت منها ميزة المفتاح والقيمة) والـ Set (اخذت منها ميزة كونها غير قابلة للتغيير). ولفهمها تخيلها على انها شئ يشابه الـ map في هياكل البيانات Data Structure. ولكن تم بنائها خصيصاً لتعمل كـ Context للـ الكروتينات Coroutines. تستطيع التعامل معها من خلال إستخدام مفاتيح Keys تتمثل كعناصر Elements, وتعطي النتائج كـ Value. وكذلك بها بعض من الدوال لإجراء بعض العمليات عليها.

وكمفهوم برمجي هي عبارة عن واجهه interface بإسم CoroutineContext تحتوي على واجهتين Key و Element في داخلها, يقومان بعمل implementation لبعضهم البعض! فكل Element يعتبر CoroutineContext وكل Key يعتبر Element وهكذا. وبهم بعض من الدوال, ومايهم الآن هي الدالة plus التي عمل لها override لتغيير تصرف دالة رمز الزائد الـ +, ونقوم بإستخدام هذه الدالة عندما نريد دمج أكثر من Context بإنشاء آخر جديد.

فلو اردنا ان نستطلع عن نوع الـ Job او نوع الموزع الـ Dispatcher او إي شئ متعلق بكروتينه ما, فيجب علينا سؤال الـ Context الخاص بها من خلال الـ Keys.

 

إنشاء Coroutine Context

لإنشائها نحتاج الى موزع Despatcher حتى يحدد لنا الخيط الحاسوبي ومن الافضل تزويدها بعنصر Job كالتالي:

private val myErrorHandler: CoroutineExceptionHandler =
    CoroutineExceptionHandler { _: CoroutineContext, error: Throwable -> println(error.toString()) }
private var myJob: Job = Job()
private val myContext: CoroutineContext = Dispatchers.Default + myJob + myErrorHandler
  • الـ myErrorHandler يعبر عن الـ Execption الذي ربما يرمى عند حدوث خطئ ما (لك الحرية في طريقة برمجته والتعامل معه).
  • الـ myJob هو عنصر الـ Job الذي سنرفقه.
  • الـ Despatchers.Default هو الموزع الافتراضي.
  • الـ myContext هي الـ Coroutine Context التي قمنا بإنشائها. لاحظ استخدام العلامة الزائد + كما جاء سابقاً حتى نقوم بإنشائها.

مثال آخر (للإيضاح فقط):

private val myContext: CoroutineContext = Dispatchers.Default

فلو اردنا الاستطلاع عن معلومات ما في داخل CoroutineContext سيكون الوضع هكذا تماماً كالـ map (للإيضاح فقط):

myContext.get(Job)       // return coroutine job
myContext[CoroutineName] // return name of that coroutine
  • استخدام الـ [] يغني عن استخدام الدالة get.

 

النطاقات الـ Scopes

والان الى صلب هذا الدرس! ففي الدرس السابق رأينا أن الكروتينات Coroutines تعيش داخل الخيط الحاسوبي Threads. وعلمنا بأننا نستطيع إنشاء الالاف منها وذلك يعود لخفتها. ولكن ماذا لو أردنا إنهاء عمل بعضها؟ لنفرض أننا قمنا بإنشاء ٢٠ كروتين للكتابة في قاعدة البيانات و ٥٠ كروتين الاتصال بسيرفر API في الانترنت و ١٠٠ لأخذ نسخة احتياطية وما إلى ذلك. وجميع تلك الكروتينات في خيط حاسوبي يعمل في الخلفية (بالعادة يطلق على تسمية الخيوط من هذا النوع Thread I/O). الان لو اردنا إطفاء وإنهاء عمل الكروتينات الـ ١٠٠ المسؤولة عن الاتصال بالانترنت كيف سنتعرف عليهم من بين هذا الكم الهائل؟ ماذا لو تعرفنا عليهم ونسينا إطفاء أحدهم! يسبب هذا مشاكل كثيرة وخاصة في الذاكرة والموارد. لهذا حتى لانقع في مثل هذه المشاكل من الافضل أن نقوم بإنشاء نطاقات حتى نحصر صناعة وعمل الكروتينات في داخلها.

كما هو ملاحظ في الدروس السابقة انه يجب توفر نطاق حتى نستطيع إنشاء الكروتينات Coroutines, أي أننا نحتاج إنشاء نطاق لإنشاء كروتين, وهذا الشئ صارم ولابد منه. لحسن الحظ تتوفر لنا الواجهة Interface المسمية بـ CoroutineScope من خلالها نستطيع إنشاء نطاق خاص يناسب احتياجاتنا. وكذلك فإن النطاق يساعدنا على تتبع الروتينات سواء النشطة أو المؤجلة, ومن خلاله نستطيع إنهاء عمل الروتينات المرتبطة به.

 

فوائد النطاق Scope

  • بداخله ننشئ الكروتين Coroutine.
  • يمكننا من حد اماكن إنشاء الكروتينات.
  • تتبع الكروتينات.
  • طريقة لهيكلة Structure الكروتينات.
  • إنهاء عمل الروتينات.

 

ربما تتسائل لماذا الشبهه بين الـ Scope والـ CoroutineContext او الـ Job؟

ببساطه لإن الـ Scope يقوم بإستخدام الـ CoroutineContext والـ Job بشكل ضمني, لذلك ستجد تشابه بينهم. 

 

انواع النطاقات

حالياً هناك عدة نطاقات تتراوح بين عنصر, ودوال تأجيلية وواجهة (هي الاساسية) وهم كالتالي:

  • الـ CoroutineScope (واجهة) نقوم بعمل implementation لها لإنشاء نطاق مخصص.
  • الـ coroutineScope (دالة تأجيلية): تستخدم لإنشاء نطاق عادي منخفض الصلاحيات.
  • الـ supervisorScope (دالة تأجيلية): تستخدم لإنشاء نطاق يتمتع بصلاحيات أكثر.
  • الـ GlobalScape (عنصر) نقوم باستخدامه بشكل مباشر, ويعتبر نطاق نشط مدام البرنامج نشط (لا ينصح بالتعامل معه).

كذلك يوفر الاندرويد بعض من هذه النطاقات التي تختصر علينا البرمجة عندما يتعلق الامر بالـ Architecture Components فمثلاً المكون ViewModel يوفر نطاق بإسم ViewModelScope خاص لكل ViewModel نقوم بإنشائها. وهذا النطاق يعتبر رائع ومفيد, فبعد الانتهاء من عمل الـ ViewModel كتنظيفها او اغلاق الكلاس\الشاشة التي تعمل فيها فإن هذا النطاق يقوم بإنهاء عمل الروتينات بشكل آلي حتى لا تحدث مشاكل. وآخر خاص للـ Lifecycle عندما نريد إنشاء شئ به دورة حياة.

وبالعادة ننشئ نطاق خاص في برمجة تطبيقات الاندرويد بإستخدام الواجهة CoroutineScope حسب شاشات المستخدم (سواء الـ Activities او الـ Fragments). فعندما يغادر المستخدم تلك الشاشة نقوم بإغلاق ذلك النطاق المرتبط بالشاشة, وهكذا ينهي النطاق عمل جميع الروتينات التي أنشأناها بداخله. وهذا الشئ يقلل من حدوث مشكلة تسرب الذاكرة Memory Leaks وما الى ذلك من مشاكل.

 

إنشاء النطاقات

كما علمنا في الدرس السابق أن استخدام النطاق العام GlobalScope غير مقبول مطلقاً. لذلك يتوجب علينا إنشاء نطاقنا الخاص, أو استخدام احد النطاقات التي قد توفرهم بعض من العناصر كما جاء ذكرهم في الدروس السابقة مثل نطاق يأتي مدمجاً مع الـ ViewModel وما الى ذلك. ولكن أذا اردنا إنشاء نطاق خاص بنا فيجب التعامل مع الواجهه interface المسمية CoroutineScope وتزويدها بـ CoroutineContext. توجد هناك عدة طرق من بينها الطريقتين كالتالي:

 

إنشاء نطاق Scope بإستخدام الواجهة CoroutineScope:

بشكل مباشر:

import kotlinx.coroutines.*
import kotlin.coroutines.CoroutineContext

private val myErrorHandler: CoroutineExceptionHandler =
    CoroutineExceptionHandler { _: CoroutineContext, error: Throwable -> println(error.toString()) }
private var myJob: Job = Job()
private val myContext: CoroutineContext = Dispatchers.Default + myJob + myErrorHandler
private val myScope: CoroutineScope = CoroutineScope(myContext)

fun main() {

    myJob = myScope.launch {
        println("Hello World")
    }
    
    myScope.cancel()
}
  • الـ myScope يمثل النطاق الخاص بنا, حيث قمنا بتزويده بـ Context كما جاء تفصيله فيما سبق.
  • لاحظ طريقة استخدام النطاق الخاص بنا myScope بدلاً من GlobalScope وكذلك عنصر الـ myJob.
  • لاحظ اننا يجب علينا إنهاء النطاق وكذلك العمل.

 

من خلال عمل implementaiton لها:

تناسب هذه الطريقة الفئات التي تحتوي على LifeCycle. مثل تلك الفئات التي تحتوي على شاشة للمستخدم مثل الـ Activity او الـ Fragment وكل فئة ترتبط بهم كالـ Adapter للـ RecyclerView وما الى ذلك في برمجة تطبيقات الاندرويد. بحيث أنك تقوم بعمل implementeion لها وعمل override لبعض من عناصرها الضرورية كالـ CoroutineContext ولاتنسى تزويدها بعنصر Job.

مثال بسيط يوضحها:

import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlin.coroutines.CoroutineContext

class ClassWithLifeCycle : CoroutineScope {
    private lateinit var job: Job
    override val coroutineContext: CoroutineContext = Dispatchers.Default

    fun fetchDataFromAPI() {
        launch {

        }
    }

    fun onDestroy() {
        job.cancel()
    }
}
  • بالنسبة لإسم الفئه ClassWithLifeCycle فهي إي فئه تحوي على LifeCycle كالـ Activity او الـ Fragment. واذا لا, فتستطيع عمل implemention للواجهة LifeCycle لجعل هذه الفئه على درايه.
  • لاحظ عمل implementaion للواجهه CoroutineScope.
  • لاحظ عمل override لعنصر الـ coroutineContext وتزويده بموزع Dispatchers.Default.
  • أما في الدالة fetchDataFromAPI فسنقوم بصنع الكروتينه وبكتابة مانريد عمله ضمن هذا النطاق.
  • لاحظ onDestory وفيها يجب إنهاء العمل واغلاق هذا النطاق.

 

إستخدام نطاق الدالتين التأجيليتين كلاً من الـ coroutineScope و الـ supervisorScope:

بإستخدام بعض من الدوال التأجيلية Suspend Functions التي يوفرها الـ API الخاص بالـ Coroutines في لغة الكوتلن, نستطيع إنشاء النطاقات بشكل سريع, دون التعامل مع الواجهة الرئيسية CoroutineScope كما سبق.

مثال:

import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.supervisorScope

fun main() = runBlocking {

    coroutineScope {
        println("Hello from coroutineScope")
    }

    supervisorScope {
        println("Hello from supervisorScope")
    }

}
  • الوضع جداً سهل هنا, فقط عليك ملاحظة بما أن الدوال coroutineScope و supervisorScope هما دالتين تأجيليتين فلايمكننا ندائهم الا من داخل كروتين او من داخل دالة تأجيلية (كما سيأتي في الدروس لاحقاً) ولتخطي هذا الامر قمنا بجعل الدالة main كروتين بإستخدام البناء runBlocking (هذا المثال فقط للتوضيح, لاينصح بإستخدام الدالة runBlocking الا في كتابة الاختبارات).

 

نقوم بإستخدام هذه النوعين الـ coroutineScope و الـ supervisorScope في الدوال التأجيلية Suspended Function وبداخل الكروتينات Coroutines.

 

هيكلة النطاقات

إنشاء الكثير من النطاقات (والعديد من الكروتينات بداخلها) بشكل عبثي وبدون ترتيب يؤدي إلى حدوث مشاكل, من بينها تسرب في الذاكرة. لذلك من الافضل ان نقوم بهيكلة النطاقات بشكل جيد, أو استخدام نطاقات خاصة كما جاء ذكرهم سابقاً. كنطاق ViewModelScope الخاص بالـ ViewModel فعندما يغادر المستخدم من الـ Activity التي تحتضن تلك الـ ViewModel فسيغلق ذلك النطاق وجميع الكروتينات سيتم إنهائها بشكل آلي. أو النطاق الخاص بالـ LifeCycle فعندما تنتهي دورة حياة أحد المكونات فإن نطاقها ينتهي معها وينهي جميع الكروتينات به.

ولكن اذا اردنا إنشاء نطاقات خاصة يجب مراعاة الهيكلة لهم, والتفكير في الأمر كعلاقة طفل Child وأب Parent. والاخذ في الحساب إن انواع النطاقات تختلف في تصرفها, فمثلاً الـ coroutineScope النطاق المنشئ من هذه الدالة المؤجله سيلغى اذا فشل احد النطاقات الموجودة بداخلة. إما الـ supervisorScope النطاق المنشئ من هذه الدالة المؤجلة لن يتم إلغاءه إذا فشل أحد النطاقات الموجودة بداخله. إما الـ GlobalScope ينشئ لنا نطاق عام, يبقى نشط دائماً خلال دورة حياة البرنامج, وهذا يعني ان كل الكروتينات التي ننشئها في داخله لن يتم الغاءها حتى لو انتهى عملها فمن الافضل تجنب هذا النوع حتى لانقع في المشاكل.

فمثلاً عندما نستخدم أكثر من نطاق مهيكل من coroutineScope لجلب البيانات من سيرفر ما, إذا فشل أي من الـ request سيتوقف عمل النطاق وهكذا تتوقف جميع الـ requests الموجودة بنطاقات بداخلة. ولتخطي هذه المشكله نستطيع الاستعانه بـ supervisorScope والامر مشابه للعنصر SupervisorJob كذلك.

 

الى هنا وصلنا في نهاية هذه الدرس. والذي تعرفنا فيه على الـ Job والنوع المشتق منه الـ Deferred الذي يستطيع إرجاع قيمة. وكذلك تعرفنا على الـ Coroutine Context والتي نستطيع تخيلها على انها خريطة لـ أين يعيش الكروتين. وتعرفنا على النطاقات بأنواعهم وفوائدهم وطرق إنشاءهم. في الدرس القادم سنتعرف على البنائين Builders.

 

المصادر

  • Offical Kotlin Docs.

المحاضر

Mohammad Laif

محتوى الدورة

الكلمات الدليلية

عن الدرس

0 إعجاب
1 متابع
0 مشاركة
2081 مشاهدات
منذ 4 سنوات

التعليقات (0)

لايوجد لديك حساب في عالم البرمجة؟

تحب تنضم لعالم البرمجة؟ وتنشئ عالمك الخاص، تنشر المقالات، الدورات، تشارك المبرمجين وتساعد الآخرين، اشترك الآن بخطوات يسيرة !