הנחיות ל-API אסינכרוני ולא חוסם ב-Android

ממשקי API לא חוסמים מבקשים לבצע עבודה ואז מחזירים את השליטה לשרשור הקריאה כדי שהוא יוכל לבצע עבודה אחרת לפני השלמת הפעולה המבוקשת. ממשקי ה-API האלה שימושיים במקרים שבהם העבודה הנדרשת עשויה להיות מתמשכת או עשויה לדרוש המתנה לסיום של קלט/פלט או IPC, זמינות של משאבי מערכת שמתחרים עליהם או קלט משתמש לפני שהעבודה יכולה להתבצע. ממשקי API שתוכננו היטב מספקים דרך לבטל את הפעולה שנמצאת בתהליך ולהפסיק את העבודה שמתבצעת בשם המתקשר המקורי, וכך לשמור על תקינות המערכת ועל חיי הסוללה כשהפעולה כבר לא נחוצה.

ממשקי API אסינכרוניים הם דרך אחת להשגת התנהגות לא חוסמת. ממשקי API אסינכרוניים מקבלים סוג מסוים של המשך או קריאה חוזרת שמקבלים הודעה כשהפעולה מושלמת, או על אירועים אחרים במהלך התקדמות הפעולה.

יש שתי סיבות עיקריות לכתיבת API אסינכרוני:

  • הפעלה של כמה פעולות בו-זמנית, כשצריך להתחיל פעולה N לפני שפעולה N-1 מסתיימת.
  • לא לחסום את שיחת הטלפון עד לסיום הפעולה.

‫Kotlin מקדמת מאוד את הקונקורנטיות המובנית, סדרה של עקרונות וממשקי API שמבוססים על פונקציות השהיה שמפרידות בין ביצוע סינכרוני ואסינכרוני של קוד לבין התנהגות של חסימת השרשור. פונקציות השהיה הן לא חוסמות וסינכרוניות.

השעיית פונקציות:

  • לא לחסום את השרשור של הקריאה, אלא להעביר את שרשור הביצוע כפרט הטמעה בזמן ההמתנה לתוצאות של פעולות שמתבצעות במקום אחר.
  • פועלות באופן סינכרוני ולא דורשות מהמתקשר של API לא חוסם להמשיך לפעול במקביל לעבודה לא חוסמת שהופעלה על ידי קריאת ה-API.

בדף הזה מפורטות ציפיות בסיסיות מינימליות שיש למפתחים כשהם עובדים עם ממשקי API לא חוסמים ואסינכרוניים. בהמשך מופיעות סדרת הוראות ליצירת ממשקי API שעומדים בציפיות האלה בשפות Kotlin או Java, בפלטפורמת Android או בספריות Jetpack. אם יש לכם ספק, כדאי להתייחס לציפיות של המפתחים כאל דרישות לכל ממשק API חדש.

מה מצפים מפתחים מממשקי API אסינכרוניים

הציפיות הבאות נכתבו מנקודת המבט של ממשקי API שלא גורמים להשעיה, אלא אם צוין אחרת.

ממשקי API שמקבלים קריאות חוזרות הם בדרך כלל אסינכרוניים

אם API מקבל קריאה חוזרת שלא מתועדת כקריאה שמתבצעת במקום (כלומר, קריאה שמתבצעת רק על ידי השרשור שקורא לפני שהקריאה ל-API עצמו מחזירה ערך), נניח שה-API הוא אסינכרוני, וה-API הזה צריך לעמוד בכל הציפיות האחרות שמתועדות בקטעים הבאים.

דוגמה לפונקציית קריאה חוזרת שנקראת רק במקום היא פונקציית מיפוי או סינון מסדר גבוה יותר שמפעילה פונקציית מיפוי או פרדיקט על כל פריט באוסף לפני ההחזרה.

ממשקי API אסינכרוניים צריכים להחזיר תשובה מהר ככל האפשר

מפתחים מצפים שממשקי API אסינכרוניים יהיו לא חוסמים ויחזירו תשובה במהירות אחרי הפעלת הבקשה לפעולה. תמיד צריך להיות בטוח לשלוח קריאה ל-API אסינכרוני בכל שלב, ושליחת קריאה ל-API אסינכרוני אף פעם לא צריכה לגרום לפריימים קופצניים או ל-ANR.

