إرشادات بشأن واجهات برمجة التطبيقات غير المحظورة وغير المتزامنة في Android

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

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

هناك سببان رئيسيان لكتابة واجهة برمجة تطبيقات غير متزامنة:

  • تنفيذ عمليات متعددة في الوقت نفسه، حيث يجب بدء العملية رقم N قبل اكتمال العملية رقم N-1
  • تجنُّب حظر سلسلة محادثات مكالمة إلى أن تكتمل العملية

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

تعليق الوظائف:

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

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

توقّعات المطوّرين بشأن واجهات برمجة التطبيقات غير المتزامنة

تمت كتابة التوقعات التالية من منظور واجهات برمجة التطبيقات غير المعلقة ما لم يُذكر خلاف ذلك.

تكون واجهات برمجة التطبيقات التي تقبل عمليات رد الاتصال غير متزامنة عادةً

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

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

يجب أن تعرض واجهات برمجة التطبيقات غير المتزامنة النتائج في أسرع وقت ممكن

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

يمكن أن يتم تشغيل العديد من العمليات وإشارات مراحل النشاط حسب الطلب من خلال المنصة أو المكتبات، ومن غير العملي أن يتوقّع المطوّر أن يكون على دراية شاملة بجميع المواقع المحتملة التي يمكن استدعاء الرمز البرمجي منها. على سبيل المثال، يمكن إضافة Fragment إلى FragmentManager في معاملة متزامنة استجابةً لقياس View وتنسيقه عندما يجب تعبئة محتوى التطبيق لملء المساحة المتاحة (مثل RecyclerView). وقد تنفّذ LifecycleObserver التي تستجيب للاستدعاء في إحدى مراحل النشاط onStart الخاصة بهذا الجزء عمليات بدء التشغيل لمرة واحدة بشكل معقول هنا، وقد يكون ذلك في مسار برمجي مهم لإنتاج إطار رسوم متحركة خالٍ من إيقاف مؤقت لعرض واجهة المستخدم. يجب أن يكون المطوِّر واثقًا دائمًا من أنّ استدعاء أي واجهة برمجة تطبيقات غير متزامنة استجابةً لأنواع عمليات معاودة الاتصال هذه في مراحل النشاط لن يكون سببًا في حدوث لقطة غير سلسة.

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

  • تنفيذ عملية IPC أساسية كمكالمة رابطة أحادية الاتجاه
  • إجراء مكالمة ربط ثنائية الاتجاه إلى خادم النظام حيث لا يتطلب إكمال التسجيل الحصول على قفل شديد التنازع
  • إرسال الطلب إلى سلسلة وحدات عاملة في عملية التطبيق لتنفيذ تسجيل حظر عبر عملية الاتصال بين العمليات

يجب أن تعرض واجهات برمجة التطبيقات غير المتزامنة قيمة فارغة وأن تعرض خطأً فقط في حال توفُّر وسيطات غير صالحة.

يجب أن تعرض واجهات برمجة التطبيقات غير المتزامنة جميع نتائج العملية المطلوبة في دالة الرجوع المقدَّمة. ويتيح ذلك للمطوّر تنفيذ مسار رمز واحد للتعامل مع حالات النجاح والأخطاء.

قد تتحقّق واجهات برمجة التطبيقات غير المتزامنة من أنّ الوسيطات ليست فارغة، ثم تعرض الخطأ NullPointerException، أو تتحقّق من أنّ الوسيطات المقدَّمة تقع ضمن نطاق صالح، ثم تعرض الخطأ IllegalArgumentException. على سبيل المثال، بالنسبة إلى دالة تقبل float في النطاق من 0 إلى 1f، قد تتحقّق الدالة من أنّ المَعلمة ضِمن هذا النطاق وتطرح IllegalArgumentException إذا كانت خارج النطاق، أو قد يتم التحقّق من String قصير للتأكّد من مطابقته لتنسيق صالح مثل الأرقام والحروف الأبجدية فقط. (تذكَّر أنّه يجب ألا يثق خادم النظام في عملية التطبيق أبدًا. يجب أن تكرّر أي خدمة تابعة لنظام التشغيل عمليات التحقّق هذه في الخدمة التابعة لنظام التشغيل نفسها.)

