הנחיות ל-Android API

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

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

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

כלי API Lint

API Lint משולב בכלי הניתוח הסטטי Metalava ופועל אוטומטית במהלך האימות ב-CI. אפשר להפעיל אותו באופן ידני מדף תשלום בפלטפורמה מקומית באמצעות m checkapi או מדף תשלום מקומי של AndroidX באמצעות ./gradlew :path:to:project:checkApi.

כללי API

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

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

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

יסודות ה-API

הקטגוריה הזו מתייחסת להיבטים המרכזיים של Android API.

צריך להטמיע את כל ממשקי ה-API

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

לממשקי API ללא הטמעות יש כמה בעיות:

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

צריך לבדוק את כל ממשקי ה-API

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

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

שינוי שמוסיף API חדש צריך לכלול בדיקות תואמות באותו CL או באותו נושא ב-Gerrit.

בנוסף, ממשקי ה-API צריכים להיות ניתנים לבדיקה. צריכה להיות לכם תשובה לשאלה: "איך מפתח אפליקציות יבדוק קוד שמשתמש ב-API שלך?"

כל ממשקי ה-API צריכים להיות מתועדים

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

כל ממשקי ה-API שנוצרו חייבים לעמוד בהנחיות

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

כלים שלא מומלץ להשתמש בהם ליצירת ממשקי API:

  • AutoValue: יש הפרות של ההנחיות בדרכים שונות, למשל, אי אפשר להטמיע מחלקות ערכים סופיות או בנאים סופיים עם האופן שבו AutoValue פועל.

סגנון קוד

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

פועלים לפי מוסכמות תכנות רגילות, אלא אם מצוין אחרת

מוסכמות התכנות של Android מתועדות כאן עבור תורמים חיצוניים:

https://source.android.com/source/code-style.html

באופן כללי, אנחנו נוטים לפעול לפי מוסכמות התכנות הסטנדרטיות של Java ו-Kotlin.

ראשי תיבות לא יכולים להיות באותיות רישיות בשמות של שיטות

לדוגמה: שם השיטה צריך להיות runCtsTests ולא runCTSTests.

שמות לא יכולים להסתיים ב-Impl

הפעולה הזו חושפת פרטי הטמעה, ולכן כדאי להימנע ממנה.

שיעורים

בקטע הזה מתוארים כללים לגבי מחלקות, ממשקים וירושה.

הורשה של מחלקות ציבוריות חדשות ממחלקת הבסיס המתאימה

הורשה חושפת רכיבי API במחלקת המשנה שאולי לא מתאימים. לדוגמה, מחלקת משנה ציבורית חדשה של FrameLayout נראית כך: FrameLayout בנוסף להתנהגויות החדשות ולרכיבי ה-API. אם ה-API שמועבר בירושה לא מתאים לתרחיש השימוש שלכם, אפשר להעביר בירושה ממחלקה גבוהה יותר בהיררכיה, למשל, ViewGroup או View.

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

שימוש במחלקות הבסיסיות של קולקציות

בין אם מעבירים אוסף כארגומנט או מחזירים אותו כערך, תמיד עדיף להשתמש במחלקת הבסיס במקום בהטמעה הספציפית (למשל, להשתמש ב-List<Foo> במקום ב-ArrayList<Foo>).

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

ב-Kotlin, עדיף להשתמש באוספים שלא ניתן לשנות. פרטים נוספים זמינים במאמר בנושא שינוי של אוספים.

מחלקות מופשטות לעומת ממשקים

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

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

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

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

public interface AnimationEndCallback {
  // Always called, must be implemented.
  public void onFinished(Animation anim);
  // Optional callbacks.
  public default void onStopped(Animation anim) { }
  public default void onCanceled(Animation anim) { }
}

שמות המחלקות צריכים לשקף את מה שהן מרחיבות

לדוגמה, כדי שהשם יהיה ברור, כיתות שמרחיבות את Service צריכות להיקרא FooService:

public class IntentHelper extends Service {}
public class IntentService extends Service {}

סיומות כלליות

מומלץ להימנע משימוש בסיומות גנריות של שמות מחלקות כמו Helper ו-Util לאוספים של שיטות עזר. במקום זאת, צריך להוסיף את השיטות ישירות למחלקות המשויכות או לפונקציות הרחבה של Kotlin.

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

במקרים מאוד מוגבלים, יכול להיות שיהיה מתאים להשתמש בסיומת Helper:

  • משמש להגדרת התנהגות ברירת המחדל
  • יכול להיות שיהיה צורך להעביר התנהגות קיימת למחלקות חדשות
  • יכול להיות שיהיה צורך במצב מתמשך
  • בדרך כלל כולל View

לדוגמה, אם כדי להוסיף תיאורי כלים לגרסה קודמת צריך לשמור את המצב שמשויך ל-View ולקרוא לכמה שיטות ב-View כדי להתקין את התוסף, TooltipHelper יהיה שם מחלקה מקובל.

לא לחשוף קוד שנוצר על ידי IDL כ-API ציבורי ישירות

שומרים את הקוד שנוצר על ידי IDL כפרטי הטמעה. כולל protobuf,‏ sockets,‏ FlatBuffers או כל משטח API אחר שאינו Java או NDK. עם זאת, רוב ה-IDL ב-Android הוא ב-AIDL, ולכן הדף הזה מתמקד ב-AIDL.

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

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

ממשקי Binder

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

לדוגמה, אל תחשפו את FooService כ-API ציבורי ישירות:

// BAD: Public API generated from IFooService.aidl
public class IFooService {
   public void doFoo(String foo);
}

במקום זאת, עוטפים את הממשק Binder בתוך מחלקה של מנהל או מחלקה אחרת:

/**
 * @hide
 */
public class IFooService {
   public void doFoo(String foo);
}

public IFooManager {
   public void doFoo(String foo) {
      mFooService.doFoo(foo);
   }
}

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

/**
 * @hide
 */
public class IFooService {
   public void doFoo(String foo, int flags);
}

public IFooManager {
   public void doFoo(String foo) {
      if (mAppTargetSdkLevel < 26) {
         useOldFooLogic(); // Apps targeting API before 26 are broken otherwise
         mFooService.doFoo(foo, FLAG_THAT_ONE_WEIRD_HACK);
      } else {
         mFooService.doFoo(foo, 0);
      }
   }

   public void doFoo(String foo, int flags) {
      mFooService.doFoo(foo, flags);
   }
}

במקרה של Binderממשקים שלא מהווים חלק מפלטפורמת Android (לדוגמה, ממשק שירות שמיוצא על ידי שירותי Google Play לשימוש באפליקציות), הדרישה לממשק IPC יציב, שפורסם וכולל גרסאות, מקשה מאוד על פיתוח הממשק עצמו. עם זאת, עדיין כדאי להשתמש בשכבת wrapper מסביב, כדי להתאים להנחיות אחרות של API וכדי להקל על השימוש באותו API ציבורי לגרסה חדשה של ממשק ה-IPC, אם אי פעם יהיה בכך צורך.

אל תשתמשו באובייקטים גולמיים של Binder ב-API ציבורי

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

public final class IdentifiableObject {
  public Binder getToken() {...}
}
public final class IdentifiableObjectToken {
  /**
   * @hide
   */
  public Binder getRawValue() {...}

  /**
   * @hide
   */
  public static IdentifiableObjectToken wrapToken(Binder rawValue) {...}
}

public final class IdentifiableObject {
  public IdentifiableObjectToken getToken() {...}
}

כיתות ניהול חייבות להיות סופיות

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

אל תשתמשו ב-CompletableFuture או ב-Future

ל-java.util.concurrent.CompletableFuture יש משטח API גדול שמאפשר שינוי שרירותי של ערך עתידי, ויש בו ערכי ברירת מחדל שנוטים לשגיאות.

לעומת זאת, ב-java.util.concurrent.Future חסרה האזנה לא חוסמת, ולכן קשה להשתמש בה עם קוד אסינכרוני.

בקוד פלטפורמה ובממשקי API של ספריות ברמה נמוכה שמשמשים גם את Kotlin וגם את Java, מומלץ להשתמש בשילוב של קריאה חוזרת להשלמה, Executor, ואם ה-API תומך בביטול CancellationSignal.

public void asyncLoadFoo(android.os.CancellationSignal cancellationSignal,
    Executor callbackExecutor,
    android.os.OutcomeReceiver<FooResult, Throwable> callback);

אם מטרגטים Kotlin, מומלץ להשתמש בפונקציות suspend.

suspend fun asyncLoadFoo(): Foo

בספריות שילוב ספציפיות ל-Java, אפשר להשתמש ב-ListenableFuture של Guava.

public com.google.common.util.concurrent.ListenableFuture<Foo> asyncLoadFoo();

לא להשתמש ב-Optional

למרות של-Optional יכולים להיות יתרונות בפלטפורמות מסוימות של API, הוא לא עקבי עם פלטפורמת ה-API הקיימת של Android. ‫@Nullable ו-@NonNull מספקים עזרה בכלי פיתוח לבטיחות של null, ו-Kotlin אוכפת חוזי אפסות ברמת הקומפיילר, ולכן אין צורך ב-Optional.

לפרימיטיבים אופציונליים, משתמשים בשיטות has ו-get. אם הערך לא מוגדר (has מחזירה false), השיטה get צריכה להפעיל IllegalStateException.

public boolean hasAzimuth() { ... }
public int getAzimuth() {
  if (!hasAzimuth()) {
    throw new IllegalStateException("azimuth is not set");
  }
  return azimuth;
}

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

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

public final class Log {
  // Not instantiable.
  private Log() {}
}

Singletons

לא מומלץ להשתמש ב-Singleton כי יש להם חסרונות שקשורים לבדיקות:

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

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

מכונה יחידה

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

האובייקט שמוחזר על ידי getInstance() צריך להיות הטמעה פרטית של מחלקה בסיסית מופשטת.

class Singleton private constructor(...) {
  companion object {
    private val _instance: Singleton by lazy { Singleton(...) }

    fun getInstance(): Singleton {
      return _instance
    }
  }
}
abstract class SingleInstance private constructor(...) {
  companion object {
    private val _instance: SingleInstance by lazy { SingleInstanceImp(...) }
    fun getInstance(): SingleInstance {
      return _instance
    }
  }
}

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

מחלקה שמשחררת משאבים צריכה להטמיע את AutoCloseable

במקרים שבהם משחררים משאבים באמצעות close,‏ release,‏ destroy או שיטות דומות, צריך להטמיע את java.lang.AutoCloseable כדי לאפשר למפתחים לנקות את המשאבים האלה באופן אוטומטי כשמשתמשים בבלוק try-with-resources.

מומלץ להימנע מהצגה של מחלקות משנה חדשות של View ב-android.*

אל תוסיפו מחלקות חדשות שיורשות ישירות או בעקיפין מ-android.view.View ב-API הציבורי של הפלטפורמה (כלומר, ב-android.*).

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

שדות

הכללים האלה מתייחסים לשדות ציבוריים בכיתות.

לא לחשוף שדות גולמיים

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

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

מחלקות Kotlin יכולות לחשוף מאפיינים.

צריך לסמן שדות גלויים כסופיים