הרבה אותות של פעולות ומחזור חיים יכולים להיות מופעלים על ידי הפלטפורמה או הספריות לפי דרישה, ואי אפשר לצפות ממפתח שיחזיק בידע גלובלי על כל האתרים הפוטנציאליים שבהם הקוד שלו יכול לפעול. לדוגמה, אפשר להוסיף Fragment ל-FragmentManager בעסקה סינכרונית בתגובה למדידה ולפריסה של View כשצריך לאכלס את תוכן האפליקציה כדי למלא את המקום הפנוי (כמו RecyclerView). LifecycleObserver בתגובה לקריאה החוזרת של מחזור החיים של onStart עשוי לבצע כאן באופן סביר פעולות הפעלה חד-פעמיות, וזה יכול להיות בנתיב קוד קריטי ליצירת פריים של אנימציה ללא ג'יטר. מפתח צריך תמיד להיות בטוח שקריאה לכל API אסינכרוני בתגובה לקריאות חוזרות (callback) כאלה של מחזור החיים לא תגרום לפריים מגומגם.

המשמעות היא שהעבודה שמבצע API אסינכרוני לפני החזרה צריכה להיות קלה מאוד. לכל היותר, הוא יוצר רשומה של הבקשה והקריאה החוזרת המשויכת, ורושם אותה במנוע הביצוע שמבצע את העבודה. אם רישום לפעולה אסינכרונית דורש IPC, ההטמעה של ה-API צריכה לכלול את כל האמצעים הנדרשים כדי לעמוד בציפייה הזו של המפתח. הפעולות האלה יכולות לכלול אחת או יותר מהאפשרויות הבאות:

  • הטמעה של IPC בסיסי כקריאה ב-Binder חד-כיווני
  • ביצוע שיחת binder דו-כיוונית לשרת המערכת, שבה השלמת ההרשמה לא מחייבת נעילה שמתחרים עליה מאוד
  • פרסום הבקשה בשרשור של תהליך האפליקציה כדי לבצע רישום חסימה באמצעות IPC

ממשקי API אסינכרוניים צריכים להחזיר void ולהפעיל חריגה רק לארגומנטים לא חוקיים

ממשקי API אסינכרוניים צריכים לדווח על כל התוצאות של הפעולה המבוקשת לפונקציית הקריאה החוזרת שצוינה. כך המפתח יכול להטמיע נתיב קוד יחיד לטיפול בהצלחה ובשגיאות.

ממשקי API אסינכרוניים עשויים לבדוק אם הארגומנטים הם null ולהחזיר NullPointerException, או לבדוק אם הארגומנטים שסופקו נמצאים בטווח תקין ולהחזיר IllegalArgumentException. לדוגמה, בפונקציה שמקבלת את הערך float בטווח 0 עד 1f, הפונקציה עשויה לבדוק אם הפרמטר נמצא בטווח הזה ולהחזיר את הערך IllegalArgumentException אם הוא לא בטווח, או שהיא עשויה לבדוק אם הערך String תואם לפורמט תקין כמו אלפאנומרי בלבד. (חשוב לזכור שלשרת המערכת אסור לסמוך על תהליך האפליקציה! כל שירות מערכת צריך לשכפל את הבדיקות האלה בשירות המערכת עצמו).

כל שאר השגיאות צריכות להיות מדווחות לקריאה החוזרת שסופקה. האיסור הזה כולל, בין היתר:

  • הפעולה המבוקשת נכשלה
  • חריגים בנושא אבטחה שקשורים להרשאות חסרות שנדרשות להשלמת הפעולה
  • חריגה מהמכסה לביצוע הפעולה
  • תהליך האפליקציה לא מספיק 'בחזית' כדי לבצע את הפעולה
  • החומרה הנדרשת נותקה
  • תקלות ברשת
  • חסימות זמניות
  • השבתת Binder או תהליך מרוחק לא זמין

ממשקי API אסינכרוניים צריכים לספק מנגנון ביטול

