تعرف على Kotlin coroutines : الجزء الرابع

وصلنا للجزء الرابع والأخير، افترض أنك وصلت هنا دا معناه أنك قريت وفهمت التلت أجزاء اللي فاتوا، دول اللينكات لو عايز ترجع بسرعة قبل ما نخش في موضوع النهاردة

النهاردة هنتعمق شوية في Cancellation و Exception، شرحنا منها شوية في الجزء التاني، والنهاردة هنكمل:

Job Cancellation:

كنا عارفين أننا عندنا أهم طاريقتين عشان نعمل Cancel وهي أننا نستخدم واحدة من الاتنين، مع العلم أنك لما تعمل cancel لأي job فاللي هيحصل أن أي job جواه هيحصله Cancel برضو:

  • job.cancel : بتعمل Cancel لJob وتكمل عادي باقي الjobs اللي موجودة في نفس الScope
  • job.cancelAndJoin : بتعمل Cancel لjob وتستنى لحد ما Job يعملها Cancel بشكل نهائي بعدين تبدأ باللي بعدها

بص للمثال الجي :

  CoroutineScope(IO).launch {
            val firstJob = launch {
              delay(2000)
            }
            
            firstJob.cancel(CancellationException("A cancel message"))

            val secondJob = launch {
               delay(3000)

            }
            secondJob.cancelAndJoin()
            val thirdJob = launch {
               delay(4000)
            }
        }

عندنا هنا مثال في Coroutine scope فيه تلاتة Jobs، كل واحد منهم بيعمل task، أول واحد بياخد ثانيتين، تاني واحد 3 ثواني، وأخر واحد بياخد 4 ثواني، السيناريو اللي هيحصل كالآتي:
هيبدأ يعمل أول job هيلاقيه بياخد شوية وقت فهيسيبه يكمل في الbackground thread ويمشي تحت يلاقي أن الjob.cancel فهيبعت اشارة ليه أنه يعمل cancel، ويكمل على طول يروح لsecond job هيلاقيها برضو بتاخد وقت فهيخليها تعمله ويكمل يلاقي أنه حصله cancelAndJoin، فهيبعت اشارة أنه يعمل cancel لsecond job، وهيستنى لحد ما الjob يحصلها cancel تمامًا ويبدأ في third job.

فيه كام ملحوظة لازم تعرفها عشان متتلخبطش في الكود، لو عندك job بتستدعي suspend fun وبتعمل حاجة وجواها عايز تعمل cancel مش هتعرف، لازم عشان تعمل Cancel لازم Job.cancel تكون من برا الjob يعني في أي حتة أو في parent job اللي هي في حالتنا الScope

هيجي السؤال على بالك، طب لو أنا logic التطبيق اللي شغال عليه محتاج أتأكد من حاجة جوا job ولو منفعتش عايز أعمل Cancel، الحل بقا أنك تستخدم:

Cancellation Exception:

دا نوع خاص من Exception خاص بالcoroutines وفي الحقيقة أنك لما اصلاً بتستدعي cancel هو بيعمل throw لcancellation exception، وممكن تتأكد منها في invokeOnCompletion برضو، فلو أنت عايز تعمل cancel من جوا الjob فكل اللي عليك أنك تعمل throw لCancelationException زي اللي جي كدا :

  val firstJob = launch {
               if(jobShouldbeCancelled){
                   throw CancellationException("You reason goes here")
              }
   }

بس كدا، وهتعتبر كدا عملت Cancel بالظبط من غير أي فرق خالص، متخفش لأنه مش هيقفل التطبيق زي باقي الexceptions، ركز في الحتة الي فاتت عشان قدام هنشرح حاجة ليها علاقة بيها