يجب إرسال جميع الأخطاء الأخرى إلى دالة الرجوع المقدَّمة. ويشمل ذلك، على سبيل المثال لا الحصر، ما يلي:

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

يجب أن توفّر واجهات برمجة التطبيقات غير المتزامنة آلية إلغاء

يجب أن توفّر واجهات برمجة التطبيقات غير المتزامنة طريقة للإشارة إلى عملية قيد التشغيل بأنّ المتصل لم يعُد مهتمًا بالنتيجة. يجب أن تشير عملية الإلغاء هذه إلى أمرين:

يجب تحرير المراجع الثابتة لبرامج معالجة المكالمات التي يقدّمها المتصل

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

قد يتوقّف محرك التنفيذ الذي ينفّذ العمل للمتصل عن تنفيذ هذا العمل

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

اعتبارات خاصة للتطبيقات المخزَّنة مؤقتًا أو المجمَّدة

عند تصميم واجهات برمجة تطبيقات غير متزامنة تنشأ فيها عمليات معاودة الاتصال في عملية نظام ويتم تسليمها إلى التطبيقات، يجب مراعاة ما يلي:

  1. العمليات ودورة حياة التطبيق: قد تكون عملية تطبيق المستلِم في حالة التخزين المؤقت.
  2. تجميد التطبيقات المخزّنة مؤقتًا: قد يتم تجميد عملية تطبيق المستلِم.

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

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

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

قيد المراجعة:

  • عليك التفكير في إيقاف إرسال معاودة الاتصال الخاصة بالتطبيق مؤقتًا أثناء تخزين عملية التطبيق مؤقتًا.
  • يجب إيقاف إرسال عمليات معاودة الاتصال في التطبيق مؤقتًا أثناء تجميد عملية التطبيق.

تتبُّع الحالة

لتتبُّع وقت دخول التطبيقات إلى حالة التخزين المؤقت أو الخروج منها، اتّبِع الخطوات التالية:

mActivityManager.addOnUidImportanceListener(
    new UidImportanceListener() { ... },
    IMPORTANCE_CACHED);

لتتبُّع وقت تجميد التطبيقات أو إلغاء تجميدها، اتّبِع الخطوات التالية:

IBinder binder = <...>;
binder.addFrozenStateChangeCallback(executor, callback);

استراتيجيات لاستئناف إرسال عمليات معاودة الاتصال في التطبيق

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

على سبيل المثال:

IBinder binder = <...>;
bool shouldSendCallbacks = true;
binder.addFrozenStateChangeCallback(executor, (who, state) -> {
    if (state == IBinder.FrozenStateChangeCallback.STATE_FROZEN) {
        shouldSendCallbacks = false;
    } else if (state == IBinder.FrozenStateChangeCallback.STATE_UNFROZEN) {
        shouldSendCallbacks = true;
    }
});

بدلاً من ذلك، يمكنك استخدام RemoteCallbackList الذي يضمن عدم إرسال عمليات رد الاتصال إلى العملية المستهدَفة عندما تكون مجمَّدة.

على سبيل المثال:

RemoteCallbackList<IInterface> rc =
        new RemoteCallbackList.Builder<IInterface>(
                        RemoteCallbackList.FROZEN_CALLEE_POLICY_DROP)
                .setExecutor(executor)
                .build();
rc.register(callback);
rc.broadcast((callback) -> callback.foo(bar));

لا يتم استدعاء callback.foo() إلا إذا لم يتم تجميد العملية.

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

interface BatteryListener {
    void onBatteryPercentageChanged(int newPercentage);
}

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

RemoteCallbackList<IInterface> rc =
        new RemoteCallbackList.Builder<IInterface>(
                        RemoteCallbackList.FROZEN_CALLEE_POLICY_ENQUEUE_MOST_RECENT)
                .setExecutor(executor)
                .build();
rc.register(callback);
rc.broadcast((callback) -> callback.onBatteryPercentageChanged(value));

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