ממשקי API אסינכרוניים צריכים לספק דרך לציין לפעולה שמתבצעת שהמתקשר כבר לא מתעניין בתוצאה. פעולת הביטול הזו צריכה לסמן שני דברים:

צריך להסיר הפניות קשיחות לקריאות חוזרות שסופקו על ידי המתקשר

פונקציות קריאה חוזרת שמועברות לממשקי API אסינכרוניים עשויות להכיל הפניות קשיחות לגרפים של אובייקטים גדולים, ועבודה שמתבצעת כרגע ומכילה הפניה קשיחה לפונקציית הקריאה החוזרת הזו יכולה למנוע את איסוף האשפה של גרפים כאלה של אובייקטים. אם מפסיקים את הפעולה, ההפניות האלה לקריאות חוזרות משוחררות, ולכן יכול להיות שהגרפים של האובייקטים האלה יהיו זמינים לאיסוף אשפה הרבה יותר מהר מאשר אם הפעולה הייתה מסתיימת.

מנוע ההפעלה שמבצע עבודה בשביל המתקשר עשוי להפסיק את העבודה הזו

עבודה שמתחילה מקריאות ל-API אסינכרוני עלולה להיות יקרה מבחינת צריכת חשמל או משאבי מערכת אחרים. ממשקי API שמאפשרים למבצעי הקריאות לסמן מתי העבודה הזו כבר לא נחוצה, מאפשרים להפסיק את העבודה לפני שהיא צורכת עוד משאבי מערכת.

שיקולים מיוחדים לגבי אפליקציות שנשמרו במטמון או שהוקפאו

כשמתכננים ממשקי API אסינכרוניים שבהם קריאות חוזרות נוצרות בתהליך מערכת ומועברות לאפליקציות, כדאי לשים לב לנקודות הבאות:

  1. תהליכים ומחזור החיים של האפליקציה: יכול להיות שהתהליך של אפליקציית הנמען נמצא במצב שמור במטמון.
  2. הקפאת אפליקציות במטמון: יכול להיות שתהליך האפליקציה של הנמען הוקפא.

כשמצב התהליך של אפליקציה משתנה למצב שמור במטמון, זה אומר שהיא לא מארחת באופן פעיל רכיבים שגלויים למשתמשים, כמו פעילויות ושירותים. האפליקציה נשמרת בזיכרון למקרה שהיא תהיה גלויה שוב למשתמש, אבל בינתיים היא לא אמורה לבצע פעולות. ברוב המקרים, צריך להשהות את השליחה של קריאות חוזרות (callback) לאפליקציה כשהאפליקציה עוברת למצב שמור במטמון, ולחדש את השליחה כשהאפליקציה יוצאת מהמצב הזה, כדי לא לגרום לעבודה בתהליכים של אפליקציות שמורות במטמון.

יכול להיות שאפליקציה שנשמרה במטמון תוקפא. כשאפליקציה מוקפאת, היא לא מקבלת זמן מעבד (CPU) ולא יכולה לבצע שום פעולה. כל השיחות אל פונקציות ה-callback הרשומות של האפליקציה נשמרות בזיכרון הזמני ומועברות כשהאפליקציה מפסיקה להיות קפואה.

יכול להיות שהנתונים של טרנזקציות שנשמרו בזיכרון הזמני של האפליקציה יהיו ישנים מדי עד שהאפליקציה תבטל את ההקפאה ותעבד אותם. המאגר מוגבל, ואם הוא יתמלא מעל ומעבר, האפליקציה של הנמען תקרוס. כדי למנוע עומס יתר באפליקציות עם אירועים ישנים או גלישה על המאגרים שלהן, אל תשלחו קריאות חוזרות (callback) לאפליקציות בזמן שהתהליך שלהן קפוא.

בבדיקה:

  • כדאי להשהות את הקריאות החוזרות (callback) של אפליקציית השליחה בזמן שהתהליך של האפליקציה נמצא במטמון.
  • חובה להשהות את שליחת הקריאות החוזרות (callback) של האפליקציה בזמן שהתהליך של האפליקציה קפוא.

מעקב אחרי מצב

כדי לעקוב אחרי הכניסה של אפליקציות למצב שמור במטמון או היציאה ממנו:

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