אנחנו ממליצים מאוד לא להשתמש בשדות גולמיים (ראו Don't expose raw fields). אבל במקרים נדירים שבהם שדה נחשף כשדה ציבורי, צריך לסמן את השדה הזה final.

לא כדאי לחשוף שדות פנימיים

אל תפנו לשמות של שדות פנימיים ב-API ציבורי.

public int mFlags;

שימוש ב-public במקום ב-protected

@see Use public instead of protected

Constants

אלה כללים לגבי קבועים ציבוריים.

קבועי דגלים לא יכולים לחפוף לערכי int או long

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

public static final int FLAG_SOMETHING = 2;
public static final int FLAG_SOMETHING = 3;
public static final int FLAG_PRIVATE = 1 << 2;
public static final int FLAG_PRESENTATION = 1 << 3;

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

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

כל המילים בקבוע צריכות להיות באותיות גדולות, וכשמדובר בכמה מילים צריך להפריד ביניהן באמצעות _. לדוגמה:

public static final int fooThing = 5
public static final int FOO_THING = 5

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

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

לדוגמה, תוספים של Intent צריכים להתחיל ב-EXTRA_. פעולות של כוונות צריכות להתחיל ב-ACTION_. קבועים שמשמשים עם Context.bindService() צריכים להתחיל ב-BIND_.

שמות וטווחים של קבועים מרכזיים

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

public static final String FOO_THING = "foo"

לא נקרא באופן עקבי ולא מוגדר בהיקף המתאים. במקום זאת, כדאי:

public static final String FOO_THING = "android.fooservice.FOO_THING"

הקידומות של android בקבועי מחרוזות בתחום מסוים שמורות לפרויקט הקוד הפתוח של Android.

צריך להשתמש במרחב שמות עבור פעולות ונתונים נוספים של Intent, וגם עבור רשומות של Bundle, באמצעות שם החבילה שבה הם מוגדרים.

package android.foo.bar {
  public static final String ACTION_BAZ = "android.foo.bar.action.BAZ"
  public static final String EXTRA_BAZ = "android.foo.bar.extra.BAZ"
}

שימוש ב-public במקום ב-protected

@see Use public instead of protected

שימוש בקידומות עקביות

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

public static final int SOME_VALUE = 0x01;

public static final int SOME_OTHER_VALUE = 0x10;

public static final int SOME_THIRD_VALUE = 0x100;
public static final int FLAG_SOME_VALUE = 0x01;

public static final int FLAG_SOME_OTHER_VALUE = 0x10;

public static final int FLAG_SOME_THIRD_VALUE = 0x100;

‫@see Use standard prefixes for constants

שימוש בשמות משאבים עקביים

שמות של מזהים, מאפיינים וערכים ציבוריים חייבים להיות בהתאם למוסכמת השמות camelCase, למשל @id/accessibilityActionPageUp או @attr/textAppearance, בדומה לשדות ציבוריים ב-Java.

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

  • ערכי הגדרות של הפלטפורמה, כמו @string/config_recentsComponentName ב-config.xml
  • מאפייני תצוגה ספציפיים לפריסה, כמו @attr/layout_marginStart בקובץ attrs.xml

כשמשתמשים בסגנונות ובתבניות ציבוריים, חייבים להקפיד על מוסכמת מתן השמות PascalCase ההיררכית, למשל @style/Theme.Material.Light.DarkActionBar או @style/Widget.Material.SearchView.ActionBar, בדומה למחלקות מקוננות ב-Java.

אסור לחשוף פריסות ומשאבים של drawable כ-API ציבורי. אם בכל זאת צריך לחשוף אותם, חובה לתת שמות לפריסות ולמשאבים הגרפיים הציבוריים באמצעות מוסכמת השמות under_score, למשל layout/simple_list_item_1.xml או drawable/title_bar_tall.xml.

כשקבועים יכולים להשתנות, כדאי להפוך אותם לדינמיים

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

CameraManager.MAX_CAMERAS
CameraManager.getMaxCameras()

כדאי להביא בחשבון תאימות קדימה לקריאות חוזרות

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

מקור היפותטי של SDK:

// Added in API level 22
public static final int STATUS_SUCCESS = 1;
public static final int STATUS_FAILURE = 2;
// Added in API level 23
public static final int STATUS_FAILURE_RETRY = 3;
// Added in API level 26
public static final int STATUS_FAILURE_ABORT = 4;

אפליקציה היפותטית עם targetSdkVersion="22":

if (result == STATUS_FAILURE) {
  // Oh no!
} else {
  // Success!
}

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

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

private int mapResultForTargetSdk(Context context, int result) {
  int targetSdkVersion = context.getApplicationInfo().targetSdkVersion;
  if (targetSdkVersion < 26) {
    if (result == STATUS_FAILURE_ABORT) {
      return STATUS_FAILURE;
    }
    if (targetSdkVersion < 23) {
      if (result == STATUS_FAILURE_RETRY) {
        return STATUS_FAILURE;
      }
    }
  }
  return result;
}

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

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

קבוע מסוג מספר שלם או מחרוזת

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

סיווגי נתונים

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

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

יצירת אובייקט

ב-Java, מחלקות נתונים צריכות לספק בנאי כשיש מעט מאפיינים, או להשתמש בתבנית Builder כשיש הרבה מאפיינים.

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

שינוי והעתקה

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

כשמספקים פונקציה copy() ב-Kotlin, הארגומנטים צריכים להתאים לקונסטרוקטור של המחלקה, וערכי ברירת המחדל צריכים להיות מאוכלסים באמצעות הערכים הנוכחיים של האובייקט:

class Typography(
  val labelMedium: TextStyle = TypographyTokens.LabelMedium,
  val labelSmall: TextStyle = TypographyTokens.LabelSmall
) {
    fun copy(
      labelMedium: TextStyle = this.labelMedium,
      labelSmall: TextStyle = this.labelSmall
    ): Typography = Typography(
      labelMedium = labelMedium,
      labelSmall = labelSmall
    )
}

התנהגויות נוספות

במחלקות הנתונים צריך להטמיע את שתי השיטות equals() ו-hashCode(), וכל מאפיין צריך להיות מפורט בהטמעות של השיטות האלה.

אפשר להטמיע מחלקות נתונים toString() בפורמט מומלץ שתואם להטמעה של מחלקת נתונים ב-Kotlin, לדוגמה User(var1=Alex, var2=42).

שיטות

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

שעה

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

עדיפות לשימוש בסוגים java.time.*, אם אפשר

java.time.Duration,‏ java.time.Instant ועוד הרבה סוגים של java.time.* זמינים בכל גרסאות הפלטפורמה באמצעות desugaring, ועדיף להשתמש בהם כשמציינים זמן בפרמטרים של API או בערכים מוחזרים.

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

שם השיטה שמשמשת לציון משך הזמן צריך להיות duration

אם ערך של זמן מבטא את משך הזמן שחלף, צריך לקרוא לפרמטר 'duration' ולא 'time'.

ValueAnimator.setTime(java.time.Duration);
ValueAnimator.setDuration(java.time.Duration);

חריגים:

המונח timeout מתאים כשמשך הזמן מתייחס ספציפית לערך של timeout.

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

שמות של שיטות שמבטאות משכי זמן או זמן כ-primitive צריכים לכלול את יחידת הזמן שלהן, ולהשתמש ב-long

בשיטות שמקבלות או מחזירות משך זמן כפרימיטיב, צריך להוסיף לשם השיטה את יחידות הזמן הרלוונטיות (כמו Millis,‏ Nanos,‏ Seconds) כדי לשמור את השם ללא קישוט לשימוש עם java.time.Duration. מידע נוסף זמין במאמר בנושא שעה.

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

  • @CurrentTimeMillisLong: הערך הוא חותמת זמן לא שלילית שנמדדת כמספר אלפיות השנייה מאז 1970-01-01T00:00:00Z.
  • @CurrentTimeSecondsLong: הערך הוא חותמת זמן לא שלילית שנמדדת כמספר השניות מאז 1970-01-01T00:00:00Z.
  • @DurationMillisLong: הערך הוא משך זמן לא שלילי באלפיות השנייה.
  • @ElapsedRealtimeLong: הערך הוא חותמת זמן לא שלילית בבסיס הזמן SystemClock.elapsedRealtime().
  • @UptimeMillisLong: הערך הוא חותמת זמן לא שלילית בבסיס הזמן SystemClock.uptimeMillis().

בפרמטרים של זמן פרימיטיבי או בערכי החזרה צריך להשתמש ב-long, ולא ב-int.

ValueAnimator.setDuration(@DurationMillisLong long);
ValueAnimator.setDurationNanos(long);

בשיטות שמבטאות יחידות זמן, עדיף להשתמש בקיצור לא מקוצר לשמות היחידות

public void setIntervalNs(long intervalNs);

public void setTimeoutUs(long timeoutUs);
public void setIntervalNanos(long intervalNanos);

public void setTimeoutMicros(long timeoutMicros);

הוספת הערות לארגומנטים ארוכים של זמן

הפלטפורמה כוללת כמה הערות כדי לספק הקלדה חזקה יותר ליחידות זמן מסוג long:

  • @CurrentTimeMillisLong: הערך הוא חותמת זמן לא שלילית שנמדדת כמספר אלפיות השנייה מאז 1970-01-01T00:00:00Z, ולכן בבסיס הזמן System.currentTimeMillis().
  • @CurrentTimeSecondsLong: הערך הוא חותמת זמן לא שלילית שנמדדת כמספר השניות מאז 1970-01-01T00:00:00Z.
  • @DurationMillisLong: הערך הוא משך זמן לא שלילי באלפיות השנייה.
  • @ElapsedRealtimeLong: הערך הוא חותמת זמן לא שלילית בבסיס הזמן SystemClock#elapsedRealtime().
  • @UptimeMillisLong: הערך הוא חותמת זמן לא שלילית בבסיס הזמן SystemClock#uptimeMillis().

יחידות מידה

בכל השיטות שבהן מציינים יחידת מידה שונה מזמן, מומלץ להשתמש בקידומות של יחידות SI בפורמט CamelCase.

public  long[] getFrequenciesKhz();

public  float getStreamVolumeDb();

מיקום פרמטרים אופציונליים בסוף העומסים העודפים

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

public int doFoo(boolean flag);

public int doFoo(int id, boolean flag);
public int doFoo(boolean flag);

public int doFoo(boolean flag, int id);

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

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

שיטות עם פרמטרים שמוגדרים כברירת מחדל צריכות להיות מסומנות ב-@JvmOverloads (רק ב-Kotlin)

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

פרטים נוספים זמינים במאמר Function overloads for defaults במדריך הרשמי בנושא פעולות הדדיות בין Kotlin ל-Java.

class Greeting @JvmOverloads constructor(
  loudness: Int = 5
) {
  @JvmOverloads
  fun sayHello(prefix: String = "Dr.", name: String) = // ...
}

לא להסיר ערכי פרמטרים שמוגדרים כברירת מחדל (Kotlin בלבד)

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

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

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

public void openFile(int flags, String name);

public void openFileAsync(OnFileOpenedListener listener, String name, int flags);

public void setFlags(int mask, int flags);
public void openFile(String name, int flags);

public void openFileAsync(String name, int flags, OnFileOpenedListener listener);

public void setFlags(int flags, int mask);

ראו גם: הצבת פרמטרים אופציונליים בסוף בעומסים עודפים

Builders

מומלץ להשתמש בתבנית Builder כדי ליצור אובייקטים מורכבים ב-Java, והיא נפוצה ב-Android במקרים הבאים:

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

כדאי לשקול אם אתם צריכים כלי לבניית אתרים. ה-builders שימושיים בממשק API אם הם משמשים ל:

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

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

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

class Tone @JvmOverloads constructor(
  val duration: Long = 1000,
  val frequency: Int = 2600,
  val dtmfConfigs: List<DtmfConfig> = emptyList()
) {
  class Builder {
    // ...
  }
}

מחלקות Builder חייבות להחזיר את ה-Builder

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

public static class Builder {
  public void setDuration(long);
  public void setFrequency(int);
  public DtmfConfigBuilder addDtmfConfig();
  public Tone build();
}
public class Tone {
  public static class Builder {
    public Builder setDuration(long);
    public Builder setFrequency(int);
    public Builder addDtmfConfig(DtmfConfig);
    public Tone build();
  }
}

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

public abstract class Builder<T extends Builder<T>> {
  abstract T setValue(int);
}

public class TypeBuilder<T extends TypeBuilder<T>> extends Builder<T> {
  T setValue(int);
  T setTypeSpecificValue(long);
}

צריך ליצור מחלקות Builder באמצעות בנאי

כדי לשמור על עקביות ביצירת אובייקטים מסוג Builder דרך פלטפורמת Android API, חובה ליצור את כל האובייקטים מסוג Builder דרך בנאי ולא דרך שיטת יצירה סטטית. בממשקי API מבוססי Kotlin, ה-Builder חייב להיות ציבורי גם אם משתמשי Kotlin אמורים להסתמך באופן מרומז על ה-builder באמצעות מנגנון יצירה בסגנון DSL או method של factory. אסור לספריות להשתמש ב-@PublishedApi internal כדי להסתיר באופן סלקטיבי את בנאי המחלקה Builder מלקוחות Kotlin.

public class Tone {
  public static Builder builder();
  public static class Builder {
  }
}
public class Tone {
  public static class Builder {
    public Builder();
  }
}

כל הארגומנטים של בנאי ה-Builder חייבים להיות נדרשים (למשל ‎ @NonNull)

אופציונלי, לדוגמה @Nullable, ארגומנטים צריכים לעבור לשיטות setter. אם לא מציינים את כל הארגומנטים הנדרשים, בנאי ה-builder צריך להחזיר שגיאה מסוג NullPointerException (מומלץ להשתמש ב-Objects.requireNonNull).

מחלקות ה-Builder צריכות להיות מחלקות פנימיות סטטיות סופיות של הסוגים שהן בונות

כדי לשמור על ארגון לוגי בחבילה, בדרך כלל כדאי לחשוף את מחלקות ה-Builder כמחלקות פנימיות סופיות של הסוגים שהן יוצרות, למשל Tone.Builder ולא ToneBuilder.

יכול להיות שיוצרים יכללו ב-Builder קונסטרקטור כדי ליצור מופע חדש ממופע קיים

‫Builders may include a copy constructor to create a new builder instance from an existing builder or built object. אסור להם לספק שיטות חלופיות ליצירת מופעים של Builder מ-Builders קיימים או מאובייקטים של Build.

public class Tone {
  public static class Builder {
    public Builder clone();
  }

  public Builder toBuilder();
}
public class Tone {
  public static class Builder {
    public Builder(Builder original);
    public Builder(Tone original);
  }
}
אם ל-Builder יש בנאי עותק, שיטות setter של Builder צריכות לקבל ארגומנטים עם הערה לציון אפשרות לערך null ‏(@Nullable)

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

public static class Builder {
  public Builder(Builder original);
  public Builder setObjectValue(@Nullable Object value);
}
יכול להיות ששיטות setter של Builder יקבלו ארגומנטים מסוג @Nullable עבור מאפיינים אופציונליים

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

בנוסף, @Nullable פונקציות setter יותאמו לפונקציות getter שלהן, שחייבות להיות @Nullable למאפיינים אופציונליים.

Value createValue(@Nullable OptionalValue optionalValue) {
  Value.Builder builder = new Value.Builder();
  if (optionalValue != null) {
    builder.setOptionalValue(optionalValue);
  }
  return builder.build();
}
Value createValue(@Nullable OptionalValue optionalValue) {
  return new Value.Builder()
    .setOptionalValue(optionalValue);
    .build();
}

// Or in other cases:

Value createValue() {
  return new Value.Builder()
    .setOptionalValue(condition ? new OptionalValue() : null);
    .build();
}

שימוש נפוץ ב-Kotlin:

fun createValue(optionalValue: OptionalValue? = null) =
  Value.Builder()
    .apply { optionalValue?.let { setOptionalValue(it) } }
    .build()
fun createValue(optionalValue: OptionalValue? = null) =
  Value.Builder()
    .setOptionalValue(optionalValue)
    .build()

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

/**
 * ...
 *
 * <p>Defaults to {@code null}, which means the optional value won't be used.
 */

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

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

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

  1. האובייקט שנוצר צריך להיות שמיש באופן מיידי, ולכן צריך לספק פונקציות setter לכל המאפיינים הרלוונטיים, בין אם הם ניתנים לשינוי ובין אם לא.

    map.put(key, new Value.Builder(requiredValue)
        .setImmutableProperty(immutableValue)
        .setUsefulMutableProperty(usefulValue)
        .build());
    
  2. יכול להיות שיהיה צורך לבצע עוד כמה קריאות לפני שאפשר יהיה להשתמש באובייקט שנוצר, ולכן לא מומלץ לספק פונקציות setter למאפיינים שניתנים לשינוי.

    Value v = new Value.Builder(requiredValue)
        .setImmutableProperty(immutableValue)
        .build();
    v.setUsefulMutableProperty(usefulValue)
    Result r = v.performSomeAction();
    Key k = callSomeMethod(r);
    map.put(k, v);
    

אל תערבבו בין שני התרחישים.

Value v = new Value.Builder(requiredValue)
    .setImmutableProperty(immutableValue)
    .setUsefulMutableProperty(usefulValue)
    .build();
Result r = v.performSomeAction();
Key k = callSomeMethod(r);
map.put(k, v);

ל-Builders לא יכולים להיות getters

הפונקציה Getter צריכה להיות באובייקט שנבנה, ולא ב-Builder.

לשיטות setter ב-Builder חייבות להיות שיטות getter תואמות במחלקה שנבנתה

public class Tone {
  public static class Builder {
    public Builder setDuration(long);
    public Builder setFrequency(int);
    public Builder addDtmfConfig(DtmfConfig);
    public Tone build();
  }
}
public class Tone {
  public static class Builder {
    public Builder setDuration(long);
    public Builder setFrequency(int);
    public Builder addDtmfConfig(DtmfConfig);
    public Tone build();
  }

  public long getDuration();
  public int getFrequency();
  public @NonNull List<DtmfConfig> getDtmfConfigs();
}

שמות של שיטות ב-Builder

שמות של שיטות ליצירת אובייקטים צריכים להיות בסגנון setFoo(), addFoo() או clearFoo().

מחלקות Builder צפויות להצהיר על שיטת build()‎

במחלקות Builder צריך להצהיר על שיטה build() שמחזירה מופע של האובייקט שנבנה.

שיטות build() של Builder חייבות להחזיר אובייקטים מסוג @NonNull

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

לא לחשוף נעילות פנימיות

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

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

public synchronized void doThing() { ... }
private final Object mThingLock = new Object();

public void doThing() {
  synchronized (mThingLock) {
    ...
  }
}

שיטות בסגנון Accessor צריכות לפעול לפי ההנחיות לגבי מאפייני Kotlin

כשמציגים שיטות בסגנון accessor ממקורות Kotlin – שיטות שמשתמשות בקידומות get, set או is – הן יהיו זמינות גם כמאפייני Kotlin. לדוגמה, int getField() שמוגדר ב-Java זמין ב-Kotlin כמאפיין val field: Int.

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

  • לשיטה יש תופעות לוואי – מומלץ להשתמש בשם שיטה יותר תיאורי
  • השיטה כוללת עבודה שדורשת הרבה משאבי מחשוב – עדיף להשתמש ב-compute
  • השיטה כוללת חסימה או עבודה ממושכת אחרת כדי להחזיר ערך, כמו IPC או קלט/פלט אחר – עדיף להשתמש ב-fetch
  • השיטה חוסמת את השרשור עד שהיא יכולה להחזיר ערך – עדיף להשתמש ב-await
  • השיטה מחזירה מופע חדש של אובייקט בכל קריאה – עדיף להשתמש ב-create
  • יכול להיות שהשיטה לא תחזיר ערך בהצלחה – עדיף להשתמש ב-request

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

השתמשו בקידומת is לשיטות אחזור נתונים בוליאניות

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

שיטות גישה בוליאניות ב-Java צריכות לפעול לפי סכימת השמות set/is, ועדיף להשתמש בשדות is, כמו בדוגמה הבאה:

// Visibility is a direct property. The object "is" visible:
void setVisible(boolean visible);
boolean isVisible();

// Factory reset protection is an indirect property.
void setFactoryResetProtectionEnabled(boolean enabled);
boolean isFactoryResetProtectionEnabled();

final boolean isAvailable;

שימוש ב-set/is לשיטות גישה של Java או ב-is לשדות של Java יאפשר להשתמש בהם כמאפיינים מ-Kotlin:

obj.isVisible = true
obj.isFactoryResetProtectionEnabled = false
if (!obj.isAvailable) return

בדרך כלל, כדאי להשתמש בשמות חיוביים למאפיינים ולשיטות גישה, למשל Enabled ולא Disabled. שימוש בטרמינולוגיה שלילית הופך את המשמעות של true ושל false, ומקשה על הסקת מסקנות לגבי ההתנהגות.

// Passing false here is a double-negative.
void setFactoryResetProtectionDisabled(boolean disabled);

במקרים שבהם הערך הבוליאני מתאר הכללה או בעלות של נכס, אפשר להשתמש ב-has במקום ב-is. עם זאת, זה לא יעבוד עם תחביר של מאפייני Kotlin:

// Transient state is an indirect property used to track state
// related to the object. The object is not transient; rather,
// the object "has" transient state associated with it:
void setHasTransientState(boolean hasTransientState);
boolean hasTransientState();

יש קידומות חלופיות שעשויות להתאים יותר, כמו can ו-should:

// "Can" describes a behavior that the object may provide,
// and here is more concise than setRecordingEnabled or
// setRecordingAllowed. The object "can" record:
void setCanRecord(boolean canRecord);
boolean canRecord();

// "Should" describes a hint or property that is not strictly
// enforced, and here is more explicit than setFitWidthEnabled.
// The object "should" fit width:
void setShouldFitWidth(boolean shouldFitWidth);
boolean shouldFitWidth();

ב-methods שמפעילות או משביתות התנהגויות או תכונות, יכול להיות שיופיע הקידומת is והסיומת Enabled:

// "Enabled" describes the availability of a property, and is
// more appropriate here than "can use" or "should use" the
// property:
void setWiFiRoamingSettingEnabled(boolean enabled)
boolean isWiFiRoamingSettingEnabled()

באופן דומה, יכול להיות ששיטות שמציינות תלות בהתנהגויות או בתכונות אחרות ישתמשו בקידומת is ובסיומת Supported או Required:

// "Supported" describes whether this API would work on devices that support
// multiple users. The API "supports" multi-user:
void setMultiUserSupported(boolean supported)
boolean isMultiUserSupported()
// "Required" describes whether this API depends on devices that support
// multiple users. The API "requires" multi-user:
void setMultiUserRequired(boolean required)
boolean isMultiUserRequired()

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

‫methods של מאפיינים ב-Kotlin

עבור מאפיין של מחלקה var foo: Foo,‏ Kotlin תיצור מתודות get/set באמצעות כלל עקבי: מוסיפים את הקידומת get והופכים את האות הראשונה לאות גדולה עבור ה-getter, ומוסיפים את הקידומת set והופכים את האות הראשונה לאות גדולה עבור ה-setter. הצהרת המאפיין תיצור שיטות בשמות public Foo getFoo() וpublic void setFoo(Foo foo), בהתאמה.

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

var isVisible: Boolean

אם הנכס שלכם הוא אחד מהחריגים שצוינו למעלה ומתחיל בקידומת מתאימה, אתם יכולים להשתמש בהערה @get:JvmName בנכס כדי לציין ידנית את השם המתאים:

@get:JvmName("hasTransientState")
var hasTransientState: Boolean

@get:JvmName("canRecord")
var canRecord: Boolean

@get:JvmName("shouldFitWidth")
var shouldFitWidth: Boolean

פונקציות גישה לביטמסק

במאמר שימוש ב-@IntDef בדגלי bitmask מוסבר איך להגדיר דגלי bitmask ב-API.

Setters

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

/**
 * Sets the state of all scroll indicators.
 * <p>
 * See {@link #setScrollIndicators(int, int)} for usage information.
 *
 * @param indicators a bitmask of indicators that should be enabled, or
 *                   {@code 0} to disable all indicators
 * @see #setScrollIndicators(int, int)
 * @see #getScrollIndicators()
 */
public void setScrollIndicators(@ScrollIndicators int indicators);

/**
 * Sets the state of the scroll indicators specified by the mask. To change
 * all scroll indicators at once, see {@link #setScrollIndicators(int)}.
 * <p>
 * When a scroll indicator is enabled, it will be displayed if the view
 * can scroll in the direction of the indicator.
 * <p>
 * Multiple indicator types may be enabled or disabled by passing the
 * logical OR of the specified types. If multiple types are specified, they
 * will all be set to the same enabled state.
 * <p>
 * For example, to enable the top scroll indicator:
 * {@code setScrollIndicators(SCROLL_INDICATOR_TOP, SCROLL_INDICATOR_TOP)}
 * <p>
 * To disable the top scroll indicator:
 * {@code setScrollIndicators(0, SCROLL_INDICATOR_TOP)}
 *
 * @param indicators a bitmask of values to set; may be a single flag,
 *                   the logical OR of multiple flags, or 0 to clear
 * @param mask a bitmask indicating which indicator flags to modify
 * @see #setScrollIndicators(int)
 * @see #getScrollIndicators()
 */
public void setScrollIndicators(@ScrollIndicators int indicators, @ScrollIndicators int mask);

Getters

צריך לספק פונקציית getter אחת כדי לקבל את מסיכת הביטים המלאה.

/**
 * Returns a bitmask representing the enabled scroll indicators.
 * <p>
 * For example, if the top and left scroll indicators are enabled and all
 * other indicators are disabled, the return value will be
 * {@code View.SCROLL_INDICATOR_TOP | View.SCROLL_INDICATOR_LEFT}.
 * <p>
 * To check whether the bottom scroll indicator is enabled, use the value
 * of {@code (getScrollIndicators() & View.SCROLL_INDICATOR_BOTTOM) != 0}.
 *
 * @return a bitmask representing the enabled scroll indicators
 */
@ScrollIndicators
public int getScrollIndicators();

שימוש ב-public במקום ב-protected

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

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

הטמעה של equals() ו-hashCode() או אי-הטמעה של אף אחת מהן

אם משנים את אחת מההגדרות, צריך לשנות גם את השנייה.

הטמעה של toString()‎ למחלקות נתונים

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

תיעוד אם הפלט הוא להתנהגות התוכנית או לניפוי באגים

מחליטים אם רוצים שההתנהגות של התוכנית תסתמך על ההטמעה שלכם או לא. לדוגמה, הפורמט הספציפי של UUID.toString() ושל File.toString() מתועד כדי שתוכנות יוכלו להשתמש בו. אם אתם חושפים מידע רק לצורך ניפוי באגים, כמו Intent, אתם יכולים להשתמש ב-inherit docs מהסופר-קלאס.

לא לכלול מידע נוסף

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

המלצה לא להסתמך על פלט ניפוי הבאגים

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

@Override
public String toString() {
  return getClass().getSimpleName() + "@" + Integer.toHexString(System.identityHashCode(this)) + " {mFoo=" + mFoo + "}";
}

הדבר יכול להרתיע מפתחים מלכתוב טענות בדיקה כמו assertThat(a.toString()).isEqualTo(b.toString()) באובייקטים שלכם.

שימוש ב-createFoo כשמחזירים אובייקטים שנוצרו לאחרונה

משתמשים בקידומת create, ולא ב-get או ב-new, לשיטות שייצרו ערכי החזרה, למשל על ידי בנייה של אובייקטים חדשים.

אם השיטה תיצור אובייקט כדי להחזיר אותו, צריך לציין זאת בבירור בשם השיטה.

public FooThing getFooThing() {
  return new FooThing();
}
public FooThing createFooThing() {
  return new FooThing();
}

שיטות שמקבלות אובייקטים של קבצים צריכות לקבל גם זרמים

מיקומי אחסון הנתונים ב-Android לא תמיד הם קבצים בדיסק. לדוגמה, תוכן שעובר בין משתמשים שונים מיוצג כ-content:// Uris. כדי לאפשר עיבוד של מקורות נתונים שונים, ממשקי API שמקבלים אובייקטים של File צריכים לקבל גם InputStream, OutputStream או את שניהם.

public void setDataSource(File file)
public void setDataSource(InputStream stream)

החזרה של פרימיטיבים גולמיים במקום גרסאות בקופסה

אם אתם צריכים להעביר ערכים חסרים או ערכי null, כדאי להשתמש ב--1,‏ Integer.MAX_VALUE או Integer.MIN_VALUE.

public java.lang.Integer getLength()
public void setLength(java.lang.Integer)
public int getLength()
public void setLength(int value)

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

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

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

מאפיין המציין אם ערך יכול להיות ריק (nullability)

ממשקי API של Java מחייבים שימוש בהערות מפורשות לגבי האפשרות להקצאת ערך null, אבל הרעיון של האפשרות להקצאת ערך null הוא חלק משפת Kotlin, ולכן אסור להשתמש בהערות לגבי האפשרות להקצאת ערך null בממשקי API של Kotlin.

@Nullable: מציין שערך ההחזרה, הפרמטר או השדה יכולים להיות null:

@Nullable
public String getName()

public void setName(@Nullable String name)

@NonNull: מציין שערך החזרה, פרמטר או שדה נתון לא יכולים להיות null. הסימון של פריטים כ-@Nullable הוא יחסית חדש ב-Android, ולכן רוב שיטות ה-API של Android לא מתועדות באופן עקבי. לכן יש לנו שלושה מצבים: 'לא ידוע', @Nullable, @NonNull. זו הסיבה לכך ש-@NonNull הוא חלק מההנחיות ל-API:

@NonNull
public String getName()

public void setName(@NonNull String name)

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

שיטות קיימות שאי אפשר להחזיר לגביהן ערך null: יכול להיות ששיטות קיימות ב-API ללא הערה מוצהרת של @Nullable יקבלו הערה של @Nullable אם השיטה יכולה להחזיר null בנסיבות ספציפיות וברורות (כמו findViewById()). צריך להוסיף שיטות נלוות של @NotNull requireFoo() שיוצרות IllegalArgumentException למפתחים שלא רוצים לבדוק אם הערך הוא null.

שיטות ממשק: כשמטמיעים שיטות ממשק בממשקי API חדשים, צריך להוסיף את ההערה המתאימה, כמו Parcelable.writeToParcel() (כלומר, השיטה הזו במחלקת ההטמעה צריכה להיות writeToParcel(@NonNull Parcel, int), ולא writeToParcel(Parcel, int)). עם זאת, אין צורך לתקן ממשקי API קיימים שחסרות בהם ההערות.

אכיפת האפשרות להשתמש בערך null

ב-Java, מומלץ להשתמש בשיטות כדי לבצע אימות של קלט לפרמטרים של @NonNull באמצעות Objects.requireNonNull(), ולהפעיל את NullPointerException כשהפרמטרים הם null. הפעולה הזו מתבצעת באופן אוטומטי ב-Kotlin.

משאבים

מזהי משאבים: פרמטרים מסוג integer שמציינים מזהים של משאבים ספציפיים צריכים להיות מתויגים בהגדרה המתאימה של סוג המשאב. יש הערה לכל סוג של משאב, כמו @StringRes,‏ @ColorRes ו-@AnimRes, בנוסף להערה הכללית @AnyRes. לדוגמה:

public void setTitle(@StringRes int resId)

‫‎@IntDef לקבוצות קבועות

Magic constants: פרמטרים String ו-int שמיועדים לקבל אחד מתוך קבוצה סופית של ערכים אפשריים שמסומנים על ידי קבועים ציבוריים, צריכים להיות מסומנים בהערה מתאימה עם @StringDef או @IntDef. ההערות האלה מאפשרות ליצור הערה חדשה שאפשר להשתמש בה כמו typedef לפרמטרים מותרים. לדוגמה:

/** @hide */
@IntDef(prefix = {"NAVIGATION_MODE_"}, value = {
  NAVIGATION_MODE_STANDARD,
  NAVIGATION_MODE_LIST,
  NAVIGATION_MODE_TABS
})
@Retention(RetentionPolicy.SOURCE)
public @interface NavigationMode {}

public static final int NAVIGATION_MODE_STANDARD = 0;
public static final int NAVIGATION_MODE_LIST = 1;
public static final int NAVIGATION_MODE_TABS = 2;

@NavigationMode
public int getNavigationMode();
public void setNavigationMode(@NavigationMode int mode);

מומלץ להשתמש בשיטות כדי לבדוק את התוקף של הפרמטרים עם ההערות ולהפעיל את IllegalArgumentException אם הפרמטר לא שייך ל-@IntDef

‫@IntDef לסימונים של מסכת ביטים

אפשר גם לציין בהערה שהקבועים הם דגלים, ולשלב אותם עם & ו-I:

/** @hide */
@IntDef(flag = true, prefix = { "FLAG_" }, value = {
  FLAG_USE_LOGO,
  FLAG_SHOW_HOME,
  FLAG_HOME_AS_UP,
})
@Retention(RetentionPolicy.SOURCE)
public @interface DisplayOptions {}

‫@StringDef לקבוצות של קבועי מחרוזות

יש גם את ההערה @StringDef, שהיא בדיוק כמו @IntDef בקטע הקודם, אבל היא מיועדת לקבועים String. אפשר לכלול כמה ערכים של prefix, שמשמשים ליצירת תיעוד אוטומטי לכל הערכים.

‫‎@SdkConstant לקבועים של SDK

@SdkConstant מוסיפים הערה לשדות ציבוריים כשהם אחד מהערכים הבאים: SdkConstant,‏ ACTIVITY_INTENT_ACTION,‏ BROADCAST_INTENT_ACTION,‏ SERVICE_ACTION,‏ INTENT_CATEGORY,‏ FEATURE.

@SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
public static final String ACTION_CALL = "android.intent.action.CALL";

הוספת תאימות לערכי null לביטולים

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

סוג הורה ילד או ילדה
סוג הערך שמוחזר לא כולל הערות לא מוערך או לא null
סוג הערך שמוחזר Nullable ניתן להגדרה כ-Null או לא ניתן להגדרה כ-Null
סוג הערך שמוחזר NonNull NonNull
טיעון משעשע לא כולל הערות לא מוערך או ניתן לערך null
טיעון משעשע Nullable Nullable
טיעון משעשע NonNull ניתן להגדרה כ-Null או לא ניתן להגדרה כ-Null

אם אפשר, כדאי להשתמש בארגומנטים שלא יכולים להיות null (כמו ‎ @NonNull)

כשמבצעים עומס יתר על שיטות, עדיף שכל הארגומנטים לא יהיו null.

public void startActivity(@NonNull Component component) { ... }
public void startActivity(@NonNull Component component, @NonNull Bundle options) { ... }

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

public void setTitleItem(@Nullable IconCompat icon, @ImageMode mode)
public void setTitleItem(@Nullable IconCompat icon, @ImageMode mode, boolean isLoading)

// Nonsense call to clear property
setTitleItem(null, MODE_RAW, false);
public void setTitleItem(@NonNull IconCompat icon, @ImageMode mode)
public void setTitleItem(@NonNull IconCompat icon, @ImageMode mode, boolean isLoading)
public void clearTitleItem()

מומלץ להשתמש בסוגי החזרה שאינם ניתנים לביטול (כמו ‎ @NonNull) עבור מאגרי תגים

בסוגי מאגרים כמו Bundle או Collection, מחזירים מאגר ריק – ובלתי ניתן לשינוי, אם רלוונטי. במקרים שבהם נעשה שימוש ב-null כדי להבחין בין זמינות של מאגר תגים, כדאי לספק שיטה בוליאנית נפרדת.

@NonNull
public Bundle getExtras() { ... }

הערות בנוגע לאפשרות של ערך null עבור זוגות של get ו-set חייבות להיות זהות

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

@NonNull
public Bundle getExtras() { ... }
public void setExtras(@NonNull Bundle bundle) { ... }

ערך ההחזרה במקרה של כשל או שגיאה

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

  1. אפשר (ואפילו מומלץ) לכלול מידע מפורט בהודעת חריגה, אבל מפתחים לא צריכים לנתח אותה כדי לטפל בשגיאה בצורה מתאימה. קודי שגיאה מפורטים או מידע אחר צריכים להיות גלויים כשיטות.
  2. חשוב לוודא שאפשרות הטיפול בשגיאות שבחרתם מאפשרת לכם להוסיף סוגי שגיאות חדשים בעתיד. במקרה של @IntDef, המשמעות היא שצריך לכלול ערך של OTHER או UNKNOWN. כשמחזירים קוד חדש, אפשר לבדוק את targetSdkVersion של המתקשר כדי להימנע מהחזרת קוד שגיאה שהאפליקציה לא מכירה. במקרה של חריגים, כדאי להשתמש במחלקת-על משותפת שהחריגים מיישמים, כדי שכל קוד שמטפל בסוג הזה יתפוס גם סוגי משנה ויטפל בהם.
  3. מפתח לא אמור להתעלם משגיאה בטעות – אם השגיאה מועברת על ידי החזרת ערך, צריך להוסיף לשיטה את ההערה @CheckResult.

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

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

צריך להגדיר את קודי הסטטוס בכיתה שמכילה אותם כשדות public static final עם הקידומת ERROR_, ולמנות אותם בהערה @hide @IntDef.

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

השם של ה-method צריך תמיד להתחיל בפועל (למשל get,‏ create,‏ reload וכו'), ולא באובייקט שעליו מבצעים את הפעולה.

public void tableReload() {
  mTable.reload();
}
public void reloadTable() {
  mTable.reload();
}

העדפה של סוגי אוספים על פני מערכים כסוג החזרה או הפרמטר

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

חריגות לגבי פרימיטיבים

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

חריג לקוד שרגיש לביצועים

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

חריג ל-Kotlin

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

העדפה של אוספים עם הערך ‎ @NonNull

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

במקרים שבהם יש תמיכה בהערות לגבי סוגים, תמיד עדיף להשתמש ב-@NonNull עבור רכיבי קולקציות.

מומלץ להשתמש גם ב-@NonNull כשמשתמשים במערכים במקום באוספים (ראו פריט קודם). אם הקצאת אובייקטים היא בעיה, אפשר ליצור קבוע ולהעביר אותו – אחרי הכול, מערך ריק הוא בלתי ניתן לשינוי. דוגמה:

private static final int[] EMPTY_USER_IDS = new int[0];

@NonNull
public int[] getUserIds() {
  int [] userIds = mService.getUserIds();
  return userIds != null ? userIds : EMPTY_USER_IDS;
}

יכולת השינוי של האוסף

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

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

@Nullable
public PermissionInfo[] getGrantedPermissions() {
  return mPermissions;
}
@NonNull
public Set<PermissionInfo> getGrantedPermissions() {
  if (mPermissions == null) {
    return Collections.emptySet();
  }
  return new ArraySet<>(mPermissions);
}

סוגי החזרה שניתנים לשינוי באופן מפורש

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

/**
 * Returns a view of this object as a list of [Item]s.
 */
fun MyObject.asList(): List<Item> = MyObjectListWrapper(this)

המוסכמה של Kotlin .asFoo() מתוארת בהמשך ומאפשרת לשנות את האוסף שמוחזר על ידי .asList() אם האוסף המקורי משתנה.

האפשרות לשנות אובייקטים של סוגי נתונים שמוחזרים

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

val tempResult = DataContainer()

fun add(other: DataContainer): DataContainer {
  tempResult.innerValue = innerValue + other.innerValue
  return tempResult
}
fun add(other: DataContainer): DataContainer {
  return DataContainer(innerValue + other.innerValue)
}

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

שימוש בסוג פרמטר vararg

מומלץ להשתמש ב-vararg בממשקי Kotlin ו-Java API במקרים שבהם סביר שהמפתח ייצור מערך באתר הקריאה למטרה היחידה של העברת כמה פרמטרים קשורים מאותו סוג.

public void setFeatures(Feature[] features) { ... }

// Developer code
setFeatures(new Feature[]{Features.A, Features.B, Features.C});
public void setFeatures(Feature... features) { ... }

// Developer code
setFeatures(Features.A, Features.B, Features.C);

עותקים להגנה

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

public void setValues(SomeObject... values) {
   this.values = Arrays.copyOf(values, values.length);
}

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

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

List<Foo> היא אפשרות ברירת המחדל, אבל כדאי לשקול סוגים אחרים כדי לספק משמעות נוספת:

  • משתמשים ב-Set<Foo> אם אין חשיבות לסדר האלמנטים ב-API, והוא לא מאפשר כפילויות או שהכפילויות לא משמעותיות.

  • Collection<Foo>, אם אין חשיבות לסדר בממשק ה-API והוא מאפשר כפילויות.

פונקציות המרה של Kotlin

ב-Kotlin נעשה שימוש תדיר ב-.toFoo() וב-.asFoo() כדי לקבל אובייקט מסוג אחר מאובייקט קיים, כאשר Foo הוא שם סוג ההחזרה של ההמרה. ההגדרה הזו תואמת ל-JDK המוכר Object.toString(). ב-Kotlin, השימוש ב-toString()‎ מתרחב גם להמרות פרימיטיביות כמו 25.toFloat().

ההבדל בין ההמרות שנקראות .toFoo() לבין ההמרות שנקראות .asFoo() הוא משמעותי:

שימוש ב-‎ .toFoo()‎ כשיוצרים אובייקט חדש ועצמאי

בדומה ל-.toString(), המרה באמצעות 'to' מחזירה אובייקט חדש ועצמאי. אם האובייקט המקורי ישונה בהמשך, השינויים האלה לא יבואו לידי ביטוי באובייקט החדש. באופן דומה, אם האובייקט new ישתנה בהמשך, השינויים האלה לא יבואו לידי ביטוי באובייקט old.

fun Foo.toBundle(): Bundle = Bundle().apply {
    putInt(FOO_VALUE_KEY, value)
}

שימוש ב-‎ .asFoo()‎ כשיוצרים wrapper תלוי, אובייקט מעוצב או cast

העברה (casting) ב-Kotlin מתבצעת באמצעות מילת המפתח as. היא משקפת שינוי בממשק אבל לא שינוי בזהות. כשמשתמשים ב-.asFoo() כקידומת בפונקציית הרחבה, היא מעטרת את המקבל. שינוי באובייקט המקורי של הנמען ישתקף באובייקט שמוחזר על ידי asFoo(). שינוי באובייקט Foo החדש עשוי לבוא לידי ביטוי באובייקט המקורי.

fun <T> Flow<T>.asLiveData(): LiveData<T> = liveData {
    collect {
        emit(it)
    }
}

פונקציות המרה צריכות להיכתב כפונקציות הרחבה

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

הקפצת הודעת שגיאה (throw) לחריגים ספציפיים מתאימים

אסור שהשיטות יחזירו חריגים כלליים כמו java.lang.Exception או java.lang.Throwable. במקום זאת, צריך להשתמש בחריג ספציפי מתאים כמו java.lang.NullPointerException כדי לאפשר למפתחים לטפל בחריגים בלי להגדיר טווח רחב מדי.

שגיאות שלא קשורות לארגומנטים שסופקו ישירות לשיטה שהופעלה באופן ציבורי צריכות להחזיר java.lang.IllegalStateException במקום java.lang.IllegalArgumentException או java.lang.NullPointerException.

מאזינים וקודים להתקשרות חזרה

אלה הכללים לגבי המחלקות והשיטות שמשמשות למנגנונים של מאזינים וקריאות חוזרות (callback).

שמות של מחלקות קריאה חוזרת צריכים להיות ביחיד

במקום זאת, צריך להשתמש ב-MyObjectCallback.MyObjectCallbacks

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

הערך onFooEvent מציין שהאירוע FooEvent מתרחש ושהקריאה החוזרת צריכה לפעול בתגובה.

השימוש בזמן עבר או בזמן הווה צריך לתאר את התנהגות התזמון

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

לדוגמה, אם המתודה מופעלת אחרי ביצוע פעולת קליק:

public void onClicked()

עם זאת, אם השיטה אחראית לביצוע פעולת הקליק:

public boolean onClick()

רישום לשיחה חוזרת

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

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

public void addFooCallback(@NonNull FooCallback callback);
public void removeFooCallback(@NonNull FooCallback callback);
public void registerFooCallback(@NonNull FooCallback callback);
public void unregisterFooCallback(@NonNull FooCallback callback);

הימנעו משימוש בשיטות getter עבור קריאות חוזרות (callback)

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

  • מפתח א' מתקשר אל setFooCallback(a)
  • מפתח ב' מתקשר אל setFooCallback(new B(getFooCallback()))
  • מפתח א' רוצה להסיר את פונקציית ה-callback שלו a ואין לו דרך לעשות זאת בלי לדעת את הסוג של B, וB לא נבנה כך שיאפשר שינויים כאלה בפונקציית ה-callback העטופה שלו.

קבלת Executor כדי לשלוט בשיגור של שיחות חוזרות

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

public void registerFooCallback(
    @NonNull @CallbackExecutor Executor executor,
    @NonNull FooCallback callback)

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

/**
 * ...
 * Note that the callback will be executed on the main thread using
 * {@link Looper.getMainLooper()}. To specify the execution thread, use
 * {@link registerFooCallback(Executor, FooCallback)}.
 * ...
 */
public void registerFooCallback(
    @NonNull FooCallback callback)

public void registerFooCallback(
    @NonNull @CallbackExecutor Executor executor,
    @NonNull FooCallback callback)

Executor נקודות חשובות לגבי ההטמעה: שימו לב שהקוד הבא הוא executor תקין.

public class SynchronousExecutor implements Executor {
    @Override
    public void execute(Runnable r) {
        r.run();
    }
}

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

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

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

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

public void setFooCallback(
    @NonNull @CallbackExecutor Executor executor,
    @NonNull FooCallback callback)

public void clearFooCallback()

שימוש ב-Executor במקום ב-Handler

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

ספריות מודרניות של פעולות מקבילות, כמו kotlinx.coroutines או RxJava, מספקות מנגנוני תזמון משלהן שמבצעים את השליחה שלהן כשצריך. לכן חשוב לספק את האפשרות להשתמש ב-executor ישיר (כמו Runnable::run) כדי למנוע חביון כתוצאה ממעברים כפולים בין השרשורים. לדוגמה, דילוג אחד כדי לפרסם בLooperשרשור באמצעות Handler ואז דילוג נוסף ממסגרת המקבילות של האפליקציה.

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

אני צריך להשתמש בLooper כי אני צריך Looper כדי epoll לאירוע. בקשת ההחרגה הזו אושרה כי אי אפשר לממש את היתרונות של Executor במצב הזה.

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

Handler עקבי באופן מקומי עם ממשקי API דומים אחרים באותו סוג. בקשת החריגה הזו מאושרת בהתאם לנסיבות. העדפה היא להוסיף עומסים יתרים מבוססי Executor, להעביר הטמעות של Handler לשימוש בהטמעה החדשה של Executor. (‫myHandler::post הוא Executor תקין!) בהתאם לגודל המחלקה, למספר השיטות הקיימות של Handler ולסבירות שהמפתחים יצטרכו להשתמש בשיטות קיימות שמבוססות על Handler לצד השיטה החדשה, יכול להיות שתינתן חריגה כדי להוסיף שיטה חדשה שמבוססת על Handler.

סימטריה ברישום

אם יש דרך להוסיף או לרשום משהו, צריכה להיות גם דרך להסיר או לבטל את הרישום שלו. השיטה

registerThing(Thing)

צריך להיות תואם

unregisterThing(Thing)

צריך לספק מזהה בקשה

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

class RequestParameters {
  public int getId() { ... }
}

class RequestExecutor {
  public void executeRequest(
    RequestParameters parameters,
    Consumer<RequestParameters> onRequestCompletedListener) { ... }
}

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

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

public interface MostlyOptionalCallback {
  void onImportantAction();
  default void onOptionalInformation() {
    // Empty stub, this method is optional.
  }
}

שימוש ב-android.os.OutcomeReceiver כשמבצעים מודלינג של קריאה לפונקציה לא חוסמת

OutcomeReceiver<R,E> מחזירה ערך תוצאה R אם הפעולה הצליחה, או E : Throwable אחרת – כמו שקורה בקריאה רגילה לשיטה. משתמשים ב-OutcomeReceiver כסוג של קריאה חוזרת כשממירים שיטה חוסמת שמחזירה תוצאה או זורקת חריגה לשיטה אסינכרונית לא חוסמת:

interface FooType {
  // Before:
  public FooResult requestFoo(FooRequest request);

  // After:
  public void requestFooAsync(FooRequest request, Executor executor,
      OutcomeReceiver<FooResult, Throwable> callback);
}

שיטות אסינכרוניות שהומרו בדרך הזו תמיד מחזירות void. כל תוצאה ש-requestFoo הייתה מחזירה מדווחת במקום זאת לפרמטר callback של requestFooAsync OutcomeReceiver.onResult על ידי קריאה ל-requestFoo ב-executor שסופק. כל חריגה ש-requestFoo הייתה יוצרת מדווחת במקום זאת ל-method‏ OutcomeReceiver.onError באותו אופן.

שימוש ב-OutcomeReceiver לדיווח על תוצאות של שיטות אסינכרוניות מספק גם עטיפה של Kotlin suspend fun לשיטות אסינכרוניות באמצעות התוסף Continuation.asOutcomeReceiver מ-androidx.core:core-ktx:

suspend fun FooType.requestFoo(request: FooRequest): FooResult =
  suspendCancellableCoroutine { continuation ->
    requestFooAsync(request, Runnable::run, continuation.asOutcomeReceiver())
  }

תוספים כאלה מאפשרים ללקוחות Kotlin לקרוא לשיטות אסינכרוניות לא חוסמות בנוחות של קריאה רגילה לפונקציה, בלי לחסום את השרשור שקורא לפונקציה. יכול להיות שתוספים כאלה של 1-1 לממשקי API של פלטפורמות יוצעו כחלק מandroidx.core:core-ktx ארטיפקט ב-Jetpack, בשילוב עם בדיקות תאימות ושיקולים של גרסה רגילה. מידע נוסף, שיקולים לגבי ביטול ודוגמאות זמינים במאמר בנושא asOutcomeReceiver.

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

עדיף להשתמש בממשקים פונקציונליים במקום ליצור סוגים חדשים של שיטות מופשטות יחידות (SAM)

ב-API ברמה 24 נוספו הסוגים java.util.function.* (מסמכי עזר), שמציעים ממשקי SAM גנריים כמו Consumer<T> שמתאימים לשימוש כפונקציות למדה של קריאה חוזרת. במקרים רבים, יצירת ממשקי SAM חדשים לא מספקת ערך רב מבחינת בטיחות סוגים או העברת כוונות, ובמקביל מרחיבה שלא לצורך את אזור ה-API של Android.

מומלץ להשתמש בממשקים הגנריים האלה במקום ליצור ממשקים חדשים:

מיקום הפרמטרים של SAM

כדי לאפשר שימוש אידיומטי מ-Kotlin, צריך להציב את פרמטר ה-SAM בסוף, גם אם השיטה מועמסת יתר על המידה עם פרמטרים נוספים.

public void schedule(Runnable runnable)

public void schedule(int delay, Runnable runnable)

Docs

אלה כללים לגבי מסמכים ציבוריים (Javadoc) של ממשקי API.

חובה לתעד את כל ממשקי ה-API הציבוריים

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

שיטות

חובה לתעד את פרמטרי השיטה ואת הערכים המוחזרים באמצעות הערות התיעוד @param ו-@return, בהתאמה. מעצבים את גוף ה-Javadoc כאילו הוא מופיע אחרי "This method...".

במקרים שבהם שיטה לא מקבלת פרמטרים, אין לה שיקולים מיוחדים והיא מחזירה את מה ששם השיטה מציין, אפשר להשמיט את @return ולכתוב מסמכים דומים ל:

/**
 * Returns the priority of the thread.
 */
@IntRange(from = 1, to = 10)
public int getPriority() { ... }

מסמכי התיעוד צריכים לכלול קישורים למסמכים אחרים שבהם מפורטים קבועים, שיטות ואלמנטים אחרים שקשורים לנושא. צריך להשתמש בתגי Javadoc (לדוגמה, @see ו-{@link foo}), ולא רק במילים של טקסט פשוט.

בדוגמה הבאה של מקור:

public static final int FOO = 0;
public static final int BAR = 1;

אל תשתמשו בטקסט גולמי או בגופן קוד:

/**
 * Sets value to one of FOO or <code>BAR</code>.
 *
 * @param value the value being set, one of FOO or BAR
 */
public void setValue(int value) { ... }

במקום זאת, אפשר להשתמש בקישורים:

/**
 * Sets value to one of {@link #FOO} or {@link #BAR}.
 *
 * @param value the value being set
 */
public void setValue(@ValueType int value) { ... }

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

הפעלת update-api או docs target כשמוסיפים Javadoc

הכלל הזה חשוב במיוחד כשמוסיפים תגי @link או @see, וצריך לוודא שהפלט נראה כמו שציפיתם. פלט שגיאה ב-Javadoc נובע לרוב מקישורים לא תקינים. הבדיקה הזו מתבצעת על ידי יעד Make‏ update-api או docs, אבל אם משנים רק את Javadoc ולא צריך להריץ את היעד update-api מסיבה אחרת, יכול להיות שהיעד docs יפעל מהר יותר.

משתמשים ב-{@code foo} כדי להבחין בין ערכי Java

כדי להבדיל בין ערכי Java כמו true, ‏ false ו-null לבין טקסט התיעוד, צריך להוסיף להם את התווים {@code...}.

כשכותבים תיעוד במקורות Kotlin, אפשר להוסיף גרשיים הפוכים לקוד, כמו ב-Markdown.

סיכומי הפרמטרים והחזרות צריכים להיות משפט חלקי אחד

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

/**
 * @param e The element to be appended to the list. This must not be
 *       null. If the list contains no entries, this element will
 *       be added at the beginning.
 * @return This method returns true on success.
 */

צריך לשנות ל:

/**
 * @param e element to be appended to this list, must be non-{@code null}
 * @return {@code true} on success, {@code false} otherwise
 */

צריך להוסיף הסברים להערות ב-Docs

למה ההערות @hide ו-@removed מוסתרות מ-API ציבורי? צריך לכלול הוראות להחלפת רכיבי API שמסומנים בהערה @deprecated.

שימוש ב-‎ @throws לתיעוד חריגים

אם שיטה מעלה חריגה מסוג checked, לדוגמה IOException, צריך לתעד את החריגה באמצעות @throws. בממשקי API שמקורם ב-Kotlin ומיועדים לשימוש על ידי לקוחות Java, מוסיפים הערות לפונקציות עם @Throws.

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

מקרים מסוימים של חריגה לא מסומנת נחשבים לחריגה מרומזת ולא צריך לתעד אותם, כמו NullPointerException או IllegalArgumentException, שבהם ארגומנט לא תואם ל-@IntDef או להערה דומה שמטמיעה את חוזה ה-API בחתימת השיטה:

/**
 * ...
 * @throws IOException If it cannot find the schema for {@code toVersion}
 * @throws IllegalStateException If the schema validation fails
 */
public SupportSQLiteDatabase runMigrationsAndValidate(String name, int version,
    boolean validateDroppedTables, Migration... migrations) throws IOException {
  // ...
  if (!dbPath.exists()) {
    throw new IllegalStateException("Cannot find the database file for " + name
        + ". Before calling runMigrations, you must first create the database "
        + "using createDatabase.");
  }
  // ...

או ב-Kotlin:

/**
 * ...
 * @throws IOException If something goes wrong reading the file, such as a bad
 *                     database header or missing permissions
 */
@Throws(IOException::class)
fun readVersion(databaseFile: File): Int {
  // ...
  val read = input.read(buffer)
    if (read != 4) {
      throw IOException("Bad database header, unable to read 4 bytes at " +
          "offset 60")
    }
  }
  // ...

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

סיום המשפט הראשון במסמכים בנקודה

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

  • אם מסמך קצר לא מסתיים בנקודה, ואם חבר בקבוצה ירש מסמכים שהכלי מזהה, התקציר יכלול גם את המסמכים האלה. לדוגמה, אפשר לראות את actionBarTabStyle במסמכי R.attr, שבהם תיאור המאפיין נוסף לתקציר.
  • מאותה סיבה, לא כדאי להשתמש בקיצור e.g. במשפט הראשון, כי Doclava מסיים את מסמכי התקציר אחרי האות g. לדוגמה, ראו TEXT_ALIGNMENT_CENTER ב-View.java. שימו לב ש-Metalava מתקן את השגיאה הזו באופן אוטומטי על ידי הוספת רווח קשיח אחרי הנקודה, אבל עדיף להימנע מהשגיאה הזו מלכתחילה.

עיצוב מסמכים לעיבוד ב-HTML

הפורמט של Javadoc הוא HTML, ולכן צריך לעצב את המסמכים האלה בהתאם:

  • צריך להשתמש בתג <p> מפורש למעברי שורה. לא מוסיפים תג סגירה </p>.

  • אל תשתמשו ב-ASCII כדי להציג רשימות או טבלאות.

  • ברשימות לא מסודרות צריך להשתמש בתג <ul> וברשימות מסודרות צריך להשתמש בתג <ol>. כל פריט צריך להתחיל בתג <li>, אבל לא צריך תג סוגר </li>. אחרי הפריט האחרון צריך להוסיף תג סגירה </ul> או </ol>.

  • בטבלאות צריך להשתמש בתגים <table>, <tr> לשורות, בתג <th> לכותרות ובתג <td> לתאים. לכל תגי הטבלה נדרשים תגי סגירה תואמים. אפשר להשתמש ב-class="deprecated" בכל תג כדי לציין הוצאה משימוש.

  • כדי ליצור גופן קוד בתוך השורה, משתמשים ב-{@code foo}.

  • כדי ליצור בלוקים של קוד, משתמשים ב-<pre>.

  • הדפדפן מנתח את כל הטקסט בתוך בלוק <pre>, לכן צריך להיזהר עם סוגריים <>. אפשר להשתמש בייצוגי HTML של &lt; ושל &gt; כדי להוסיף אותם.

  • אפשר גם להשאיר סוגריים מרובעים גולמיים <> בקטע הקוד אם עוטפים את החלקים הבעייתיים ב-{@code foo}. לדוגמה:

    <pre>{@code <manifest>}</pre>
    

פועלים לפי מדריך הסגנון של הפניית ה-API

כדי לשמור על עקביות בסגנון של סיכומי הכיתות, תיאורי השיטות, תיאורי הפרמטרים ופריטים אחרים, מומלץ לפעול לפי ההמלצות בהנחיות הרשמיות של שפת Java במאמר How to Write Doc Comments for the Javadoc Tool.

כללים ספציפיים ל-Android Framework

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

כלי ליצירת כוונות צריכים להשתמש בתבנית create*Intent()‎

יוצרים של כוונות צריכים להשתמש בשיטות שנקראות createFooIntent().

שימוש ב-Bundle במקום ליצור מבני נתונים חדשים לשימוש כללי

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

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

במקרים שבהם הפלטפורמה קוראת את הנתונים, מומלץ להימנע משימוש ב-Bundle ולהשתמש במחלקת נתונים עם הקלדה חזקה.

ליישומים של Parcelable חייב להיות שדה CREATOR ציבורי

ה-inflation של Parcelable נחשף דרך CREATOR, ולא דרך constructors גולמיים. אם מחלקה מטמיעה את Parcelable, השדה CREATOR שלה צריך להיות גם API ציבורי, והבונה של המחלקה שמקבל ארגומנט Parcel צריך להיות פרטי.

שימוש ב-CharSequence למחרוזות בממשק המשתמש

כשמחרוזת מוצגת בממשק משתמש, צריך להשתמש ב-CharSequence כדי לאפשר מקרים של Spannable.

אם מדובר רק במפתח או בתווית או בערך אחרים שלא גלויים למשתמשים, אפשר להשתמש ב-String.

הימנעות משימוש ב-Enums

צריך להשתמש ב-IntDef במקום ב-enums בכל ממשקי ה-API של הפלטפורמה, ומומלץ מאוד להשתמש בו בממשקי API של ספריות לא מקובצות. משתמשים ב-enums רק כשבטוחים שלא יתווספו ערכים חדשים.

היתרונות של IntDef:

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

היתרונות של טיפוס בן מנייה (enum)

  • תכונה לשונית אופיינית של Java, ‏ Kotlin
  • הפעלה של מעבר מקיף, when שימוש בהצהרה
    • הערה – הערכים לא יכולים להשתנות לאורך זמן, ראו את הרשימה הקודמת
  • שמות עם היקף ברור וקל לזיהוי
  • הפעלה של אימות בזמן הידור
    • לדוגמה, משפט when ב-Kotlin שמחזיר ערך
  • היא מחלקה פונקציונלית שיכולה להטמיע ממשקים, לכלול פונקציות עזר סטטיות, לחשוף שיטות של חברים או הרחבות ולחשוף שדות.

פועלים לפי היררכיית השכבות של חבילת Android

להיררכיית החבילות android.* יש סדר מרומז, שבו חבילות ברמה נמוכה לא יכולות להיות תלויות בחבילות ברמה גבוהה יותר.

לא להזכיר את Google, חברות אחרות והמוצרים שלהן

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

ההטמעות של Parcelable צריכות להיות סופיות

מחלקות Parcelable שהוגדרו על ידי הפלטפורמה תמיד נטענות מ-framework.jar, ולכן ניסיון של אפליקציה להחליף הטמעה של Parcelable הוא לא חוקי.

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

שיטות שקוראות לתהליך המערכת צריכות להפעיל מחדש את RemoteException כ-RuntimeException

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

אם אתם יודעים שהצד השני של קריאת Binder הוא תהליך המערכת, קוד ה-boilerplate הזה הוא השיטה המומלצת:

try {
    ...
} catch (RemoteException e) {
    throw e.rethrowFromSystemServer();
}

הצגת חריגים ספציפיים לשינויים ב-API

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

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

כך מפתחי אפליקציות וכלים יוכלו לזהות שינויים בהתנהגות של ה-API.

הטמעה של בנאי העתקה במקום שיבוט

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

/**
 * Constructs a shallow copy of {@code other}.
 */
public Foo(Foo other)

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

public class Foo {
    public static final class Builder {
        /**
         * Constructs a Foo builder using data from {@code other}.
         */
        public Builder(Foo other)

שימוש ב-ParcelFileDescriptor במקום FileDescriptor

הגדרת הבעלות של אובייקט java.io.FileDescriptor לא טובה, ולכן עלולות להתרחש שגיאות לא ברורות של שימוש אחרי סגירה. במקום זאת, ממשקי ה-API צריכים להחזיר או לקבל מופעים של ParcelFileDescriptor. קוד מדור קודם יכול להמיר בין PFD ל-FD אם צריך, באמצעות dup()‎ או getFileDescriptor()‎.

הימנעו משימוש בערכים מספריים בגודל אי-זוגי

מומלץ להימנע משימוש ישיר בערכים short או byte, כי לעיתים קרובות הם מגבילים את האופן שבו אפשר יהיה לפתח את ה-API בעתיד.

אל תשתמשו ב-BitSet

java.util.BitSet מצוין להטמעה אבל לא ל-API ציבורי. הוא ניתן לשינוי, דורש הקצאה לקריאות שיטה בתדירות גבוהה ולא מספק משמעות סמנטית למה שכל ביט מייצג.

בתרחישים שבהם נדרשים ביצועים גבוהים, כדאי להשתמש ב-int או ב-long עם @IntDef. בתרחישים של ביצועים נמוכים, כדאי לשקול Set<EnumType>. לנתונים בינאריים גולמיים: byte[].

העדפה ל-android.net.Uri

android.net.Uri היא שיטת האנקפסולציה המועדפת למזהי URI בממשקי API של Android.

מומלץ להימנע משימוש ב-java.net.URI, כי הוא מחמיר מדי בניתוח של כתובות URI, ואסור להשתמש ב-java.net.URL, כי ההגדרה שלו לשוויון פגומה מאוד.

הסתרת הערות שמסומנות כ-@IntDef,‏ @LongDef או @StringDef

ההערות שמסומנות ב-@IntDef, ב-@LongDef או ב-@StringDef מציינות קבוצה של קבועים תקינים שאפשר להעביר ל-API. עם זאת, כשמייצאים אותם כ-API, הקומפיילר מבצע החלפה של הקבועים, ורק הערכים (שכבר לא שימושיים) נשארים ב-API stub של ההערה (לפלטפורמה) או ב-JAR (לספריות).

לכן, השימוש בהערות האלה צריך להיות מסומן בהערת התיעוד @hide בפלטפורמה או בהערת הקוד @RestrictTo.Scope.LIBRARY) בספריות. בשני המקרים צריך לסמן אותם בסימן @Retention(RetentionPolicy.SOURCE) כדי למנוע את ההצגה שלהם ב-API stubs או ב-JARs.

@RestrictTo(RestrictTo.Scope.LIBRARY)
@Retention(RetentionPolicy.SOURCE)
@IntDef({
  STREAM_TYPE_FULL_IMAGE_DATA,
  STREAM_TYPE_EXIF_DATA_ONLY,
})
public @interface ExifStreamType {}

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

לא להוסיף מפתחות חדשים של ספקי הגדרות

לא לחשוף מפתחות חדשים מ-Settings.Global,‏ Settings.System או Settings.Secure.

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

יש כמה בעיות בהגדרות של SettingsProvider בהשוואה ל-getters/setters:

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

דוגמה: ‫Settings.Secure.LOCATION_MODE קיים כבר הרבה זמן, אבל צוות המיקום הוציא אותו משימוש לטובת Java API מתאים ‫LocationManager.isLocationEnabled() ושידור ‫MODE_CHANGED_ACTION שנתן לצוות הרבה יותר גמישות, והסמנטיקה של ממשקי ה-API ברורה הרבה יותר עכשיו.

לא להרחיב את Activity ו-AsyncTask

AsyncTask הוא פרט הטמעה. במקום זאת, כדאי לחשוף מאזין או, ב-androidx, API של ListenableFuture.

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

שימוש בפונקציה getUser()‎ של Context

במקרים שבהם מחלקות קשורות ל-Context, כמו כל מה שמוחזר מ-Context.getSystemService(), צריך להשתמש במשתמש שקשור ל-Context במקום לחשוף חברים שמטרגטים משתמשים ספציפיים.

class FooManager {
  Context mContext;

  void fooBar() {
    mIFooBar.fooBarForUser(mContext.getUser());
  }
}
class FooManager {
  Context mContext;

  Foobar getFoobar() {
    // Bad: doesn't appy mContext.getUserId().
    mIFooBar.fooBarForUser(Process.myUserHandle());
  }

  Foobar getFoobar() {
    // Also bad: doesn't appy mContext.getUserId().
    mIFooBar.fooBar();
  }

  Foobar getFoobarForUser(UserHandle user) {
    mIFooBar.fooBarForUser(user);
  }
}

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

שימוש ב-UserHandle במקום במספרים שלמים פשוטים

מומלץ להשתמש ב-UserHandle כדי לספק בטיחות סוגים ולמנוע בלבול בין מזהי משתמשים לבין מזהי משתמשים (uid).

Foobar getFoobarForUser(UserHandle user);
Foobar getFoobarForUser(int userId);

במקרים שבהם אין ברירה, יש להוסיף הערה לint שמייצג מזהה משתמש באמצעות התג @UserIdInt.

Foobar getFoobarForUser(@UserIdInt int user);

העדפה של listeners או callbacks על פני broadcast intents

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

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

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

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

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

לכן, אנחנו ממליצים להשתמש ב-listeners או ב-callbacks או במתקנים אחרים כמו JobScheduler במקום ב-broadcast intents בתכונות חדשות.

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

  • אם אפשר, משתמשים ב-Intent.FLAG_RECEIVER_REGISTERED_ONLY כדי להגביל את השידור לאפליקציות שכבר פועלות. לדוגמה, ACTION_SCREEN_ON משתמש בעיצוב הזה כדי למנוע הפעלה של אפליקציות.
  • אם אפשר, כדאי להשתמש ב-Intent.setPackage() או ב-Intent.setComponent() כדי לטרגט את השידור לאפליקציה ספציפית שמעניינת אתכם. לדוגמה, ב-ACTION_MEDIA_BUTTON נעשה שימוש בעיצוב הזה כדי להתמקד באמצעי הבקרה של ההפעלה באפליקציה הנוכחית.
  • אם אפשר, צריך להגדיר את השידור כ<protected-broadcast> כדי למנוע מאפליקציות זדוניות להתחזות למערכת ההפעלה.

כוונות בשירותים למפתחים שקשורים למערכת

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

  1. מגדירים קבוע מחרוזת SERVICE_INTERFACE במחלקה שמכילה את שם המחלקה המלא של השירות. צריך להוסיף לערך הקבוע הזה את ההערה @SdkConstant(SdkConstant.SdkConstantType.SERVICE_ACTION).
  2. מסמך על הכיתה שמפתח צריך להוסיף <intent-filter> ל-AndroidManifest.xml שלו כדי לקבל כוונות מהפלטפורמה.
  3. מומלץ מאוד להוסיף הרשאה ברמת המערכת כדי למנוע מאפליקציות לא רצויות לשלוח Intent לשירותים למפתחים.

יכולת פעולה הדדית בין Kotlin ל-Java

רשימה מלאה של הנחיות זמינה במדריך הרשמי של Android בנושא פעולות הדדיות בין Kotlin ל-Java. העתקנו חלק מההנחיות למדריך הזה כדי לשפר את יכולת הגילוי.

ניראות של ממשק ה-API

חלק מממשקי ה-API של Kotlin, כמו suspend funs, לא מיועדים לשימוש על ידי מפתחי Java. עם זאת, לא מומלץ לנסות לשלוט בנראות הספציפית לשפה באמצעות @JvmSynthetic, כי יש לכך תופעות לוואי שמשפיעות על האופן שבו ה-API מוצג במאגרי באגים, מה שמקשה על איתור באגים.

הנחיות ספציפיות זמינות במדריך בנושא פעולות הדדיות בין Kotlin ו-Java או במדריך בנושא פעולות אסינכרוניות.

אובייקטים נלווים

ב-Kotlin משתמשים ב-companion object כדי לחשוף חברים סטטיים. במקרים מסוימים, הם יופיעו מ-Java בכיתה פנימית בשם Companion ולא בכיתה המכילה. יכול להיות שקבצים של טקסט API של כיתות Companion יופיעו ככיתות ריקות – זה תקין.

כדי למקסם את התאימות ל-Java, צריך להוסיף הערות לאובייקטים נלווים: שדות לא קבועים עם @JvmField ופונקציות ציבוריות עם @JvmStatic כדי לחשוף אותם ישירות במחלקה המכילה.

companion object {
  @JvmField val BIG_INTEGER_ONE = BigInteger.ONE
  @JvmStatic fun fromPointF(pointf: PointF) {
    /* ... */
  }
}

התפתחות של ממשקי API בפלטפורמת Android

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

שינויים שגורמים לשגיאות בינאריות

מומלץ להימנע משינויים שגורמים לשגיאות בינאריות בממשקי API ציבוריים סופיים. בדרך כלל, שינויים מהסוג הזה גורמים לשגיאות כשמריצים את make update-api, אבל יכול להיות שיש מקרים חריגים שבהם בדיקת ה-API של Metalava לא מזהה אותם. אם יש לכם ספק, תוכלו לעיין במדריך של Eclipse Foundation בנושא Evolving Java-based APIs (פיתוח ממשקי API מבוססי Java) כדי לקבל הסבר מפורט על סוגי השינויים ב-API שתואמים ל-Java. שינויים שגורמים לשבירת תאימות בינארית בממשקי API מוסתרים (לדוגמה, מערכת) צריכים לפעול לפי מחזור הוצאה משימוש/החלפה.

שינויים שגורמים לבעיות במקור

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

במקרים מסוימים, שינויים שגורמים לבעיות בקוד המקור נדרשים כדי לשפר את חוויית המפתחים או את נכונות הקוד. לדוגמה, הוספת הערות לגבי אפשרות קבלת ערך null למקורות Java משפרת את יכולת הפעולה ההדדית עם קוד Kotlin ומקטינה את הסיכוי לשגיאות, אבל לרוב נדרשים שינויים – לפעמים שינויים משמעותיים – בקוד המקור.

שינויים בממשקי API פרטיים

אפשר לשנות ממשקי API עם ההערה @TestApi בכל שלב.

חובה לשמור ממשקי API עם ההערה @SystemApi למשך שלוש שנים. צריך להסיר או לארגן מחדש (Refactor) API של מערכת לפי לוח הזמנים הבא:

  • ‫API y - Added
  • ‫API y+1 – הוצאה משימוש
    • מסמנים את הקוד באמצעות @Deprecated.
    • מוסיפים תחליפים ומקשרים לתחליף ב-Javadoc של הקוד שהוצא משימוש באמצעות הערת התיעוד @deprecated.
    • במהלך מחזור הפיתוח, מדווחים על באגים למשתמשים פנימיים ומציינים שה-API יוצא משימוש. כך אפשר לוודא שממשקי ה-API החלופיים מתאימים.
  • ‫API y+2 – הסרה רכה
    • מסמנים את הקוד באמצעות @removed.
    • אופציונלי: אפשר להגדיר שהמערכת תבצע פעולה או לא תבצע פעולה באפליקציות שמטרגטות את רמת ה-SDK הנוכחית של הגרסה.
  • ‫API y+3 – הסרה סופית
    • מסירים לחלוטין את הקוד מעץ המקור.

הוצאה משימוש

אנחנו רואים בהוצאה משימוש שינוי ב-API, והיא יכולה להתרחש בגרסה ראשית (למשל, גרסה שמסומנת באות). כשמוציאים משימוש ממשקי API, כדאי להשתמש יחד ב-@Deprecated source annotation וב-@deprecated <summary> docs annotation. הסיכום חייב לכלול אסטרטגיית העברה. יכול להיות שהאסטרטגיה הזו תכלול קישור ל-API חלופי או הסבר למה לא כדאי להשתמש ב-API:

/**
 * Simple version of ...
 *
 * @deprecated Use the {@link androidx.fragment.app.DialogFragment}
 *             class with {@link androidx.fragment.app.FragmentManager}
 *             instead.
 */
@Deprecated
public final void showDialog(int id)

חובה גם להוציא משימוש ממשקי API שמוגדרים ב-XML ונחשפים ב-Java, כולל מאפיינים ומאפיינים שניתנים לעיצוב שנחשפים במחלקה android.R, עם סיכום:

<!-- Attribute whether the accessibility service ...
     {@deprecated Not used by the framework}
 -->
<attr name="canRequestEnhancedWebAccessibility" format="boolean" />

מתי מוציאים משימוש API

הוצאה משימוש הכי שימושית כדי למנוע שימוש ב-API בקוד חדש.

אנחנו גם דורשים לסמן ממשקי API כ@deprecated לפני שהם @removed, אבל זה לא מספק למפתחים תמריץ חזק להפסיק להשתמש בממשק API שהם כבר משתמשים בו.

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

  • javac מציג אזהרה במהלך ההידור.
    • אי אפשר להשבית את אזהרות ההוצאה משימוש באופן גלובלי או להגדיר אותן כנקודת בסיס, ולכן מפתחים שמשתמשים ב--Werror צריכים לתקן או להשבית כל שימוש בממשק API שהוצא משימוש לפני שהם יכולים לעדכן את גרסת ה-SDK של הקומפילציה.
    • אי אפשר להשבית אזהרות על הוצאה משימוש בייבוא של מחלקות שהוצאו משימוש. לכן, מפתחים צריכים להוסיף את שם המחלקה המלא לכל שימוש במחלקה שהוצאה משימוש לפני שהם יכולים לעדכן את גרסת ה-SDK של הקומפילציה.
  • בתיעוד בנושא d.android.com מופיעה הודעה על הוצאה משימוש.
  • בסביבות פיתוח משולבות (IDE) כמו Android Studio מוצגת אזהרה באתר שבו נעשה שימוש ב-API.
  • יכול להיות שסביבות פיתוח משולבות יורידו את הדירוג של ה-API או יסתירו אותו מההשלמה האוטומטית.

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

ערכת SDK שכוללת מספר גדול של הוצאות משימוש מחמירה את שני המקרים האלה.

לכן, מומלץ להוציא משימוש ממשקי API רק במקרים הבאים:

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

כשמוציאים API משימוש ומחליפים אותו ב-API חדש, מומלץ מאוד להוסיף API תאימות תואם לספריית Jetpack כמו androidx.core כדי לפשט את התמיכה במכשירים ישנים וחדשים.

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

/**
 * ...
 * @deprecated Use {@link #doThing(int, Bundle)} instead.
 */
@Deprecated
public void doThing(int action) {
  ...
}

public void doThing(int action, @Nullable Bundle extras) {
  ...
}

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

/**
 * ...
 * @deprecated No longer displayed in the status bar as of API 21.
 */
@Deprecated
public RemoteViews tickerView;

שינויים ברכיבי API שהוצאו משימוש

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

לא מרחיבים ממשקי API שהוצאו משימוש בגרסאות עתידיות. אפשר להוסיף הערות של lint לגבי נכונות (לדוגמה, @Nullable) ל-API קיים שהוצא משימוש, אבל לא כדאי להוסיף ממשקי API חדשים.

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

הסרה חלקית

הסרה רכה היא שינוי שגורם לבעיות בקוד המקור, ולכן מומלץ להימנע ממנה בממשקי API ציבוריים, אלא אם מועצת ה-API מאשרת אותה באופן מפורש. במקרה של ממשקי API של המערכת, צריך להוציא את ה-API משימוש למשך גרסה ראשית לפני הסרה רכה. מסירים את כל ההפניות לממשקי ה-API במסמכי Docs ומשתמשים בהערה @removed <summary> של Docs כשמסירים ממשקי API באופן זמני. הסיכום חייב לכלול את הסיבה להסרה, ויכול לכלול אסטרטגיית העברה, כפי שהסברנו במאמר בנושא הוצאה משימוש.

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

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

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

ברמה הטכנית, אנחנו מסירים את ה-API מ-SDK stub JAR ומ-compile-time classpath באמצעות @remove Javadoc annotation, אבל הוא עדיין קיים ב-run-time classpath – בדומה ל-APIs של @hide:

/**
 * Ringer volume. This is ...
 *
 * @removed Not functional since API 2.
 */
public static final String VOLUME_RING = ...

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

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

שיטות מופשטות

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

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

הסרה סופית

הסרה מלאה היא שינוי שובר תאימות בינארית, ואסור שתתרחש בממשקי API ציבוריים.

הערה לא מומלצת

אנחנו משתמשים בהערה @Discouraged כדי לציין שברוב המקרים (מעל 95%) לא מומלץ להשתמש ב-API. ממשקי API לא מומלצים שונים מממשקי API שהוצאו משימוש בכך שיש תרחיש שימוש קריטי וספציפי שמונע את הוצאתם משימוש. כשמסמנים API כלא מומלץ, צריך לספק הסבר ופתרון חלופי:

@Discouraged(message = "Use of this function is discouraged because resource
                        reflection makes it harder to perform build
                        optimizations and compile-time verification of code. It
                        is much more efficient to retrieve resources by
                        identifier (such as `R.foo.bar`) than by name (such as
                        `getIdentifier()`)")
public int getIdentifier(String name, String defType, String defPackage) {
    return mResourcesImpl.getIdentifier(name, defType, defPackage);
}

לא מומלץ להוסיף ממשקי API חדשים כלא מומלצים.

שינויים בהתנהגות של ממשקי API קיימים

במקרים מסוימים, יכול להיות שתרצו לשנות את התנהגות ההטמעה של API קיים. לדוגמה, ב-Android 7.0 שיפרנו את DropBoxManager כדי להעביר בבירור את המסר כשמפתחים ניסו לפרסם אירועים שהיו גדולים מדי לשליחה ב-Binder.

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

import android.app.compat.CompatChanges;
import android.compat.annotation.ChangeId;
import android.compat.annotation.EnabledSince;

public class MyClass {
  @ChangeId
  // This means the change will be enabled for target SDK R and higher.
  @EnabledSince(targetSdkVersion=android.os.Build.VERSION_CODES.R)
  // Use a bug number as the value, provide extra detail in the bug.
  // FOO_NOW_DOES_X will be the change name, and 123456789 the change ID.
  static final long FOO_NOW_DOES_X = 123456789L;

  public void doFoo() {
    if (CompatChanges.isChangeEnabled(FOO_NOW_DOES_X)) {
      // do the new thing
    } else {
      // do the old thing
    }
  }
}

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

תאימות קדימה

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

הגורמים הבאים הם הסיבות הנפוצות ביותר לבעיות תאימות קדימה ב-Android:

  • הוספת קבועים חדשים לקבוצה (כמו @IntDef או enum) שבעבר נחשבה לקבוצה מלאה (לדוגמה, אם ל-switch יש default שיוצר חריגה).
  • הוספת תמיכה בתכונה שלא נכללת ישירות בממשק ה-API (לדוגמה, תמיכה בהקצאת משאבים מסוג ColorStateList ב-XML, כשקודם לכן נתמכו רק משאבים מסוג <color>).
  • הסרת הגבלות על בדיקות בזמן ריצה, למשל הסרת בדיקה של requireNotNull() שהייתה קיימת בגרסאות קודמות.

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

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

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

סכימות XML

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

הוצאה משימוש של XML

אם רוצים להוציא משימוש רכיב או מאפיין XML, אפשר להוסיף את התו xs:annotation, אבל צריך להמשיך לתמוך בכל קובצי ה-XML הקיימים בהתאם למחזור החיים הרגיל של @SystemApi.

<xs:element name="foo">
    <xs:complexType>
        <xs:sequence>
            <xs:element name="name" type="xs:string">
                <xs:annotation name="Deprecated"/>
            </xs:element>
        </xs:sequence>
    </xs:complexType>
</xs:element>

חובה לשמור על סוגי הרכיבים

סכימות תומכות ברכיב sequence, ברכיב choice וברכיבים all כרכיבי צאצא של רכיב complexType. עם זאת, יש הבדלים בין רכיבי הצאצא האלה במספר ובסדר של רכיבי הצאצא שלהם, ולכן שינוי של סוג קיים יהיה שינוי לא תואם.

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

<!-- Original "sequence" value -->
<xs:element name="foo">
    <xs:complexType>
        <xs:sequence>
            <xs:element name="name" type="xs:string">
                <xs:annotation name="Deprecated"/>
            </xs:element>
        </xs:sequence>
    </xs:complexType>
</xs:element>

<!-- New "choice" value -->
<xs:element name="fooChoice">
    <xs:complexType>
        <xs:choice>
            <xs:element name="name" type="xs:string"/>
        </xs:choice>
    </xs:complexType>
</xs:element>

דפוסים ספציפיים ל-Mainline

Mainline הוא פרויקט שמאפשר לעדכן תת-מערכות ("מודולים של Mainline") של מערכת ההפעלה Android בנפרד, במקום לעדכן את כל תמונת המערכת.

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

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

הדוגמה <Module>FrameworkInitializer

אם מודול ראשי צריך לחשוף מחלקות @SystemService (לדוגמה, JobScheduler), צריך להשתמש בתבנית הבאה:

  • חשיפת מחלקה <YourModule>FrameworkInitializer מהמודול. הכיתה הזו צריכה להיות ב-$BOOTCLASSPATH. דוגמה: StatsFrameworkInitializer

  • מסמנים אותו באמצעות @SystemApi(client = MODULE_LIBRARIES).

  • מוסיפים לו שיטת public static void registerServiceWrappers().

  • משתמשים ב-SystemServiceRegistry.registerContextAwareService() כדי לרשום מחלקה של מנהל שירות כשהיא צריכה הפניה ל-Context.

  • משתמשים ב-SystemServiceRegistry.registerStaticService() כדי לרשום מחלקה של מנהל שירות כשלא צריך הפניה ל-Context.

  • מפעילים את method registerServiceWrappers() מתוך המאחֵל הסטטי של SystemServiceRegistry.

התבנית <Module>ServiceManager

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

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

  • ליצור מחלקה <YourModule>ServiceManager בהתאם לעיצוב של TelephonyServiceManager

  • הצגת הכיתה בתור @SystemApi. אם אתם צריכים לגשת אליו רק משיעורים או משיעורים של שרת המערכת, אתם יכולים להשתמש ב-@SystemApi(client = MODULE_LIBRARIES). אחרת, @SystemApi(client = PRIVILEGED_APPS) יתאים.$BOOTCLASSPATH

  • הכיתה הזו תכלול:

    • בונה מוסתר, כך שרק קוד הפלטפורמה הסטטי יכול ליצור מופע שלו.
    • שיטות getter ציבוריות שמחזירות מופע ServiceRegisterer עבור שם ספציפי. אם יש לכם אובייקט אחד של Binder, אתם צריכים שיטת getter אחת. אם יש לכם שניים, תצטרכו שני getters.
    • ב-ActivityThread.initializeMainlineModules(), יוצרים מופע של המחלקה הזו ומעבירים אותו לשיטה סטטית שנחשפת על ידי המודול. בדרך כלל, מוסיפים API סטטי @SystemApi(client = MODULE_LIBRARIES) בכיתה FrameworkInitializer שמקבלת אותו.

הדפוס הזה ימנע ממודולים אחרים ב-mainline לגשת לממשקי ה-API האלה, כי אין דרך למודולים אחרים לקבל מופע של <YourModule>ServiceManager, גם אם ממשקי ה-API‏ get() ו-register() גלויים להם.

כך הטלפוניה מקבלת הפניה לשירות הטלפוניה: קישור לחיפוש קוד.

אם אתם מטמיעים אובייקט של שירות binder בקוד מקורי, אתם משתמשים ב-native APIs של AServiceManager. ממשקי ה-API האלה מקבילים לממשקי ה-API של Java ServiceManager, אבל ממשקי ה-API המקוריים נחשפים ישירות למודולים הראשיים. אל תשתמשו בהם כדי להירשם או להפנות לאובייקטים של Binder שלא נמצאים בבעלות המודול שלכם. אם חושפים אובייקט binder מ-native, לא צריך להשתמש בשיטה register() ב-<YourModule>ServiceManager.ServiceRegisterer.

הגדרות הרשאות במודולים ראשיים

מודולים של Mainline שמכילים חבילות APK יכולים להגדיר הרשאות (מותאמות אישית) ב-APK שלהם AndroidManifest.xml באותו אופן כמו ב-APK רגיל.

אם ההרשאה המוגדרת משמשת רק באופן פנימי בתוך מודול, שם ההרשאה צריך להתחיל בקידומת של שם חבילת ה-APK, לדוגמה:

<permission
    android:name="com.android.permissioncontroller.permission.MANAGE_ROLES_FROM_CONTROLLER"
    android:protectionLevel="signature" />

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

<permission
    android:name="android.permission.health.READ_ACTIVE_CALORIES_BURNED"
    android:label="@string/active_calories_burned_read_content_description"
    android:protectionLevel="dangerous"
    android:permissionGroup="android.permission-group.HEALTH" />

לאחר מכן, המודול יכול לחשוף את שם ההרשאה הזה כקבוע של API בממשק ה-API שלו, לדוגמה HealthPermissions.READ_ACTIVE_CALORIES_BURNED.