يمكن التعبير عن الحالة كبيانات أكثر تعقيدًا. لنفترض أنّ هناك واجهة برمجة تطبيقات لإعلام التطبيقات بواجهات الشبكة:

interface NetworkListener {
    void onAvailable(Network network);
    void onLost(Network network);
    void onChanged(Network network);
}

عند إيقاف الإشعارات مؤقتًا لتطبيق ما، عليك تذكُّر مجموعة الشبكات والحالات التي رآها التطبيق آخر مرة. عند الاستئناف، يُنصح بإعلام التطبيق بالشبكات القديمة التي تم فقدانها، والشبكات الجديدة التي أصبحت متاحة، والشبكات الحالية التي تغيّر حالتها، وذلك بهذا الترتيب.

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

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

اعتبارات بشأن مستندات المطوّرين

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

ننصح المطوّرين بعدم وضع افتراضات بشأن الوقت الفاصل بين تلقّي التطبيق إشعارًا بحدث ووقت حدوث الحدث فعليًا.

توقّعات المطوّرين بشأن تعليق واجهات برمجة التطبيقات

يتوقّع المطوّرون الذين يعرفون التزامن المنظَّم في Kotlin السلوكيات التالية من أي واجهة برمجة تطبيقات معلَّقة:

يجب أن تُكمل الدوال المعلقة جميع الأعمال المرتبطة بها قبل إرجاع القيمة أو طرح الاستثناء

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

يجب أن تستدعي الدوال المعلقة مَعلمات رد الاتصال في مكانها فقط

يجب أن تكمل الدوال المعلقة جميع المهام المرتبطة بها قبل إرجاع القيمة، لذلك يجب ألا تستدعي أبدًا دالة رد اتصال أو مَعلمة دالة أخرى أو تحتفظ بمرجع إليها بعد أن تُرجع الدالة المعلقة القيمة.

يجب أن تحافظ الدوال المعلقة التي تقبل مَعلمات رد الاتصال على السياق ما لم يتم توثيق خلاف ذلك

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

يجب أن تتوافق دوال التعليق مع إلغاء مهمة kotlinx.coroutines

يجب أن تتوافق أي وظيفة تعليق مؤقت مع إلغاء المهمة على النحو المحدّد في kotlinx.coroutines. إذا تم إلغاء مهمة استدعاء عملية قيد التقدّم، يجب أن تستأنف الدالة العمل مع CancellationException في أقرب وقت ممكن ليتمكّن المستدعي من تنظيف البيانات ومتابعة العملية في أقرب وقت ممكن. تتولّى suspendCancellableCoroutine وغيرها من واجهات برمجة التطبيقات المعلقة التي تقدّمها kotlinx.coroutines معالجة ذلك تلقائيًا. بشكل عام، يجب ألا تستخدم عمليات تنفيذ المكتبة suspendCoroutine مباشرةً، لأنّها لا تتوافق مع سلوك الإلغاء هذا تلقائيًا.

يجب أن توفّر الدوال المعلقة التي تنفّذ عمليات حظر في الخلفية (غير الرئيسية أو سلسلة محادثات واجهة المستخدم) طريقة لضبط أداة الإرسال المستخدَمة

لا يُنصح بتعليق وظيفة حظر بالكامل للتبديل بين سلاسل التنفيذ.

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

يجب أن تعرض الدوال المعلقة التي تقبل المَعلمة الاختيارية CoroutineContext أو Dispatcher فقط للتبديل إلى أداة الإرسال هذه من أجل تنفيذ العمليات الحظرية، الدالة الحظرية الأساسية، وأن تنصح المطوّرين الذين يستدعونها باستخدام استدعائهم الخاص للدالة withContext لتوجيه العمل إلى أداة إرسال مختارة.

الفئات التي تُطلق إجراءات روتينية

يجب أن تحتوي الفئات التي تبدأ إجراءات روتينية متزامنة على CoroutineScope لتنفيذ عمليات البدء هذه. إنّ الالتزام بمبادئ التزامن المنظَّم يستلزم اتّباع أنماط هيكلية معيّنة للحصول على هذا النطاق وإدارته.

