הנחיות ל-Android API

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

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

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

כלי API Lint

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

כללי API

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

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

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

יסודות של ממשקי API

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

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

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

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

  • אין ערובה לכך שחשפה פני שטח תקינים או מלאים. עד שהלקוחות בודקים את ממשק ה-API או משתמשים בו, אין דרך לוודא שללקוח יש את ממשקי ה-API המתאימים כדי להשתמש בתכונה.
  • אי אפשר לבדוק ממשקי API ללא הטמעה בגרסאות Developer Preview.
  • אי אפשר לבדוק ממשקי 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 מומלץ להעדיף ממשקים על פני מחלקות מופשטים – כלומר, שיטות ממשק ברירת המחדל יכולות להיות מוטמעות כקריאות לשיטות ממשק אחרות.

במקרים שבהם יש צורך ב-constructor או במצב פנימי בהטמעת ברירת המחדל, חובה להשתמש ב-classes abstract.

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

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, שקעים, 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 יציב, שפורסם ויש לו גרסה, מובילה לכך שקשה הרבה יותר לפתח את הממשק עצמו. עם זאת, עדיין כדאי להוסיף שכבת עטיפה, כדי לעמוד בהנחיות אחרות של ממשקי 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, עדיף להשתמש בשילוב של קריאה חוזרת (callback) עם הודעה על השלמה, 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 יכולים להיות יתרונות בפלטפורמות מסוימות של ממשקי API, הוא לא עקבי עם שטח הפלטפורמה הקיים של Android API. @Nullable ו-@NonNull מספקים כלי עזרה לשמירה על הבטיחות של null, ו-Kotlin אוכפת חוזים של תכונות Nullable ברמת המהדר, כך שאין צורך ב-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() {}
}

אובייקטים מסוג singleton

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

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

מומלץ להשתמש בדפוס single instance, שמסתמך על סיווג בסיס מופשט כדי לטפל בבעיות האלה.

מכונה יחידה

במחלקות עם מופע יחיד נעשה שימוש במחלקת בסיס מופשטת עם קונסטרוקטור 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
    }
  }
}

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

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

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

הימנעו משימוש בסוגי משנה חדשים של View ב-Android.*

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

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

שדות

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

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

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

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

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

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

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

לא צריך לחשוף שדות פנימיים

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

public int mFlags;

שימוש בסטטוס 'גלוי לכולם' במקום 'מוגן'

@ראו שימוש בסטטוס 'גלוי לכולם' במקום 'מוגן'

קבועים

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

ערכי קבועים של דגלים לא יכולים לחפוף לערכים מסוג 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 extras צריכים להתחיל ב-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 ורכיבי Extra, וגם של רשומות ב-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 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;

@ראו שימוש בתחיליות סטנדרטיות לקבועים

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

חובה לתת שמות למזהים, למאפיינים ולערכים ציבוריים לפי כלל השמות 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 ציבוריים. עם זאת, אם צריך לחשוף אותם, חובה לתת לשמות של פריטי ה-layout ופריטי ה-drawable הציבוריים את הסימן _, למשל layout/simple_list_item_1.xml או drawable/title_bar_tall.xml.

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

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

CameraManager.MAX_CAMERAS
CameraManager.getMaxCameras()

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

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

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

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

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

סיווגים של נתונים

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

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

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

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

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

שינוי והעתקה

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

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

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() ב-data class עם פורמט מומלץ שמתאים להטמעה של data class ב-Kotlin, למשל User(var1=Alex, var2=42).

שיטות

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

שעה

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

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

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

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

שיטות שמציגות משכי זמן צריכות להיקרא duration

אם ערך הזמן מייצג את משך הזמן הרלוונטי, נותנים לפרמטר את השם 'duration' ולא 'time'.

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

החרגות:

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

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

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

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 interop.

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

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

בנאים

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

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

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

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

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

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

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

כיתות ה-builder חייבות להחזיר את ה-builder

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

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

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

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 באמצעות קונסטרוקטור

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

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

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

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

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

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

ב-builders יכול להיות קונסטרוקטור ליצירת מכונה חדשה ממכונה קיימת

יכול להיות ש-builders יכללו קונסטרוקטור להעתקה כדי ליצור מופע חדש של builder מאובייקט קיים או מאובייקט שנוצר. אסור לספק שיטות חלופיות ליצירת מכונות build ממכונות build קיימות או מאובייקטים של 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 שלו צריכות לקבל ארגומנטים מסוג @Nullable

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

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

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

בנוסף, ה-setters של @Nullable יתואמו ל-getters שלהם, שצריכים להיות @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()