כדי לעקוב אחרי מקרים שבהם אפליקציות מוקפאות או מבוטלת ההקפאה שלהן:

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

אסטרטגיות להמשך שליחת קריאות חוזרות (callback) לאפליקציה

אם אתם משהים את השליחה של קריאות חוזרות (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() מופעלת רק אם התהליך לא מושהה.

אפליקציות שומרות לעיתים קרובות עדכונים שהן מקבלות באמצעות קריאות חוזרות (callbacks) כתמונת מצב של המצב האחרון. נניח שיש API היפותטי לאפליקציות שמאפשר לעקוב אחרי אחוז הסוללה שנותר:

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));

במקרים מסוימים, אפשר לעקוב אחרי הערך האחרון שמועבר לאפליקציה, כך שלא יהיה צורך להודיע לאפליקציה על אותו ערך אחרי שהיא תופשר.

המצב יכול להיות מיוצג כנתונים מורכבים יותר. נניח שיש API היפותטי לאפליקציות שמאפשר לקבל הודעות על ממשקי רשת:

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

כשמשהים את ההתראות באפליקציה, חשוב לזכור את קבוצת הרשתות והמצבים שהאפליקציה ראתה בפעם האחרונה. כשממשיכים את הפעולה, מומלץ להודיע לאפליקציה על רשתות ישנות שאבדו, על רשתות חדשות שהפכו לזמינות ועל רשתות קיימות שהמצב שלהן השתנה – בסדר הזה.

לא לשלוח לאפליקציה התראות על רשתות שהיו זמינות ואז נעלמו בזמן שההתקשרות חזרה (callback) הייתה מושהית. אפליקציות לא אמורות לקבל דיווח מלא על אירועים שהתרחשו בזמן שהן היו במצב קפוא, והתיעוד של ה-API לא אמור להבטיח אספקת זרמי אירועים ללא הפרעה מחוץ למצבי מחזור חיים מפורשים. בדוגמה הזו, אם האפליקציה צריכה לעקוב באופן רציף אחרי הזמינות של הרשת, היא צריכה להישאר במצב מחזור חיים שלא מאפשר לה להיכנס למטמון או להיות קפואה.

במהלך הבדיקה, צריך לאחד אירועים שהתרחשו אחרי השהיית ההתראות ולפני חידוש ההתראות, ולהעביר את המצב העדכני לפונקציות הקריאה החוזרת (callback) של האפליקציה הרשומה בצורה תמציתית.

שיקולים לגבי תיעוד למפתחים

יכול להיות שיהיה עיכוב בשליחת אירועים אסינכרוניים, או כי השולח השהה את השליחה למשך זמן מסוים כמו שמוצג בקטע הקודם, או כי אפליקציית הנמען לא קיבלה מספיק משאבי מכשיר כדי לעבד את האירוע בזמן.

מומלץ להניא מפתחים מלהניח הנחות לגבי הזמן שחולף בין הרגע שבו האפליקציה שלהם מקבלת הודעה על אירוע לבין הרגע שבו האירוע קרה בפועל.

ציפיות המפתחים לגבי השעיה של ממשקי API

מפתחים שמכירים את המקביליות המובנית של Kotlin מצפים להתנהגויות הבאות מכל API שניתן להשהיה:

פונקציות מושהות צריכות להשלים את כל העבודה שמשויכת אליהן לפני שהן מחזירות ערך או מעלות שגיאה

התוצאות של פעולות לא חוסמות מוחזרות כערכי החזרה רגילים של פונקציות, ושגיאות מדווחות באמצעות הפעלת חריגים. (בדרך כלל זה אומר שפרמטרים של קריאה חוזרת לא נחוצים).

פונקציות השהיה צריכות להפעיל פרמטרים של קריאה חוזרת רק במקום

פונקציות השהיה תמיד צריכות להשלים את כל העבודה שמשויכת אליהן לפני שהן מחזירות ערך, ולכן הן אף פעם לא צריכות להפעיל קריאה חוזרת שסופקה או פרמטר פונקציה אחר, או לשמור הפניה אליהם אחרי שפונקציית ההשהיה החזירה ערך.

פונקציות שמושהות ומקבלות פרמטרים של קריאה חוזרת צריכות לשמור על ההקשר, אלא אם צוין אחרת במסמכים