قبل كتابة فئة تشغّل مهامًا متزامنة في نطاق آخر، ننصحك بالاطّلاع على الأنماط البديلة التالية:

class MyClass {
    private val requests = Channel<MyRequest>(Channel.UNLIMITED)

    suspend fun handleRequests() {
        coroutineScope {
            for (request in requests) {
                // Allow requests to be processed concurrently;
                // alternatively, omit the [launch] and outer [coroutineScope]
                // to process requests serially
                launch {
                    processRequest(request)
                }
            }
        }
    }

    fun submitRequest(request: MyRequest) {
        requests.trySend(request).getOrThrow()
    }
}

يتيح عرض suspend fun لتنفيذ عمل متزامن للمتصل استدعاء العملية في سياقه الخاص، ما يزيل الحاجة إلى أن يدير MyClass CoroutineScope. يصبح تسلسل معالجة الطلبات أبسط، ويمكن غالبًا أن توجد الحالة كمتغيرات محلية في handleRequests بدلاً من خصائص الفئة التي تتطلب مزامنة إضافية.

يجب أن توفّر الفئات التي تدير إجراءات روتينية متزامنة طريقتَي إغلاق وإلغاء

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

private val myJob = Job(parent = `CoroutineContext`[Job])
private val myScope = CoroutineScope(`CoroutineContext` + myJob)

fun cancel() {
    myJob.cancel()
}

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

suspend fun join() {
    myJob.join()
}

تسمية العمليات النهائية

يجب أن يعكس الاسم المستخدَم للطُرق التي توقف المهام المتزامنة التي يملكها كائن ما والتي لا تزال قيد التنفيذ، العقد السلوكي لكيفية حدوث الإيقاف:

استخدِم close() عندما تكون العمليات الجارية قد تكتمل ولكن لا يمكن بدء عمليات جديدة بعد أن تعرض الدالة close() النتيجة.

استخدِم القيمة cancel() عندما يمكن إلغاء العمليات الجارية قبل اكتمالها. لا يمكن بدء أي عمليات جديدة بعد أن تعرض الدالة cancel() القيمة.

تقبل دوال إنشاء الصف CoroutineContext، وليس CoroutineScope

عندما يُمنع إطلاق العناصر مباشرةً في نطاق رئيسي محدّد، تتوقف ملاءمة CoroutineScope كمعلَمة منشئ على النحو التالي:

// Don't do this
class MyClass(scope: CoroutineScope) {
    private val myJob = Job(parent = scope.`CoroutineContext`[Job])
    private val myScope = CoroutineScope(scope.`CoroutineContext` + myJob)

    // ... the [scope] constructor parameter is never used again
}

يصبح CoroutineScope غلافًا غير ضروري ومضلّلاً، وفي بعض حالات الاستخدام، قد يتم إنشاؤه فقط ليتم تمريره كمَعلمة في الدالة الإنشائية، ثم يتم تجاهله:

// Don't do this; just pass the context
val myObject = MyClass(CoroutineScope(parentScope.`CoroutineContext` + Dispatchers.IO))

يتم ضبط المَعلمات CoroutineContext تلقائيًا على EmptyCoroutineContext

عندما تظهر المَعلمة الاختيارية CoroutineContext في مساحة عرض واجهة برمجة التطبيقات، يجب أن تكون القيمة التلقائية هي Empty`CoroutineContext`. يتيح ذلك إمكانية دمج سلوكيات واجهة برمجة التطبيقات بشكل أفضل، إذ يتم التعامل مع قيمة Empty`CoroutineContext` من المتصل بالطريقة نفسها التي يتم بها قبول القيمة التلقائية:

class MyOuterClass(
    `CoroutineContext`: `CoroutineContext` = Empty`CoroutineContext`
) {
    private val innerObject = MyInnerClass(`CoroutineContext`)

    // ...
}

class MyInnerClass(
    `CoroutineContext`: `CoroutineContext` = Empty`CoroutineContext`
) {
    private val job = Job(parent = `CoroutineContext`[Job])
    private val scope = CoroutineScope(`CoroutineContext` + job)

    // ...
}