הדף הזה מיועד למפתחים, כדי להסביר להם את העקרונות הכלליים שמועצת ה-API אוכפת בבדיקות של ממשקי API.
בנוסף להקפדה על ההנחיות האלה כשכותבים ממשקי API, מפתחים צריכים להריץ את הכלי API Lint, שמקודד רבים מהכללים האלה בבדיקות שהוא מריץ על ממשקי API.
אפשר לחשוב על זה כעל מדריך לכללים שהכלי Lint פועל לפיהם, וגם כעל עצות כלליות לגבי כללים שלא ניתן להגדיר בכלי הזה ברמת דיוק גבוהה.
כלי API Lint
API Lint משולב בכלי הניתוח הסטטי Metalava ופועל אוטומטית במהלך האימות ב-CI. אפשר להפעיל אותו באופן ידני מדף תשלום בפלטפורמה מקומית באמצעות m
checkapi
או מדף תשלום מקומי של AndroidX באמצעות ./gradlew :path:to:project:checkApi
.
כללי API
פלטפורמת Android וספריות Jetpack רבות היו קיימות לפני שנוצרה קבוצת ההנחיות הזו, והמדיניות שמוצגת בהמשך הדף הזה מתפתחת כל הזמן כדי לענות על הצרכים של מערכת Android.
כתוצאה מכך, יכול להיות שחלק מממשקי ה-API הקיימים לא עומדים בהנחיות. במקרים אחרים, יכול להיות שחוויית המשתמש של מפתחי האפליקציות תהיה טובה יותר אם API חדש יהיה עקבי עם ממשקי API קיימים, במקום לפעול בהתאם להנחיות באופן קפדני.
מומלץ להשתמש בשיקול הדעת שלכם ולפנות למועצת ה-API אם יש שאלות מורכבות לגבי API שצריך לפתור או הנחיות שצריך לעדכן.
יסודות ה-API
הקטגוריה הזו מתייחסת להיבטים המרכזיים של Android API.
צריך להטמיע את כל ממשקי ה-API
ללא קשר לקהל של API (לדוגמה, ציבורי או @SystemApi
), צריך להטמיע את כל הממשקים של ה-API כשממזגים אותו או חושפים אותו כ-API. לא למזג קובצי stub של API עם הטמעה שתתבצע במועד מאוחר יותר.
לממשקי API ללא הטמעות יש כמה בעיות:
- אין ערובה לכך שהוצגה פני שטח מתאימה או מלאה. עד שממשק API נבדק או נמצא בשימוש של לקוחות, אין דרך לוודא שללקוח יש את ממשקי ה-API המתאימים כדי להשתמש בתכונה.
- אי אפשר לבדוק ממשקי API ללא הטמעה בתצוגות מקדימות למפתחים.
- אי אפשר לבדוק ממשקי API ללא הטמעה ב-CTS.
צריך לבדוק את כל ממשקי ה-API
הדרישה הזו תואמת לדרישות של CTS בפלטפורמה, למדיניות AndroidX ולרעיון הכללי שממשקי API צריכים להיות מוטמעים.
בדיקת ממשקי API מספקת בסיס להבטחה שאפשר להשתמש בממשק ה-API, ושהתייחסנו לתרחישי שימוש צפויים. בדיקה של קיום לא מספיקה, צריך לבדוק את ההתנהגות של ממשק ה-API עצמו.
שינוי שמוסיף API חדש צריך לכלול בדיקות תואמות באותו CL או באותו נושא ב-Gerrit.
בנוסף, ממשקי ה-API צריכים להיות ניתנים לבדיקה. צריכה להיות לכם תשובה לשאלה: "איך מפתח אפליקציות יבדוק קוד שמשתמש ב-API שלך?"
כל ממשקי ה-API צריכים להיות מתועדים
התיעוד הוא חלק חשוב בשימושיות של API. יכול להיות שהתחביר של משטח API ייראה ברור, אבל לקוחות חדשים לא יבינו את הסמנטיקה, ההתנהגות או ההקשר שמאחורי ה-API.
כל ממשקי ה-API שנוצרו חייבים לעמוד בהנחיות
ממשקי API שנוצרו על ידי כלים צריכים לפעול בהתאם לאותן הנחיות לגבי ממשקי API כמו קוד שנכתב ידנית.
כלים שלא מומלץ להשתמש בהם ליצירת ממשקי API:
-
AutoValue
: יש הפרות של ההנחיות בדרכים שונות, למשל, אי אפשר להטמיע מחלקות ערכים סופיות או בנאים סופיים עם האופן שבו AutoValue פועל.
סגנון קוד
הקטגוריה הזו מתייחסת לסגנון הקוד הכללי שבו מפתחים צריכים להשתמש, במיוחד כשכותבים ממשקי API ציבוריים.
פועלים לפי מוסכמות תכנות רגילות, אלא אם מצוין אחרת
מוסכמות התכנות של Android מתועדות כאן עבור תורמים חיצוניים:
https://source.android.com/source/code-style.html
באופן כללי, אנחנו נוטים לפעול לפי מוסכמות התכנות הסטנדרטיות של Java ו-Kotlin.
ראשי תיבות לא יכולים להיות באותיות רישיות בשמות של שיטות
לדוגמה: שם השיטה צריך להיות runCtsTests
ולא runCTSTests
.
שמות לא יכולים להסתיים ב-Impl
הפעולה הזו חושפת פרטי הטמעה, ולכן כדאי להימנע ממנה.
שיעורים
בקטע הזה מתוארים כללים לגבי מחלקות, ממשקים וירושה.
הורשה של מחלקות ציבוריות חדשות ממחלקת הבסיס המתאימה
הורשה חושפת רכיבי API במחלקת המשנה שאולי לא מתאימים.
לדוגמה, מחלקת משנה ציבורית חדשה של FrameLayout
נראית כך: FrameLayout
בנוסף להתנהגויות החדשות ולרכיבי ה-API. אם ה-API שמועבר בירושה לא מתאים לתרחיש השימוש שלכם, אפשר להעביר בירושה ממחלקה גבוהה יותר בהיררכיה, למשל, ViewGroup
או View
.
אם אתם רוצים לדרוס שיטות ממחלקת הבסיס כדי להפעיל את UnsupportedOperationException
, כדאי לשקול מחדש באיזו מחלקת בסיס אתם משתמשים.
שימוש במחלקות הבסיסיות של קולקציות
בין אם מעבירים אוסף כארגומנט או מחזירים אותו כערך, תמיד עדיף להשתמש במחלקת הבסיס במקום בהטמעה הספציפית (למשל, להשתמש ב-List<Foo>
במקום ב-ArrayList<Foo>
).
משתמשים במחלקת בסיס שמבטאת אילוצים מתאימים ל-API. לדוגמה, משתמשים ב-List
עבור API שבו צריך להזמין את האוסף, וב-Set
עבור API שבו האוסף צריך לכלול רכיבים ייחודיים.
ב-Kotlin, עדיף להשתמש באוספים שלא ניתן לשנות. פרטים נוספים זמינים במאמר בנושא שינוי של אוספים.
מחלקות מופשטות לעומת ממשקים
ב-Java 8 נוספה תמיכה בשיטות ברירת מחדל של ממשקים, שמאפשרת למעצבי API להוסיף שיטות לממשקים תוך שמירה על תאימות בינארית. קוד הפלטפורמה וכל ספריות Jetpack צריכים לטרגט Java 8 ואילך.
במקרים שבהם ההטמעה שמוגדרת כברירת מחדל היא חסרת מצב, מעצבי API צריכים להעדיף ממשקים על פני מחלקות מופשטות – כלומר, אפשר להטמיע שיטות ממשק שמוגדרות כברירת מחדל כקריאות לשיטות ממשק אחרות.
במקרים שבהם נדרש בנאי או מצב פנימי בהטמעה שמוגדרת כברירת מחדל, חובה להשתמש במחלקות מופשטות.
בשני המקרים, מעצבי API יכולים לבחור להשאיר שיטה אחת מופשטת כדי לפשט את השימוש כביטוי למדא:
public interface AnimationEndCallback {
// Always called, must be implemented.
public void onFinished(Animation anim);
// Optional callbacks.
public default void onStopped(Animation anim) { }
public default void onCanceled(Animation anim) { }
}
שמות המחלקות צריכים לשקף את מה שהן מרחיבות
לדוגמה, כדי שהשם יהיה ברור, כיתות שמרחיבות את Service
צריכות להיקרא FooService
:
public class IntentHelper extends Service {}
public class IntentService extends Service {}
סיומות כלליות
מומלץ להימנע משימוש בסיומות גנריות של שמות מחלקות כמו Helper
ו-Util
לאוספים של שיטות עזר. במקום זאת, צריך להוסיף את השיטות ישירות למחלקות המשויכות או לפונקציות הרחבה של Kotlin.
במקרים שבהם שיטות מגשרות בין כמה כיתות, צריך לתת לכיתה המכילה שם משמעותי שמסביר מה היא עושה.
במקרים מאוד מוגבלים, יכול להיות שיהיה מתאים להשתמש בסיומת Helper
:
- משמש להגדרת התנהגות ברירת המחדל
- יכול להיות שיהיה צורך להעביר התנהגות קיימת למחלקות חדשות
- יכול להיות שיהיה צורך במצב מתמשך
- בדרך כלל כולל
View
לדוגמה, אם כדי להוסיף תיאורי כלים לגרסה קודמת צריך לשמור את המצב שמשויך ל-View
ולקרוא לכמה שיטות ב-View
כדי להתקין את התוסף, TooltipHelper
יהיה שם מחלקה מקובל.
לא לחשוף קוד שנוצר על ידי IDL כ-API ציבורי ישירות
שומרים את הקוד שנוצר על ידי IDL כפרטי הטמעה. כולל protobuf, sockets, FlatBuffers או כל משטח API אחר שאינו Java או NDK. עם זאת, רוב ה-IDL ב-Android הוא ב-AIDL, ולכן הדף הזה מתמקד ב-AIDL.
מחלקות AIDL שנוצרו לא עומדות בדרישות של מדריך הסגנון של ה-API (לדוגמה, אי אפשר להשתמש בהן בעומס יתר), והכלי AIDL לא מיועד באופן מפורש לשמירה על תאימות של שפת ה-API, ולכן אי אפשר להטמיע אותן ב-API ציבורי.
במקום זאת, מוסיפים שכבת API ציבורית מעל ממשק ה-AIDL, גם אם היא עטיפה רדודה בהתחלה.
ממשקי Binder
אם הממשק Binder
הוא פרט הטמעה, אפשר לשנות אותו באופן חופשי בעתיד, והשכבה הציבורית מאפשרת לשמור על תאימות לאחור. לדוגמה, יכול להיות שתצטרכו להוסיף ארגומנטים חדשים לקריאות הפנימיות, או לבצע אופטימיזציה של תעבורת IPC באמצעות אצווה או סטרימינג, שימוש בזיכרון משותף או פעולות דומות. אי אפשר לבצע אף אחת מהפעולות האלה אם ממשק ה-AIDL שלכם הוא גם ה-API הציבורי.
לדוגמה, אל תחשפו את FooService
כ-API ציבורי ישירות:
// BAD: Public API generated from IFooService.aidl
public class IFooService {
public void doFoo(String foo);
}
במקום זאת, עוטפים את הממשק Binder
בתוך מחלקה של מנהל או מחלקה אחרת:
/**
* @hide
*/
public class IFooService {
public void doFoo(String foo);
}
public IFooManager {
public void doFoo(String foo) {
mFooService.doFoo(foo);
}
}
אם בהמשך יידרג ארגומנט חדש לקריאה הזו, הממשק הפנימי יכול להיות מינימלי, ואפשר להוסיף עומסים נוחים לממשק ה-API הציבורי. אתם יכולים להשתמש בשכבת העטיפה כדי לטפל בבעיות אחרות שקשורות לתאימות לאחור, ככל שההטמעה מתפתחת:
/**
* @hide
*/
public class IFooService {
public void doFoo(String foo, int flags);
}
public IFooManager {
public void doFoo(String foo) {
if (mAppTargetSdkLevel < 26) {
useOldFooLogic(); // Apps targeting API before 26 are broken otherwise
mFooService.doFoo(foo, FLAG_THAT_ONE_WEIRD_HACK);
} else {
mFooService.doFoo(foo, 0);
}
}
public void doFoo(String foo, int flags) {
mFooService.doFoo(foo, flags);
}
}
במקרה של Binder
ממשקים שלא מהווים חלק מפלטפורמת Android (לדוגמה, ממשק שירות שמיוצא על ידי שירותי Google Play לשימוש באפליקציות), הדרישה לממשק IPC יציב, שפורסם וכולל גרסאות, מקשה מאוד על פיתוח הממשק עצמו. עם זאת, עדיין כדאי להשתמש בשכבת wrapper מסביב, כדי להתאים להנחיות אחרות של API וכדי להקל על השימוש באותו API ציבורי לגרסה חדשה של ממשק ה-IPC, אם אי פעם יהיה בכך צורך.
אל תשתמשו באובייקטים גולמיים של Binder ב-API ציבורי
לאובייקט Binder
אין משמעות בפני עצמו, ולכן אין להשתמש בו ב-API ציבורי. תרחיש נפוץ לשימוש הוא שימוש ב-Binder
או ב-IBinder
כאסימון, כי יש להם סמנטיקה של זהות. במקום להשתמש באובייקט Binder
גולמי, צריך להשתמש במחלקת אסימון עוטפת.
public final class IdentifiableObject {
public Binder getToken() {...}
}
public final class IdentifiableObjectToken {
/**
* @hide
*/
public Binder getRawValue() {...}
/**
* @hide
*/
public static IdentifiableObjectToken wrapToken(Binder rawValue) {...}
}
public final class IdentifiableObject {
public IdentifiableObjectToken getToken() {...}
}
כיתות ניהול חייבות להיות סופיות
צריך להצהיר על מחלקות ניהול כ-final
. מחלקות הניהול מתקשרות עם שירותי המערכת ומהוות את נקודת האינטראקציה היחידה. אין צורך בהתאמה אישית, ולכן צריך להצהיר על כך באמצעות הערך final
.
אל תשתמשו ב-CompletableFuture או ב-Future
ל-java.util.concurrent.CompletableFuture
יש משטח API גדול שמאפשר שינוי שרירותי של ערך עתידי, ויש בו ערכי ברירת מחדל שנוטים לשגיאות.
לעומת זאת, ב-java.util.concurrent.Future
חסרה האזנה לא חוסמת, ולכן קשה להשתמש בה עם קוד אסינכרוני.
בקוד פלטפורמה ובממשקי API של ספריות ברמה נמוכה שמשמשים גם את Kotlin וגם את Java, מומלץ להשתמש בשילוב של קריאה חוזרת להשלמה, Executor
, ואם ה-API תומך בביטול CancellationSignal
.
public void asyncLoadFoo(android.os.CancellationSignal cancellationSignal,
Executor callbackExecutor,
android.os.OutcomeReceiver<FooResult, Throwable> callback);
אם מטרגטים Kotlin, מומלץ להשתמש בפונקציות suspend
.
suspend fun asyncLoadFoo(): Foo
בספריות שילוב ספציפיות ל-Java, אפשר להשתמש ב-ListenableFuture
של Guava.
public com.google.common.util.concurrent.ListenableFuture<Foo> asyncLoadFoo();
לא להשתמש ב-Optional
למרות של-Optional
יכולים להיות יתרונות בפלטפורמות מסוימות של API, הוא לא עקבי עם פלטפורמת ה-API הקיימת של Android. @Nullable
ו-@NonNull
מספקים עזרה בכלי פיתוח לבטיחות של null
, ו-Kotlin אוכפת חוזי אפסות ברמת הקומפיילר, ולכן אין צורך ב-Optional
.
לפרימיטיבים אופציונליים, משתמשים בשיטות has
ו-get
. אם הערך לא מוגדר (has
מחזירה false
), השיטה get
צריכה להפעיל IllegalStateException
.
public boolean hasAzimuth() { ... }
public int getAzimuth() {
if (!hasAzimuth()) {
throw new IllegalStateException("azimuth is not set");
}
return azimuth;
}
שימוש בבנאים פרטיים למחלקות שלא ניתן ליצור מהן מופעים
במקרים הבאים, צריך לכלול לפחות בנאי פרטי אחד כדי למנוע יצירת מופע באמצעות בנאי ברירת המחדל ללא ארגומנטים: מחלקות שאפשר ליצור רק באמצעות Builder
, מחלקות שמכילות רק קבועים או שיטות סטטיות, או מחלקות שלא ניתן ליצור מהן מופע.
public final class Log {
// Not instantiable.
private Log() {}
}
Singletons
לא מומלץ להשתמש ב-Singleton כי יש להם חסרונות שקשורים לבדיקות:
- הבנייה מנוהלת על ידי הכיתה, כדי למנוע שימוש בזיופים
- אי אפשר לבצע בדיקות הרמטיות בגלל האופי הסטטי של סינגלטון
- כדי לעקוף את הבעיות האלה, מפתחים צריכים לדעת את הפרטים הפנימיים של הסינגלטון או ליצור wrapper סביבו.
מומלץ להשתמש בדפוס מופע יחיד, שמסתמך על מחלקת בסיס מופשטת כדי לפתור את הבעיות האלה.
מכונה יחידה
מחלקות עם מופע יחיד משתמשות במחלקת בסיס מופשטת עם בנאי private
או internal
, ומספקות שיטת getInstance()
סטטית כדי לקבל מופע. השיטה getInstance()
חייבת להחזיר את אותו אובייקט בקריאות הבאות.
האובייקט שמוחזר על ידי getInstance()
צריך להיות הטמעה פרטית של
מחלקה בסיסית מופשטת.
class Singleton private constructor(...) {
companion object {
private val _instance: Singleton by lazy { Singleton(...) }
fun getInstance(): Singleton {
return _instance
}
}
}
abstract class SingleInstance private constructor(...) {
companion object {
private val _instance: SingleInstance by lazy { SingleInstanceImp(...) }
fun getInstance(): SingleInstance {
return _instance
}
}
}
המונח 'מופע יחיד' שונה מסינגלטון בכך שהמפתחים יכולים ליצור גרסה מזויפת של SingleInstance
ולהשתמש במסגרת הזרקת התלות שלהם כדי לנהל את ההטמעה בלי ליצור עטיפה, או שהספרייה יכולה לספק זיוף משלה בארטיפקט -testing
.
מחלקה שמשחררת משאבים צריכה להטמיע את AutoCloseable
במקרים שבהם משחררים משאבים באמצעות close
, release
, destroy
או שיטות דומות, צריך להטמיע את java.lang.AutoCloseable
כדי לאפשר למפתחים לנקות את המשאבים האלה באופן אוטומטי כשמשתמשים בבלוק try-with-resources
.
מומלץ להימנע מהצגה של מחלקות משנה חדשות של View ב-android.*
אל תוסיפו מחלקות חדשות שיורשות ישירות או בעקיפין מ-android.view.View
ב-API הציבורי של הפלטפורמה (כלומר, ב-android.*
).
ערכת הכלים לבניית ממשק המשתמש ב-Android היא עכשיו Compose-first. תכונות חדשות בממשק המשתמש שנחשפות על ידי הפלטפורמה צריכות להיחשף כממשקי API ברמה נמוכה יותר, שאפשר להשתמש בהם כדי להטמיע את Jetpack Compose ורכיבי ממשק משתמש מבוססי-View (אופציונלי) למפתחים בספריות Jetpack. הצעת הרכיבים האלה בספריות מאפשרת ליישם אותם בגרסאות קודמות של פלטפורמות שבהן התכונות האלה לא זמינות.
שדות
הכללים האלה מתייחסים לשדות ציבוריים בכיתות.
לא לחשוף שדות גולמיים
מחלקות Java לא צריכות לחשוף שדות באופן ישיר. השדות צריכים להיות פרטיים, וניתן לגשת אליהם רק באמצעות שיטות getter ו-setter ציבוריות, בלי קשר לשאלה אם השדות האלה סופיים או לא.
יוצאים מן הכלל הם מבני נתונים בסיסיים שבהם אין צורך לשפר את ההתנהגות של ציון שדה או אחזור שדה. במקרים כאלה, צריך לתת לשדות שמות לפי מוסכמות סטנדרטיות למתן שמות למשתנים, למשל Point.x
ו-Point.y
.
מחלקות Kotlin יכולות לחשוף מאפיינים.
צריך לסמן שדות גלויים כסופיים
אנחנו ממליצים מאוד לא להשתמש בשדות גולמיים (ראו Don't expose raw fields). אבל במקרים נדירים שבהם שדה נחשף כשדה ציבורי, צריך לסמן את השדה הזה final
.
לא כדאי לחשוף שדות פנימיים
אל תפנו לשמות של שדות פנימיים ב-API ציבורי.
public int mFlags;
שימוש ב-public במקום ב-protected
@see Use public instead of protected
Constants
אלה כללים לגבי קבועים ציבוריים.
קבועי דגלים לא יכולים לחפוף לערכי int או long
דגלים מרמזים על ביטים שאפשר לשלב אותם לערך איחוד מסוים. אם זה לא המצב, אל תקראו למשתנה או לקבוע flag
.
public static final int FLAG_SOMETHING = 2;
public static final int FLAG_SOMETHING = 3;
public static final int FLAG_PRIVATE = 1 << 2;
public static final int FLAG_PRESENTATION = 1 << 3;
מידע נוסף על הגדרת קבועים של דגלים ציבוריים זמין במאמר בנושא @IntDef
דגלים של מסיכת ביטים.
קבועים סטטיים סופיים צריכים להיות מוגדרים לפי מוסכמת מתן שמות שבה כל האותיות גדולות ומופרדות באמצעות קו תחתון
כל המילים בקבוע צריכות להיות באותיות גדולות, וכשמדובר בכמה מילים צריך להפריד ביניהן באמצעות _
. לדוגמה:
public static final int fooThing = 5
public static final int FOO_THING = 5
שימוש בקידומות רגילות לקבועים
רבים מהקבועים שמשמשים ב-Android הם לדברים סטנדרטיים, כמו דגלים, מפתחות ופעולות. לקבועים האלה צריכות להיות תחיליות סטנדרטיות כדי שיהיה קל יותר לזהות אותם.
לדוגמה, תוספים של Intent צריכים להתחיל ב-EXTRA_
. פעולות של כוונות צריכות להתחיל ב-ACTION_
. קבועים שמשמשים עם Context.bindService()
צריכים להתחיל ב-BIND_
.
שמות וטווחים של קבועים מרכזיים
ערכי מחרוזת קבועים צריכים להיות עקביים עם שם הקבוע עצמו, ובדרך כלל הם צריכים להיות מוגבלים לחבילה או לדומיין. לדוגמה:
public static final String FOO_THING = "foo"
לא נקרא באופן עקבי ולא מוגדר בהיקף המתאים. במקום זאת, כדאי:
public static final String FOO_THING = "android.fooservice.FOO_THING"
הקידומות של android
בקבועי מחרוזות בתחום מסוים שמורות לפרויקט הקוד הפתוח של Android.
צריך להשתמש במרחב שמות עבור פעולות ונתונים נוספים של Intent, וגם עבור רשומות של Bundle, באמצעות שם החבילה שבה הם מוגדרים.
package android.foo.bar {
public static final String ACTION_BAZ = "android.foo.bar.action.BAZ"
public static final String EXTRA_BAZ = "android.foo.bar.extra.BAZ"
}
שימוש ב-public במקום ב-protected
@see Use public instead of protected
שימוש בקידומות עקביות
כל הקבועים שקשורים זה לזה צריכים להתחיל באותה תחילית. לדוגמה, כדי להשתמש בקבוצה של קבועים עם ערכי דגלים:
public static final int SOME_VALUE = 0x01;
public static final int SOME_OTHER_VALUE = 0x10;
public static final int SOME_THIRD_VALUE = 0x100;
public static final int FLAG_SOME_VALUE = 0x01;
public static final int FLAG_SOME_OTHER_VALUE = 0x10;
public static final int FLAG_SOME_THIRD_VALUE = 0x100;
@see Use standard prefixes for constants
שימוש בשמות משאבים עקביים
שמות של מזהים, מאפיינים וערכים ציבוריים חייבים להיות בהתאם למוסכמת השמות camelCase, למשל @id/accessibilityActionPageUp
או @attr/textAppearance
, בדומה לשדות ציבוריים ב-Java.
במקרים מסוימים, מזהה ציבורי או מאפיין כוללים קידומת משותפת שמופרדת באמצעות קו תחתון:
- ערכי הגדרות של הפלטפורמה, כמו
@string/config_recentsComponentName
ב-config.xml - מאפייני תצוגה ספציפיים לפריסה, כמו
@attr/layout_marginStart
בקובץ attrs.xml
כשמשתמשים בסגנונות ובתבניות ציבוריים, חייבים להקפיד על מוסכמת מתן השמות PascalCase ההיררכית, למשל @style/Theme.Material.Light.DarkActionBar
או @style/Widget.Material.SearchView.ActionBar
, בדומה למחלקות מקוננות ב-Java.
אסור לחשוף פריסות ומשאבים של drawable כ-API ציבורי. אם בכל זאת צריך לחשוף אותם, חובה לתת שמות לפריסות ולמשאבים הגרפיים הציבוריים באמצעות מוסכמת השמות under_score, למשל layout/simple_list_item_1.xml
או drawable/title_bar_tall.xml
.
כשקבועים יכולים להשתנות, כדאי להפוך אותם לדינמיים
יכול להיות שהקומפיילר יבצע החלפה של ערכים קבועים, ולכן שמירה על ערכים זהים נחשבת לחלק מחוזה ה-API. אם הערך של קבוע MIN_FOO
או MAX_FOO
עשוי להשתנות בעתיד, כדאי להשתמש במקום זאת בשיטות דינמיות.
CameraManager.MAX_CAMERAS
CameraManager.getMaxCameras()
כדאי להביא בחשבון תאימות קדימה לקריאות חוזרות
אפליקציות שמטרגטות ממשקי API ישנים יותר לא מכירות קבועים שמוגדרים בגרסאות עתידיות של API. לכן, כשמעבירים קבועים לאפליקציות, צריך לקחת בחשבון את גרסת ה-API לטירגוט של האפליקציה ולמפות קבועים חדשים לערך עקבי. לדוגמה, נבחן את התרחיש הבא:
מקור היפותטי של SDK:
// Added in API level 22
public static final int STATUS_SUCCESS = 1;
public static final int STATUS_FAILURE = 2;
// Added in API level 23
public static final int STATUS_FAILURE_RETRY = 3;
// Added in API level 26
public static final int STATUS_FAILURE_ABORT = 4;
אפליקציה היפותטית עם targetSdkVersion="22"
:
if (result == STATUS_FAILURE) {
// Oh no!
} else {
// Success!
}
במקרה הזה, האפליקציה תוכננה בהתאם למגבלות של רמת API 22, והניחה (במידה מסוימת) הנחה סבירה שיש רק שני מצבים אפשריים. אבל אם האפליקציה מקבלת את STATUS_FAILURE_RETRY
שנוסף, היא מפרשת את זה כהצלחה.
שיטות שמחזירות קבועים יכולות לטפל במקרים כאלה בצורה בטוחה על ידי הגבלת הפלט שלהן כך שיתאים לרמת ה-API שהאפליקציה מטרגטת:
private int mapResultForTargetSdk(Context context, int result) {
int targetSdkVersion = context.getApplicationInfo().targetSdkVersion;
if (targetSdkVersion < 26) {
if (result == STATUS_FAILURE_ABORT) {
return STATUS_FAILURE;
}
if (targetSdkVersion < 23) {
if (result == STATUS_FAILURE_RETRY) {
return STATUS_FAILURE;
}
}
}
return result;
}
מפתחים לא יכולים לצפות אם רשימה של קבועים עשויה להשתנות בעתיד. אם מגדירים API עם קבוע UNKNOWN
או UNSPECIFIED
שנראה כמו catch-all, המפתחים מניחים שהקבועים שפורסמו כשהם כתבו את האפליקציה שלהם הם ממצים. אם אתם לא רוצים להגדיר את הציפייה הזו, כדאי לשקול מחדש אם קבוע כללי הוא רעיון טוב ל-API שלכם.
בנוסף, ספריות לא יכולות לציין targetSdkVersion
משלהן בנפרד מהאפליקציה, וטיפול בשינויים בהתנהגות targetSdkVersion
מקוד הספרייה הוא מסובך ועלול לגרום לשגיאות.
קבוע מסוג מספר שלם או מחרוזת
משתמשים בקבועים של מספרים שלמים וב-@IntDef
אם מרחב השמות של הערכים לא ניתן להרחבה מחוץ לחבילה. משתמשים בקבועי מחרוזת אם מרחב השמות משותף או שאפשר להרחיב אותו באמצעות קוד מחוץ לחבילה.
סיווגי נתונים
מחלקות נתונים מייצגות קבוצה של מאפיינים שלא ניתן לשנות, ומספקות קבוצה קטנה ומוגדרת היטב של פונקציות עזר לאינטראקציה עם הנתונים האלה.
אין להשתמש ב-data class
בממשקי API ציבוריים של Kotlin, כי קומפיילר Kotlin לא מבטיח תאימות של שפת API או תאימות בינארית לקוד שנוצר. במקום זאת, צריך להטמיע את הפונקציות הנדרשות באופן ידני.
יצירת אובייקט
ב-Java, מחלקות נתונים צריכות לספק בנאי כשיש מעט מאפיינים, או להשתמש בתבנית Builder
כשיש הרבה מאפיינים.
ב-Kotlin, מחלקות נתונים צריכות לספק בנאי עם ארגומנטים שמוגדרים כברירת מחדל, ללא קשר למספר המאפיינים. יכול להיות שגם מחלקות נתונים שמוגדרות ב-Kotlin ירוויחו מהוספת Builder כשמטרגטים לקוחות Java.
שינוי והעתקה
במקרים שבהם צריך לשנות את הנתונים, צריך לספק מחלקה מסוג Builder
עם בנאי עותק (Java) או פונקציית חבר מסוג copy()
(Kotlin) שמחזירה אובייקט חדש.
כשמספקים פונקציה copy()
ב-Kotlin, הארגומנטים צריכים להתאים לקונסטרוקטור של המחלקה, וערכי ברירת המחדל צריכים להיות מאוכלסים באמצעות הערכים הנוכחיים של האובייקט:
class Typography(
val labelMedium: TextStyle = TypographyTokens.LabelMedium,
val labelSmall: TextStyle = TypographyTokens.LabelSmall
) {
fun copy(
labelMedium: TextStyle = this.labelMedium,
labelSmall: TextStyle = this.labelSmall
): Typography = Typography(
labelMedium = labelMedium,
labelSmall = labelSmall
)
}
התנהגויות נוספות
במחלקות הנתונים צריך להטמיע את שתי השיטות equals()
ו-hashCode()
, וכל מאפיין צריך להיות מפורט בהטמעות של השיטות האלה.
אפשר להטמיע מחלקות נתונים toString()
בפורמט מומלץ שתואם להטמעה של מחלקת נתונים ב-Kotlin, לדוגמה User(var1=Alex, var2=42)
.
שיטות
אלה כללים לגבי פרטים שונים בשיטות, לגבי פרמטרים, שמות שיטות, סוגי החזרה ומגדירי גישה.
שעה
הכללים האלה מתייחסים לאופן שבו צריך להשתמש במושגים שקשורים לזמן, כמו תאריכים ומשך זמן, בממשקי API.
עדיפות לשימוש בסוגים java.time.*, אם אפשר
java.time.Duration
, java.time.Instant
ועוד הרבה סוגים של java.time.*
זמינים בכל גרסאות הפלטפורמה באמצעות desugaring, ועדיף להשתמש בהם כשמציינים זמן בפרמטרים של API או בערכים מוחזרים.
מומלץ לחשוף רק וריאציות של API שמקבלות או מחזירות java.time.Duration
או java.time.Instant
, ולהשמיט וריאציות פרימיטיביות עם אותו תרחיש שימוש, אלא אם דומיין ה-API הוא כזה שהקצאת אובייקטים בדפוסי שימוש מיועדים תשפיע באופן משמעותי על הביצועים.
שם השיטה שמשמשת לציון משך הזמן צריך להיות duration
אם ערך של זמן מבטא את משך הזמן שחלף, צריך לקרוא לפרמטר 'duration' ולא 'time'.
ValueAnimator.setTime(java.time.Duration);
ValueAnimator.setDuration(java.time.Duration);
חריגים:
המונח timeout מתאים כשמשך הזמן מתייחס ספציפית לערך של timeout.
הערך 'time' עם הסוג java.time.Instant
מתאים כשמתייחסים לנקודה ספציפית בזמן, ולא למשך זמן.
שמות של שיטות שמבטאות משכי זמן או זמן כ-primitive צריכים לכלול את יחידת הזמן שלהן, ולהשתמש ב-long
בשיטות שמקבלות או מחזירות משך זמן כפרימיטיב, צריך להוסיף לשם השיטה את יחידות הזמן הרלוונטיות (כמו Millis
, Nanos
, Seconds
) כדי לשמור את השם ללא קישוט לשימוש עם java.time.Duration
. מידע נוסף זמין במאמר בנושא שעה.
בנוסף, צריך להוסיף הערות מתאימות לשיטות עם יחידת הבסיס וזמן הבסיס שלהן:
-
@CurrentTimeMillisLong
: הערך הוא חותמת זמן לא שלילית שנמדדת כמספר אלפיות השנייה מאז 1970-01-01T00:00:00Z. -
@CurrentTimeSecondsLong
: הערך הוא חותמת זמן לא שלילית שנמדדת כמספר השניות מאז 1970-01-01T00:00:00Z. -
@DurationMillisLong
: הערך הוא משך זמן לא שלילי באלפיות השנייה. -
@ElapsedRealtimeLong
: הערך הוא חותמת זמן לא שלילית בבסיס הזמןSystemClock.elapsedRealtime()
. -
@UptimeMillisLong
: הערך הוא חותמת זמן לא שלילית בבסיס הזמןSystemClock.uptimeMillis()
.
בפרמטרים של זמן פרימיטיבי או בערכי החזרה צריך להשתמש ב-long
, ולא ב-int
.
ValueAnimator.setDuration(@DurationMillisLong long);
ValueAnimator.setDurationNanos(long);
בשיטות שמבטאות יחידות זמן, עדיף להשתמש בקיצור לא מקוצר לשמות היחידות
public void setIntervalNs(long intervalNs);
public void setTimeoutUs(long timeoutUs);
public void setIntervalNanos(long intervalNanos);
public void setTimeoutMicros(long timeoutMicros);
הוספת הערות לארגומנטים ארוכים של זמן
הפלטפורמה כוללת כמה הערות כדי לספק הקלדה חזקה יותר ליחידות זמן מסוג long
:
-
@CurrentTimeMillisLong
: הערך הוא חותמת זמן לא שלילית שנמדדת כמספר אלפיות השנייה מאז1970-01-01T00:00:00Z
, ולכן בבסיס הזמןSystem.currentTimeMillis()
. -
@CurrentTimeSecondsLong
: הערך הוא חותמת זמן לא שלילית שנמדדת כמספר השניות מאז1970-01-01T00:00:00Z
. -
@DurationMillisLong
: הערך הוא משך זמן לא שלילי באלפיות השנייה. -
@ElapsedRealtimeLong
: הערך הוא חותמת זמן לא שלילית בבסיס הזמןSystemClock#elapsedRealtime()
. -
@UptimeMillisLong
: הערך הוא חותמת זמן לא שלילית בבסיס הזמןSystemClock#uptimeMillis()
.
יחידות מידה
בכל השיטות שבהן מציינים יחידת מידה שונה מזמן, מומלץ להשתמש בקידומות של יחידות SI בפורמט CamelCase.
public long[] getFrequenciesKhz();
public float getStreamVolumeDb();
מיקום פרמטרים אופציונליים בסוף העומסים העודפים
אם יש לכם עומסי יתר של שיטה עם פרמטרים אופציונליים, כדאי להשאיר את הפרמטרים האלה בסוף ולשמור על סדר עקבי עם שאר הפרמטרים:
public int doFoo(boolean flag);
public int doFoo(int id, boolean flag);
public int doFoo(boolean flag);
public int doFoo(boolean flag, int id);
כשמוסיפים עומסים יתרים לארגומנטים אופציונליים, ההתנהגות של השיטות הפשוטות יותר צריכה להיות זהה בדיוק להתנהגות שהייתה מתקבלת אם ארגומנטים שמוגדרים כברירת מחדל היו מסופקים לשיטות המורכבות יותר.
מסקנה: אל תעמיסו על שיטות אלא אם אתם מוסיפים ארגומנטים אופציונליים או מקבלים סוגים שונים של ארגומנטים אם השיטה היא פולימורפית. אם השיטה העמוסה מדי עושה משהו שונה באופן מהותי, צריך לתת לה שם חדש.
שיטות עם פרמטרים שמוגדרים כברירת מחדל צריכות להיות מסומנות ב-@JvmOverloads (רק ב-Kotlin)
כדי לשמור על תאימות בינארית, צריך להוסיף הערה עם @JvmOverloads
לשיטות ולקונסטרוקטורים עם פרמטרים שמוגדרים כברירת מחדל.
פרטים נוספים זמינים במאמר Function overloads for defaults במדריך הרשמי בנושא פעולות הדדיות בין Kotlin ל-Java.
class Greeting @JvmOverloads constructor(
loudness: Int = 5
) {
@JvmOverloads
fun sayHello(prefix: String = "Dr.", name: String) = // ...
}
לא להסיר ערכי פרמטרים שמוגדרים כברירת מחדל (Kotlin בלבד)
אם שיטה נשלחה עם פרמטר עם ערך ברירת מחדל, הסרת ערך ברירת המחדל היא שינוי שגורם לשבירת התאימות לאחור.
הפרמטרים של שיטת הזיהוי הכי ייחודית צריכים להיות ראשונים
אם יש לכם שיטה עם כמה פרמטרים, כדאי להוסיף קודם את הפרמטרים הכי רלוונטיים. פרמטרים שמציינים דגלים ואפשרויות אחרות חשובים פחות מפרמטרים שמתארים את האובייקט שעליו מתבצעת הפעולה. אם יש קריאה חוזרת (callback) להשלמה, צריך להוסיף אותה בסוף.
public void openFile(int flags, String name);
public void openFileAsync(OnFileOpenedListener listener, String name, int flags);
public void setFlags(int mask, int flags);
public void openFile(String name, int flags);
public void openFileAsync(String name, int flags, OnFileOpenedListener listener);
public void setFlags(int flags, int mask);
ראו גם: הצבת פרמטרים אופציונליים בסוף בעומסים עודפים
Builders
מומלץ להשתמש בתבנית Builder כדי ליצור אובייקטים מורכבים ב-Java, והיא נפוצה ב-Android במקרים הבאים:
- המאפיינים של האובייקט שמתקבל צריכים להיות קבועים
- יש מספר גדול של מאפיינים נדרשים, למשל הרבה ארגומנטים של בנאי
- יש קשר מורכב בין נכסים בזמן הבנייה, למשל נדרש שלב אימות. שימו לב שרמת מורכבות כזו לרוב מעידה על בעיות בשימושיות של ה-API.
כדאי לשקול אם אתם צריכים כלי לבניית אתרים. ה-builders שימושיים בממשק API אם הם משמשים ל:
- הגדרת רק חלק קטן מתוך קבוצה גדולה של פרמטרים אופציונליים ליצירה
- להגדיר הרבה פרמטרים שונים של יצירה, חלקם אופציונליים וחלקם נדרשים, לפעמים מסוגים דומים או זהים, במקרים שבהם קשה לקרוא את האתרים של הקמפיינים להתקשרות או שיש סיכון לשגיאות בכתיבה שלהם
- הגדרת יצירה של אובייקט באופן מצטבר, שבו כמה קטעים שונים של קוד הגדרה עשויים לבצע קריאות ל-builder כפרטי הטמעה
- אפשר להגדיל את הסוג על ידי הוספת פרמטרים אופציונליים נוספים ליצירה בגרסאות API עתידיות
אם יש לכם סוג עם שלושה פרמטרים נדרשים או פחות, ואין פרמטרים אופציונליים, כמעט תמיד תוכלו לדלג על builder ולהשתמש ב-constructor פשוט.
במקרים שבהם יש מחלקות שמקורן ב-Kotlin, מומלץ להשתמש בבנאים עם הערה @JvmOverloads
וארגומנטים שמוגדרים כברירת מחדל, ולא ב-Builders. עם זאת, כדי לשפר את נוחות השימוש עבור לקוחות Java, אפשר גם לספק Builders במקרים שצוינו קודם.
class Tone @JvmOverloads constructor(
val duration: Long = 1000,
val frequency: Int = 2600,
val dtmfConfigs: List<DtmfConfig> = emptyList()
) {
class Builder {
// ...
}
}
מחלקות Builder חייבות להחזיר את ה-Builder
בכל שיטה של מחלקת Builder, חוץ מ-build()
, צריך להחזיר את אובייקט ה-Builder (למשל this
) כדי לאפשר שרשור של שיטות. צריך להעביר אובייקטים מובנים נוספים כארגומנטים – לא להחזיר בונה של אובייקט אחר.
לדוגמה:
public static class Builder {
public void setDuration(long);
public void setFrequency(int);
public DtmfConfigBuilder addDtmfConfig();
public Tone build();
}
public class Tone {
public static class Builder {
public Builder setDuration(long);
public Builder setFrequency(int);
public Builder addDtmfConfig(DtmfConfig);
public Tone build();
}
}
במקרים נדירים שבהם מחלקה בסיסית של בונה צריכה לתמוך בהרחבה, צריך להשתמש בסוג החזרה כללי:
public abstract class Builder<T extends Builder<T>> {
abstract T setValue(int);
}
public class TypeBuilder<T extends TypeBuilder<T>> extends Builder<T> {
T setValue(int);
T setTypeSpecificValue(long);
}
צריך ליצור מחלקות Builder באמצעות בנאי
כדי לשמור על עקביות ביצירת אובייקטים מסוג Builder דרך פלטפורמת Android API, חובה ליצור את כל האובייקטים מסוג Builder דרך בנאי ולא דרך שיטת יצירה סטטית. בממשקי API מבוססי Kotlin, ה-Builder
חייב להיות ציבורי גם אם משתמשי Kotlin אמורים להסתמך באופן מרומז על ה-builder באמצעות מנגנון יצירה בסגנון DSL או method של factory. אסור לספריות להשתמש ב-@PublishedApi internal
כדי להסתיר באופן סלקטיבי את בנאי המחלקה Builder
מלקוחות Kotlin.
public class Tone {
public static Builder builder();
public static class Builder {
}
}
public class Tone {
public static class Builder {
public Builder();
}
}
כל הארגומנטים של בנאי ה-Builder חייבים להיות נדרשים (למשל @NonNull)
אופציונלי, לדוגמה @Nullable
, ארגומנטים צריכים לעבור לשיטות setter.
אם לא מציינים את כל הארגומנטים הנדרשים, בנאי ה-builder צריך להחזיר שגיאה מסוג NullPointerException
(מומלץ להשתמש ב-Objects.requireNonNull
).
מחלקות ה-Builder צריכות להיות מחלקות פנימיות סטטיות סופיות של הסוגים שהן בונות
כדי לשמור על ארגון לוגי בחבילה, בדרך כלל כדאי לחשוף את מחלקות ה-Builder כמחלקות פנימיות סופיות של הסוגים שהן יוצרות, למשל Tone.Builder
ולא ToneBuilder
.
יכול להיות שיוצרים יכללו ב-Builder קונסטרקטור כדי ליצור מופע חדש ממופע קיים
Builders may include a copy constructor to create a new builder instance from an existing builder or built object. אסור להם לספק שיטות חלופיות ליצירת מופעים של Builder מ-Builders קיימים או מאובייקטים של Build.
public class Tone {
public static class Builder {
public Builder clone();
}
public Builder toBuilder();
}
public class Tone {
public static class Builder {
public Builder(Builder original);
public Builder(Tone original);
}
}
אם ל-Builder יש בנאי עותק, שיטות setter של Builder צריכות לקבל ארגומנטים עם הערה לציון אפשרות לערך null (@Nullable)
איפוס הוא חיוני אם יכול להיות שייווצר מופע חדש של כלי בנייה ממופע קיים. אם אין בנאי עותק, יכול להיות שהבנאי יכלול ארגומנטים של @Nullable
או @NonNullable
.
public static class Builder {
public Builder(Builder original);
public Builder setObjectValue(@Nullable Object value);
}
יכול להיות ששיטות setter של Builder יקבלו ארגומנטים מסוג @Nullable עבור מאפיינים אופציונליים
לרוב, פשוט יותר להשתמש בערך שניתן לאיפוס עבור קלט מדרגה שנייה, במיוחד ב-Kotlin, שמשתמשת בארגומנטים שמוגדרים כברירת מחדל במקום ב-builders וב-overloads.
בנוסף, @Nullable
פונקציות setter יותאמו לפונקציות getter שלהן, שחייבות להיות @Nullable
למאפיינים אופציונליים.
Value createValue(@Nullable OptionalValue optionalValue) {
Value.Builder builder = new Value.Builder();
if (optionalValue != null) {
builder.setOptionalValue(optionalValue);
}
return builder.build();
}
Value createValue(@Nullable OptionalValue optionalValue) {
return new Value.Builder()
.setOptionalValue(optionalValue);
.build();
}
// Or in other cases:
Value createValue() {
return new Value.Builder()
.setOptionalValue(condition ? new OptionalValue() : null);
.build();
}
שימוש נפוץ ב-Kotlin:
fun createValue(optionalValue: OptionalValue? = null) =
Value.Builder()
.apply { optionalValue?.let { setOptionalValue(it) } }
.build()
fun createValue(optionalValue: OptionalValue? = null) =
Value.Builder()
.setOptionalValue(optionalValue)
.build()
ערך ברירת המחדל (אם לא קוראים לשיטת ההגדרה), והמשמעות של null
, צריכים להיות מתועדים בצורה נכונה גם בשיטת ההגדרה וגם בשיטת האחזור.
/**
* ...
*
* <p>Defaults to {@code null}, which means the optional value won't be used.
*/
אפשר לספק שיטות setter של Builder למאפיינים שניתנים לשינוי, אם שיטות setter זמינות במחלקה שנוצרה
אם למחלקה שלכם יש מאפיינים שניתנים לשינוי והיא צריכה מחלקה Builder
, כדאי לשאול את עצמכם קודם אם למחלקה שלכם באמת צריכים להיות מאפיינים שניתנים לשינוי.
לאחר מכן, אם אתם בטוחים שאתם צריכים מאפיינים שניתנים לשינוי, צריך להחליט איזה מהתרחישים הבאים מתאים יותר לתרחיש השימוש הצפוי שלכם:
האובייקט שנוצר צריך להיות שמיש באופן מיידי, ולכן צריך לספק פונקציות setter לכל המאפיינים הרלוונטיים, בין אם הם ניתנים לשינוי ובין אם לא.
map.put(key, new Value.Builder(requiredValue) .setImmutableProperty(immutableValue) .setUsefulMutableProperty(usefulValue) .build());
יכול להיות שיהיה צורך לבצע עוד כמה קריאות לפני שאפשר יהיה להשתמש באובייקט שנוצר, ולכן לא מומלץ לספק פונקציות setter למאפיינים שניתנים לשינוי.
Value v = new Value.Builder(requiredValue) .setImmutableProperty(immutableValue) .build(); v.setUsefulMutableProperty(usefulValue) Result r = v.performSomeAction(); Key k = callSomeMethod(r); map.put(k, v);
אל תערבבו בין שני התרחישים.
Value v = new Value.Builder(requiredValue)
.setImmutableProperty(immutableValue)
.setUsefulMutableProperty(usefulValue)
.build();
Result r = v.performSomeAction();
Key k = callSomeMethod(r);
map.put(k, v);
ל-Builders לא יכולים להיות getters
הפונקציה Getter צריכה להיות באובייקט שנבנה, ולא ב-Builder.
לשיטות setter ב-Builder חייבות להיות שיטות getter תואמות במחלקה שנבנתה
public class Tone {
public static class Builder {
public Builder setDuration(long);
public Builder setFrequency(int);
public Builder addDtmfConfig(DtmfConfig);
public Tone build();
}
}
public class Tone {
public static class Builder {
public Builder setDuration(long);
public Builder setFrequency(int);
public Builder addDtmfConfig(DtmfConfig);
public Tone build();
}
public long getDuration();
public int getFrequency();
public @NonNull List<DtmfConfig> getDtmfConfigs();
}
שמות של שיטות ב-Builder
שמות של שיטות ליצירת אובייקטים צריכים להיות בסגנון setFoo()
, addFoo()
או clearFoo()
.
מחלקות Builder צפויות להצהיר על שיטת build()
במחלקות Builder צריך להצהיר על שיטה build()
שמחזירה מופע של האובייקט שנבנה.
שיטות build() של Builder חייבות להחזיר אובייקטים מסוג @NonNull
השיטה build()
של Builder אמורה להחזיר מופע לא ריק של האובייקט שנבנה. אם אי אפשר ליצור את האובייקט בגלל פרמטרים לא תקינים, אפשר לדחות את האימות לשיטת הבנייה, וצריך להפעיל את IllegalStateException
.
לא לחשוף נעילות פנימיות
ב-methods ב-API הציבורי אסור להשתמש במילת המפתח synchronized
. מילת המפתח הזו גורמת לשימוש באובייקט או במחלקה שלכם כנעילה, ומכיוון שהיא חשופה לאחרים, יכול להיות שתיתקלו בתופעות לוואי לא צפויות אם קוד אחר מחוץ למחלקה שלכם יתחיל להשתמש בה למטרות נעילה.
במקום זאת, מבצעים את הנעילה הנדרשת באובייקט פנימי פרטי.
public synchronized void doThing() { ... }
private final Object mThingLock = new Object();
public void doThing() {
synchronized (mThingLock) {
...
}
}
שיטות בסגנון Accessor צריכות לפעול לפי ההנחיות לגבי מאפייני Kotlin
כשמציגים שיטות בסגנון accessor ממקורות Kotlin – שיטות שמשתמשות בקידומות get
, set
או is
– הן יהיו זמינות גם כמאפייני Kotlin.
לדוגמה, int getField()
שמוגדר ב-Java זמין ב-Kotlin כמאפיין val field: Int
.
לכן, כדי לעמוד בציפיות של מפתחים לגבי התנהגות של שיטות גישה, שיטות שמשתמשות בקידומות של שיטות גישה צריכות להתנהג באופן דומה לשדות Java. לא כדאי להשתמש בקידומות בסגנון רכיבי גישה במקרים הבאים:
- לשיטה יש תופעות לוואי – מומלץ להשתמש בשם שיטה יותר תיאורי
- השיטה כוללת עבודה שדורשת הרבה משאבי מחשוב – עדיף להשתמש ב-
compute
- השיטה כוללת חסימה או עבודה ממושכת אחרת כדי להחזיר ערך, כמו IPC או קלט/פלט אחר – עדיף להשתמש ב-
fetch
- השיטה חוסמת את השרשור עד שהיא יכולה להחזיר ערך – עדיף להשתמש ב-
await
- השיטה מחזירה מופע חדש של אובייקט בכל קריאה – עדיף להשתמש ב-
create
- יכול להיות שהשיטה לא תחזיר ערך בהצלחה – עדיף להשתמש ב-
request
שימו לב: ביצוע עבודה שדורשת הרבה משאבי מחשוב פעם אחת ושמירת הערך במטמון לקריאות הבאות עדיין נחשב כביצוע עבודה שדורשת הרבה משאבי מחשוב. הבעיה Jank לא מפוזרת על פני פריימים.
השתמשו בקידומת is לשיטות אחזור נתונים בוליאניות
זוהי מוסכמת מתן השמות הסטנדרטית לשיטות ולשדות בוליאניים ב-Java. באופן כללי, שמות של משתנים ושיטות בוליאניות צריכים להיות כתובים כשאלות שהערך המוחזר עונה עליהן.
שיטות גישה בוליאניות ב-Java צריכות לפעול לפי סכימת השמות set
/is
, ועדיף להשתמש בשדות is
, כמו בדוגמה הבאה:
// Visibility is a direct property. The object "is" visible:
void setVisible(boolean visible);
boolean isVisible();
// Factory reset protection is an indirect property.
void setFactoryResetProtectionEnabled(boolean enabled);
boolean isFactoryResetProtectionEnabled();
final boolean isAvailable;
שימוש ב-set
/is
לשיטות גישה של Java או ב-is
לשדות של Java יאפשר להשתמש בהם כמאפיינים מ-Kotlin:
obj.isVisible = true
obj.isFactoryResetProtectionEnabled = false
if (!obj.isAvailable) return
בדרך כלל, כדאי להשתמש בשמות חיוביים למאפיינים ולשיטות גישה, למשל Enabled
ולא Disabled
. שימוש בטרמינולוגיה שלילית הופך את המשמעות של true
ושל false
, ומקשה על הסקת מסקנות לגבי ההתנהגות.
// Passing false here is a double-negative.
void setFactoryResetProtectionDisabled(boolean disabled);
במקרים שבהם הערך הבוליאני מתאר הכללה או בעלות של נכס, אפשר להשתמש ב-has במקום ב-is. עם זאת, זה לא יעבוד עם תחביר של מאפייני Kotlin:
// Transient state is an indirect property used to track state
// related to the object. The object is not transient; rather,
// the object "has" transient state associated with it:
void setHasTransientState(boolean hasTransientState);
boolean hasTransientState();
יש קידומות חלופיות שעשויות להתאים יותר, כמו can ו-should:
// "Can" describes a behavior that the object may provide,
// and here is more concise than setRecordingEnabled or
// setRecordingAllowed. The object "can" record:
void setCanRecord(boolean canRecord);
boolean canRecord();
// "Should" describes a hint or property that is not strictly
// enforced, and here is more explicit than setFitWidthEnabled.
// The object "should" fit width:
void setShouldFitWidth(boolean shouldFitWidth);
boolean shouldFitWidth();
ב-methods שמפעילות או משביתות התנהגויות או תכונות, יכול להיות שיופיע הקידומת is והסיומת Enabled:
// "Enabled" describes the availability of a property, and is
// more appropriate here than "can use" or "should use" the
// property:
void setWiFiRoamingSettingEnabled(boolean enabled)
boolean isWiFiRoamingSettingEnabled()
באופן דומה, יכול להיות ששיטות שמציינות תלות בהתנהגויות או בתכונות אחרות ישתמשו בקידומת is ובסיומת Supported או Required:
// "Supported" describes whether this API would work on devices that support
// multiple users. The API "supports" multi-user:
void setMultiUserSupported(boolean supported)
boolean isMultiUserSupported()
// "Required" describes whether this API depends on devices that support
// multiple users. The API "requires" multi-user:
void setMultiUserRequired(boolean required)
boolean isMultiUserRequired()
באופן כללי, שמות של שיטות צריכים להיות שאלות שהתשובה עליהן היא ערך ההחזרה.
methods של מאפיינים ב-Kotlin
עבור מאפיין של מחלקה var foo: Foo
, Kotlin תיצור מתודות get
/set
באמצעות כלל עקבי: מוסיפים את הקידומת get
והופכים את האות הראשונה לאות גדולה עבור ה-getter, ומוסיפים את הקידומת set
והופכים את האות הראשונה לאות גדולה עבור ה-setter. הצהרת המאפיין תיצור שיטות בשמות public Foo getFoo()
וpublic void setFoo(Foo foo)
, בהתאמה.
אם המאפיין הוא מסוג Boolean
, חל כלל נוסף לגבי יצירת השם: אם שם המאפיין מתחיל ב-is
, לא מתווסף get
לשם של שיטת ה-getter, אלא שם המאפיין עצמו משמש כ-getter.
לכן, מומלץ לתת למאפיינים Boolean
שמות עם הקידומת is
כדי לפעול בהתאם להנחיות לבחירת שמות:
var isVisible: Boolean
אם הנכס שלכם הוא אחד מהחריגים שצוינו למעלה ומתחיל בקידומת מתאימה, אתם יכולים להשתמש בהערה @get:JvmName
בנכס כדי לציין ידנית את השם המתאים:
@get:JvmName("hasTransientState")
var hasTransientState: Boolean
@get:JvmName("canRecord")
var canRecord: Boolean
@get:JvmName("shouldFitWidth")
var shouldFitWidth: Boolean
פונקציות גישה לביטמסק
במאמר שימוש ב-@IntDef
בדגלי bitmask מוסבר איך להגדיר דגלי bitmask ב-API.
Setters
צריך לספק שתי שיטות setter: אחת שמקבלת מחרוזת ביטים מלאה ומחליפה את כל הדגלים הקיימים, ואחת שמקבלת מסכת ביטים מותאמת אישית כדי לאפשר גמישות רבה יותר.
/**
* Sets the state of all scroll indicators.
* <p>
* See {@link #setScrollIndicators(int, int)} for usage information.
*
* @param indicators a bitmask of indicators that should be enabled, or
* {@code 0} to disable all indicators
* @see #setScrollIndicators(int, int)
* @see #getScrollIndicators()
*/
public void setScrollIndicators(@ScrollIndicators int indicators);
/**
* Sets the state of the scroll indicators specified by the mask. To change
* all scroll indicators at once, see {@link #setScrollIndicators(int)}.
* <p>
* When a scroll indicator is enabled, it will be displayed if the view
* can scroll in the direction of the indicator.
* <p>
* Multiple indicator types may be enabled or disabled by passing the
* logical OR of the specified types. If multiple types are specified, they
* will all be set to the same enabled state.
* <p>
* For example, to enable the top scroll indicator:
* {@code setScrollIndicators(SCROLL_INDICATOR_TOP, SCROLL_INDICATOR_TOP)}
* <p>
* To disable the top scroll indicator:
* {@code setScrollIndicators(0, SCROLL_INDICATOR_TOP)}
*
* @param indicators a bitmask of values to set; may be a single flag,
* the logical OR of multiple flags, or 0 to clear
* @param mask a bitmask indicating which indicator flags to modify
* @see #setScrollIndicators(int)
* @see #getScrollIndicators()
*/
public void setScrollIndicators(@ScrollIndicators int indicators, @ScrollIndicators int mask);
Getters
צריך לספק פונקציית getter אחת כדי לקבל את מסיכת הביטים המלאה.
/**
* Returns a bitmask representing the enabled scroll indicators.
* <p>
* For example, if the top and left scroll indicators are enabled and all
* other indicators are disabled, the return value will be
* {@code View.SCROLL_INDICATOR_TOP | View.SCROLL_INDICATOR_LEFT}.
* <p>
* To check whether the bottom scroll indicator is enabled, use the value
* of {@code (getScrollIndicators() & View.SCROLL_INDICATOR_BOTTOM) != 0}.
*
* @return a bitmask representing the enabled scroll indicators
*/
@ScrollIndicators
public int getScrollIndicators();
שימוש ב-public במקום ב-protected
תמיד מעדיפים את public
על פני protected
בממשק API ציבורי. בטווח הארוך, גישה מוגנת עלולה להיות בעייתית, כי המפתחים צריכים לבטל אותה כדי לספק רכיבי גישה ציבוריים במקרים שבהם גישה חיצונית כברירת מחדל הייתה מספיקה.
חשוב לזכור שprotected
הגדרת הרשאות הגישה לא מונעת ממפתחים לקרוא ל-API – היא רק הופכת את זה למעט יותר מסורבל.
הטמעה של equals() ו-hashCode() או אי-הטמעה של אף אחת מהן
אם משנים את אחת מההגדרות, צריך לשנות גם את השנייה.
הטמעה של toString() למחלקות נתונים
מומלץ להגדיר מחלקות נתונים כך שיבטלו את ההגדרה של toString()
, כדי לעזור למפתחים לנפות באגים בקוד שלהם.
תיעוד אם הפלט הוא להתנהגות התוכנית או לניפוי באגים
מחליטים אם רוצים שההתנהגות של התוכנית תסתמך על ההטמעה שלכם או לא. לדוגמה, הפורמט הספציפי של UUID.toString() ושל File.toString() מתועד כדי שתוכנות יוכלו להשתמש בו. אם אתם חושפים מידע רק לצורך ניפוי באגים, כמו Intent, אתם יכולים להשתמש ב-inherit docs מהסופר-קלאס.
לא לכלול מידע נוסף
כל המידע שזמין מ-toString()
צריך להיות זמין גם דרך ממשק ה-API הציבורי של האובייקט. אחרת, אתם מעודדים מפתחים לנתח את הפלט של toString()
ולהסתמך עליו, מה שימנע שינויים עתידיים. מומלץ להטמיע את toString()
באמצעות ה-API הציבורי של האובייקט בלבד.
המלצה לא להסתמך על פלט ניפוי הבאגים
אי אפשר למנוע ממפתחים להסתמך על פלט של ניפוי באגים, אבל אם כוללים את System.identityHashCode
של האובייקט בפלט toString()
, הסיכוי ששני אובייקטים שונים יפיקו פלט toString()
זהה יהיה נמוך מאוד.
@Override
public String toString() {
return getClass().getSimpleName() + "@" + Integer.toHexString(System.identityHashCode(this)) + " {mFoo=" + mFoo + "}";
}
הדבר יכול להרתיע מפתחים מלכתוב טענות בדיקה כמו assertThat(a.toString()).isEqualTo(b.toString())
באובייקטים שלכם.
שימוש ב-createFoo כשמחזירים אובייקטים שנוצרו לאחרונה
משתמשים בקידומת create
, ולא ב-get
או ב-new
, לשיטות שייצרו ערכי החזרה, למשל על ידי בנייה של אובייקטים חדשים.
אם השיטה תיצור אובייקט כדי להחזיר אותו, צריך לציין זאת בבירור בשם השיטה.
public FooThing getFooThing() {
return new FooThing();
}
public FooThing createFooThing() {
return new FooThing();
}
שיטות שמקבלות אובייקטים של קבצים צריכות לקבל גם זרמים
מיקומי אחסון הנתונים ב-Android לא תמיד הם קבצים בדיסק. לדוגמה,
תוכן שעובר בין משתמשים שונים מיוצג כ-content://
Uri
s. כדי לאפשר עיבוד של מקורות נתונים שונים, ממשקי API שמקבלים אובייקטים של File
צריכים לקבל גם InputStream
, OutputStream
או את שניהם.
public void setDataSource(File file)
public void setDataSource(InputStream stream)
החזרה של פרימיטיבים גולמיים במקום גרסאות בקופסה
אם אתם צריכים להעביר ערכים חסרים או ערכי null, כדאי להשתמש ב--1
, Integer.MAX_VALUE
או Integer.MIN_VALUE
.
public java.lang.Integer getLength()
public void setLength(java.lang.Integer)
public int getLength()
public void setLength(int value)
הימנעות ממחלקות שוות ערך של טיפוסים פרימיטיביים מאפשרת להימנע מתקורה של זיכרון של המחלקות האלה, מגישת שיטה לערכים, וחשוב מכך, מ-autoboxing שנובע מהמרת טיפוסים בין טיפוסים פרימיטיביים לטיפוסי אובייקט. הימנעות מהתנהגויות כאלה חוסכת זיכרון והקצאות זמניות שעלולות להוביל לאיסוף אשפה יקר ותכוף יותר.
שימוש בהערות כדי להבהיר ערכים תקינים של פרמטרים וערכי החזרה
הוספנו הערות למפתחים כדי להבהיר את הערכים המותרים במצבים שונים. כך קל יותר לכלים לעזור למפתחים כשהם מספקים ערכים שגויים (לדוגמה, העברת int
שרירותי כשנדרש אחד מתוך קבוצה ספציפית של ערכים קבועים). אפשר להשתמש בכל ההערות הבאות כשזה מתאים:
מאפיין המציין אם ערך יכול להיות ריק (nullability)
ממשקי API של Java מחייבים שימוש בהערות מפורשות לגבי האפשרות להקצאת ערך null, אבל הרעיון של האפשרות להקצאת ערך null הוא חלק משפת Kotlin, ולכן אסור להשתמש בהערות לגבי האפשרות להקצאת ערך null בממשקי API של Kotlin.
@Nullable
: מציין שערך ההחזרה, הפרמטר או השדה יכולים להיות null:
@Nullable
public String getName()
public void setName(@Nullable String name)
@NonNull
: מציין שערך החזרה, פרמטר או שדה נתון לא יכולים להיות null. הסימון של פריטים כ-@Nullable
הוא יחסית חדש ב-Android, ולכן רוב שיטות ה-API של Android לא מתועדות באופן עקבי. לכן יש לנו שלושה מצבים: 'לא ידוע', @Nullable
, @NonNull
. זו הסיבה לכך ש-@NonNull
הוא חלק מההנחיות ל-API:
@NonNull
public String getName()
public void setName(@NonNull String name)
במסמכי הפלטפורמה של Android, הוספת הערות לפרמטרים של השיטה תיצור באופן אוטומטי תיעוד בפורמט 'הערך הזה עשוי להיות null', אלא אם נעשה שימוש מפורש ב-null במקום אחר במסמך הפרמטר.
שיטות קיימות שאי אפשר להחזיר לגביהן ערך null: יכול להיות ששיטות קיימות ב-API ללא הערה מוצהרת של @Nullable
יקבלו הערה של @Nullable
אם השיטה יכולה להחזיר null
בנסיבות ספציפיות וברורות (כמו findViewById()
). צריך להוסיף שיטות נלוות של @NotNull requireFoo()
שיוצרות IllegalArgumentException
למפתחים שלא רוצים לבדוק אם הערך הוא null.
שיטות ממשק: כשמטמיעים שיטות ממשק בממשקי API חדשים, צריך להוסיף את ההערה המתאימה, כמו Parcelable.writeToParcel()
(כלומר, השיטה הזו במחלקת ההטמעה צריכה להיות writeToParcel(@NonNull Parcel,
int)
, ולא writeToParcel(Parcel, int)
). עם זאת, אין צורך לתקן ממשקי API קיימים שחסרות בהם ההערות.
אכיפת האפשרות להשתמש בערך null
ב-Java, מומלץ להשתמש בשיטות כדי לבצע אימות של קלט לפרמטרים של @NonNull
באמצעות Objects.requireNonNull()
, ולהפעיל את NullPointerException
כשהפרמטרים הם null. הפעולה הזו מתבצעת באופן אוטומטי ב-Kotlin.
משאבים
מזהי משאבים: פרמטרים מסוג integer שמציינים מזהים של משאבים ספציפיים צריכים להיות מתויגים בהגדרה המתאימה של סוג המשאב.
יש הערה לכל סוג של משאב, כמו @StringRes
, @ColorRes
ו-@AnimRes
, בנוסף להערה הכללית @AnyRes
. לדוגמה:
public void setTitle(@StringRes int resId)
@IntDef לקבוצות קבועות
Magic constants: פרמטרים String
ו-int
שמיועדים לקבל אחד מתוך קבוצה סופית של ערכים אפשריים שמסומנים על ידי קבועים ציבוריים, צריכים להיות מסומנים בהערה מתאימה עם @StringDef
או @IntDef
. ההערות האלה מאפשרות ליצור הערה חדשה שאפשר להשתמש בה כמו typedef לפרמטרים מותרים. לדוגמה:
/** @hide */
@IntDef(prefix = {"NAVIGATION_MODE_"}, value = {
NAVIGATION_MODE_STANDARD,
NAVIGATION_MODE_LIST,
NAVIGATION_MODE_TABS
})
@Retention(RetentionPolicy.SOURCE)
public @interface NavigationMode {}
public static final int NAVIGATION_MODE_STANDARD = 0;
public static final int NAVIGATION_MODE_LIST = 1;
public static final int NAVIGATION_MODE_TABS = 2;
@NavigationMode
public int getNavigationMode();
public void setNavigationMode(@NavigationMode int mode);
מומלץ להשתמש בשיטות כדי לבדוק את התוקף של הפרמטרים עם ההערות ולהפעיל את IllegalArgumentException
אם הפרמטר לא שייך ל-@IntDef
@IntDef לסימונים של מסכת ביטים
אפשר גם לציין בהערה שהקבועים הם דגלים, ולשלב אותם עם & ו-I:
/** @hide */
@IntDef(flag = true, prefix = { "FLAG_" }, value = {
FLAG_USE_LOGO,
FLAG_SHOW_HOME,
FLAG_HOME_AS_UP,
})
@Retention(RetentionPolicy.SOURCE)
public @interface DisplayOptions {}
@StringDef לקבוצות של קבועי מחרוזות
יש גם את ההערה @StringDef
, שהיא בדיוק כמו @IntDef
בקטע הקודם, אבל היא מיועדת לקבועים String
. אפשר לכלול כמה ערכים של prefix, שמשמשים ליצירת תיעוד אוטומטי לכל הערכים.
@SdkConstant לקבועים של SDK
@SdkConstant מוסיפים הערה לשדות ציבוריים כשהם אחד מהערכים הבאים: SdkConstant
, ACTIVITY_INTENT_ACTION
, BROADCAST_INTENT_ACTION
, SERVICE_ACTION
, INTENT_CATEGORY
, FEATURE
.
@SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
public static final String ACTION_CALL = "android.intent.action.CALL";
הוספת תאימות לערכי null לביטולים
כדי לשמור על תאימות ל-API, הערך של מאפייני השינוי צריך להיות תואם לערך הנוכחי של מאפייני האב. בטבלה הבאה מפורטות ציפיות התאימות. במילים פשוטות, הגדרות ברירת המחדל צריכות להיות מגבילות או מגבילות יותר מהרכיב שהן מבטלות.
סוג | הורה | ילד או ילדה |
---|---|---|
סוג הערך שמוחזר | לא כולל הערות | לא מוערך או לא null |
סוג הערך שמוחזר | Nullable | ניתן להגדרה כ-Null או לא ניתן להגדרה כ-Null |
סוג הערך שמוחזר | NonNull | NonNull |
טיעון משעשע | לא כולל הערות | לא מוערך או ניתן לערך null |
טיעון משעשע | Nullable | Nullable |
טיעון משעשע | NonNull | ניתן להגדרה כ-Null או לא ניתן להגדרה כ-Null |
אם אפשר, כדאי להשתמש בארגומנטים שלא יכולים להיות null (כמו @NonNull)
כשמבצעים עומס יתר על שיטות, עדיף שכל הארגומנטים לא יהיו null.
public void startActivity(@NonNull Component component) { ... }
public void startActivity(@NonNull Component component, @NonNull Bundle options) { ... }
הכלל הזה חל גם על שיטות setter של מאפיינים עם עומס יתר. הארגומנט הראשי לא יכול להיות null, וצריך להטמיע את ניקוי הנכס כשיטה נפרדת. כך נמנעות קריאות "לא הגיוניות" שבהן המפתח צריך להגדיר פרמטרים מסוימים גם אם הם לא נדרשים.
public void setTitleItem(@Nullable IconCompat icon, @ImageMode mode)
public void setTitleItem(@Nullable IconCompat icon, @ImageMode mode, boolean isLoading)
// Nonsense call to clear property
setTitleItem(null, MODE_RAW, false);
public void setTitleItem(@NonNull IconCompat icon, @ImageMode mode)
public void setTitleItem(@NonNull IconCompat icon, @ImageMode mode, boolean isLoading)
public void clearTitleItem()
מומלץ להשתמש בסוגי החזרה שאינם ניתנים לביטול (כמו @NonNull) עבור מאגרי תגים
בסוגי מאגרים כמו Bundle
או Collection
, מחזירים מאגר ריק – ובלתי ניתן לשינוי, אם רלוונטי. במקרים שבהם נעשה שימוש ב-null
כדי להבחין בין זמינות של מאגר תגים, כדאי לספק שיטה בוליאנית נפרדת.
@NonNull
public Bundle getExtras() { ... }
הערות בנוגע לאפשרות של ערך null עבור זוגות של get ו-set חייבות להיות זהות
זוגות של שיטות get ו-set עבור מאפיין לוגי יחיד צריכים תמיד להיות זהים בהערות בנוגע לאפשרות של ערך null. אם לא פועלים לפי ההנחיות האלה, תחביר המאפיינים של Kotlin לא יעבוד, ולכן הוספת הערות שונות לגבי האפשרות להגדיר ערך null לשיטות מאפיינים קיימות היא שינוי שגורם לבעיות בקוד המקור של משתמשי Kotlin.
@NonNull
public Bundle getExtras() { ... }
public void setExtras(@NonNull Bundle bundle) { ... }
ערך ההחזרה במקרה של כשל או שגיאה
כל ממשקי ה-API צריכים לאפשר לאפליקציות להגיב לשגיאות. החזרת הערכים false
, -1
, null
או ערכים אחרים של 'משהו השתבש' לא מספקת למפתח מידע מספיק על הכשל כדי להגדיר ציפיות של משתמשים או לעקוב בצורה מדויקת אחרי המהימנות של האפליקציה בשטח. כשמתכננים API, כדאי לדמיין שאתם בונים אפליקציה. אם נתקלים בשגיאה, האם ה-API מספק מספיק מידע כדי להציג אותה למשתמש או להגיב בצורה מתאימה?
- אפשר (ואפילו מומלץ) לכלול מידע מפורט בהודעת חריגה, אבל מפתחים לא צריכים לנתח אותה כדי לטפל בשגיאה בצורה מתאימה. קודי שגיאה מפורטים או מידע אחר צריכים להיות גלויים כשיטות.
- חשוב לוודא שאפשרות הטיפול בשגיאות שבחרתם מאפשרת לכם להוסיף סוגי שגיאות חדשים בעתיד. במקרה של
@IntDef
, המשמעות היא שצריך לכלול ערך שלOTHER
אוUNKNOWN
. כשמחזירים קוד חדש, אפשר לבדוק אתtargetSdkVersion
של המתקשר כדי להימנע מהחזרת קוד שגיאה שהאפליקציה לא מכירה. במקרה של חריגים, כדאי להשתמש במחלקת-על משותפת שהחריגים מיישמים, כדי שכל קוד שמטפל בסוג הזה יתפוס גם סוגי משנה ויטפל בהם. - מפתח לא אמור להתעלם משגיאה בטעות – אם השגיאה מועברת על ידי החזרת ערך, צריך להוסיף לשיטה את ההערה
@CheckResult
.
מומלץ להשתמש ב-? extends RuntimeException
כשמגיעים למצב של כשל או שגיאה בגלל משהו שהמפתח עשה לא נכון, למשל התעלמות ממגבלות על פרמטרים של קלט או אי-בדיקה של מצב שניתן לצפייה.
שיטות של הגדרת ערך או פעולה (לדוגמה, perform
) עשויות להחזיר קוד סטטוס של מספר שלם <0x0A>אם הפעולה עלולה להיכשל כתוצאה ממצב או מתנאים שמתעדכנים באופן אסינכרוני ושלא נמצאים בשליטת המפתח.
צריך להגדיר את קודי הסטטוס בכיתה שמכילה אותם כשדות public static final
עם הקידומת ERROR_
, ולמנות אותם בהערה @hide
@IntDef
.
שמות השיטות צריכים תמיד להתחיל בפועל, ולא בנושא
השם של ה-method צריך תמיד להתחיל בפועל (למשל get
, create
, reload
וכו'), ולא באובייקט שעליו מבצעים את הפעולה.
public void tableReload() {
mTable.reload();
}
public void reloadTable() {
mTable.reload();
}
העדפה של סוגי אוספים על פני מערכים כסוג החזרה או הפרמטר
ממשקי אוסף עם הקלדה גנרית מספקים כמה יתרונות על פני מערכים, כולל חוזי API חזקים יותר לגבי ייחודיות וסדר, תמיכה בגנריות ומספר שיטות נוחות וידידותיות למפתחים.
חריגות לגבי פרימיטיבים
אם הרכיבים הם פרימיטיבים, עדיף להשתמש במערכים כדי להימנע מהעלות של המרה אוטומטית של ערכים פרימיטיביים לאובייקטים. איך להשתמש בפרימיטיבים גולמיים במקום בגרסאות ארוזות
חריג לקוד שרגיש לביצועים
בתרחישים מסוימים שבהם נעשה שימוש ב-API בקוד שרגיש לביצועים (כמו גרפיקה או ממשקי API אחרים של מדידה, פריסה או ציור), אפשר להשתמש במערכים במקום באוספים כדי לצמצם את ההקצאות ואת תנודתיות הזיכרון.
חריג ל-Kotlin
מערכים ב-Kotlin הם אינווריאנטים, ושפת Kotlin מספקת מספיק ממשקי API של כלי עזר שקשורים למערכים, כך שמערכים שווים ל-List
ול-Collection
בממשקי API של Kotlin שמיועדים לגישה מ-Kotlin.
העדפה של אוספים עם הערך @NonNull
תמיד עדיף להשתמש ב-@NonNull
לאובייקטים של אוספים. כשמחזירים אוסף ריק, צריך להשתמש בשיטת Collections.empty
המתאימה כדי להחזיר אובייקט אוסף זול, עם הקלדה נכונה ובלתי ניתן לשינוי.
במקרים שבהם יש תמיכה בהערות לגבי סוגים, תמיד עדיף להשתמש ב-@NonNull
עבור רכיבי קולקציות.
מומלץ להשתמש גם ב-@NonNull
כשמשתמשים במערכים במקום באוספים (ראו פריט קודם). אם הקצאת אובייקטים היא בעיה, אפשר ליצור קבוע ולהעביר אותו – אחרי הכול, מערך ריק הוא בלתי ניתן לשינוי. דוגמה:
private static final int[] EMPTY_USER_IDS = new int[0];
@NonNull
public int[] getUserIds() {
int [] userIds = mService.getUserIds();
return userIds != null ? userIds : EMPTY_USER_IDS;
}
יכולת השינוי של האוסף
בממשקי API של Kotlin, מומלץ להשתמש כברירת מחדל בסוגי החזרה לקריאה בלבד (לא Mutable
) עבור אוספים, אלא אם חוזה ה-API מחייב במפורש סוג החזרה שניתן לשינוי.
עם זאת, בממשקי API של Java, עדיף להשתמש כברירת מחדל בסוגי החזרה ניתנים לשינוי, כי הטמעת פלטפורמת Android של ממשקי API של Java עדיין לא מספקת הטמעה נוחה של אוספים שלא ניתן לשנות. היוצא מן הכלל במקרה הזה הוא Collections.empty
סוגי החזרה, שהם בלתי ניתנים לשינוי. במקרים שבהם לקוחות יכולים לנצל את האפשרות לשינוי – בכוונה או בטעות – כדי לשבש את דפוס השימוש המיועד ב-API, מומלץ מאוד ש-Java APIs יחזירו עותק שטחי של האוסף.
@Nullable
public PermissionInfo[] getGrantedPermissions() {
return mPermissions;
}
@NonNull
public Set<PermissionInfo> getGrantedPermissions() {
if (mPermissions == null) {
return Collections.emptySet();
}
return new ArraySet<>(mPermissions);
}
סוגי החזרה שניתנים לשינוי באופן מפורש
מומלץ ש-APIs שמחזירים אוספים לא ישנו את אובייקט האוסף שמוחזר אחרי ההחזרה. אם צריך לשנות את האוסף שמוחזר או לעשות בו שימוש חוזר בדרך כלשהי – לדוגמה, תצוגה מותאמת של מערך נתונים שניתן לשינוי – צריך לתעד במפורט את ההתנהגות המדויקת של מתי התוכן יכול להשתנות, או לפעול בהתאם למוסכמות השמות המקובלות של ה-API.
/**
* Returns a view of this object as a list of [Item]s.
*/
fun MyObject.asList(): List<Item> = MyObjectListWrapper(this)
המוסכמה של Kotlin .asFoo()
מתוארת בהמשך ומאפשרת לשנות את האוסף שמוחזר על ידי .asList()
אם האוסף המקורי משתנה.
האפשרות לשנות אובייקטים של סוגי נתונים שמוחזרים
בדומה לממשקי API שמחזירים אוספים, ממשקי API שמחזירים אובייקטים מסוג נתונים לא אמורים לשנות את המאפיינים של האובייקט שמוחזר אחרי ההחזרה.
val tempResult = DataContainer()
fun add(other: DataContainer): DataContainer {
tempResult.innerValue = innerValue + other.innerValue
return tempResult
}
fun add(other: DataContainer): DataContainer {
return DataContainer(innerValue + other.innerValue)
}
במקרים מאוד מוגבלים, קוד שרגיש לביצועים עשוי להפיק תועלת מאיגום או משימוש חוזר באובייקטים. אל תיצרו מבנה נתונים משלכם של מאגר אובייקטים, ואל תחשפו אובייקטים שנעשה בהם שימוש חוזר בממשקי API ציבוריים. בכל מקרה, חשוב מאוד לנהל את הגישה בו-זמנית בזהירות.
שימוש בסוג פרמטר vararg
מומלץ להשתמש ב-vararg
בממשקי Kotlin ו-Java API במקרים שבהם סביר שהמפתח ייצור מערך באתר הקריאה למטרה היחידה של העברת כמה פרמטרים קשורים מאותו סוג.
public void setFeatures(Feature[] features) { ... }
// Developer code
setFeatures(new Feature[]{Features.A, Features.B, Features.C});
public void setFeatures(Feature... features) { ... }
// Developer code
setFeatures(Features.A, Features.B, Features.C);
עותקים להגנה
ההטמעות של פרמטרים של vararg
ב-Java וב-Kotlin עוברות קומפילציה לאותו קוד בייט שמגובה על ידי מערך, ולכן אפשר להפעיל אותן מקוד Java עם מערך שניתן לשינוי. מומלץ מאוד למעצבי API ליצור עותק שטחי של פרמטר המערך במקרים שבהם הוא יישמר בשדה או במחלקה פנימית אנונימית.
public void setValues(SomeObject... values) {
this.values = Arrays.copyOf(values, values.length);
}
חשוב לדעת: יצירת עותק הגנתי לא מספקת הגנה מפני שינוי בו-זמני בין הקריאה הראשונית של השיטה לבין יצירת העותק, וגם לא מפני מוטציה של האובייקטים שנכללים במערך.
צריך לספק סמנטיקה נכונה באמצעות פרמטרים של סוג האוסף או סוגים שמוחזרים
List<Foo>
היא אפשרות ברירת המחדל, אבל כדאי לשקול סוגים אחרים כדי לספק משמעות נוספת:
משתמשים ב-
Set<Foo>
אם אין חשיבות לסדר האלמנטים ב-API, והוא לא מאפשר כפילויות או שהכפילויות לא משמעותיות.
Collection<Foo>,
אם אין חשיבות לסדר בממשק ה-API והוא מאפשר כפילויות.
פונקציות המרה של Kotlin
ב-Kotlin נעשה שימוש תדיר ב-.toFoo()
וב-.asFoo()
כדי לקבל אובייקט מסוג אחר מאובייקט קיים, כאשר Foo
הוא שם סוג ההחזרה של ההמרה. ההגדרה הזו תואמת ל-JDK המוכר Object.toString()
. ב-Kotlin, השימוש ב-toString() מתרחב גם להמרות פרימיטיביות כמו 25.toFloat()
.
ההבדל בין ההמרות שנקראות .toFoo()
לבין ההמרות שנקראות .asFoo()
הוא משמעותי:
שימוש ב- .toFoo() כשיוצרים אובייקט חדש ועצמאי
בדומה ל-.toString()
, המרה באמצעות 'to' מחזירה אובייקט חדש ועצמאי. אם האובייקט המקורי ישונה בהמשך, השינויים האלה לא יבואו לידי ביטוי באובייקט החדש.
באופן דומה, אם האובייקט new ישתנה בהמשך, השינויים האלה לא יבואו לידי ביטוי באובייקט old.
fun Foo.toBundle(): Bundle = Bundle().apply {
putInt(FOO_VALUE_KEY, value)
}
שימוש ב- .asFoo() כשיוצרים wrapper תלוי, אובייקט מעוצב או cast
העברה (casting) ב-Kotlin מתבצעת באמצעות מילת המפתח as
. היא משקפת שינוי בממשק אבל לא שינוי בזהות. כשמשתמשים ב-.asFoo()
כקידומת בפונקציית הרחבה, היא מעטרת את המקבל. שינוי באובייקט המקורי של הנמען ישתקף באובייקט שמוחזר על ידי asFoo()
.
שינוי באובייקט Foo
החדש עשוי לבוא לידי ביטוי באובייקט המקורי.
fun <T> Flow<T>.asLiveData(): LiveData<T> = liveData {
collect {
emit(it)
}
}
פונקציות המרה צריכות להיכתב כפונקציות הרחבה
כתיבת פונקציות המרה מחוץ להגדרות של מחלקת המקבל ומחלקת התוצאה מצמצמת את הצימוד בין הסוגים. המרות אידיאליות דורשות רק גישה ל-API ציבורי לאובייקט המקורי. הדוגמה הזו מוכיחה שמפתח יכול לכתוב המרות דומות לסוגים המועדפים עליו.
הקפצת הודעת שגיאה (throw) לחריגים ספציפיים מתאימים
אסור שהשיטות יחזירו חריגים כלליים כמו java.lang.Exception
או java.lang.Throwable
. במקום זאת, צריך להשתמש בחריג ספציפי מתאים כמו java.lang.NullPointerException
כדי לאפשר למפתחים לטפל בחריגים בלי להגדיר טווח רחב מדי.
שגיאות שלא קשורות לארגומנטים שסופקו ישירות לשיטה שהופעלה באופן ציבורי צריכות להחזיר java.lang.IllegalStateException
במקום java.lang.IllegalArgumentException
או java.lang.NullPointerException
.
מאזינים וקודים להתקשרות חזרה
אלה הכללים לגבי המחלקות והשיטות שמשמשות למנגנונים של מאזינים וקריאות חוזרות (callback).
שמות של מחלקות קריאה חוזרת צריכים להיות ביחיד
במקום זאת, צריך להשתמש ב-MyObjectCallback
.MyObjectCallbacks
שמות של שיטות קריאה חוזרת צריכים להיות בפורמט on
הערך onFooEvent
מציין שהאירוע FooEvent
מתרחש ושהקריאה החוזרת צריכה לפעול בתגובה.
השימוש בזמן עבר או בזמן הווה צריך לתאר את התנהגות התזמון
כשנותנים שמות לשיטות של קריאה חוזרת שקשורות לאירועים, צריך לציין אם האירוע כבר התרחש או שהוא בתהליך התרחשות.
לדוגמה, אם המתודה מופעלת אחרי ביצוע פעולת קליק:
public void onClicked()
עם זאת, אם השיטה אחראית לביצוע פעולת הקליק:
public boolean onClick()
רישום לשיחה חוזרת
כשניתן להוסיף או להסיר מאובייקט מאזין או קריאה חוזרת (callback), צריך לקרוא לשיטות המשויכות add ו-remove או register ו-unregister. צריך לשמור על עקביות עם המוסכמה הקיימת שבה נעשה שימוש בכיתה או בכיתות אחרות באותו חבילה. אם אין תקדים כזה, עדיף להשתמש בפעולות add ו-remove.
בשיטות שכוללות רישום או ביטול רישום של קריאות חוזרות (callback), צריך לציין את השם המלא של סוג הקריאה החוזרת.
public void addFooCallback(@NonNull FooCallback callback);
public void removeFooCallback(@NonNull FooCallback callback);
public void registerFooCallback(@NonNull FooCallback callback);
public void unregisterFooCallback(@NonNull FooCallback callback);
הימנעו משימוש בשיטות getter עבור קריאות חוזרות (callback)
לא מוסיפים שיטות getFooCallback()
. זוהי דרך מפתה לצאת ממצבים שבהם מפתחים רוצים לשרשר קריאה חוזרת קיימת עם קריאה חוזרת משלהם, אבל היא לא יציבה ומקשה על מפתחי רכיבים להבין את המצב הנוכחי. לדוגמה,
- מפתח א' מתקשר אל
setFooCallback(a)
- מפתח ב' מתקשר אל
setFooCallback(new B(getFooCallback()))
- מפתח א' רוצה להסיר את פונקציית ה-callback שלו
a
ואין לו דרך לעשות זאת בלי לדעת את הסוג שלB
, וB
לא נבנה כך שיאפשר שינויים כאלה בפונקציית ה-callback העטופה שלו.
קבלת Executor כדי לשלוט בשיגור של שיחות חוזרות
כשרושמים קריאות חוזרות שאין להן ציפיות מפורשות לגבי שרשור (כמעט בכל מקום מחוץ לערכת הכלים של ממשק המשתמש), מומלץ מאוד לכלול פרמטר Executor
כחלק מהרישום, כדי לאפשר למפתח לציין את השרשור שבו יופעלו הקריאות החוזרות.
public void registerFooCallback(
@NonNull @CallbackExecutor Executor executor,
@NonNull FooCallback callback)
בניגוד להנחיות הרגילות שלנו לגבי פרמטרים אופציונליים, אפשר לספק עומס יתר שבו לא מציינים את Executor
, גם אם הוא לא הארגומנט האחרון ברשימת הפרמטרים. אם לא מספקים את Executor
, צריך להפעיל את הקריאה החוזרת בשרשור הראשי באמצעות Looper.getMainLooper()
, ולתעד את זה בשיטה המשויכת שהועמסה יתר על המידה.
/**
* ...
* Note that the callback will be executed on the main thread using
* {@link Looper.getMainLooper()}. To specify the execution thread, use
* {@link registerFooCallback(Executor, FooCallback)}.
* ...
*/
public void registerFooCallback(
@NonNull FooCallback callback)
public void registerFooCallback(
@NonNull @CallbackExecutor Executor executor,
@NonNull FooCallback callback)
Executor
נקודות חשובות לגבי ההטמעה: שימו לב שהקוד הבא הוא executor תקין.
public class SynchronousExecutor implements Executor {
@Override
public void execute(Runnable r) {
r.run();
}
}
המשמעות היא שכשמטמיעים ממשקי API שמוגדרים בצורה הזו, ההטמעה של אובייקט ה-Binder הנכנס בצד של תהליך האפליקציה חייבת להפעיל את Binder.clearCallingIdentity()
לפני הפעלת הקריאה החוזרת של האפליקציה ב-Executor
שסופק על ידי האפליקציה. כך, כל קוד אפליקציה שמשתמש בזהות של Binder (כמו Binder.getCallingUid()
) לבדיקות הרשאות משייך בצורה נכונה את הקוד שפועל לאפליקציה ולא לתהליך המערכת שקורא לאפליקציה. אם משתמשי ה-API רוצים את פרטי ה-UID או ה-PID של המתקשר, הפרטים האלה צריכים להיות חלק מפורש מממשק ה-API ולא חלק מרומז על סמך המיקום שבו הקוד שסופק על ידי Executor
פעל.
ממשק ה-API שלכם צריך לתמוך בציון של Executor
. במקרים שבהם הביצועים קריטיים, יכול להיות שהאפליקציות יצטרכו להריץ קוד באופן מיידי או באופן סינכרוני עם משוב מממשק ה-API. אישור של Executor
מאפשר זאת.
יצירה של HandlerThread
נוסף או דומה ל-trampoline באופן הגנתי, כדי למנוע את התרחיש הזה, לא תאפשר את השימוש הרצוי.
אם אפליקציה עומדת להריץ קוד יקר איפשהו בתהליך שלה, אפשרו לה לעשות זאת. יהיה קשה יותר לתמוך בפתרונות העקיפים שמפתחי האפליקציות ימצאו כדי לעקוף את ההגבלות שלכם בטווח הארוך.
חריג לגבי קריאה חוזרת יחידה: אם אופי האירועים שמדווחים מצריך תמיכה רק במופע יחיד של קריאה חוזרת, צריך להשתמש בסגנון הבא:
public void setFooCallback(
@NonNull @CallbackExecutor Executor executor,
@NonNull FooCallback callback)
public void clearFooCallback()
שימוש ב-Executor במקום ב-Handler
בעבר, נעשה שימוש ב-Handler
של Android כסטנדרט להפניה אוטומטית של ביצוע קריאה חוזרת (callback) לשרשור Looper
ספציפי. התקן הזה שונה כדי להעדיף את Executor
, כי רוב מפתחי האפליקציות מנהלים את מאגרי השרשורים שלהם, ולכן השרשור הראשי או שרשור ממשק המשתמש הוא השרשור היחיד מסוג Executor
שזמין לאפליקציה. כדאי להשתמש ב-Executor
כדי לתת למפתחים את השליטה שהם צריכים כדי לעשות שימוש חוזר בהקשרים הקיימים או המועדפים שלהם.Looper
ספריות מודרניות של פעולות מקבילות, כמו kotlinx.coroutines או RxJava, מספקות מנגנוני תזמון משלהן שמבצעים את השליחה שלהן כשצריך. לכן חשוב לספק את האפשרות להשתמש ב-executor ישיר (כמו Runnable::run
) כדי למנוע חביון כתוצאה ממעברים כפולים בין השרשורים. לדוגמה, דילוג אחד כדי לפרסם בLooper
שרשור באמצעות Handler
ואז דילוג נוסף ממסגרת המקבילות של האפליקציה.
יש מעט מאוד מקרים חריגים להנחיה הזו. אלה חלק מהסיבות הנפוצות לשליחת ערעור על חריגה:
אני צריך להשתמש בLooper
כי אני צריך Looper
כדי epoll
לאירוע.
בקשת ההחרגה הזו אושרה כי אי אפשר לממש את היתרונות של Executor
במצב הזה.
אני לא רוצה שקוד האפליקציה יחסום את פרסום האירוע בשרשור. בקשת החריגה הזו בדרך כלל לא מאושרת לקוד שפועל בתהליך של אפליקציה. אפליקציות שמבצעות את הפעולה הזו בצורה שגויה פוגעות רק בעצמן, ולא משפיעות על מצב המערכת הכולל. אפליקציות שמבצעות את הפעולות האלה בצורה נכונה או משתמשות במסגרת נפוצה של פעולות מקבילות לא צריכות לשלם קנסות נוספים על זמן האחזור.
Handler
עקבי באופן מקומי עם ממשקי API דומים אחרים באותו סוג.
בקשת החריגה הזו מאושרת בהתאם לנסיבות. העדפה היא להוסיף עומסים יתרים מבוססי Executor
, להעביר הטמעות של Handler
לשימוש בהטמעה החדשה של Executor
. (myHandler::post
הוא Executor
תקין!) בהתאם לגודל המחלקה, למספר השיטות הקיימות של Handler
ולסבירות שהמפתחים יצטרכו להשתמש בשיטות קיימות שמבוססות על Handler
לצד השיטה החדשה, יכול להיות שתינתן חריגה כדי להוסיף שיטה חדשה שמבוססת על Handler
.
סימטריה ברישום
אם יש דרך להוסיף או לרשום משהו, צריכה להיות גם דרך להסיר או לבטל את הרישום שלו. השיטה
registerThing(Thing)
צריך להיות תואם
unregisterThing(Thing)
צריך לספק מזהה בקשה
אם סביר שמפתח ישתמש שוב בפונקציית קריאה חוזרת, צריך לספק אובייקט מזהה כדי לקשר את הקריאה החוזרת לבקשה.
class RequestParameters {
public int getId() { ... }
}
class RequestExecutor {
public void executeRequest(
RequestParameters parameters,
Consumer<RequestParameters> onRequestCompletedListener) { ... }
}
אובייקטים של קריאה חוזרת עם כמה שיטות
במקרה של קריאות חוזרות (callback) עם כמה שיטות, מומלץ להשתמש ב-interface
ובשיטות default
כשמוסיפים אותן לממשקי API שפורסמו בעבר. בעבר, ההמלצה בהנחיה הזו הייתה להשתמש ב-abstract class
בגלל היעדר שיטות default
ב-Java 7.
public interface MostlyOptionalCallback {
void onImportantAction();
default void onOptionalInformation() {
// Empty stub, this method is optional.
}
}
שימוש ב-android.os.OutcomeReceiver כשמבצעים מודלינג של קריאה לפונקציה לא חוסמת
OutcomeReceiver<R,E>
מחזירה ערך תוצאה R
אם הפעולה הצליחה, או E : Throwable
אחרת – כמו שקורה בקריאה רגילה לשיטה. משתמשים ב-OutcomeReceiver
כסוג של קריאה חוזרת כשממירים שיטה חוסמת שמחזירה תוצאה או זורקת חריגה לשיטה אסינכרונית לא חוסמת:
interface FooType {
// Before:
public FooResult requestFoo(FooRequest request);
// After:
public void requestFooAsync(FooRequest request, Executor executor,
OutcomeReceiver<FooResult, Throwable> callback);
}
שיטות אסינכרוניות שהומרו בדרך הזו תמיד מחזירות void
. כל תוצאה ש-requestFoo
הייתה מחזירה מדווחת במקום זאת לפרמטר callback
של requestFooAsync
OutcomeReceiver.onResult
על ידי קריאה ל-requestFoo
ב-executor
שסופק.
כל חריגה ש-requestFoo
הייתה יוצרת מדווחת במקום זאת ל-method OutcomeReceiver.onError
באותו אופן.
שימוש ב-OutcomeReceiver
לדיווח על תוצאות של שיטות אסינכרוניות מספק גם עטיפה של Kotlin
suspend fun
לשיטות אסינכרוניות באמצעות התוסף Continuation.asOutcomeReceiver
מ-androidx.core:core-ktx
:
suspend fun FooType.requestFoo(request: FooRequest): FooResult =
suspendCancellableCoroutine { continuation ->
requestFooAsync(request, Runnable::run, continuation.asOutcomeReceiver())
}
תוספים כאלה מאפשרים ללקוחות Kotlin לקרוא לשיטות אסינכרוניות לא חוסמות בנוחות של קריאה רגילה לפונקציה, בלי לחסום את השרשור שקורא לפונקציה. יכול להיות שתוספים כאלה של 1-1 לממשקי API של פלטפורמות יוצעו כחלק מandroidx.core:core-ktx
ארטיפקט ב-Jetpack, בשילוב עם בדיקות תאימות ושיקולים של גרסה רגילה. מידע נוסף, שיקולים לגבי ביטול ודוגמאות זמינים במאמר בנושא asOutcomeReceiver.
בשיטות אסינכרוניות שלא תואמות לסמנטיקה של שיטה שמחזירה תוצאה או מעלה חריגה כשהעבודה שלה מסתיימת, לא מומלץ להשתמש ב-OutcomeReceiver
כסוג של קריאה חוזרת. במקום זאת, כדאי לשקול אחת מהאפשרויות האחרות שמפורטות בקטע הבא.
עדיף להשתמש בממשקים פונקציונליים במקום ליצור סוגים חדשים של שיטות מופשטות יחידות (SAM)
ב-API ברמה 24 נוספו הסוגים java.util.function.*
(מסמכי עזר), שמציעים ממשקי SAM גנריים כמו Consumer<T>
שמתאימים לשימוש כפונקציות למדה של קריאה חוזרת. במקרים רבים, יצירת ממשקי SAM חדשים לא מספקת ערך רב מבחינת בטיחות סוגים או העברת כוונות, ובמקביל מרחיבה שלא לצורך את אזור ה-API של Android.
מומלץ להשתמש בממשקים הגנריים האלה במקום ליצור ממשקים חדשים:
Runnable
:() -> Unit
Supplier<R>
:() -> R
Consumer<T>
:(T) -> Unit
Function<T,R>
:(T) -> R
Predicate<T>
:(T) -> Boolean
- עוד הרבה אפשרויות זמינות במאמרי העזרה
מיקום הפרמטרים של SAM
כדי לאפשר שימוש אידיומטי מ-Kotlin, צריך להציב את פרמטר ה-SAM בסוף, גם אם השיטה מועמסת יתר על המידה עם פרמטרים נוספים.
public void schedule(Runnable runnable)
public void schedule(int delay, Runnable runnable)
Docs
אלה כללים לגבי מסמכים ציבוריים (Javadoc) של ממשקי API.
חובה לתעד את כל ממשקי ה-API הציבוריים
לכל ממשקי ה-API הציבוריים צריכה להיות תיעוד מספק שמסביר איך מפתחים יכולים להשתמש ב-API. נניח שהמפתח מצא את השיטה באמצעות השלמה אוטומטית או בזמן עיון במסמכי העיון של ה-API, ויש לו כמות מינימלית של הקשר מפני השטח הסמוך של ה-API (לדוגמה, אותה מחלקה).
שיטות
חובה לתעד את פרמטרי השיטה ואת הערכים המוחזרים באמצעות הערות התיעוד @param
ו-@return
, בהתאמה. מעצבים את גוף ה-Javadoc כאילו הוא מופיע אחרי "This method...".
במקרים שבהם שיטה לא מקבלת פרמטרים, אין לה שיקולים מיוחדים והיא מחזירה את מה ששם השיטה מציין, אפשר להשמיט את @return
ולכתוב מסמכים דומים ל:
/**
* Returns the priority of the thread.
*/
@IntRange(from = 1, to = 10)
public int getPriority() { ... }
שימוש תמיד בקישורים ב-Javadoc
מסמכי התיעוד צריכים לכלול קישורים למסמכים אחרים שבהם מפורטים קבועים, שיטות ואלמנטים אחרים שקשורים לנושא. צריך להשתמש בתגי Javadoc (לדוגמה, @see
ו-{@link foo}
), ולא רק במילים של טקסט פשוט.
בדוגמה הבאה של מקור:
public static final int FOO = 0;
public static final int BAR = 1;
אל תשתמשו בטקסט גולמי או בגופן קוד:
/**
* Sets value to one of FOO or <code>BAR</code>.
*
* @param value the value being set, one of FOO or BAR
*/
public void setValue(int value) { ... }
במקום זאת, אפשר להשתמש בקישורים:
/**
* Sets value to one of {@link #FOO} or {@link #BAR}.
*
* @param value the value being set
*/
public void setValue(@ValueType int value) { ... }
שימו לב שאם משתמשים בהערה IntDef
כמו @ValueType
בפרמטר, נוצר באופן אוטומטי תיעוד שמציין את הסוגים המותרים. מידע נוסף על IntDef
זמין בהנחיות בנושא הערות.
הפעלת update-api או docs target כשמוסיפים Javadoc
הכלל הזה חשוב במיוחד כשמוסיפים תגי @link
או @see
, וצריך לוודא שהפלט נראה כמו שציפיתם. פלט שגיאה ב-Javadoc נובע לרוב מקישורים לא תקינים. הבדיקה הזו מתבצעת על ידי יעד Make update-api
או docs
, אבל אם משנים רק את Javadoc ולא צריך להריץ את היעד update-api
מסיבה אחרת, יכול להיות שהיעד docs
יפעל מהר יותר.
משתמשים ב-{@code foo} כדי להבחין בין ערכי Java
כדי להבדיל בין ערכי Java כמו true
, false
ו-null
לבין טקסט התיעוד, צריך להוסיף להם את התווים {@code...}
.
כשכותבים תיעוד במקורות Kotlin, אפשר להוסיף גרשיים הפוכים לקוד, כמו ב-Markdown.
סיכומי הפרמטרים והחזרות צריכים להיות משפט חלקי אחד
סיכומי הפרמטרים וערכי ההחזרה צריכים להתחיל באות קטנה ולהכיל רק חלק משפט אחד. אם יש לכם מידע נוסף שחורג מגבולות המשפט הבודד, צריך להעביר אותו לגוף של Javadoc של השיטה:
/**
* @param e The element to be appended to the list. This must not be
* null. If the list contains no entries, this element will
* be added at the beginning.
* @return This method returns true on success.
*/
צריך לשנות ל:
/**
* @param e element to be appended to this list, must be non-{@code null}
* @return {@code true} on success, {@code false} otherwise
*/
צריך להוסיף הסברים להערות ב-Docs
למה ההערות @hide
ו-@removed
מוסתרות מ-API ציבורי?
צריך לכלול הוראות להחלפת רכיבי API שמסומנים בהערה @deprecated
.
שימוש ב- @throws לתיעוד חריגים
אם שיטה מעלה חריגה מסוג checked, לדוגמה IOException
, צריך לתעד את החריגה באמצעות @throws
. בממשקי API שמקורם ב-Kotlin ומיועדים לשימוש על ידי לקוחות Java, מוסיפים הערות לפונקציות עם @Throws
.
אם שיטה מעלה חריגה לא מסומנת שמציינת שגיאה שאפשר למנוע, למשל IllegalArgumentException
או IllegalStateException
, צריך לתעד את החריגה עם הסבר לסיבה להעלאת החריגה. החריגה שמועברת צריכה גם לציין למה היא הועברה.
מקרים מסוימים של חריגה לא מסומנת נחשבים לחריגה מרומזת ולא צריך לתעד אותם, כמו NullPointerException
או IllegalArgumentException
, שבהם ארגומנט לא תואם ל-@IntDef
או להערה דומה שמטמיעה את חוזה ה-API בחתימת השיטה:
/**
* ...
* @throws IOException If it cannot find the schema for {@code toVersion}
* @throws IllegalStateException If the schema validation fails
*/
public SupportSQLiteDatabase runMigrationsAndValidate(String name, int version,
boolean validateDroppedTables, Migration... migrations) throws IOException {
// ...
if (!dbPath.exists()) {
throw new IllegalStateException("Cannot find the database file for " + name
+ ". Before calling runMigrations, you must first create the database "
+ "using createDatabase.");
}
// ...
או ב-Kotlin:
/**
* ...
* @throws IOException If something goes wrong reading the file, such as a bad
* database header or missing permissions
*/
@Throws(IOException::class)
fun readVersion(databaseFile: File): Int {
// ...
val read = input.read(buffer)
if (read != 4) {
throw IOException("Bad database header, unable to read 4 bytes at " +
"offset 60")
}
}
// ...
אם השיטה מפעילה קוד אסינכרוני שעשוי להחזיר חריגים, כדאי לחשוב איך המפתח יגלה את החריגים האלה ויתייחס אליהם. בדרך כלל זה כולל העברה של החריג לקריאה חוזרת (callback) ותיעוד של החריגים שהוחזרו בשיטה שמקבלת אותם. אין לתעד חריגים אסינכרוניים באמצעות @throws
אלא אם הם מושלכים מחדש מהשיטה עם ההערה.
סיום המשפט הראשון במסמכים בנקודה
הכלי Doclava מנתח מסמכים בצורה פשוטה, ומסיים את מסמך התקציר (המשפט הראשון, שמשמש לתיאור הקצר בחלק העליון של מסמכי המחלקה) ברגע שהוא מזהה נקודה (.) ואחריה רווח. כתוצאה מכך, נוצרות שתי בעיות:
- אם מסמך קצר לא מסתיים בנקודה, ואם חבר בקבוצה ירש מסמכים שהכלי מזהה, התקציר יכלול גם את המסמכים האלה. לדוגמה, אפשר לראות את
actionBarTabStyle
במסמכיR.attr
, שבהם תיאור המאפיין נוסף לתקציר. - מאותה סיבה, לא כדאי להשתמש בקיצור e.g. במשפט הראשון, כי Doclava מסיים את מסמכי התקציר אחרי האות g. לדוגמה, ראו
TEXT_ALIGNMENT_CENTER
ב-View.java
. שימו לב ש-Metalava מתקן את השגיאה הזו באופן אוטומטי על ידי הוספת רווח קשיח אחרי הנקודה, אבל עדיף להימנע מהשגיאה הזו מלכתחילה.
עיצוב מסמכים לעיבוד ב-HTML
הפורמט של Javadoc הוא HTML, ולכן צריך לעצב את המסמכים האלה בהתאם:
צריך להשתמש בתג
<p>
מפורש למעברי שורה. לא מוסיפים תג סגירה</p>
.אל תשתמשו ב-ASCII כדי להציג רשימות או טבלאות.
ברשימות לא מסודרות צריך להשתמש בתג
<ul>
וברשימות מסודרות צריך להשתמש בתג<ol>
. כל פריט צריך להתחיל בתג<li>
, אבל לא צריך תג סוגר</li>
. אחרי הפריט האחרון צריך להוסיף תג סגירה</ul>
או</ol>
.בטבלאות צריך להשתמש בתגים
<table>
,<tr>
לשורות, בתג<th>
לכותרות ובתג<td>
לתאים. לכל תגי הטבלה נדרשים תגי סגירה תואמים. אפשר להשתמש ב-class="deprecated"
בכל תג כדי לציין הוצאה משימוש.כדי ליצור גופן קוד בתוך השורה, משתמשים ב-
{@code foo}
.כדי ליצור בלוקים של קוד, משתמשים ב-
<pre>
.הדפדפן מנתח את כל הטקסט בתוך בלוק
<pre>
, לכן צריך להיזהר עם סוגריים<>
. אפשר להשתמש בייצוגי HTML של<
ושל>
כדי להוסיף אותם.אפשר גם להשאיר סוגריים מרובעים גולמיים
<>
בקטע הקוד אם עוטפים את החלקים הבעייתיים ב-{@code foo}
. לדוגמה:<pre>{@code <manifest>}</pre>
פועלים לפי מדריך הסגנון של הפניית ה-API
כדי לשמור על עקביות בסגנון של סיכומי הכיתות, תיאורי השיטות, תיאורי הפרמטרים ופריטים אחרים, מומלץ לפעול לפי ההמלצות בהנחיות הרשמיות של שפת Java במאמר How to Write Doc Comments for the Javadoc Tool.
כללים ספציפיים ל-Android Framework
הכללים האלה מתייחסים לממשקי API, לדפוסים ולמבני נתונים שספציפיים לממשקי API ולהתנהגויות שמוטמעות במסגרת Android (לדוגמה, Bundle
או Parcelable
).
כלי ליצירת כוונות צריכים להשתמש בתבנית create*Intent()
יוצרים של כוונות צריכים להשתמש בשיטות שנקראות createFooIntent()
.
שימוש ב-Bundle במקום ליצור מבני נתונים חדשים לשימוש כללי
לא מומלץ ליצור מבני נתונים חדשים לשימוש כללי כדי לייצג מיפויים שרירותיים של מפתח לערך מוקלד. במקום זאת, כדאי להשתמש ב-Bundle
.
המצב הזה קורה בדרך כלל כשכותבים ממשקי API של פלטפורמה שמשמשים כערוצי תקשורת בין אפליקציות ושירותים שלא שייכים לפלטפורמה, כשהפלטפורמה לא קוראת את הנתונים שנשלחים בערוץ והסכם ה-API מוגדר באופן חלקי מחוץ לפלטפורמה (לדוגמה, בספריית Jetpack).
במקרים שבהם הפלטפורמה קוראת את הנתונים, מומלץ להימנע משימוש ב-Bundle
ולהשתמש במחלקת נתונים עם הקלדה חזקה.
ליישומים של Parcelable חייב להיות שדה CREATOR ציבורי
ה-inflation של Parcelable נחשף דרך CREATOR
, ולא דרך constructors גולמיים. אם מחלקה מטמיעה את Parcelable
, השדה CREATOR
שלה צריך להיות גם API ציבורי, והבונה של המחלקה שמקבל ארגומנט Parcel
צריך להיות פרטי.
שימוש ב-CharSequence למחרוזות בממשק המשתמש
כשמחרוזת מוצגת בממשק משתמש, צריך להשתמש ב-CharSequence
כדי לאפשר מקרים של Spannable
.
אם מדובר רק במפתח או בתווית או בערך אחרים שלא גלויים למשתמשים, אפשר להשתמש ב-String
.
הימנעות משימוש ב-Enums
צריך להשתמש ב-IntDef
במקום ב-enums בכל ממשקי ה-API של הפלטפורמה, ומומלץ מאוד להשתמש בו בממשקי API של ספריות לא מקובצות. משתמשים ב-enums רק כשבטוחים שלא יתווספו ערכים חדשים.
היתרונות של IntDef
:
- האפשרות הזו מאפשרת להוסיף ערכים לאורך זמן
- הצהרות Kotlin
when
יכולות להיכשל בזמן הריצה אם הן כבר לא ממצות את כל האפשרויות בגלל ערך enum שנוסף בפלטפורמה.
- הצהרות Kotlin
- אין שימוש בכיתות או באובייקטים בזמן הריצה, רק בפרימיטיבים
- אמנם R8 או מזעור יכולים למנוע את העלות הזו בממשקי API של ספריות לא מאוגדות, אבל האופטימיזציה הזו לא יכולה להשפיע על מחלקות של ממשקי API של פלטפורמות.
היתרונות של טיפוס בן מנייה (enum)
- תכונה לשונית אופיינית של Java, Kotlin
- הפעלה של מעבר מקיף,
when
שימוש בהצהרה- הערה – הערכים לא יכולים להשתנות לאורך זמן, ראו את הרשימה הקודמת
- שמות עם היקף ברור וקל לזיהוי
- הפעלה של אימות בזמן הידור
- לדוגמה, משפט
when
ב-Kotlin שמחזיר ערך
- לדוגמה, משפט
- היא מחלקה פונקציונלית שיכולה להטמיע ממשקים, לכלול פונקציות עזר סטטיות, לחשוף שיטות של חברים או הרחבות ולחשוף שדות.
פועלים לפי היררכיית השכבות של חבילת Android
להיררכיית החבילות android.*
יש סדר מרומז, שבו חבילות ברמה נמוכה לא יכולות להיות תלויות בחבילות ברמה גבוהה יותר.
לא להזכיר את Google, חברות אחרות והמוצרים שלהן
פלטפורמת Android היא פרויקט קוד פתוח, והמטרה היא שהיא תהיה ניטרלית מבחינת הספקים. ממשק ה-API צריך להיות כללי ושימושי באותה מידה על ידי משלבי מערכות או אפליקציות עם ההרשאות הנדרשות.
ההטמעות של Parcelable צריכות להיות סופיות
מחלקות Parcelable שהוגדרו על ידי הפלטפורמה תמיד נטענות מ-framework.jar
, ולכן ניסיון של אפליקציה להחליף הטמעה של Parcelable
הוא לא חוקי.
אם האפליקציה השולחת מרחיבה את Parcelable
, לאפליקציה המקבלת לא תהיה הטמעה מותאמת אישית של השולח כדי לפתוח את האריזה. הערה לגבי תאימות לאחור: אם מחלקה מסוימת לא הייתה סופית בעבר, אבל לא היה לה בנאי שזמין לציבור, עדיין אפשר לסמן אותה כ-final
.
שיטות שקוראות לתהליך המערכת צריכות להפעיל מחדש את RemoteException כ-RuntimeException
בדרך כלל השגיאה RemoteException
מוחזרת על ידי AIDL פנימי, ומציינת שתהליך המערכת הסתיים או שהאפליקציה מנסה לשלוח יותר מדי נתונים. בשני המקרים, ה-API הציבורי צריך להפעיל מחדש את השגיאה כ-RuntimeException
כדי למנוע מאפליקציות לשמור החלטות אבטחה או מדיניות.
אם אתם יודעים שהצד השני של קריאת Binder
הוא תהליך המערכת, קוד ה-boilerplate הזה הוא השיטה המומלצת:
try {
...
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
הצגת חריגים ספציפיים לשינויים ב-API
ההתנהגויות של ממשקי API ציבוריים עשויות להשתנות בין רמות API שונות ולגרום לקריסת האפליקציה (לדוגמה, כדי לאכוף מדיניות אבטחה חדשה).
אם ה-API צריך להפעיל חריגה לבקשה שהייתה תקפה בעבר, צריך להפעיל חריגה ספציפית חדשה במקום חריגה כללית. לדוגמה, ExportedFlagRequired
במקום SecurityException
(ו-ExportedFlagRequired
יכול להרחיב את SecurityException
).
כך מפתחי אפליקציות וכלים יוכלו לזהות שינויים בהתנהגות של ה-API.
הטמעה של בנאי העתקה במקום שיבוט
לא מומלץ להשתמש בשיטה clone()
של Java בגלל היעדר חוזי API שמסופקים על ידי המחלקה Object
, ובגלל הקשיים שקיימים בהרחבת מחלקות שמשתמשות ב-clone()
. במקום זאת, צריך להשתמש בבנאי העתקה שמקבל אובייקט מאותו סוג.
/**
* Constructs a shallow copy of {@code other}.
*/
public Foo(Foo other)
במקרים שבהם מחלקות מסתמכות על Builder כדי ליצור אובייקט, כדאי להוסיף ל-Builder בנאי עותק כדי לאפשר שינויים בעותק.
public class Foo {
public static final class Builder {
/**
* Constructs a Foo builder using data from {@code other}.
*/
public Builder(Foo other)
שימוש ב-ParcelFileDescriptor במקום FileDescriptor
הגדרת הבעלות של אובייקט java.io.FileDescriptor
לא טובה, ולכן עלולות להתרחש שגיאות לא ברורות של שימוש אחרי סגירה. במקום זאת, ממשקי ה-API צריכים להחזיר או לקבל מופעים של ParcelFileDescriptor
. קוד מדור קודם יכול להמיר בין PFD ל-FD אם צריך, באמצעות dup() או getFileDescriptor().
הימנעו משימוש בערכים מספריים בגודל אי-זוגי
מומלץ להימנע משימוש ישיר בערכים short
או byte
, כי לעיתים קרובות הם מגבילים את האופן שבו אפשר יהיה לפתח את ה-API בעתיד.
אל תשתמשו ב-BitSet
java.util.BitSet
מצוין להטמעה אבל לא ל-API ציבורי. הוא ניתן לשינוי, דורש הקצאה לקריאות שיטה בתדירות גבוהה ולא מספק משמעות סמנטית למה שכל ביט מייצג.
בתרחישים שבהם נדרשים ביצועים גבוהים, כדאי להשתמש ב-int
או ב-long
עם @IntDef
. בתרחישים של ביצועים נמוכים, כדאי לשקול Set<EnumType>
. לנתונים בינאריים גולמיים: byte[]
.
העדפה ל-android.net.Uri
android.net.Uri
היא שיטת האנקפסולציה המועדפת למזהי URI בממשקי API של Android.
מומלץ להימנע משימוש ב-java.net.URI
, כי הוא מחמיר מדי בניתוח של כתובות URI, ואסור להשתמש ב-java.net.URL
, כי ההגדרה שלו לשוויון פגומה מאוד.
הסתרת הערות שמסומנות כ-@IntDef, @LongDef או @StringDef
ההערות שמסומנות ב-@IntDef
, ב-@LongDef
או ב-@StringDef
מציינות קבוצה של קבועים תקינים שאפשר להעביר ל-API. עם זאת, כשמייצאים אותם כ-API, הקומפיילר מבצע החלפה של הקבועים, ורק הערכים (שכבר לא שימושיים) נשארים ב-API stub של ההערה (לפלטפורמה) או ב-JAR (לספריות).
לכן, השימוש בהערות האלה צריך להיות מסומן בהערת התיעוד @hide
בפלטפורמה או בהערת הקוד @RestrictTo.Scope.LIBRARY)
בספריות. בשני המקרים צריך לסמן אותם בסימן @Retention(RetentionPolicy.SOURCE)
כדי למנוע את ההצגה שלהם ב-API stubs או ב-JARs.
@RestrictTo(RestrictTo.Scope.LIBRARY)
@Retention(RetentionPolicy.SOURCE)
@IntDef({
STREAM_TYPE_FULL_IMAGE_DATA,
STREAM_TYPE_EXIF_DATA_ONLY,
})
public @interface ExifStreamType {}
כשבונים את ערכת ה-SDK של הפלטפורמה ואת ספריות ה-AAR, כלי מסוים מחלץ את ההערות ומאגד אותן בנפרד מהמקורות המהודרים. Android Studio קורא את הפורמט הזה שצורף לחבילה ומחיל את הגדרות הסוג.
לא להוסיף מפתחות חדשים של ספקי הגדרות
לא לחשוף מפתחות חדשים מ-Settings.Global
, Settings.System
או Settings.Secure
.
במקום זאת, מוסיפים getter ו-setter מתאימים של Java API בכיתה רלוונטית, שבדרך כלל היא כיתת 'ניהול'. מוסיפים מנגנון האזנה או שידור כדי להודיע ללקוחות על שינויים לפי הצורך.
יש כמה בעיות בהגדרות של SettingsProvider
בהשוואה ל-getters/setters:
- אין בטיחות סוגים.
- אין דרך מאוחדת לספק ערך ברירת מחדל.
- אין דרך מתאימה להתאים אישית את ההרשאות.
- לדוגמה, אי אפשר להגן על ההגדרה באמצעות הרשאה מותאמת אישית.
- אין דרך מתאימה להוסיף לוגיקה מותאמת אישית.
- לדוגמה, אי אפשר לשנות את הערך של הגדרה א' בהתאם לערך של הגדרה ב'.
דוגמה:
Settings.Secure.LOCATION_MODE
קיים כבר הרבה זמן, אבל צוות המיקום הוציא אותו משימוש לטובת
Java API מתאים
LocationManager.isLocationEnabled()
ושידור
MODE_CHANGED_ACTION
שנתן לצוות הרבה יותר גמישות, והסמנטיקה של ממשקי ה-API ברורה הרבה יותר עכשיו.
לא להרחיב את Activity ו-AsyncTask
AsyncTask
הוא פרט הטמעה. במקום זאת, כדאי לחשוף מאזין או, ב-androidx, API של ListenableFuture
.
אי אפשר ליצור מחלקות משנה של Activity
. הארכת הפעילות של התכונה הופכת אותה ללא תואמת לתכונות אחרות שמחייבות את המשתמשים לעשות את אותו הדבר. במקום זאת, כדאי להסתמך על קומפוזיציה באמצעות כלים כמו LifecycleObserver.
שימוש בפונקציה getUser() של Context
במקרים שבהם מחלקות קשורות ל-Context
, כמו כל מה שמוחזר מ-Context.getSystemService()
, צריך להשתמש במשתמש שקשור ל-Context
במקום לחשוף חברים שמטרגטים משתמשים ספציפיים.
class FooManager {
Context mContext;
void fooBar() {
mIFooBar.fooBarForUser(mContext.getUser());
}
}
class FooManager {
Context mContext;
Foobar getFoobar() {
// Bad: doesn't appy mContext.getUserId().
mIFooBar.fooBarForUser(Process.myUserHandle());
}
Foobar getFoobar() {
// Also bad: doesn't appy mContext.getUserId().
mIFooBar.fooBar();
}
Foobar getFoobarForUser(UserHandle user) {
mIFooBar.fooBarForUser(user);
}
}
חריג: שיטה יכולה לקבל ארגומנט של משתמש אם היא מקבלת ערכים שלא מייצגים משתמש יחיד, כמו UserHandle.ALL
.
שימוש ב-UserHandle במקום במספרים שלמים פשוטים
מומלץ להשתמש ב-UserHandle
כדי לספק בטיחות סוגים ולמנוע בלבול בין מזהי משתמשים לבין מזהי משתמשים (uid).
Foobar getFoobarForUser(UserHandle user);
Foobar getFoobarForUser(int userId);
במקרים שבהם אין ברירה, יש להוסיף הערה לint
שמייצג מזהה משתמש באמצעות התג @UserIdInt
.
Foobar getFoobarForUser(@UserIdInt int user);
העדפה של listeners או callbacks על פני broadcast intents
לשידורי Intent יש יכולות רבות, אבל הם הובילו להתנהגויות בלתי צפויות שיכולות להשפיע באופן שלילי על תקינות המערכת, ולכן צריך להוסיף שידורי Intent חדשים בזהירות.
ריכזנו כאן כמה דאגות ספציפיות שמובילות אותנו להמליץ שלא להציג כוונות שידור חדשות:
כששולחים שידורים בלי הסימון
FLAG_RECEIVER_REGISTERED_ONLY
, כל האפליקציות שלא פועלות כבר מופעלות בכוח. לפעמים זה יכול להיות מצב מכוון, אבל הוא עלול לגרום להפעלת עשרות אפליקציות בו-זמנית, מה שמשפיע לרעה על תקינות המערכת. מומלץ להשתמש באסטרטגיות חלופיות, כמוJobScheduler
, כדי לתאם בצורה טובה יותר את המקרים שבהם מתקיימים תנאים מוקדמים שונים.כששולחים שידורים, אין הרבה אפשרויות לסנן או לשנות את התוכן שמוצג באפליקציות. כך קשה או בלתי אפשרי להגיב לבעיות פרטיות עתידיות, או להציג שינויים בהתנהגות על סמך ה-SDK של אפליקציית היעד שמקבלת את הנתונים.
תורי שידור הם משאב משותף, ולכן יכול להיות שהם יהיו עמוסים מדי ולא יאפשרו את שידור האירוע בזמן. זיהינו כמה תורים של שידורים שזמן האחזור הכולל שלהם הוא 10 דקות או יותר.
לכן, אנחנו ממליצים להשתמש ב-listeners או ב-callbacks או במתקנים אחרים כמו JobScheduler
במקום ב-broadcast intents בתכונות חדשות.
במקרים שבהם שימוש ב-broadcast intents עדיין נחשב לעיצוב האידיאלי, כדאי לפעול לפי השיטות המומלצות הבאות:
- אם אפשר, משתמשים ב-
Intent.FLAG_RECEIVER_REGISTERED_ONLY
כדי להגביל את השידור לאפליקציות שכבר פועלות. לדוגמה,ACTION_SCREEN_ON
משתמש בעיצוב הזה כדי למנוע הפעלה של אפליקציות. - אם אפשר, כדאי להשתמש ב-
Intent.setPackage()
או ב-Intent.setComponent()
כדי לטרגט את השידור לאפליקציה ספציפית שמעניינת אתכם. לדוגמה, ב-ACTION_MEDIA_BUTTON
נעשה שימוש בעיצוב הזה כדי להתמקד באמצעי הבקרה של ההפעלה באפליקציה הנוכחית. - אם אפשר, צריך להגדיר את השידור כ
<protected-broadcast>
כדי למנוע מאפליקציות זדוניות להתחזות למערכת ההפעלה.
כוונות בשירותים למפתחים שקשורים למערכת
שירותים שהמפתח מתכוון להרחיב ושמוגבלים על ידי המערכת, למשל שירותים מופשטים כמו NotificationListenerService
, עשויים להגיב לפעולה Intent
מהמערכת. שירותים כאלה צריכים לעמוד בקריטריונים הבאים:
- מגדירים קבוע מחרוזת
SERVICE_INTERFACE
במחלקה שמכילה את שם המחלקה המלא של השירות. צריך להוסיף לערך הקבוע הזה את ההערה@SdkConstant(SdkConstant.SdkConstantType.SERVICE_ACTION)
. - מסמך על הכיתה שמפתח צריך להוסיף
<intent-filter>
ל-AndroidManifest.xml
שלו כדי לקבל כוונות מהפלטפורמה. - מומלץ מאוד להוסיף הרשאה ברמת המערכת כדי למנוע מאפליקציות לא רצויות לשלוח
Intent
לשירותים למפתחים.
יכולת פעולה הדדית בין Kotlin ל-Java
רשימה מלאה של הנחיות זמינה במדריך הרשמי של Android בנושא פעולות הדדיות בין Kotlin ל-Java. העתקנו חלק מההנחיות למדריך הזה כדי לשפר את יכולת הגילוי.
ניראות של ממשק ה-API
חלק מממשקי ה-API של Kotlin, כמו suspend fun
s, לא מיועדים לשימוש על ידי מפתחי Java. עם זאת, לא מומלץ לנסות לשלוט בנראות הספציפית לשפה באמצעות @JvmSynthetic
, כי יש לכך תופעות לוואי שמשפיעות על האופן שבו ה-API מוצג במאגרי באגים, מה שמקשה על איתור באגים.
הנחיות ספציפיות זמינות במדריך בנושא פעולות הדדיות בין Kotlin ו-Java או במדריך בנושא פעולות אסינכרוניות.
אובייקטים נלווים
ב-Kotlin משתמשים ב-companion object
כדי לחשוף חברים סטטיים. במקרים מסוימים, הם יופיעו מ-Java בכיתה פנימית בשם Companion
ולא בכיתה המכילה. יכול להיות שקבצים של טקסט API של כיתות Companion
יופיעו ככיתות ריקות – זה תקין.
כדי למקסם את התאימות ל-Java, צריך להוסיף הערות לאובייקטים נלווים: שדות לא קבועים עם @JvmField
ופונקציות ציבוריות עם @JvmStatic
כדי לחשוף אותם ישירות במחלקה המכילה.
companion object {
@JvmField val BIG_INTEGER_ONE = BigInteger.ONE
@JvmStatic fun fromPointF(pointf: PointF) {
/* ... */
}
}
התפתחות של ממשקי API בפלטפורמת Android
בקטע הזה מוסבר על כללי המדיניות בנוגע לסוגי השינויים שאפשר לבצע בממשקי Android API קיימים, ואיך צריך להטמיע את השינויים האלה כדי למקסם את התאימות לאפליקציות ולבסיסי קוד קיימים.
שינויים שגורמים לשגיאות בינאריות
מומלץ להימנע משינויים שגורמים לשגיאות בינאריות בממשקי API ציבוריים סופיים. בדרך כלל, שינויים מהסוג הזה גורמים לשגיאות כשמריצים את make update-api
, אבל יכול להיות שיש מקרים חריגים שבהם בדיקת ה-API של Metalava לא מזהה אותם. אם יש לכם ספק, תוכלו לעיין במדריך של Eclipse Foundation בנושא Evolving Java-based APIs (פיתוח ממשקי API מבוססי Java) כדי לקבל הסבר מפורט על סוגי השינויים ב-API שתואמים ל-Java. שינויים שגורמים לשבירת תאימות בינארית בממשקי API מוסתרים (לדוגמה, מערכת) צריכים לפעול לפי מחזור הוצאה משימוש/החלפה.
שינויים שגורמים לבעיות במקור
אנחנו לא ממליצים על שינויים שגורמים לבעיות בקוד המקור, גם אם הם לא גורמים לבעיות בבינארי. דוגמה לשינוי שגורם לבעיות בקוד המקור אבל לא בבינארי היא הוספת גנריקה למחלקה קיימת. השינוי הזה תואם לבינארי אבל עלול לגרום לשגיאות קומפילציה בגלל ירושה או הפניות לא חד-משמעיות.
שינויים שגורמים לשבירה של קוד המקור לא יגרמו לשגיאות כשמריצים את הפקודה make update-api
, לכן חשוב להבין את ההשפעה של שינויים בחתימות קיימות של API.
במקרים מסוימים, שינויים שגורמים לבעיות בקוד המקור נדרשים כדי לשפר את חוויית המפתחים או את נכונות הקוד. לדוגמה, הוספת הערות לגבי אפשרות קבלת ערך null למקורות Java משפרת את יכולת הפעולה ההדדית עם קוד Kotlin ומקטינה את הסיכוי לשגיאות, אבל לרוב נדרשים שינויים – לפעמים שינויים משמעותיים – בקוד המקור.
שינויים בממשקי API פרטיים
אפשר לשנות ממשקי API עם ההערה @TestApi
בכל שלב.
חובה לשמור ממשקי API עם ההערה @SystemApi
למשך שלוש שנים. צריך להסיר או לארגן מחדש (Refactor) API של מערכת לפי לוח הזמנים הבא:
- API y - Added
- API y+1 – הוצאה משימוש
- מסמנים את הקוד באמצעות
@Deprecated
. - מוסיפים תחליפים ומקשרים לתחליף ב-Javadoc של הקוד שהוצא משימוש באמצעות הערת התיעוד
@deprecated
. - במהלך מחזור הפיתוח, מדווחים על באגים למשתמשים פנימיים ומציינים שה-API יוצא משימוש. כך אפשר לוודא שממשקי ה-API החלופיים מתאימים.
- מסמנים את הקוד באמצעות
- API y+2 – הסרה רכה
- מסמנים את הקוד באמצעות
@removed
. - אופציונלי: אפשר להגדיר שהמערכת תבצע פעולה או לא תבצע פעולה באפליקציות שמטרגטות את רמת ה-SDK הנוכחית של הגרסה.
- מסמנים את הקוד באמצעות
- API y+3 – הסרה סופית
- מסירים לחלוטין את הקוד מעץ המקור.
הוצאה משימוש
אנחנו רואים בהוצאה משימוש שינוי ב-API, והיא יכולה להתרחש בגרסה ראשית (למשל, גרסה שמסומנת באות). כשמוציאים משימוש ממשקי API, כדאי להשתמש יחד ב-@Deprecated
source annotation וב-@deprecated
<summary>
docs annotation. הסיכום חייב לכלול אסטרטגיית העברה. יכול להיות שהאסטרטגיה הזו תכלול קישור ל-API חלופי או הסבר למה לא כדאי להשתמש ב-API:
/**
* Simple version of ...
*
* @deprecated Use the {@link androidx.fragment.app.DialogFragment}
* class with {@link androidx.fragment.app.FragmentManager}
* instead.
*/
@Deprecated
public final void showDialog(int id)
חובה גם להוציא משימוש ממשקי API שמוגדרים ב-XML ונחשפים ב-Java, כולל מאפיינים ומאפיינים שניתנים לעיצוב שנחשפים במחלקה android.R
, עם סיכום:
<!-- Attribute whether the accessibility service ...
{@deprecated Not used by the framework}
-->
<attr name="canRequestEnhancedWebAccessibility" format="boolean" />
מתי מוציאים משימוש API
הוצאה משימוש הכי שימושית כדי למנוע שימוש ב-API בקוד חדש.
אנחנו גם דורשים לסמן ממשקי API כ@deprecated
לפני שהם @removed
, אבל זה לא מספק למפתחים תמריץ חזק להפסיק להשתמש בממשק API שהם כבר משתמשים בו.
לפני שמוציאים משימוש API, חשוב לקחת בחשבון את ההשפעה על המפתחים. ההשפעות של הוצאה משימוש של API כוללות:
javac
מציג אזהרה במהלך ההידור.- אי אפשר להשבית את אזהרות ההוצאה משימוש באופן גלובלי או להגדיר אותן כנקודת בסיס, ולכן מפתחים שמשתמשים ב-
-Werror
צריכים לתקן או להשבית כל שימוש בממשק API שהוצא משימוש לפני שהם יכולים לעדכן את גרסת ה-SDK של הקומפילציה. - אי אפשר להשבית אזהרות על הוצאה משימוש בייבוא של מחלקות שהוצאו משימוש. לכן, מפתחים צריכים להוסיף את שם המחלקה המלא לכל שימוש במחלקה שהוצאה משימוש לפני שהם יכולים לעדכן את גרסת ה-SDK של הקומפילציה.
- אי אפשר להשבית את אזהרות ההוצאה משימוש באופן גלובלי או להגדיר אותן כנקודת בסיס, ולכן מפתחים שמשתמשים ב-
- בתיעוד בנושא
d.android.com
מופיעה הודעה על הוצאה משימוש. - בסביבות פיתוח משולבות (IDE) כמו Android Studio מוצגת אזהרה באתר שבו נעשה שימוש ב-API.
- יכול להיות שסביבות פיתוח משולבות יורידו את הדירוג של ה-API או יסתירו אותו מההשלמה האוטומטית.
כתוצאה מכך, הוצאה משימוש של API עלולה להרתיע מפתחים שהכי חשוב להם לשמור על תקינות הקוד (אלה שמשתמשים ב--Werror
) מלעבור לשימוש בערכות SDK חדשות.
מפתחים שלא מתייחסים לאזהרות בקוד הקיים שלהם, סביר להניח שיתעלמו לגמרי מהוצאה משימוש.
ערכת SDK שכוללת מספר גדול של הוצאות משימוש מחמירה את שני המקרים האלה.
לכן, מומלץ להוציא משימוש ממשקי API רק במקרים הבאים:
- אנחנו מתכננים
@remove
את ה-API בגרסה עתידית. - שימוש ב-API מוביל להתנהגות שגויה או לא מוגדרת, ואין לנו אפשרות לתקן אותה בלי לפגוע בתאימות.
כשמוציאים API משימוש ומחליפים אותו ב-API חדש, מומלץ מאוד להוסיף API תאימות תואם לספריית Jetpack כמו androidx.core
כדי לפשט את התמיכה במכשירים ישנים וחדשים.
לא מומלץ להוציא משימוש ממשקי API שפועלים כמצופה בגרסאות הנוכחיות והעתידיות:
/**
* ...
* @deprecated Use {@link #doThing(int, Bundle)} instead.
*/
@Deprecated
public void doThing(int action) {
...
}
public void doThing(int action, @Nullable Bundle extras) {
...
}
הוצאה משימוש מתאימה במקרים שבהם ממשקי API לא יכולים יותר לשמור על ההתנהגויות המתועדות שלהם:
/**
* ...
* @deprecated No longer displayed in the status bar as of API 21.
*/
@Deprecated
public RemoteViews tickerView;
שינויים ברכיבי API שהוצאו משימוש
חובה לשמור על ההתנהגות של ממשקי API שהוצאו משימוש. המשמעות היא שההטמעות של הבדיקות צריכות להישאר זהות, והבדיקות צריכות להמשיך לפעול אחרי שהוצאתם את ה-API משימוש. אם אין בדיקות ל-API, צריך להוסיף בדיקות.
לא מרחיבים ממשקי API שהוצאו משימוש בגרסאות עתידיות. אפשר להוסיף הערות של lint לגבי נכונות (לדוגמה, @Nullable
) ל-API קיים שהוצא משימוש, אבל לא כדאי להוסיף ממשקי API חדשים.
אל תוסיפו ממשקי API חדשים ככאלה שהוצאו משימוש. אם נוספו ממשקי API כלשהם ובהמשך הם הוצאו משימוש במהלך מחזור של גרסת טרום-הפצה (כלומר, הם נכנסו לראשונה לממשקי ה-API הציבוריים כשהם כבר הוצאו משימוש), חובה להסיר אותם לפני השלמת ה-API.
הסרה חלקית
הסרה רכה היא שינוי שגורם לבעיות בקוד המקור, ולכן מומלץ להימנע ממנה בממשקי API ציבוריים, אלא אם מועצת ה-API מאשרת אותה באופן מפורש.
במקרה של ממשקי API של המערכת, צריך להוציא את ה-API משימוש למשך גרסה ראשית לפני הסרה רכה. מסירים את כל ההפניות לממשקי ה-API במסמכי Docs ומשתמשים בהערה @removed <summary>
של Docs כשמסירים ממשקי API באופן זמני. הסיכום חייב לכלול את הסיבה להסרה, ויכול לכלול אסטרטגיית העברה, כפי שהסברנו במאמר בנושא הוצאה משימוש.
אפשר לשמור את ההתנהגות של ממשקי API שהוסרו באופן זמני כמו שהיא, אבל חשוב יותר לשמור אותה כדי שהקריאות הקיימות לא יגרמו לקריסה כשקוראים ל-API. במקרים מסוימים, זה עשוי להיות כרוך בשמירה על אופן הפעולה.
חובה לשמור על כיסוי הבדיקות, אבל יכול להיות שיהיה צורך לשנות את תוכן הבדיקות כדי להתאים לשינויים בהתנהגות. הבדיקות צריכות לוודא שמי שכבר התקשרו לא יגרמו לקריסה בזמן הריצה. אתם יכולים לשמור על ההתנהגות של ממשקי API שהוסרו באופן רך כמו שהיא, אבל מה שחשוב יותר הוא שחובה לשמור אותה כך שקריאות קיימות לא יגרמו לקריסה כשקוראים ל-API. במקרים מסוימים, זה עשוי להיות כרוך בשמירה על התנהגות מסוימת.
חובה לשמור על כיסוי הבדיקות, אבל יכול להיות שיהיה צורך לשנות את תוכן הבדיקות כדי להתאים לשינויים בהתנהגות. הבדיקות צריכות לוודא שמי שכבר התקשרו לא יגרמו לקריסה בזמן הריצה.
ברמה הטכנית, אנחנו מסירים את ה-API מ-SDK stub JAR ומ-compile-time classpath באמצעות @remove
Javadoc annotation, אבל הוא עדיין קיים ב-run-time classpath – בדומה ל-APIs של @hide
:
/**
* Ringer volume. This is ...
*
* @removed Not functional since API 2.
*/
public static final String VOLUME_RING = ...
מנקודת המבט של מפתחי אפליקציות, ה-API לא מופיע יותר בהשלמה האוטומטית, וקוד המקור שמפנה אל ה-API לא עובר קומפילציה כשהערך של compileSdk
שווה לגרסה של ה-SDK שבה הוסר ה-API או לגרסה מאוחרת יותר. עם זאת, קוד המקור ממשיך לעבור קומפילציה בהצלחה בגרסאות קודמות של ה-SDK, וקבצים בינאריים שמפנים אל ה-API ממשיכים לפעול.
אסור להסיר חלק מקטגוריות ה-API באופן זמני. אסור לבצע הסרה רכה של קטגוריות מסוימות של API.
שיטות מופשטות
אסור להסיר באופן זמני שיטות מופשטות במחלקות שמפתחים עשויים להרחיב. במקרה כזה, מפתחים לא יכולים להרחיב את המחלקה בכל רמות ה-SDK.
במקרים נדירים שבהם לא הייתה ולא תהיה אפשרות למפתחים להרחיב מחלקה, עדיין אפשר להסיר בזהירות שיטות מופשטות.
הסרה סופית
הסרה מלאה היא שינוי שובר תאימות בינארית, ואסור שתתרחש בממשקי API ציבוריים.
הערה לא מומלצת
אנחנו משתמשים בהערה @Discouraged
כדי לציין שברוב המקרים (מעל 95%) לא מומלץ להשתמש ב-API. ממשקי API לא מומלצים שונים מממשקי API שהוצאו משימוש בכך שיש תרחיש שימוש קריטי וספציפי שמונע את הוצאתם משימוש. כשמסמנים API כלא מומלץ, צריך לספק הסבר ופתרון חלופי:
@Discouraged(message = "Use of this function is discouraged because resource
reflection makes it harder to perform build
optimizations and compile-time verification of code. It
is much more efficient to retrieve resources by
identifier (such as `R.foo.bar`) than by name (such as
`getIdentifier()`)")
public int getIdentifier(String name, String defType, String defPackage) {
return mResourcesImpl.getIdentifier(name, defType, defPackage);
}
לא מומלץ להוסיף ממשקי API חדשים כלא מומלצים.
שינויים בהתנהגות של ממשקי API קיימים
במקרים מסוימים, יכול להיות שתרצו לשנות את התנהגות ההטמעה של API קיים. לדוגמה, ב-Android 7.0 שיפרנו את DropBoxManager
כדי להעביר בבירור את המסר כשמפתחים ניסו לפרסם אירועים שהיו גדולים מדי לשליחה ב-Binder
.
עם זאת, כדי למנוע בעיות באפליקציות קיימות, אנחנו ממליצים מאוד לשמור על התנהגות בטוחה באפליקציות ישנות יותר. בעבר, הגנו על השינויים בהתנהגות האפליקציה על סמך ApplicationInfo.targetSdkVersion
שלה, אבל לאחרונה עברנו לדרישה לשימוש במסגרת התאימות של האפליקציה. הנה דוגמה להטמעה של שינוי בהתנהגות באמצעות המסגרת החדשה הזו:
import android.app.compat.CompatChanges;
import android.compat.annotation.ChangeId;
import android.compat.annotation.EnabledSince;
public class MyClass {
@ChangeId
// This means the change will be enabled for target SDK R and higher.
@EnabledSince(targetSdkVersion=android.os.Build.VERSION_CODES.R)
// Use a bug number as the value, provide extra detail in the bug.
// FOO_NOW_DOES_X will be the change name, and 123456789 the change ID.
static final long FOO_NOW_DOES_X = 123456789L;
public void doFoo() {
if (CompatChanges.isChangeEnabled(FOO_NOW_DOES_X)) {
// do the new thing
} else {
// do the old thing
}
}
}
השימוש בעיצוב של מסגרת התאימות לאפליקציות מאפשר למפתחים להשבית באופן זמני שינויים ספציפיים בהתנהגות במהלך גרסאות טרום-הפצה וגרסאות בטא כחלק מניפוי הבאגים באפליקציות שלהם, במקום לחייב אותם להתאים את האפליקציות לעשרות שינויים בהתנהגות בו-זמנית.
תאימות קדימה
תאימות קדימה היא מאפיין עיצובי שמאפשר למערכת לקבל קלט שמיועד לגרסה מאוחרת יותר של עצמה. במקרה של עיצוב API, צריך לשים לב במיוחד לעיצוב הראשוני ולשינויים עתידיים, כי מפתחים מצפים לכתוב קוד פעם אחת, לבדוק אותו פעם אחת ולהריץ אותו בכל מקום בלי בעיות.
הגורמים הבאים הם הסיבות הנפוצות ביותר לבעיות תאימות קדימה ב-Android:
- הוספת קבועים חדשים לקבוצה (כמו
@IntDef
אוenum
) שבעבר נחשבה לקבוצה מלאה (לדוגמה, אם ל-switch
ישdefault
שיוצר חריגה). - הוספת תמיכה בתכונה שלא נכללת ישירות בממשק ה-API (לדוגמה, תמיכה בהקצאת משאבים מסוג
ColorStateList
ב-XML, כשקודם לכן נתמכו רק משאבים מסוג<color>
). - הסרת הגבלות על בדיקות בזמן ריצה, למשל הסרת בדיקה של
requireNotNull()
שהייתה קיימת בגרסאות קודמות.
בכל המקרים האלה, המפתחים מגלים שמשהו לא בסדר רק בזמן הריצה. יותר גרוע מכך, הם עלולים לגלות את זה כתוצאה מדוחות קריסה ממכשירים ישנים יותר בשטח.
בנוסף, כל המקרים האלה הם שינויים ב-API שהם תקינים מבחינה טכנית. הן לא פוגעות בתאימות בינארית או בתאימות של קוד המקור, והכלי API lint לא יזהה אף אחת מהבעיות האלה.
לכן, מעצבי API צריכים לשים לב היטב כשהם משנים מחלקות קיימות. שואלים את השאלה: "האם השינוי הזה יגרום לכך שקוד שנכתב ונבדק רק בגרסה העדכנית של הפלטפורמה ייכשל בגרסאות ישנות יותר?"
סכימות XML
אם סכימת XML משמשת כממשק יציב בין רכיבים, צריך לציין את הסכימה באופן מפורש, והיא צריכה להתפתח באופן שתואם לאחור, בדומה לממשקי API אחרים של Android. לדוגמה, צריך לשמור על המבנה של רכיבי XML ומאפיינים, בדומה לשמירה על שיטות ומשתנים בממשקי API אחרים של Android.
הוצאה משימוש של XML
אם רוצים להוציא משימוש רכיב או מאפיין XML, אפשר להוסיף את התו xs:annotation
, אבל צריך להמשיך לתמוך בכל קובצי ה-XML הקיימים בהתאם למחזור החיים הרגיל של @SystemApi
.
<xs:element name="foo">
<xs:complexType>
<xs:sequence>
<xs:element name="name" type="xs:string">
<xs:annotation name="Deprecated"/>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
חובה לשמור על סוגי הרכיבים
סכימות תומכות ברכיב sequence
, ברכיב choice
וברכיבים all
כרכיבי צאצא של רכיב complexType
. עם זאת, יש הבדלים בין רכיבי הצאצא האלה במספר ובסדר של רכיבי הצאצא שלהם, ולכן שינוי של סוג קיים יהיה שינוי לא תואם.
אם רוצים לשנות סוג קיים, מומלץ להוציא משימוש את הסוג הישן ולהציג סוג חדש שיחליף אותו.
<!-- Original "sequence" value -->
<xs:element name="foo">
<xs:complexType>
<xs:sequence>
<xs:element name="name" type="xs:string">
<xs:annotation name="Deprecated"/>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
<!-- New "choice" value -->
<xs:element name="fooChoice">
<xs:complexType>
<xs:choice>
<xs:element name="name" type="xs:string"/>
</xs:choice>
</xs:complexType>
</xs:element>
דפוסים ספציפיים ל-Mainline
Mainline הוא פרויקט שמאפשר לעדכן תת-מערכות ("מודולים של Mainline") של מערכת ההפעלה Android בנפרד, במקום לעדכן את כל תמונת המערכת.
צריך "לפרק" מודולים של mainline מהפלטפורמה המרכזית, כלומר כל האינטראקציות בין כל מודול לבין שאר העולם צריכות להתבצע באמצעות ממשקי API רשמיים (ציבוריים או של המערכת).
יש דפוסי עיצוב מסוימים שמודולים ראשיים צריכים לפעול לפיהם. בקטע הזה מוסבר על כל אחד מהם.
הדוגמה <Module>FrameworkInitializer
אם מודול ראשי צריך לחשוף מחלקות @SystemService
(לדוגמה, JobScheduler
), צריך להשתמש בתבנית הבאה:
חשיפת מחלקה
<YourModule>FrameworkInitializer
מהמודול. הכיתה הזו צריכה להיות ב-$BOOTCLASSPATH
. דוגמה: StatsFrameworkInitializerמסמנים אותו באמצעות
@SystemApi(client = MODULE_LIBRARIES)
.מוסיפים לו שיטת
public static void registerServiceWrappers()
.משתמשים ב-
SystemServiceRegistry.registerContextAwareService()
כדי לרשום מחלקה של מנהל שירות כשהיא צריכה הפניה ל-Context
.משתמשים ב-
SystemServiceRegistry.registerStaticService()
כדי לרשום מחלקה של מנהל שירות כשלא צריך הפניה ל-Context
.מפעילים את method
registerServiceWrappers()
מתוך המאחֵל הסטטי שלSystemServiceRegistry
.
התבנית <Module>ServiceManager
בדרך כלל, כדי לרשום אובייקטים של שירותי מערכת או לקבל הפניות אליהם, משתמשים ב-ServiceManager
, אבל מודולים ראשיים לא יכולים להשתמש בו כי הוא מוסתר. הסתרנו את המחלקה הזו כי מודולים ראשיים לא אמורים להירשם או להפנות לאובייקטים של שירותי מערכת binder שנחשפים על ידי הפלטפורמה הסטטית או על ידי מודולים אחרים.
מודולים ראשיים יכולים להשתמש בדפוס הבא במקום זאת כדי להירשם ולקבל הפניות לשירותי Binder שמוטמעים בתוך המודול.
ליצור מחלקה
<YourModule>ServiceManager
בהתאם לעיצוב של TelephonyServiceManagerהצגת הכיתה בתור
@SystemApi
. אם אתם צריכים לגשת אליו רק משיעורים או משיעורים של שרת המערכת, אתם יכולים להשתמש ב-@SystemApi(client = MODULE_LIBRARIES)
. אחרת,@SystemApi(client = PRIVILEGED_APPS)
יתאים.$BOOTCLASSPATH
הכיתה הזו תכלול:
- בונה מוסתר, כך שרק קוד הפלטפורמה הסטטי יכול ליצור מופע שלו.
- שיטות getter ציבוריות שמחזירות מופע
ServiceRegisterer
עבור שם ספציפי. אם יש לכם אובייקט אחד של Binder, אתם צריכים שיטת getter אחת. אם יש לכם שניים, תצטרכו שני getters. - ב-
ActivityThread.initializeMainlineModules()
, יוצרים מופע של המחלקה הזו ומעבירים אותו לשיטה סטטית שנחשפת על ידי המודול. בדרך כלל, מוסיפים API סטטי@SystemApi(client = MODULE_LIBRARIES)
בכיתהFrameworkInitializer
שמקבלת אותו.
הדפוס הזה ימנע ממודולים אחרים ב-mainline לגשת לממשקי ה-API האלה, כי אין דרך למודולים אחרים לקבל מופע של <YourModule>ServiceManager
, גם אם ממשקי ה-API get()
ו-register()
גלויים להם.
כך הטלפוניה מקבלת הפניה לשירות הטלפוניה: קישור לחיפוש קוד.
אם אתם מטמיעים אובייקט של שירות binder בקוד מקורי, אתם משתמשים ב-native APIs של AServiceManager
.
ממשקי ה-API האלה מקבילים לממשקי ה-API של Java ServiceManager
, אבל ממשקי ה-API המקוריים נחשפים ישירות למודולים הראשיים. אל תשתמשו בהם כדי להירשם או להפנות לאובייקטים של Binder שלא נמצאים בבעלות המודול שלכם. אם חושפים אובייקט binder מ-native, לא צריך להשתמש בשיטה register()
ב-<YourModule>ServiceManager.ServiceRegisterer
.
הגדרות הרשאות במודולים ראשיים
מודולים של Mainline שמכילים חבילות APK יכולים להגדיר הרשאות (מותאמות אישית) ב-APK שלהם AndroidManifest.xml
באותו אופן כמו ב-APK רגיל.
אם ההרשאה המוגדרת משמשת רק באופן פנימי בתוך מודול, שם ההרשאה צריך להתחיל בקידומת של שם חבילת ה-APK, לדוגמה:
<permission
android:name="com.android.permissioncontroller.permission.MANAGE_ROLES_FROM_CONTROLLER"
android:protectionLevel="signature" />
אם ההרשאה המוגדרת אמורה להינתן כחלק מ-API של פלטפורמה שאפשר לעדכן באפליקציות אחרות, שם ההרשאה צריך להתחיל ב-'android.permission.' (כמו כל הרשאה סטטית של פלטפורמה) בתוספת שם חבילת המודול, כדי לציין שמדובר ב-API של פלטפורמה ממודול, תוך הימנעות מהתנגשויות בשמות. לדוגמה:
<permission
android:name="android.permission.health.READ_ACTIVE_CALORIES_BURNED"
android:label="@string/active_calories_burned_read_content_description"
android:protectionLevel="dangerous"
android:permissionGroup="android.permission-group.HEALTH" />
לאחר מכן, המודול יכול לחשוף את שם ההרשאה הזה כקבוע של API בממשק ה-API שלו, לדוגמה HealthPermissions.READ_ACTIVE_CALORIES_BURNED
.