כשקוראים לפונקציה בתוך פונקציית השהיה, היא פועלת בהקשר של הפונקציה שקוראת לה.CoroutineContext מכיוון שפונקציות השהיה אמורות להשלים את כל העבודה המשויכת לפני שהן מחזירות או מעלות חריגה, והן אמורות להפעיל פרמטרים של קריאה חוזרת רק במקום, ההנחה היא שכל קריאה חוזרת כזו מופעלת גם ב-CoroutineContext שקורא לפונקציה באמצעות מנגנון השליחה המשויך. אם המטרה של ה-API היא להפעיל קריאה חוזרת (Callback) מחוץ ל-CoroutineContext שקורא ל-API, צריך לתעד את ההתנהגות הזו בצורה ברורה.

פונקציות השהיה צריכות לתמוך בביטול משימות של kotlinx.coroutines

כל פונקציית השהיה שמוצעת צריכה לפעול בשיתוף עם ביטול משימות, כפי שמוגדר ב-kotlinx.coroutines. אם עבודת ההתקשרות של פעולה שנמצאת בתהליך מבוטלת, הפונקציה צריכה להתחדש עם CancellationException בהקדם האפשרי, כדי שהמתקשר יוכל לנקות ולהמשיך בהקדם האפשרי. הטיפול בזה מתבצע באופן אוטומטי על ידי suspendCancellableCoroutine וממשקי API אחרים להשעיה שמוצעים על ידי kotlinx.coroutines. בדרך כלל, בהטמעות של ספריות לא מומלץ להשתמש ישירות ב-suspendCoroutine, כי היא לא תומכת בהתנהגות הביטול הזו כברירת מחדל.

פונקציות שמשעות עבודה חוסמת ברקע (לא בשרשור הראשי או בשרשור של ממשק המשתמש) צריכות לספק דרך להגדיר את ה-dispatcher שבו נעשה שימוש

לא מומלץ להשהות פונקציה חוסמת לחלוטין כדי להחליף שרשורים.

קריאה לפונקציית השהיה לא אמורה לגרום ליצירה של שרשורים נוספים בלי לאפשר למפתח לספק שרשור משלו או מאגר שרשורים משלו כדי לבצע את העבודה הזו. לדוגמה, יכול להיות שפונקציית הבנאי תקבל CoroutineContext שמשמשת לביצוע עבודה ברקע עבור השיטות של המחלקה.

פונקציות שמושהות ומקבלות פרמטר אופציונלי CoroutineContext או Dispatcher רק כדי לעבור ל-Dispatcher הזה כדי לבצע עבודה חוסמת, צריכות לחשוף במקום זאת את הפונקציה החוסמת הבסיסית, ולהמליץ למפתחים שקוראים להן להשתמש בקריאה משלהם ל-withContext כדי להפנות את העבודה ל-Dispatcher שנבחר.

מחלקות שמפעילות קורוטינות

בכיתות שמפעילות קורוטינות צריך להיות 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 לביצוע עבודה בו-זמנית מאפשרת לקורא להפעיל את הפעולה בהקשר שלו, ומבטלת את הצורך בניהול CoroutineScope על ידי MyClass. הפיכת העיבוד של בקשות לסדרתי הופכת לפשוטה יותר, ולעתים קרובות המצב יכול להתקיים כמשתנים מקומיים של handleRequests במקום כמאפייני מחלקה, שאחרת היו דורשים סנכרון נוסף.

בכיתות שמנהלות קורוטינות צריך להיות אפשר להשתמש בשיטות close ו-cancel

מחלקות שמפעילות קורוטינות כפרטי הטמעה חייבות להציע דרך לסגור בצורה נקייה את המשימות המקבילות המתמשכות האלה, כדי שלא יתרחשו דליפות של עבודה מקבילה לא מבוקרת לטווח של הורה. בדרך כלל זה נעשה על ידי יצירת צאצא 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 מופיע בממשק API, ערך ברירת המחדל שלו חייב להיות ערך השמירה Empty`CoroutineContext`. כך אפשר ליצור התנהגויות טובות יותר של API, כי ערך 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)

    // ...
}