طيب افترض أنك عندك كود عايز تخليه يعمل Compelete حتى لو job اللي هو فيها حصله Cancel، الموضوع دا هتحتاجه كتير جدًا أنك في جزء في الكود لازم يكمل للأخر بغض النظر عن الباقي cancel أو لأ، ساعتها ممكن تستخدم (withContext(NonCancellable، زي المثال الجي :

    val firstJob = launch {
                
                //will be cancelled if the job is cancelled
                mainTask()
                
                withContext(NonCancellable) {
                    //This code will be run even if the job is cancelled 
                        delay(500)
                    
      }
  }

كدا التاسك الأساسي هيحصلها cancel وبالنسبة للي متحدد withContext مش هيحصلها cancel وهيكمل برضو

لو أي job حصلها Cancel -حتى لو رمينا Cancellation exception- مش هتأثر على باقي jobs في نفس الScope أو على parent job كلوا هيكمل عادي، النقطة ديه مهمة جدًا.

Exceptions:

تخيل أنك بتعمل job معينة واترمي Exception عادي خالص، زي مثلاً أنك كنت بتعمل network request في MainThread، ايه اللي هيحصل في الحالة ديه، شوف المثال الجي:

 CoroutineScope(IO).launch {
            val firstJob = launch {
                 delay(2000)
                println("first job is working")
            }


            val secondJob = launch {
                println("second Job is working")

                throw Exception("This A normal Exception")
            }

          
            val thirdJob = launch {
                println("third Job is working")

            }
        }

عندنا هنا تلاتة jobs، التانية هيحصل فيها Exception لو جربت الكود دا عندك، هتيجي تشغل التطبيق هتلاقي أن التطبيق عمل crash، ودا بسبب أن تاني job عملت Exception، وCoroutine معملتش Hnadle ليها، وأنت فوق مش حاطط اي Try وCatch تعالى نحل المشكلة خطوة خطوة:

Exception Handler :

coroutine ليها طريقة عشان تعمل Handle لأي Exception ممكن يحصل في الparent job أو الchild، شوف المثال بعدين تابع الشرح :

 val handler :CoroutineExceptionHandler = CoroutineExceptionHandler{_,exception->
            println("an Exception is detected")
            print("Exception Message : ${exception.localizedMessage}")
        }

        CoroutineScope(IO).launch(handler) {
            val firstJob = launch {
                delay(1000)
                println("first job is working")
            }


            val secondJob = launch {
              
                delay(2000)

                println("second Job is working")

                throw Exception("This A normal Exception")
            }

    
            val thirdJob = launch {
                delay(3000)
                println("third Job is working")

            }
        }

أول حاجة عملنا handler من نوع CoroutineExceptionHandler، دا لو حصل أي Exception هيمنع أن coroutine تعمل crash للتطبيق وبدل كدا هيتم عمل الكود اللي جواها، في الحالة اللي فوق هطبع الmessage بس، يعني لما يترمي Exception فوق في تاني job هيروح يبعت الException دا للhandler وأنت تتعامل معاه، زي أنك تظهر رسالة للمستخدم بالخطأ

تاني خطوة أننا جيطها الhandler في launch بتاع الScope، ولازم هنا نقول حاجة مهمة بس وهي :

Handler is only working for parent jobs

يعني مينفعش كل job يكون ليها handler لأ هي handler بس للparent job وأي child job مش هتنفع معاها Handler.

طيب لو شغلنا الكود كدا التطبيق مش هيقفل ودا شئ كويس بس في مشكلة بقا وهي أن لما تاني Job يحصلها فيها أي Exception غير CancelationException فاللي هيحصل أن parent job هتفشل ومش هتكمل وبالتالي أي job جواها لسه مخلصتش مش هتكمل كمان، دا بالظبط عكس السيناريو بتاع الCancellation اللي قولتلك ركز فيه فوق، هقولك الحل اللي ممكن نعمله دلوقتي بس قبليها في حاجتين هقولهم عشان لو عايز تhandle exception في child job

الصورة من blog الرسمي لجوجل

InvokeOnCompletion:

ديه شرحناها قبل كدا مش هطول فيها، قولنا انها بتستدعى في جميع الأحوال أول ما الjob تخلص، سواء cancel أو حصل مشكلة ورمت Exception أو حتى خلص بنجاح، طيب لو انا جواها عايز اتأكد هي exception ولا كملت هعمل الآتي :

  val firstJob = launch {
                println("first job is working")
            }
            firstJob.invokeOnCompletion { cause: Throwable? ->
                if(cause == null){
                    print("there's exception")
                }
                else{
                    print("Completed with no exception ")

                }
            }

الlambada بيكون جواها argument من نوع Throwables لو هو Null معناه كدا أن الjob خلصت ومحصلش Exception لو مش null يبقى حصل Exception وممكن ساعتها تشوف الMessage

Try, Catch& finally

الطريقة العادية اللي ممكن نعمل handle لأي Exception حتى لو CancellationException :

 val firstJob = launch {
                try {
                    work()
                } catch (e: CancellationException){
                    print("Work cancelled!" )
                } finally {
                    print("clean up")
                }
            }

أعتقد مش محتاجة شرح، نرجع لنقطتنا بقا وهي أن لو حصل Exception عايزين الParent job تكمل، فهنستخدم :

Supervisor Job :

لما نيجي نعمل SupervisorJob جواه أكتر من Child job، لما واحدة منهم هتفشل وهيحصل فيها Exception الباقي هيكمل عادي وحتى أن الParent هتكمل عادي وتقولك أنها خلت بنجاح، يعني أي حاجة جوا SupervisorJob  لما تفشل بتفشل لوحدها

خلي بالك أن هو كدا لسه في Exception جوا، ولازم يكون في Handler في parent job لو متعملش هيقفل التطبيق وهيحصل Crash، شوف الكود الجي نفس المثال بس شوف أنا حدد في Scope انها SupervisorJob 

   CoroutineScope(IO + SupervisorJob()).launch(handler) {
            val firstJob = launch {
                println("first Job is working")

            }

            val secondJob = launch {
                println("second Job is working")
                throw Exception("This A normal Exception")
            }

            val thirdJob = launch {
                println("third Job is working")

            }
        }

كدا تاني هيحصل Exception عادي بس التطبيق مش هيتقفل عشان أنا حاطط handler قبل launch بتاع الparent، وباقي الjob هتكمل عادي خالص، حتى الparent job.

وأخر حاجة، ديه طريقة تانية عشان تعمل supervisorScope :

 CoroutineScope(IO).launch {
            supervisorScope {
                val firstJob = launch {
                    println("first Job is working")

                }
            }

        }

وبكدا نكون خلصنا شرح، ، اتمنى تكون استفدت منها على أكبر قدر ممكن، بكدا أنت بشكل كبير قادر تستخدم Kotlin coroutines اللي أنا شايفها أحسن طريقة عشان أي حاجة لازم تتعمل في background thread، وخاصة أن فيه حاجة جديدة اسمها Kotlin flow بتخليها أقوى بكتير هنبقا نشرحها برضو..

لو عايز أكمل أروح فين؟

انصحك في البداية تروح Github، تشوف ازاي بيستخدموها في المشاريع الكبيرة وفي Clean code وازاي تقدر تطبيقها في MVVM و MVI، لو عايز تزود المعرفة فأنصحك الموقع الرسمي الdocs عليه سهلة وفيه المعلومة من غير لف، وممكن تقرا الأربع أجزاء اللي جوجل نزلتهم من فترة هتلاقيهم هنا، شرحهم حلو وبسيط.

وطبعًا لو في اي سؤال ممكن في التعليقات أو على جروب الفيس.

Happy coding

الكاتب: Mohamed Saber

Pharmacist, Android developer and UI/UX enthusiast

(2) تعليقات

  1. في ال code sample اللي قبل ده
    “الlambada بيكون جواها argument من نوع Throwables لو هو Null معناه كدا أن الjob خلصت ومحصلش Exception لو مش null يبقى حصل Exception وممكن ساعتها تشوف الMessage”
    حضرتك عاكس
    شكراً على المجهود العظيم tho استمتعت بالقراءة :’)

اترك ردا