צריך לתעד כראוי את ערך ברירת המחדל (אם לא קוראים ל-setter) ואת המשמעות של null גם ב-setter וגם ב-getter.

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

אפשר לספק setters של ה-builder למאפיינים שניתן לשינוי, אם ה-setters זמינים בכיתה שנוצרה

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

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

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

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

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

לא צריך להשתמש ב-getters ב-builders

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

ל-setters של ה-builder חייבים להיות getters תואמים בכיתה שנוצרה

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

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

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

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

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

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

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

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

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

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

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

שימוש בקידומת 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();

שיטות שמפעילות או משביתות התנהגויות או תכונות יכולות להשתמש בתחילית 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()

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

שיטות של מאפיינים ב-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) מפורטות הנחיות ל-API בנוגע להגדרת דגלים של מסיכות ביט.

ערכים מוגדרים מראש

צריך לספק שתי שיטות 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);

פונקציות getter

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

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

לא מטמיעים את equals() ואת hashCode() או מטמיעים את שניהם

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

הטמעת toString()‎ עבור כיתות נתונים

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

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

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

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

כל המידע שזמין ב-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();
}

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

מיקומי האחסון של נתונים ב-Android הם לא תמיד קבצים בדיסק. לדוגמה, תוכן שמועברים מעבר לגבולות של משתמשים מיוצג כ-content:// Uri. כדי לאפשר עיבוד של מקורות נתונים שונים, ממשקי 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)

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

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

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

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

הערות מודעות מפורשות לגבי אפשרות האפס של משתנים נדרשות בממשקי API של Java, אבל המושג של אפשרות האפס של משתנים הוא חלק מ-Kotlin, ואף פעם לא צריך להשתמש בהערות לגבי אפשרות האפס של משתנים בממשקי 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, הוספת הערות לפרמטרים של השיטות תיצור באופן אוטומטי מסמך עזרה עם הכיתוב 'This value may be null' (הערה: הערך הזה עשוי להיות null), אלא אם נעשה שימוש מפורש ב-null במקום אחר במסמך העזרה של הפרמטרים.

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

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

אכיפת תכונה של ביטול

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

משאבים

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

public void setTitle(@StringRes int resId)

@IntDef למערכי קבועים

קבועים קסומים: פרמטרים מסוג 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";

מתן אפשרות ל-nullability תואמת עבור ערכי ברירת המחדל

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

סוג הורה ילד או ילדה
סוג הערך המוחזר ללא הערות ללא הערות או לא null
סוג הערך המוחזר Nullable nullable או nonnull
סוג הערך המוחזר Nonnull Nonnull
ארגומנט מהנה ללא הערות ללא הערות או עם אפשרות ל-null
ארגומנט מהנה Nullable Nullable
ארגומנט מהנה Nonnull nullable או nonnull

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

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

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

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

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 חייבות להיות זהות

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

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

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

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

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

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

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

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

חריג לגבי פריימים בסיסיים

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

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

בתרחישים מסוימים, שבהם ה-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;
}

יכולת השינוי של אוספים

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

עם זאת, מומלץ להשתמש ב-Java APIs עם סוגי חזרה ניתנים לשינוי כברירת מחדל, כי ההטמעה של ממשקי ה-API ב-Java בפלטפורמת Android עדיין לא מספקת הטמעה נוחה של קולקציות חסרות שינוי. היוצא מן הכלל הוא סוגי ההחזרה של 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);
}

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

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

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

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

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

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

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

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

פונקציות המרה ב-Kotlin

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

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

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

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

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

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

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

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

צריך לכתוב פונקציות המרה כפונקציות תוסף

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

השלכת חריגים ספציפיים מתאימים

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

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

Listeners ו-callbacks

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

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

משתמשים ב-MyObjectCallback במקום ב-MyObjectCallbacks.

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

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

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

השמות של שיטות ה-Callback לגבי אירועים צריכים לציין אם האירוע כבר התרחש או שהוא בתהליך התרחשות.

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

public void onClicked()

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

public boolean onClick()

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

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

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

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

הימנעות מ-getters עבור פונקציות קריאה חוזרת

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

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

איך מאפשרים ל-Executor לשלוט בשליחת קריאה חוזרת

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

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

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

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

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

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

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

אני צריך להשתמש ב-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) { ... }
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

public void schedule(Runnable runnable)

public void schedule(int delay, Runnable runnable)

Docs

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

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

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

שיטות

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

במקרים שבהם השיטה לא מקבלת פרמטרים, אין בה שיקולים מיוחדים והיא מחזירה את מה שמצוין בשם השיטה, אפשר להשמיט את @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 כשמוסיפים Javadoc

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

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

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

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

