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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

אפליקציות שומרות לעיתים קרובות עדכונים שהן מקבלות באמצעות קריאות חוזרות (callback) כתמונת מצב של המצב האחרון. נניח שיש 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);
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Classes launching coroutines

במחלקות שמפעילות קורוטינות צריך להיות 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 מופיע בממשק 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)

    // ...
}