הדף הזה מיועד למפתחים, כדי להסביר את העקרונות הכלליים שמועצת ה-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
לדוגמה, אם כדי לבצע backport של תיאורי כלים צריך לשמור את המצב שמשויך ל-View ולקרוא לכמה שיטות ב-View כדי להתקין את ה-backport, 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 Services לשימוש באפליקציות), הדרישה לממשק 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 אוכפת חוזים של ערכי null ברמת הקומפיילר, כך שאין צורך ב-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 לא צריכות לחשוף שדות ישירות. השדות צריכים להיות פרטיים, וניתן לגשת אליהם רק באמצעות שיטות ציבוריות לקבלת ערכים (getters) ולהגדרת ערכים (setters), בלי קשר לשאלה אם השדות האלה סופיים או לא.
יוצאים מן הכלל נדירים כוללים מבני נתונים בסיסיים שבהם אין צורך לשפר את ההתנהגות של ציון שדה או אחזור שדה. במקרים כאלה, צריך לתת לשדות שמות לפי מוסכמות סטנדרטיות למתן שמות למשתנים, למשל Point.x ו-Point.y.
מחלקות Kotlin יכולות לחשוף מאפיינים.
צריך לסמן שדות גלויים כסופיים
אנחנו ממליצים מאוד לא להשתמש בשדות גולמיים (ראו לא לחשוף שדות גולמיים). אבל במקרים נדירים שבהם שדה נחשף כשדה ציבורי, צריך לסמן את השדה הזה final.
לא מומלץ לחשוף שדות פנימיים
אל תפנו לשמות של שדות פנימיים ב-API ציבורי.
public int mFlags;
שימוש בגישה ציבורית במקום בגישה מוגנת
@see Use public instead of protected
ערכים קבועים
אלה כללים לגבי קבועים ציבוריים.
קבועי דגלים לא יכולים לחפוף לערכי 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 הם לדברים סטנדרטיים, כמו דגלים, מקשים ופעולות. לקבועים האלה צריכות להיות תחיליות סטנדרטיות כדי שיהיה קל יותר לזהות אותם.
לדוגמה, תוספים של כוונות צריכים להתחיל ב-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"
}
שימוש בגישה ציבורית במקום בגישה מוגנת
@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.
אסור לחשוף משאבים של פריסות ומשאבים שניתנים לציור כממשקי API ציבוריים. אם בכל זאת צריך לחשוף אותם, חובה לתת שמות לפריסות ולמשאבים הגרפיים הציבוריים באמצעות מוסכמת השמות under_score, למשל layout/simple_list_item_1.xml או drawable/title_bar_tall.xml.
כשקבועים יכולים להשתנות, כדאי להפוך אותם לדינמיים
יכול להיות שהקומפיילר יטמיע ערכים קבועים, ולכן שמירה על ערכים זהים נחשבת לחלק מחוזה ה-API. אם הערך של קבוע MIN_FOO או MAX_FOO עשוי להשתנות בעתיד, כדאי להשתמש במקום זאת בשיטות דינמיות.
CameraManager.MAX_CAMERAS
CameraManager.getMaxCameras()
כדאי להביא בחשבון תאימות קדימה לקריאות חוזרות (callback)
אפליקציות שמטרגטות ממשקי 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, מחלקות נתונים צריכות לספק בנאי עם ארגומנטים שמוגדרים כברירת מחדל, ללא קשר למספר המאפיינים. יכול להיות שיהיה יתרון גם בהוספת builder למחלקות נתונים שמוגדרות ב-Kotlin כשמטרגטים לקוחות 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 בלבד)
כדי לשמור על תאימות בינארית, צריך להוסיף הערה לשיטות ול-constructors עם פרמטרים שמוגדרים כברירת מחדל באמצעות @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, אבל אפשר גם להשתמש ב-Builders כדי לשפר את השימושיות עבור לקוחות Java, כמו במקרים שצוינו קודם.
class Tone @JvmOverloads constructor(
val duration: Long = 1000,
val frequency: Int = 2600,
val dtmfConfigs: List<DtmfConfig> = emptyList()
) {
class Builder {
// ...
}
}
מחלקות Builder צריך להחזיר את ה-Builder
במחלקות Builder צריך להפעיל שרשור של שיטות על ידי החזרת אובייקט Builder (למשל this) מכל שיטה, חוץ מ-build(). צריך להעביר אובייקטים מובנים נוספים כארגומנטים – לא להחזיר את ה-builder של אובייקט אחר.
לדוגמה:
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.
אם לא צוינו ארגומנטים נדרשים, ה-constructor של ה-builder צריך להקפיץ הודעת שגיאה (throw) NullPointerException (מומלץ להשתמש ב-Objects.requireNonNull).
מחלקות ה-Builder צריכות להיות מחלקות פנימיות סטטיות סופיות של הסוגים שהן בונות
כדי לשמור על ארגון לוגי בחבילה, בדרך כלל כדאי לחשוף את מחלקות ה-builder כמחלקות פנימיות סופיות של הסוגים שהן יוצרות, למשל Tone.Builder ולא ToneBuilder.
יכול להיות שיוצרים יכללו ב-Builder קונסטרקטור כדי ליצור מופע חדש ממופע קיים
יכול להיות ש-Builders יכללו בנאי העתקה כדי ליצור מופע Builder חדש מ-Builder קיים או מאובייקט בנוי. לא מומלץ לספק שיטות חלופיות ליצירת מופעים של 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 צריכות לקבל ארגומנטים עם הערה @Nullable
איפוס הוא חיוני אם יכול להיות שייווצר מופע חדש של כלי בנייה ממופע קיים. אם אין constructor של עותק, יכול להיות ש-builder יכלול ארגומנטים של @Nullable או @NonNullable.
public static class Builder {
public Builder(Builder original);
public Builder setObjectValue(@Nullable Object value);
}
יכול להיות ששיטות setter של Builder יקבלו ארגומנטים מסוג @Nullable עבור מאפיינים אופציונליים
לרוב, קל יותר להשתמש בערך שניתן להגדרה כ-null עבור קלט מדרגה שנייה, במיוחד ב-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()
ערך ברירת המחדל (אם לא קוראים לשיטת ה-setter) והמשמעות של null חייבים להיות מתועדים בצורה נכונה גם בשיטת ה-setter וגם בשיטת ה-getter.
/**
* ...
*
* <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 אמורה להחזיר מופע של האובייקט שנבנה, שלא יכול להיות null. אם אי אפשר ליצור את האובייקט בגלל פרמטרים לא תקינים, אפשר לדחות את האימות לשיטת הבנייה ולהפעיל את 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 בדגלים של מסיכת ביטים מפורטות הנחיות לשימוש ב-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 ב-API ציבורי. בטווח הארוך, גישה מוגנת עלולה להקשות על העבודה, כי המפתחים צריכים לבטל את ההגדרה כדי לספק רכיבי גישה ציבוריים במקרים שבהם גישה חיצונית כברירת מחדל הייתה מספיקה.
חשוב לזכור שprotected הגדרת החשיפה לא מונעת ממפתחים לקרוא ל-API – היא רק הופכת את הפעולה למעט יותר מעצבנת.
הטמעה של אף אחת מהשיטות equals() ו-hashCode() או הטמעה של שתיהן
אם מבטלים את ההגדרה של אחד מהם, צריך לבטל גם את ההגדרה של השני.
הטמעה של toString() למחלקות נתונים
מומלץ להגדיר מחלקות נתונים כך שיבטלו את ההגדרה של toString(), כדי לעזור למפתחים לנפות באגים בקוד שלהם.
תיעוד אם הפלט הוא להתנהגות התוכנית או לניפוי באגים
מחליטים אם רוצים שההתנהגות של התוכנית תסתמך על ההטמעה שלכם או לא. לדוגמה, הפורמט הספציפי של התוכניות לשימוש מפורט במסמכים UUID.toString() ו-File.toString(). אם אתם חושפים מידע רק לצורך ניפוי באגים, כמו Intent, אתם יכולים להסיק שהמסמכים עוברים בירושה ממחלקת העל.
לא לכלול מידע נוסף
כל המידע שזמין מ-toString() צריך להיות זמין גם דרך ממשק ה-API הציבורי של האובייקט. אחרת, אתם מעודדים מפתחים לנתח את הפלט של toString() ולהסתמך עליו, מה שימנע שינויים עתידיים. מומלץ להטמיע את toString() באמצעות ה-API הציבורי של האובייקט בלבד.
המלצה לא להסתמך על פלט של ניפוי באגים
אי אפשר למנוע ממפתחים להסתמך על פלט של ניפוי באגים, אבל אם תכללו את System.identityHashCode של האובייקט בפלט toString() שלו, הסיכוי ששני אובייקטים שונים יפיקו פלט toString() זהה יהיה נמוך מאוד.
@Override
public String toString() {
return getClass().getSimpleName() + "@" + Integer.toHexString(System.identityHashCode(this)) + " {mFoo=" + mFoo + "}";
}
הדבר הזה יכול להרתיע מפתחים מלכתוב טענות בדיקה כמו
assertThat(a.toString()).isEqualTo(b.toString()) באובייקטים שלכם.
שימוש בפונקציה createFoo כשמחזירים אובייקטים שנוצרו לאחרונה
משתמשים בקידומת create, ולא ב-get או ב-new, לשיטות שייצרו ערכי החזרה, למשל על ידי בניית אובייקטים חדשים.
אם השיטה תיצור אובייקט כדי להחזיר אותו, צריך לציין זאת בבירור בשם השיטה.
public FooThing getFooThing() {
return new FooThing();
}
public FooThing createFooThing() {
return new FooThing();
}
בשיטות שמקבלות אובייקטים של קבצים צריך להיות אפשר לקבל גם זרמים
מיקומי אחסון הנתונים ב-Android לא תמיד הם קבצים בדיסק. לדוגמה,
תוכן שעובר בין גבולות משתמשים מיוצג כ-content:// Uris. כדי לאפשר עיבוד של מקורות נתונים שונים, ממשקי API שמקבלים אובייקטים של File צריכים לקבל גם InputStream, OutputStream או את שניהם.
public void setDataSource(File file)
public void setDataSource(InputStream stream)
החזרה של פרימיטיבים גולמיים במקום גרסאות ממוסגרות
אם אתם צריכים להעביר ערכים חסרים או ערכי null, כדאי להשתמש ב--1, Integer.MAX_VALUE או Integer.MIN_VALUE.
public java.lang.Integer getLength()
public void setLength(java.lang.Integer)
public int getLength()
public void setLength(int value)
הימנעות משימוש במחלקות שוות ערך לסוגים פרימיטיביים מונעת את התקורה של הזיכרון של המחלקות האלה, גישת method לערכים, וחשוב מכך, autoboxing שנובע מהמרת סוגים בין סוגים פרימיטיביים לסוגי אובייקטים. הימנעות מהתנהגויות כאלה חוסכת זיכרון והקצאות זמניות שעלולות להוביל לאיסוף אשפה יקר ותכוף יותר.
שימוש בהערות כדי להבהיר ערכים תקינים של פרמטרים וערכי החזרה
הוספנו הערות למפתחים כדי להבהיר את הערכים המותרים במצבים שונים. כך קל יותר לכלים לעזור למפתחים כשהם מספקים ערכים שגויים (לדוגמה, העברת ערך שרירותי int כשנדרש ערך קבוע מתוך קבוצה ספציפית של ערכים). אפשר להשתמש בכל אחת מהאנוטציות הבאות כשזה מתאים:
מאפיין המציין אם ערך יכול להיות ריק (nullability)
בממשקי API של Java נדרשות הערות מפורשות לגבי האפשרות שערך יהיה ריק (nullability), אבל הרעיון של nullability הוא חלק משפת Kotlin, ולכן אסור להשתמש בהערות לגבי nullability בממשקי 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.
משאבים
מזהי משאבים: פרמטרים של מספרים שלמים שמציינים מזהים של משאבים ספציפיים צריכים להיות מתויגים בהגדרה המתאימה של סוג המשאב.
יש הערה לכל סוג משאב, כמו @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 |
| טיעון משעשע | לא כולל הערות | לא מוערך או ניתן לאיפוס |
| טיעון משעשע | 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 עבור מאפיין לוגי יחיד צריכים תמיד להיות זהים בהערות בנוגע למאפיין המציין אם ערך יכול להיות ריק (nullability). אם לא פועלים לפי ההנחיות האלה, תחביר המאפיינים של Kotlin לא יעבוד, ולכן הוספת אנוטציות שונות לגבי מאפיין המציין אם ערך יכול להיות ריק (nullability) לשיטות מאפיינים קיימות היא שינוי תוכנה שעלול לגרום לכשל עבור משתמשי 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 חזקים יותר לגבי ייחודיות וסדר, תמיכה בגנריות ומספר שיטות נוחות לידידותיות למפתחים.
חריגות לגבי פרימיטיבים
אם הרכיבים הם פרימיטיבים, עדיף להשתמש במערכים כדי להימנע מהעלות של המרה אוטומטית של ערכים פרימיטיביים לאובייקטים. איך מחזירים פרימיטיבים גולמיים במקום גרסאות boxed
חריג לקוד שרגיש לביצועים
בתרחישים מסוימים, שבהם נעשה שימוש ב-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, עדיף להשתמש כברירת מחדל בסוגי החזרה ניתנים לשינוי, כי ההטמעה של ממשקי API של Java בפלטפורמת Android עדיין לא מספקת הטמעה נוחה של אוספים שלא ניתן לשנות. היוצא מן הכלל במקרה הזה הוא Collections.empty סוגי החזרה, שהם בלתי ניתנים לשינוי. במקרים שבהם לקוחות יכולים לנצל את האפשרות לשינוי – בכוונה או בטעות – כדי לשבש את דפוס השימוש המיועד של ה-API, ממשקי Java API צריכים לשקול בחיוב להחזיר עותק שטחי של האוסף.
@Nullable
public PermissionInfo[] getGrantedPermissions() {
return mPermissions;
}
@NonNull
public Set<PermissionInfo> getGrantedPermissions() {
if (mPermissions == null) {
return Collections.emptySet();
}
return new ArraySet<>(mPermissions);
}
סוגי החזרה שניתנים לשינוי באופן מפורש
ממשקי API שמחזירים אוספים לא אמורים לשנות את אובייקט האוסף שמוחזר אחרי ההחזרה. אם צריך לשנות את האוסף שמוחזר או לעשות בו שימוש חוזר בדרך כלשהי – לדוגמה, תצוגה מותאמת של מערך נתונים שניתן לשינוי – צריך לתעד במפורש את ההתנהגות המדויקת של מתי התוכן יכול להשתנות, או לפעול בהתאם למוסכמות השמות המקובלות של API.
/**
* Returns a view of this object as a list of [Item]s.
*/
fun MyObject.asList(): List<Item> = MyObjectListWrapper(this)
מוסכמת השמות של Kotlin .asFoo() מתוארת בהמשך, והיא מאפשרת לשנות את האוסף שמוחזר על ידי .asList() אם האוסף המקורי משתנה.
האפשרות לשנות אובייקטים של סוגי נתונים שהוחזרו
בדומה לממשקי API שמחזירים אוספים, ממשקי API שמחזירים אובייקטים של סוג נתונים לא אמורים לשנות את המאפיינים של האובייקט שמוחזר אחרי ההחזרה.
val tempResult = DataContainer()
fun add(other: DataContainer): DataContainer {
tempResult.innerValue = innerValue + other.innerValue
return tempResult
}
fun add(other: DataContainer): DataContainer {
return DataContainer(innerValue + other.innerValue)
}
במקרים מאוד מוגבלים, קוד שרגיש לביצועים עשוי להפיק תועלת מאיגום או משימוש חוזר באובייקטים. אל תיצרו מבנה נתונים משלכם של מאגר אובייקטים ואל תחשפו אובייקטים שעברו שימוש חוזר בממשקי API ציבוריים. בכל מקרה, חשוב מאוד לנהל את הגישה בו-זמנית בזהירות רבה.
שימוש בסוג פרמטר vararg
מומלץ להשתמש ב-vararg בממשקי Kotlin ו-Java API במקרים שבהם סביר שהמפתח ייצור מערך באתר הקריאה רק כדי להעביר כמה פרמטרים קשורים מאותו סוג.
public void setFeatures(Feature[] features) { ... }
// Developer code
setFeatures(new Feature[]{Features.A, Features.B, Features.C});
public void setFeatures(Feature... features) { ... }
// Developer code
setFeatures(Features.A, Features.B, Features.C);
עותקים להגנה
ההטמעות של הפרמטרים vararg ב-Java וב-Kotlin עוברות קומפילציה לאותו בייטקוד שמגובה במערך, ולכן אפשר להפעיל אותן מקוד Java עם מערך שניתן לשינוי. מומלץ מאוד למעצבי API ליצור עותק שטחי של פרמטר המערך במקרים שבהם הוא יישמר בשדה או במחלקה פנימית אנונימית.
public void setValues(SomeObject... values) {
this.values = Arrays.copyOf(values, values.length);
}
שימו לב: יצירת עותק הגנתי לא מספקת הגנה מפני שינוי בו-זמני בין הפעלת method הראשונית לבין יצירת העותק, וגם לא מפני מוטציה של האובייקטים שנכללים במערך.
צריך לספק סמנטיקה נכונה באמצעות פרמטרים של סוג האוסף או סוגים שמוחזרים
List<Foo> היא אפשרות ברירת המחדל, אבל כדאי לשקול סוגים אחרים כדי לספק משמעות נוספת:
משתמשים ב-
Set<Foo>אם אין חשיבות לסדר האלמנטים ב-API, והוא לא מאפשר כפילויות או שהן חסרות משמעות.
Collection<Foo>,אם אין חשיבות לסדר בממשק ה-API והוא מאפשר כפילויות.
פונקציות המרה ב-Kotlin
ב-Kotlin נעשה שימוש תדיר ב-.toFoo() וב-.asFoo() כדי לקבל אובייקט מסוג אחר מאובייקט קיים, כאשר Foo הוא שם סוג ההחזרה של ההמרה. התקופה הזו נקבעה בהתאם ל-JDK המוכר
Object.toString(). ב-Kotlin, השימוש ב-nullable מתרחב גם להמרות פרימיטיביות כמו 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 כדי לאפשר למפתחים לטפל בחריגות בלי להגדיר טווח רחב מדי.
שגיאות שלא קשורות לארגומנטים שסופקו ישירות לשיטה שהופעלה באופן ציבורי צריכות להקפיץ הודעת שגיאה (throw) java.lang.IllegalStateException במקום java.lang.IllegalArgumentException או java.lang.NullPointerException.
מאזינים ושיחות חוזרות
אלה הכללים לגבי המחלקות וה-methods שמשמשים למנגנוני listener ו-callback.
שמות של מחלקות קריאה חוזרת צריכים להיות ביחיד
במקום זאת, צריך להשתמש ב-MyObjectCallback.MyObjectCallbacks
שמות של שיטות קריאה חוזרת צריכים להיות בפורמט on
הערך onFooEvent מציין שמתרחש FooEvent ופונקציית ה-callback צריכה לפעול בתגובה.
השימוש בזמן עבר או בזמן הווה צריך לתאר את התנהגות התזמון
כשנותנים שמות לשיטות של קריאה חוזרת שקשורות לאירועים, צריך לציין אם האירוע כבר קרה או שהוא קורה כרגע.
לדוגמה, אם המתודה מופעלת אחרי ביצוע פעולת קליק:
public void onClicked()
עם זאת, אם השיטה אחראית לביצוע פעולת הקליק:
public boolean onClick()
רישום לשיחה חוזרת
כשניתן להוסיף או להסיר listener או 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);
הימנעות משימוש ב-getters לקריאות חוזרות (callbacks)
לא מוסיפים שיטות 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 נקודות חשובות לגבי ההטמעה: שימו לב שהקוד הבא הוא קוד תקף לביצוע!
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 מתוך
מבטלת את התרחיש הרצוי הזה.
אם אפליקציה עומדת להריץ קוד יקר איפשהו בתהליך שלה, כדאי לאפשר לה לעשות את זה. יהיה קשה יותר לתמוך בפתרונות העקיפים שמפתחי האפליקציות ימצאו כדי לעקוף את ההגבלות שלכם בטווח הארוך.
חריג לשיחת callback יחידה: אם אופי האירועים שמדווחים מחייב תמיכה רק במופע יחיד של שיחת callback, צריך להשתמש בסגנון הבא:
public void setFooCallback(
@NonNull @CallbackExecutor Executor executor,
@NonNull FooCallback callback)
public void clearFooCallback()
שימוש ב-Executor במקום ב-Handler
בעבר, נעשה שימוש ב-Handler של Android כסטנדרט להפניה אוטומטית של ביצוע קריאה חוזרת (callback) לשרשור Looper ספציפי. התקן הזה שונה כדי להעדיף את Executor, כי רוב מפתחי האפליקציות מנהלים את מאגרי השרשורים שלהם, ולכן השרשור הראשי או שרשור UI הוא השרשור היחיד Executor שזמין לאפליקציה. כדאי להשתמש ב-Executor כדי לתת למפתחים את השליטה שהם צריכים כדי לעשות שימוש חוזר בהקשרים הקיימים או המועדפים שלהם.Looper
ספריות מודרניות של פעולות מקבילות, כמו kotlinx.coroutines או RxJava, מספקות מנגנוני תזמון משלהן שמבצעים שליחה משלהן כשצריך, ולכן חשוב לספק את האפשרות להשתמש ב-executor ישיר (כמו Runnable::run) כדי למנוע זמן אחזור כתוצאה ממעברים כפולים בין השרשורים. לדוגמה, צעד אחד כדי לשלוח בשרשור Looper באמצעות Handler ואז צעד נוסף ממסגרת הבו-זמניות של האפליקציה.
יש מעט מאוד מקרים חריגים להנחיה הזו. אלה כמה מהערעורים הנפוצים לבקשת חריגה:
אני צריך להשתמש בLooper כי אני צריך Looper כדי epoll לאירוע.
הבקשה להחרגה אושרה כי אי אפשר לממש את היתרונות של Executor במצב הזה.
אני לא רוצה שקוד האפליקציה יחסום את פרסום האירוע בשרשור. בקשת החריגה הזו בדרך כלל לא מאושרת לקוד שפועל בתהליך של אפליקציה. אפליקציות שמציגות את המידע הזה בצורה שגויה פוגעות רק בעצמן, ולא משפיעות על תקינות המערכת באופן כללי. אפליקציות שמבצעות את הפעולות האלה בצורה נכונה או משתמשות במסגרת נפוצה של בו-זמניות (concurrency) לא צריכות לשלם קנסות נוספים על זמן טעינה.
Handler עקבי באופן מקומי עם ממשקי API דומים אחרים באותו סוג.
בקשת ההחרגה הזו מאושרת בהתאם לנסיבות. העדפה היא להוסיף עומסים יתרים מבוססי Executor, להעביר הטמעות של Handler לשימוש בהטמעה החדשה של Executor. (myHandler::post הוא Executor חוקי!) בהתאם לגודל המחלקה, למספר ה-methods הקיימים מסוג Handler ולסבירות שהמפתחים יצטרכו להשתמש ב-methods קיימים שמבוססים על Handler לצד ה-method החדש, יכול להיות שתינתן חריגה כדי להוסיף method חדש שמבוסס על Handler.
סימטריה ברישום
אם יש דרך להוסיף או לרשום משהו, צריכה להיות גם דרך להסיר או לבטל את הרישום שלו. השיטה
registerThing(Thing)
צריך להיות תואם
unregisterThing(Thing)
צריך לספק מזהה בקשה
אם הגיוני שמפתח ישתמש שוב בפונקציית callback, צריך לספק אובייקט מזהה כדי לקשר את פונקציית ה-callback לבקשה.
class RequestParameters {
public int getId() { ... }
}
class RequestExecutor {
public void executeRequest(
RequestParameters parameters,
Consumer<RequestParameters> onRequestCompletedListener) { ... }
}
אובייקטים של קריאה חוזרת עם כמה שיטות
במקרה של קריאות חוזרות (callback) עם כמה שיטות, מומלץ להשתמש ב-interface ובשיטות default כשמוסיפים לממשקים שפורסמו בעבר. בעבר, ההמלצה בהנחיה הזו הייתה להשתמש ב-abstract class בגלל היעדר שיטות default ב-Java 7.
public interface MostlyOptionalCallback {
void onImportantAction();
default void onOptionalInformation() {
// Empty stub, this method is optional.
}
}
שימוש ב-android.os.OutcomeReceiver כשמבצעים מודלינג של קריאה לפונקציה לא חוסמת
OutcomeReceiver<R,E> מחזירה ערך תוצאה R אם הפעולה הצליחה, או E : Throwable אחרת – בדיוק כמו שקורה כשמבצעים הפעלת method רגילה. משתמשים ב-OutcomeReceiver כסוג של קריאה חוזרת (callback) כשממירים method חוסם שמחזיר תוצאה או מקפיץ הודעת שגיאה (throw) ל-method אסינכרוני לא חוסם:
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 הייתה יוצרת מדווחת במקום זאת לשיטה 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:() -> UnitSupplier<R>: () -> RConsumer<T>: (T) -> UnitFunction<T,R>: (T) -> RPredicate<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
במסמכי Docs צריך להיות קישור למסמכי Docs אחרים שכוללים קבועים, שיטות ואלמנטים אחרים שקשורים לנושא. צריך להשתמש בתגי 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, וצריך לוודא שהפלט נראה כמו שציפיתם. פלט ERROR ב-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 לתיעוד חריגים
אם מתודה זורקת חריג מסומן, לדוגמה 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 מתקן את השגיאה הזו באופן אוטומטי על ידי הוספת רווח קשיח אחרי הנקודה, אבל עדיף להימנע מהשגיאה הזו מלכתחילה.
עיצוב מסמכי Docs לעיבוד ב-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 framework (לדוגמה, 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
חייב לשמש במקום enum בכל ממשקי ה-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, ולכן ניסיון של אפליקציה לבטל הטמעה של framework.jar הוא לא חוקי.Parcelable
אם האפליקציה השולחת מרחיבה את Parcelable, לאפליקציה המקבלת לא תהיה הטמעה מותאמת אישית של השולח כדי לפתוח את האריזה. הערה לגבי תאימות לאחור: אם בעבר המחלקה לא הייתה סופית, אבל לא היה לה בנאי שזמין לציבור, עדיין אפשר לסמן אותה כ-final.
שיטות שקוראות לתהליך המערכת צריכות להפעיל מחדש את RemoteException כ-RuntimeException
RemoteException בדרך כלל מוחזר על ידי AIDL פנימי, ומציין שתהליך המערכת הסתיים או שהאפליקציה מנסה לשלוח יותר מדי נתונים. בשני המקרים, ה-API הציבורי צריך להפעיל מחדש את הפעולה כ-RuntimeException כדי למנוע מאפליקציות לשמור החלטות אבטחה או מדיניות.
אם אתם יודעים שהצד השני של קריאת Binder הוא תהליך המערכת, קוד שחוזר על עצמו (boilerplate) זה הוא השיטה המומלצת:
try {
...
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
הצגת חריגים ספציפיים לשינויים ב-API
ההתנהגויות של ממשקי API ציבוריים עשויות להשתנות בין רמות API שונות ולגרום לקריסת האפליקציה (לדוגמה, כדי לאכוף מדיניות אבטחה חדשה).
כאשר ה-API צריך להקפיץ הודעת שגיאה (throw) לבקשה שהייתה תקפה בעבר, יש להקפיץ הודעת שגיאה (throw) ספציפית חדשה במקום הודעת שגיאה (throw) כללית. לדוגמה, ExportedFlagRequired במקום SecurityException (ו-ExportedFlagRequired יכול להרחיב את SecurityException).
כך מפתחי אפליקציות וכלים יוכלו לזהות שינויים בהתנהגות של ה-API.
הטמעה של בנאי העתקה במקום שיבוט
מומלץ מאוד להימנע משימוש בשיטה clone() של Java בגלל היעדר חוזי API שסופקו על ידי המחלקה Object, ובגלל הקשיים שקיימים בהרחבת מחלקות שמשתמשות ב-clone(). במקום זאת, צריך להשתמש בבנאי עותק שמקבל אובייקט מאותו סוג.
/**
* Constructs a shallow copy of {@code other}.
*/
public Foo(Foo other)
במקרים שבהם מחלקות מסתמכות על Builder לצורך בנייה, כדאי להוסיף Builder copy constructor כדי לאפשר שינויים בעותק.
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 בכיתה רלוונטית, שבדרך כלל היא כיתה מסוג 'manager'. מוסיפים מנגנון של מאזין או שידור כדי להודיע ללקוחות על שינויים לפי הצורך.
יש כמה בעיות בהגדרות של 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 interop
רשימה מלאה של הנחיות זמינה במדריך הרשמי של Android בנושא פעולות הדדיות בין Kotlin ל-Java. העתקנו חלק מההנחיות למדריך הזה כדי לשפר את יכולת הגילוי.
הרשאות גישה ל-API
חלק מממשקי ה-API של Kotlin, כמו suspend funs, לא מיועדים לשימוש על ידי מפתחי Java. עם זאת, לא מומלץ לנסות לשלוט בנראות הספציפית לשפה באמצעות @JvmSynthetic, כי יש לכך תופעות לוואי שמשפיעות על האופן שבו ה-API מוצג במאגרי באגים, מה שמקשה על איתור באגים.
הנחיות ספציפיות זמינות במדריך לשימוש משולב ב-Kotlin וב-Java או במדריך לשימוש אסינכרוני.
אובייקטים נלווים
ב-Kotlin משתמשים ב-companion object כדי לחשוף חברים סטטיים. במקרים מסוימים, הם יופיעו מ-Java במחלקה פנימית בשם Companion ולא במחלקה המכילה. יכול להיות שקבצים של טקסט API של כיתות Companion יופיעו ככיתות ריקות – זה תקין.
כדי למקסם את התאימות ל-Java, צריך להוסיף הערות לאובייקטים נלווים של שדות לא קבועים עם @JvmField ופונקציות ציבוריות עם @JvmStatic כדי לחשוף אותם ישירות במחלקה המכילה.
companion object {
@JvmField val BIG_INTEGER_ONE = BigInteger.ONE
@JvmStatic fun fromPointF(pointf: PointF) {
/* ... */
}
}
התפתחות של ממשקי API בפלטפורמת Android
בקטע הזה מפורטים כללי מדיניות לגבי סוגי השינויים שאפשר לבצע בממשקי Android API קיימים, ואיך צריך להטמיע את השינויים האלה כדי למקסם את התאימות לאפליקציות ולבסיסי קוד קיימים.
שינויים שגורמים לשגיאות בינאריות
מומלץ להימנע משינויים שגורמים לשגיאות בינאריות בממשקי API ציבוריים סופיים. בדרך כלל, שינויים מהסוג הזה גורמים לשגיאות כשמריצים את make update-api, אבל יכול להיות שיש מקרים חריגים שבהם בדיקת ה-API של Metalava לא מזהה אותם. אם אתם לא בטוחים, תוכלו לעיין במדריך של Eclipse Foundation בנושא Evolving Java-based APIs (פיתוח ממשקי API מבוססי Java) כדי לקבל הסבר מפורט על סוגי השינויים ב-API שתואמים ל-Java. שינויים שגורמים לשבירת תאימות בינארית בממשקי API מוסתרים (לדוגמה, מערכת) צריכים לפעול לפי מחזור הוצאה משימוש/החלפה.
שינויים שגורמים לבעיות במקור
אנחנו לא ממליצים על שינויים שגורמים לבעיות בקוד המקור, גם אם הם לא גורמים לבעיות בבינארי. דוגמה לשינוי ששומר על תאימות בינארית אבל לא תאימות למקור היא הוספת גנריקה למחלקה קיימת. השינוי הזה שומר על תאימות בינארית אבל עלול לגרום לשגיאות קומפילציה בגלל ירושה או הפניות לא חד-משמעיות.
שינויים שגורמים לשבירת תאימות לא יגרמו לשגיאות כשמריצים את הפקודה make update-api, ולכן חשוב להבין את ההשפעה של שינויים בחתימות קיימות של API.
במקרים מסוימים, שינויים שגורמים לשבירת קוד המקור נדרשים כדי לשפר את חוויית המפתחים או את נכונות הקוד. לדוגמה, הוספת אנוטציות לגבי מאפיין המציין אם ערך יכול להיות ריק (nullability) למקורות Java משפרת את יכולת הפעולה ההדדית עם קוד Kotlin ומקטינה את הסיכוי לשגיאות, אבל לרוב דורשת שינויים – לפעמים שינויים משמעותיים – בקוד המקור.
שינויים בממשקי API פרטיים
אפשר לשנות ממשקי API עם ההערה @TestApi בכל שלב.
חובה לשמור ממשקי API עם ההערה @SystemApi למשך שלוש שנים. צריך להסיר או לארגן מחדש (Refactor) API של מערכת לפי לוח הזמנים הבא:
- API y - נוסף
- API y+1 – הוצאה משימוש
- מסמנים את הקוד עם
@Deprecated. - מוסיפים תחליפים ומקשרים לתחליף ב-Javadoc של הקוד שהוצא משימוש באמצעות ההערה
@deprecateddocs. - במהלך מחזור הפיתוח, מדווחים על באגים למשתמשים פנימיים ומציינים שה-API יוצא משימוש. כך אפשר לוודא שממשקי ה-API החלופיים מתאימים.
- מסמנים את הקוד עם
- API y+2 – הסרה רכה
- מסמנים את הקוד עם
@removed. - אופציונלי: אפשר להפעיל או להשבית את האפשרות 'throw' באפליקציות שמטרגטות את רמת ה-SDK הנוכחית של הגרסה.
- מסמנים את הקוד עם
- API y+3 – הסרה סופית
- מסירים לחלוטין את הקוד מעץ המקור.
הוצאה משימוש
אנחנו רואים בהוצאה משימוש שינוי ב-API, והיא יכולה להתרחש בגרסה ראשית (למשל, גרסה שמסומנת באות). כשמוציאים משימוש ממשקי API, כדאי להשתמש יחד בהערת המקור @Deprecated ובהערת התיעוד @deprecated
<summary>. חובה לכלול באסטרטגיה סיכום של תהליך ההעברה. יכול להיות שהאסטרטגיה תכלול קישור ל-API חלופי או הסבר למה לא כדאי להשתמש ב-API:
/**
* Simple version of ...
*
* @deprecated Use the {@link androidx.fragment.app.DialogFragment}
* class with {@link androidx.fragment.app.FragmentManager}
* instead.
*/
@Deprecated
public final void showDialog(int id)
חובה גם להוציא משימוש ממשקי API שמוגדרים ב-XML ונחשפים ב-Java, כולל מאפיינים ומאפיינים שניתנים לעיצוב שנחשפים במחלקה android.R, עם סיכום:
<!-- Attribute whether the accessibility service ...
{@deprecated Not used by the framework}
-->
<attr name="canRequestEnhancedWebAccessibility" format="boolean" />
מתי מוציאים משימוש API
הוצאה משימוש מועילה בעיקר כדי למנוע שימוש ב-API בקוד חדש.
אנחנו גם דורשים לסמן ממשקי API כ@deprecated לפני שהם @removed, אבל זה לא מספק למפתחים תמריץ חזק מספיק להפסיק להשתמש בממשק API שהם כבר משתמשים בו.
לפני שמוציאים משימוש API, חשוב לחשוב על ההשפעה על מפתחים. ההשפעות של הוצאה משימוש של API כוללות:
javacמציג אזהרה במהלך ההידור.- אי אפשר להשבית את אזהרות ההוצאה משימוש באופן גלובלי או להגדיר אותן כנקודת בסיס, ולכן מפתחים שמשתמשים ב-
-Werrorצריכים לתקן או להשבית כל שימוש בממשק API שהוצא משימוש לפני שהם יכולים לעדכן את גרסת ה-SDK של הקומפילציה. - אי אפשר להסתיר אזהרות על הוצאה משימוש בייבוא של מחלקות שהוצאו משימוש. לכן, מפתחים צריכים להוסיף את שם המחלקה שמוגדר במלואו לכל שימוש במחלקה שהוצאה משימוש, לפני שהם מעדכנים את גרסת ה-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 ושימוש בהערה @removed <summary> של מסמכי API כשמסירים API באופן רך. הסיכום חייב לכלול את הסיבה להסרה, והוא יכול לכלול אסטרטגיית העברה, כפי שהסברנו במאמר בנושא הוצאה משימוש.
ההתנהגות של ממשקי API שהוסרו באופן זמני יכולה להישאר כמו שהיא, אבל חשוב יותר שתישמר כדי שקריאות קיימות לא יגרמו לקריסה כשמפעילים את ה-API. במקרים מסוימים, זה יכול להיות שימור של התנהגות מסוימת.
חובה לשמור על כיסוי הבדיקות, אבל יכול להיות שיהיה צורך לשנות את תוכן הבדיקות כדי להתאים לשינויים בהתנהגות. הבדיקות צריכות עדיין לוודא ששיחות קיימות לא קורסות בזמן הריצה. אתם יכולים לשמור על ההתנהגות של ממשקי API שהוסרו באופן זמני כמו שהיא, אבל מה שחשוב יותר הוא לשמור על ההתנהגות כך שקריאות קיימות ל-API לא יגרמו לקריסה. במקרים מסוימים, זה עשוי להיות כרוך בשמירה על התנהגות מסוימת.
חובה לשמור על כיסוי הבדיקות, אבל יכול להיות שיהיה צורך לשנות את תוכן הבדיקות כדי להתאים לשינויים בהתנהגות. הבדיקות צריכות עדיין לוודא ששיחות קיימות לא קורסות בזמן הריצה.
ברמה הטכנית, אנחנו מסירים את ה-API מקובץ ה-JAR של ה-SDK stub ומנתיב המחלקות בזמן ההידור באמצעות @remove Javadoc annotation, אבל הוא עדיין קיים בנתיב המחלקות בזמן הריצה – בדומה לממשקי @hide API:
/**
* 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 שלה, אבל לאחרונה עברנו לדרישה לשימוש ב-Framework לתאימות אפליקציות. הנה דוגמה להטמעה של שינוי בהתנהגות באמצעות המסגרת החדשה הזו:
import android.app.compat.CompatChanges;
import android.compat.annotation.ChangeId;
import android.compat.annotation.EnabledSince;
public class MyClass {
@ChangeId
// This means the change will be enabled for target SDK R and higher.
@EnabledSince(targetSdkVersion=android.os.Build.VERSION_CODES.R)
// Use a bug number as the value, provide extra detail in the bug.
// FOO_NOW_DOES_X will be the change name, and 123456789 the change ID.
static final long FOO_NOW_DOES_X = 123456789L;
public void doFoo() {
if (CompatChanges.isChangeEnabled(FOO_NOW_DOES_X)) {
// do the new thing
} else {
// do the old thing
}
}
}
השימוש בעיצוב של מסגרת התאימות לאפליקציות מאפשר למפתחים להשבית באופן זמני שינויים ספציפיים בהתנהגות במהלך גרסאות טרום-הפצה וגרסאות בטא כחלק מניפוי הבאגים באפליקציות שלהם, במקום לחייב אותם להתאים את האפליקציות לעשרות שינויים בהתנהגות בו-זמנית.
תאימות קדימה
תאימות קדימה היא מאפיין עיצובי שמאפשר למערכת לקבל קלט שמיועד לגרסה מאוחרת יותר של עצמה. במקרה של עיצוב API, צריך לשים לב במיוחד לעיצוב הראשוני ולשינויים עתידיים, כי מפתחים מצפים לכתוב קוד פעם אחת, לבדוק אותו פעם אחת ולהריץ אותו בכל מקום ללא בעיות.
הגורמים הבאים הם הסיבות הנפוצות ביותר לבעיות תאימות קדימה ב-Android:
- הוספה של קבועים חדשים לקבוצה (כמו
@IntDefאוenum) שבעבר נחשבה לקבוצה מלאה (לדוגמה, אם ל-switchישdefaultשיוצר חריגה). - הוספת תמיכה בתכונה שלא נכללת ישירות בממשק ה-API (לדוגמה, תמיכה בהקצאת משאבים מסוג
ColorStateListב-XML, כאשר קודם לכן נתמכו רק משאבים מסוג<color>). - הסרת הגבלות על בדיקות בזמן ריצה, למשל הסרת בדיקה של
requireNotNull()שהייתה קיימת בגרסאות קודמות.
בכל המקרים האלה, המפתחים מגלים שמשהו לא בסדר רק בזמן הריצה. גרוע מכך, הם עלולים לגלות זאת כתוצאה מדוחות קריסה ממכשירים ישנים יותר בשטח.
בנוסף, כל המקרים האלה הם שינויים ב-API שהם תקינים מבחינה טכנית. הם לא פוגעים בתאימות בינארית או בתאימות למקור, וכלי ה-lint של ה-API לא יזהה אף אחת מהבעיות האלה.
לכן, מעצבי API צריכים לשים לב היטב כשהם משנים מחלקות קיימות. שואלים את השאלה: "האם השינוי הזה יגרום לכך שקוד שנכתב ונבדק רק בגרסה העדכנית ביותר של הפלטפורמה ייכשל בגרסאות ישנות יותר?"
סכימות XML
אם סכימת XML משמשת כממשק יציב בין רכיבים, צריך לציין אותה באופן מפורש, והיא צריכה להתפתח באופן שתואם לאחור, בדומה לממשקי API אחרים של Android. לדוגמה, צריך לשמור על המבנה של רכיבי XML ומאפיינים, בדומה לאופן שבו נשמרים methods ומשתנים בממשקי 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 OS בנפרד, במקום לעדכן את כל קובץ אימג' של המערכת.
צריך להפריד את המודולים של 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 בקוד מקורי, אתם משתמשים בממשקי ה-API המקוריים של 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.