הסיכומים של @param ושל @return צריכים להיות משפט אחד

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

אם שיטה מסוימת גורמת לחריגה מאומתת, למשל 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, שבו מופיע התיאור של המאפיין שנוסף לסיכום.
  • מאותה סיבה, מומלץ להימנע משימוש ב-'למשל' במשפט הראשון, כי ב-Doclava מסמכי הסיכום מסתיימים אחרי 'למשל'. לדוגמה, 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 במאמר איך כותבים תגובות Doc לכלי Javadoc.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

הימנעו מהתייחסות ל-Google, לחברות אחרות ולמוצרים שלהן

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

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

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

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

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

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

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

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

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

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

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

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

הטמעת מאסטר קונסטרוקטור במקום קלון

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

/**
 * 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 בעצמם, המהדר מוסיף את הקבועים לקוד, ונותרים רק הערכים (החסרי תועלת עכשיו) ב-stub של ה-API של ההערה (לפלטפורמה) או ב-JAR (לספריות).

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

@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:

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

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

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

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

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

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

בכיתות שמקושרות ל-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 במקום ב-ints רגילים

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

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

במקרים שבהם אי אפשר להימנע מכך, צריך להוסיף הערה @UserIdInt ל-int שמייצג מזהה משתמש.

Foobar getFoobarForUser(@UserIdInt int user);

העדפה של פונקציות האזנה או פונקציות קריאה חוזרת (callbacks) לשידור כוונות

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

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

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

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

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

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

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

  • אם אפשר, כדאי להשתמש ב-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 כדי לקבל כוונות (Intents) מהפלטפורמה.
  3. מומלץ מאוד להוסיף הרשאה ברמת המערכת כדי למנוע מאפליקציות זדוניות לשלוח Intent לשירותי הפיתוח.

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

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

חשיפה של ממשקי API

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

הנחיות ספציפיות זמינות במדריך ל-Kotlin-Java interop או במדריך ל-Async.

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

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

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

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

האבולוציה של ממשקי ה-API של פלטפורמת Android

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

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

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

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

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

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

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

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

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

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

הפסקת תמיכה

אנחנו מתייחסים להוצאה משימוש כאל שינוי ב-API, והיא יכולה להתרחש במהדורה ראשית (כמו גרסה עם אות). כשמבטלים את השימוש בממשקי API, צריך להשתמש ביחד בהערה למקור @Deprecated ובהערה למסמכי התיעוד @deprecated <summary>. חובה לכלול בסיכום אסטרטגיית העברה. האסטרטגיה הזו עשויה לקשר ל-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 שלהם ל-compile.
    • אי אפשר להסתיר אזהרות על הוצאה משימוש בייבוא של כיתות שהוצאו משימוש. כתוצאה מכך, המפתחים צריכים להוסיף את שם המחלקה המלא בקוד לכל שימוש במחלקה שהוצאה משימוש, לפני שהם יכולים לעדכן את גרסת ה-SDK שלהם לצורך הידור.
  • במסמכי העזרה של d.android.com מופיעה הודעה על הוצאה משימוש.
  • סביבות פיתוח משולבות (IDE) כמו Android Studio מציגות אזהרה באתר השימוש ב-API.
  • סבירות גבוהה שסביבות פיתוח משולבות (IDE) יורידו את דירוג ה-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 הוצאו משימוש בגרסאות עתידיות. אפשר להוסיף הערות של אימות שגיאות (למשל, @Nullable) ל-API קיים שהוצא משימוש, אבל לא כדאי להוסיף ממשקי API חדשים.

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

הסרה חלקית

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

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

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

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

ברמה הטכנית, אנחנו מסירים את ה-API מקובץ ה-JAR של ה-stub של ה-SDK ומנתיב ה-classpath בזמן הידור באמצעות ההערה @remove ב-Javadoc, אבל הוא עדיין קיים בנתיב ה-classpath בזמן הריצה – בדומה לממשקי ה-API של @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 כדי לציין שלא מומלץ להשתמש ב-API ברוב המקרים (יותר מ-95%). ממשקי 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. הן לא משבשות את התאימות של הקוד הבינארי או של המקור, וגם לא מתגלות על ידי בדיקת ה-lint של ממשקי ה-API.

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

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

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

  • יוצרים את הכיתה <YourModule>ServiceManager לפי העיצוב של TelephonyServiceManager.

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

  • הכיתה הזו תכלול את הפרטים הבאים:

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

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

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

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

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

מודולים ראשיים שמכילים חבילות 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.