本頁面旨在為開發人員提供指南,瞭解 API 委員會在 API 審查中強制執行的一般原則。
除了在編寫 API 時遵守這些規範,開發人員也應執行 API Lint 工具,該工具會將許多規則編碼為對 API 執行的檢查。
您可以將這份文件視為 Lint 工具遵守的規則指南,以及無法準確編碼至該工具的規則一般建議。
API Lint 工具
API Lint 已整合至 Metalava 靜態分析工具,並會在 CI 驗證期間自動執行。您可以使用 m
checkapi
從本機平台結帳手動執行,也可以使用 ./gradlew :path:to:project:checkApi
從本機 AndroidX 結帳執行。
API 規則
Android 平台和許多 Jetpack 程式庫是在這組指南建立之前就已存在,而本頁稍後列出的政策也不斷演進,以滿足 Android 生態系統的需求。
因此,部分現有 API 可能不符合這些規範。在其他情況下,如果新 API 與現有 API 保持一致,而非嚴格遵守指南,應用程式開發人員可能會獲得更優質的使用體驗。
請自行判斷,如有難以解決的 API 問題或需要更新的指南,請與 API 委員會聯絡。
API 基本概念
這個類別與 Android API 的核心層面有關。
必須實作所有 API
無論 API 的目標對象為何 (例如公開或 @SystemApi
),合併或公開為 API 時,都必須實作所有 API 介面。請勿合併 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 設下適當限制的基底類別。舉例來說,如果 API 的集合必須排序,請使用 List
;如果 API 的集合必須包含不重複的元素,請使用 Set
。
在 Kotlin 中,建議使用不可變動的集合。詳情請參閱「集合可變動性」。
抽象類別與介面
Java 8 新增了對預設介面方法的支援,讓 API 設計人員能夠在介面中新增方法,同時維持二進位檔的相容性。平台程式碼和所有 Jetpack 程式庫都應以 Java 8 以上版本為目標。
如果預設實作是無狀態,API 設計人員應優先使用介面而非抽象類別,也就是說,預設介面方法可實作為對其他介面方法的呼叫。
如果預設實作需要建構函式或內部狀態,必須使用抽象類別。
在這兩種情況下,API 設計人員都可以選擇將單一方法保留為抽象方法,簡化做為 lambda 的用法:
public interface AnimationEndCallback {
// Always called, must be implemented.
public void onFinished(Animation anim);
// Optional callbacks.
public default void onStopped(Animation anim) { }
public default void onCanceled(Animation anim) { }
}
類別名稱應反映擴充的內容
舉例來說,擴充 Service
的類別應命名為 FooService
,以利清楚辨識:
public class IntentHelper extends Service {}
public class IntentService extends Service {}
一般後綴
避免為公用程式方法集合使用 Helper
和 Util
等一般類別名稱後置字元。請改為將方法直接放在相關聯的類別中,或是放在 Kotlin 擴充功能函式中。
如果方法要橋接多個類別,請為包含的類別提供有意義的名稱,說明其用途。
在極少數情況下,使用 Helper
後置字元可能較為合適:
- 用於組合預設行為
- 可能涉及將現有行為委派給新類別
- 可能需要保存狀態
- 通常涉及
View
舉例來說,如果回溯移植工具提示需要保存與 View
相關聯的狀態,並在 View
上呼叫多種方法來安裝回溯移植,TooltipHelper
就是可接受的類別名稱。
請勿直接將 IDL 產生的程式碼公開做為 API
將 IDL 產生的程式碼保留為實作詳細資料。包括 protobuf、通訊端、FlatBuffers 或任何其他非 Java、非 NDK API 介面。不過,Android 中的大多數 IDL 都是 AIDL,因此本頁面著重於 AIDL。
產生的 AIDL 類別不符合 API 樣式指南規定 (例如無法使用多載),且 AIDL 工具並非專為維持語言 API 相容性而設計,因此您無法將這些類別嵌入公用 API。
請在 AIDL 介面上方新增公用 API 層,即使一開始只是淺層包裝函式也沒關係。
繫結器介面
如果 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);
}
}
對於不屬於 Android 平台一部分的 Binder
介面 (例如 Google Play 服務為應用程式匯出的服務介面),穩定、已發布且已版本化的 IPC 介面要求,表示介面本身更難演進。不過,為了符合其他 API 指南,並在日後需要為新版 IPC 介面使用相同公開 API 時,能更輕鬆地完成作業,建議您還是要為其建立包裝函式層。
請勿在公開 API 中使用原始 Binder 物件
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
缺少非阻斷式接聽功能,因此難以搭配非同步程式碼使用。
在平台程式碼和 Kotlin 與 Java 皆可使用的低階程式庫 API 中,建議使用完成回呼、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 專用整合程式庫中,您可以使用 Guava 的 ListenableFuture
。
public com.google.common.util.concurrent.ListenableFuture<Foo> asyncLoadFoo();
請勿使用 Optional
雖然 Optional
在某些 API 介面中可能具有優勢,但與現有的 Android API 介面區域不一致。@Nullable
和 @NonNull
可為 null
安全性提供工具輔助,而 Kotlin 會在編譯器層級強制執行是否可為空值的合約,因此不需要 Optional
。
如為選用基本型別,請使用成對的 has
和 get
方法。如果未設定值 (has
會傳回 false
),get
方法應擲回 IllegalStateException
。
public boolean hasAzimuth() { ... }
public int getAzimuth() {
if (!hasAzimuth()) {
throw new IllegalStateException("azimuth is not set");
}
return azimuth;
}
針對無法例項化的類別使用私有建構函式
只能由 Builder
建立的類別、只包含常數或靜態方法的類別,或是其他無法例項化的類別,都應至少包含一個私有建構函式,防止使用預設的無引數建構函式例項化。
public final class Log {
// Not instantiable.
private Log() {}
}
單例模式
單例模式有下列與測試相關的缺點,因此不建議使用:
- 建構作業由類別管理,可防止使用偽造品
- 單例項的靜態性質會導致測試無法密封
- 為解決這些問題,開發人員必須瞭解單例模式的內部詳細資料,或為單例模式建立包裝函式
建議採用「單一執行個體」模式,這個模式會依據抽象基礎類別解決這些問題。
單一執行個體
單一例項類別會使用具有 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
區塊時,自動清除這些資源。
避免在 android.* 中導入新的 View 子類別。
請勿在平台公開 API (即 android.*
) 中,導入直接或間接繼承自 android.view.View
的新類別。
Android 的 UI 工具包現在是以 Compose 為優先。平台公開的新 UI 功能應以低階 API 的形式公開,可用於實作 Jetpack Compose,以及 Jetpack 程式庫中開發人員可選用的 View 型 UI 元件。在程式庫中提供這些元件,有助於在平台功能無法使用時,進行回溯移植實作。
欄位
這些規則與類別的公開欄位有關。
請勿公開原始欄位
Java 類別不應直接公開欄位。無論這些欄位是否為最終欄位,都應為私有欄位,且只能使用公開的 getter 和 setter 存取。
少數例外情況包括基本資料結構,這類結構不需要強化指定或擷取欄位的行為。在這種情況下,欄位應使用標準變數命名慣例命名,例如 Point.x
和 Point.y
。
Kotlin 類別可以公開屬性。
公開欄位應標示為最終欄位
強烈建議不要使用原始欄位 (@see
Don't expose raw fields)。但如果欄位公開,請將該欄位標示為 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
位元遮罩旗標。
static final 常數應使用全大寫、以底線分隔的命名慣例
常數中的所有字詞都應大寫,多個字詞應以 _
分隔。例如:
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 開放原始碼專案。
意圖動作和額外項目,以及 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
使用一致的資源名稱
公開 ID、屬性和值必須使用 camelCase 命名慣例命名,例如 @id/accessibilityActionPageUp
或 @attr/textAppearance
,類似於 Java 中的公開欄位。
在某些情況下,公開 ID 或屬性會包含以底線分隔的常見前置字元:
- config.xml 中的平台設定值,例如
@string/config_recentsComponentName
- 版面配置專屬的檢視區塊屬性,例如 attrs.xml 中的
@attr/layout_marginStart
公開主題和樣式必須遵循階層式 PascalCase 命名慣例,例如 @style/Theme.Material.Light.DarkActionBar
或 @style/Widget.Material.SearchView.ActionBar
,類似於 Java 中的巢狀類別。
版面配置和可繪資源不應公開為 API。但如果必須公開,則公開版面配置和可繪項目必須使用底線命名慣例,例如 layout/simple_list_item_1.xml
或 drawable/title_bar_tall.xml
。
如果常數可能會變更,請設為動態常數
編譯器可能會內嵌常數值,因此保持值不變視為 API 合約的一部分。如果 MIN_FOO
或 MAX_FOO
常數的值日後可能會變更,建議改為動態方法。
CameraManager.MAX_CAMERAS
CameraManager.getMaxCameras()
考慮回呼的前瞻相容性
以舊版 API 為目標的應用程式無法辨識日後 API 版本中定義的常數。因此,傳送至應用程式的常數應考量應用程式的目標 API 版本,並將較新的常數對應至一致的值。請參考下列情境:
假設的 SDK 來源:
// Added in API level 22
public static final int STATUS_SUCCESS = 1;
public static final int STATUS_FAILURE = 2;
// Added in API level 23
public static final int STATUS_FAILURE_RETRY = 3;
// Added in API level 26
public static final int STATUS_FAILURE_ABORT = 4;
假設應用程式有 targetSdkVersion="22"
:
if (result == STATUS_FAILURE) {
// Oh no!
} else {
// Success!
}
在本例中,應用程式是在 API 級別 22 的限制下設計,並做出 (某種程度上) 合理的假設,認為只有兩種可能的狀態。但如果應用程式收到新加入的 STATUS_FAILURE_RETRY
,就會將此視為成功。
傳回常數的方法可以將輸出內容限制為符合應用程式指定的 API 級別,安全地處理這類情況:
private int mapResultForTargetSdk(Context context, int result) {
int targetSdkVersion = context.getApplicationInfo().targetSdkVersion;
if (targetSdkVersion < 26) {
if (result == STATUS_FAILURE_ABORT) {
return STATUS_FAILURE;
}
if (targetSdkVersion < 23) {
if (result == STATUS_FAILURE_RETRY) {
return STATUS_FAILURE;
}
}
}
return result;
}
開發人員無法預測常數清單是否會在未來變更。如果您使用類似萬用字元的 UNKNOWN
或 UNSPECIFIED
常數定義 API,開發人員會假設他們編寫應用程式時發布的常數已詳盡列出。如果您不願設定這項預期行為,請重新考慮是否適合為 API 使用萬用常數。
此外,程式庫無法指定自己的 targetSdkVersion
(與應用程式分開),而且處理程式庫程式碼的 targetSdkVersion
行為變更相當複雜,容易出錯。
整數或字串常數
如果值的命名空間無法在套件外部擴充,請使用整數常數和 @IntDef
。如果命名空間是由套件外部的程式碼共用或擴充,請使用字串常數。
資料類別
資料類別代表一組不可變更的屬性,並提供一小組定義完善的公用程式函式,用於與該資料互動。
請勿在公開 Kotlin API 中使用 data class
,因為 Kotlin 編譯器無法保證所產生程式碼的語言 API 或二進位檔相容性。請改為手動實作必要函式。
例項化
在 Java 中,如果屬性不多,資料類別應提供建構函式;如果屬性很多,則應使用 Builder
模式。
在 Kotlin 中,無論屬性數量為何,資料類別都應提供含有預設引數的建構函式。以 Java 用戶端為目標時,Kotlin 中定義的資料類別也可能受益於提供建構函式。
修改和複製
如果需要修改資料,請提供具有副本建構函式 (Java) 的 Builder
類別,或傳回新物件的 copy()
成員函式 (Kotlin)。
在 Kotlin 中提供 copy()
函式時,引數必須與類別的建構函式相符,且預設值必須使用物件的目前值填入:
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.*
型別都可透過去糖化在所有平台版本上使用,因此在 API 參數或傳回值中表示時間時,建議使用這些型別。
建議只公開接受或傳回 java.time.Duration
或 java.time.Instant
的 API 變體,並省略具有相同用途的原始變體,除非 API 網域是物件分配在預期使用模式中會對效能造成重大影響的網域。
表示時間長度的方法應命名為「duration」
如果時間值表示涉及的時間長度,請將參數命名為「duration」,而非「time」。
ValueAnimator.setTime(java.time.Duration);
ValueAnimator.setDuration(java.time.Duration);
例外狀況:
如果時間長度特別適用於逾時值,則「逾時」是適當的選項。
如要參照特定時間點而非時間長度,請使用 java.time.Instant
類型的「時間」。
以基元表示時間長度或時間的方法應以時間單位命名,並使用 long
接受或傳回原始型別時間長度的方法,應在方法名稱後方加上相關聯的時間單位 (例如 Millis
、Nanos
、Seconds
),保留未裝飾的名稱供 java.time.Duration
使用。請參閱「時間」。
方法也應使用適當的單位和時間基準加上註解:
@CurrentTimeMillisLong
:值為非負時間戳記,以自 1970-01-01T00:00:00Z 起算的毫秒數為單位。@CurrentTimeSecondsLong
:值為非負時間戳記,以自 1970-01-01T00:00:00Z 起的秒數計算。@DurationMillisLong
:值為非負數的時間長度 (以毫秒為單位)。@ElapsedRealtimeLong
:值是SystemClock.elapsedRealtime()
時基中的非負時間戳記。@UptimeMillisLong
:值為SystemClock.uptimeMillis()
時基中的非負時間戳記。
原始時間參數或回傳值應使用 long
,而非 int
。
ValueAnimator.setDuration(@DurationMillisLong long);
ValueAnimator.setDurationNanos(long);
表示時間單位的函式應優先使用單位的非縮寫簡寫
public void setIntervalNs(long intervalNs);
public void setTimeoutUs(long timeoutUs);
public void setIntervalNanos(long intervalNanos);
public void setTimeoutMicros(long timeoutMicros);
為長時間引數加上註解
這個平台包含多個註解,可為 long
類型的時間單位提供更強大的型別:
@CurrentTimeMillisLong
:值為非負時間戳記,以自1970-01-01T00:00:00Z
起算的毫秒數為單位,因此位於System.currentTimeMillis()
時基中。@CurrentTimeSecondsLong
:值為非負時間戳記,以自1970-01-01T00:00:00Z
起的秒數計算。@DurationMillisLong
:值為非負數的時間長度 (以毫秒為單位)。@ElapsedRealtimeLong
:值是SystemClock#elapsedRealtime()
時基中的非負時間戳記。@UptimeMillisLong
:值是SystemClock#uptimeMillis()
時基中的非負時間戳記。
測量單位
對於所有以時間以外的測量單位表示的方法,請優先使用 CamelCased SI 單位前置字串。
public long[] getFrequenciesKhz();
public float getStreamVolumeDb();
將選用參數放在多載結尾
如果方法有選用參數的超載,請將這些參數放在結尾,並與其他參數保持一致的順序:
public int doFoo(boolean flag);
public int doFoo(int id, boolean flag);
public int doFoo(boolean flag);
public int doFoo(boolean flag, int id);
為選用引數新增多載時,較簡單的方法應與提供預設引數給較複雜方法時的行為完全相同。
附註:除了新增選用引數或接受不同類型的引數 (如果方法是多型) 之外,請勿過度多載方法。如果多載方法執行的作業與原本的方法有根本上的差異,請為多載方法命名。
具有預設參數的方法必須使用 @JvmOverloads 註解 (僅限 Kotlin)
如要維持二進位檔相容性,含有預設參數的方法和建構函式必須加上 @JvmOverloads
註解。
詳情請參閱官方 Kotlin-Java 互通性指南中的「預設值的函式多載」。
class Greeting @JvmOverloads constructor(
loudness: Int = 5
) {
@JvmOverloads
fun sayHello(prefix: String = "Dr.", name: String) = // ...
}
請勿移除預設參數值 (僅限 Kotlin)
如果方法已隨附預設值的參數發布,移除預設值會造成來源中斷。
最獨特且可識別的方法參數應優先顯示
如果方法有多個參數,請先放置最相關的參數。 指定旗標和其他選項的參數,重要性不如描述所要處理物件的參數。如有完成回呼,請放在最後。
public void openFile(int flags, String name);
public void openFileAsync(OnFileOpenedListener listener, String name, int flags);
public void setFlags(int mask, int flags);
public void openFile(String name, int flags);
public void openFileAsync(String name, int flags, OnFileOpenedListener listener);
public void setFlags(int flags, int mask);
另請參閱:在多載中將選用參數放在結尾
建構工具
建議使用 Builder 模式建立複雜的 Java 物件,且通常用於 Android 的下列情況:
- 產生的物件屬性應不可變更
- 需要大量屬性,例如許多建構函式引數
- 建構時的屬性關係很複雜,例如需要驗證步驟。請注意,這種複雜程度通常表示 API 的可用性有問題。
請考慮是否需要建構工具。如果建構工具用於下列用途,則在 API 介面中相當實用:
- 設定一小部分選用建立參數 (可能有很多)
- 設定許多不同的選用或必要建立參數,有時類型相似或相符,否則呼叫網站可能會難以閱讀或容易出錯
- 設定以遞增方式建立物件,其中多個不同的設定程式碼片段可能會各自對建構工具進行呼叫,做為實作詳細資料
- 在未來的 API 版本中新增其他選用建立參數,允許型別成長
如果型別最多只有三個必要參數,且沒有選用參數,您幾乎一律可以略過建構工具,改用一般建構函式。
Kotlin 來源的類別應優先使用附有 @JvmOverloads
註解的建構函式和預設引數,而非建構工具,但也可以選擇在先前列出的情況中提供建構工具,以提升 Java 用戶的可用性。
class Tone @JvmOverloads constructor(
val duration: Long = 1000,
val frequency: Int = 2600,
val dtmfConfigs: List<DtmfConfig> = emptyList()
) {
class Builder {
// ...
}
}
建構工具類別必須傳回建構工具
建構工具類別必須從 build()
以外的每個方法傳回建構工具物件 (例如 this
),啟用方法鏈結。其他建構的物件應做為引數傳遞,請勿傳回其他物件的建構工具。例如:
public static class Builder {
public void setDuration(long);
public void setFrequency(int);
public DtmfConfigBuilder addDtmfConfig();
public Tone build();
}
public class Tone {
public static class Builder {
public Builder setDuration(long);
public Builder setFrequency(int);
public Builder addDtmfConfig(DtmfConfig);
public Tone build();
}
}
在基礎建構工具類別必須支援擴充功能的少數情況下,請使用泛型回傳型別:
public abstract class Builder<T extends Builder<T>> {
abstract T setValue(int);
}
public class TypeBuilder<T extends TypeBuilder<T>> extends Builder<T> {
T setValue(int);
T setTypeSpecificValue(long);
}
建構工具類別必須透過建構函式建立
為透過 Android API 介面維持一致的建構工具建立作業,所有建構工具必須透過建構函式建立,而非靜態建立工具方法。如果是以 Kotlin 為基礎的 API,即使 Kotlin 使用者預期會透過工廠方法/DSL 樣式建立機制隱含地依附於建構工具,Builder
也必須是公開的。程式庫不得使用 @PublishedApi internal
,從 Kotlin 用戶端選擇性隱藏 Builder
類別建構函式。
public class Tone {
public static Builder builder();
public static class Builder {
}
}
public class Tone {
public static class Builder {
public Builder();
}
}
建構函式建構工具的所有引數都必須為必要引數 (例如 @NonNull)
選用引數 (例如 @Nullable
) 應移至 setter 方法。
如果未指定任何必要引數,建構工具建構函式應會擲回 NullPointerException
(請考慮使用 Objects.requireNonNull
)。
建構工具類別應為其建構類型的最終靜態內部類別
為了在套件中進行邏輯組織,建構工具類別通常應公開為其建構型別的最終內部類別,例如 Tone.Builder
,而非 ToneBuilder
。
建構函式可包含建構函式,以便從現有執行個體建立新執行個體
建構工具可以包含複製建構函式,可從現有建構工具或建構物件建立新的建構工具例項。他們不應提供替代方法,從現有建構工具或建構物件建立建構工具執行個體。
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);
}
}
如果建構工具具有複製建構函式,建構工具設定者應採用 @Nullable 引數
如果可能從現有例項建立新的建構工具例項,就必須重設。如果沒有可用的副本建構函式,建構工具可能會使用 @Nullable
或 @NonNullable
引數。
public static class Builder {
public Builder(Builder original);
public Builder setObjectValue(@Nullable Object value);
}
建構工具設定器可能會針對選用屬性採用 @Nullable 引數
第二級輸入通常較適合使用可空值,尤其是在 Kotlin 中,因為 Kotlin 使用預設引數,而非建構工具和多載。
此外,@Nullable
設定器會與對應的擷取器相符,且必須是選用屬性的 @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.
*/
如果建構的類別提供設定器,即可為可變動的屬性提供建構工具設定器
如果類別具有可變動的屬性,且需要 Builder
類別,請先問自己類別「是否真的」應具備可變動的屬性。
接著,如果您確定需要可變動的屬性,請根據預期用途,決定下列哪種情境較適合:
建構的物件應可立即使用,因此應為所有相關屬性 (無論可變動或不可變動) 提供設定器。
map.put(key, new Value.Builder(requiredValue) .setImmutableProperty(immutableValue) .setUsefulMutableProperty(usefulValue) .build());
在建構的物件可用之前,可能需要進行一些額外的呼叫,因此不應為可變動的屬性提供設定器。
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);
建構工具不應有 Getter
Getter 應位於建構的物件上,而非建構工具。
建構工具設定者必須在建構的類別中具有對應的 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();
}
建構工具方法命名
建構工具方法名稱應使用 setFoo()
、addFoo()
或 clearFoo()
樣式。
建構工具類別應宣告 build() 方法
建構工具類別應宣告 build()
方法,傳回所建構物件的例項。
建構工具 build() 方法必須傳回 @NonNull 物件
建構工具的 build()
方法應傳回所建構物件的非空值執行個體。如果因參數無效而無法建立物件,驗證作業可以延後至建構方法,並擲回 IllegalStateException
。
不要公開內部鎖定
公開 API 中的方法不應使用 synchronized
關鍵字。這個關鍵字會導致物件或類別做為鎖定項目使用,而且由於這個關鍵字會向其他人公開,如果類別外部的其他程式碼開始將其用於鎖定用途,您可能會遇到非預期的副作用。
請改為對內部私人物件執行任何必要的鎖定作業。
public synchronized void doThing() { ... }
private final Object mThingLock = new Object();
public void doThing() {
synchronized (mThingLock) {
...
}
}
存取子樣式方法應遵循 Kotlin 屬性規範
從 Kotlin 來源檢視時,存取子樣式的方法 (使用 get
、set
或 is
前置字元的方法) 也會以 Kotlin 屬性形式提供。舉例來說,Java 中定義的 int getField()
可在 Kotlin 中做為 val field: Int
屬性使用。
基於這個原因,且為了普遍滿足開發人員對存取子方法行為的期望,使用存取子方法前置字元的方法應與 Java 欄位類似。請避免在下列情況使用存取子樣式前置字串:
- 這個方法有副作用,請使用更具描述性的方法名稱
- 這個方法涉及運算成本高昂的工作,建議使用
compute
- 這個方法會封鎖或以其他方式長時間執行工作,以傳回值,例如 IPC 或其他 I/O - 建議使用
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;
針對 Java 存取子方法使用 set
/is
,或針對 Java 欄位使用 is
,即可將這些方法/欄位做為 Kotlin 的屬性使用:
obj.isVisible = true
obj.isFactoryResetProtectionEnabled = false
if (!obj.isAvailable) return
屬性和存取子方法一般應使用正向命名,例如 Enabled
,而非 Disabled
。使用負面用語會反轉 true
和 false
的意義,導致行為更難以理解。
// Passing false here is a double-negative.
void setFactoryResetProtectionDisabled(boolean disabled);
如果布林值描述屬性的納入或擁有權,您可以使用 has,而非 is;不過,這不適用於 Kotlin 屬性語法:
// Transient state is an indirect property used to track state
// related to the object. The object is not transient; rather,
// the object "has" transient state associated with it:
void setHasTransientState(boolean hasTransientState);
boolean hasTransientState();
其他可能更適合的前置字元包括 can 和 should:
// "Can" describes a behavior that the object may provide,
// and here is more concise than setRecordingEnabled or
// setRecordingAllowed. The object "can" record:
void setCanRecord(boolean canRecord);
boolean canRecord();
// "Should" describes a hint or property that is not strictly
// enforced, and here is more explicit than setFitWidthEnabled.
// The object "should" fit width:
void setShouldFitWidth(boolean shouldFitWidth);
boolean shouldFitWidth();
切換行為或功能的方法可能會使用 is 前置字元和 Enabled 後置字元:
// "Enabled" describes the availability of a property, and is
// more appropriate here than "can use" or "should use" the
// property:
void setWiFiRoamingSettingEnabled(boolean enabled)
boolean isWiFiRoamingSettingEnabled()
同樣地,指出依附於其他行為或功能的方法可能會使用 is 前置字串和 Supported 或 Required 後置字串:
// "Supported" describes whether this API would work on devices that support
// multiple users. The API "supports" multi-user:
void setMultiUserSupported(boolean supported)
boolean isMultiUserSupported()
// "Required" describes whether this API depends on devices that support
// multiple users. The API "requires" multi-user:
void setMultiUserRequired(boolean required)
boolean isMultiUserRequired()
一般來說,方法名稱應寫成問題,並以傳回值做為答案。
Kotlin 屬性方法
對於類別屬性 var foo: Foo
,Kotlin 會使用一致的規則產生 get
/set
方法:在 getter 前面加上 get
,並將第一個字元設為大寫;在 setter 前面加上 set
,並將第一個字元設為大寫。屬性宣告會分別產生名為 public Foo getFoo()
和 public void setFoo(Foo foo)
的方法。
如果屬性屬於 Boolean
型別,則名稱產生時會套用額外規則:如果屬性名稱開頭為 is
,則不會為 getter 方法名稱加上 get
前置字串,屬性名稱本身會做為 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
位元遮罩存取子
如需定義位元遮罩標記的 API 指南,請參閱「使用 @IntDef
做為位元遮罩標記」。
Setter
應提供兩個設定器方法:一個採用完整位元字串並覆寫所有現有旗標,另一個採用自訂位元遮罩,以提供更大的彈性。
/**
* Sets the state of all scroll indicators.
* <p>
* See {@link #setScrollIndicators(int, int)} for usage information.
*
* @param indicators a bitmask of indicators that should be enabled, or
* {@code 0} to disable all indicators
* @see #setScrollIndicators(int, int)
* @see #getScrollIndicators()
*/
public void setScrollIndicators(@ScrollIndicators int indicators);
/**
* Sets the state of the scroll indicators specified by the mask. To change
* all scroll indicators at once, see {@link #setScrollIndicators(int)}.
* <p>
* When a scroll indicator is enabled, it will be displayed if the view
* can scroll in the direction of the indicator.
* <p>
* Multiple indicator types may be enabled or disabled by passing the
* logical OR of the specified types. If multiple types are specified, they
* will all be set to the same enabled state.
* <p>
* For example, to enable the top scroll indicator:
* {@code setScrollIndicators(SCROLL_INDICATOR_TOP, SCROLL_INDICATOR_TOP)}
* <p>
* To disable the top scroll indicator:
* {@code setScrollIndicators(0, SCROLL_INDICATOR_TOP)}
*
* @param indicators a bitmask of values to set; may be a single flag,
* the logical OR of multiple flags, or 0 to clear
* @param mask a bitmask indicating which indicator flags to modify
* @see #setScrollIndicators(int)
* @see #getScrollIndicators()
*/
public void setScrollIndicators(@ScrollIndicators int indicators, @ScrollIndicators int mask);
Getter
應提供一個 Getter 來取得完整位元遮罩。
/**
* Returns a bitmask representing the enabled scroll indicators.
* <p>
* For example, if the top and left scroll indicators are enabled and all
* other indicators are disabled, the return value will be
* {@code View.SCROLL_INDICATOR_TOP | View.SCROLL_INDICATOR_LEFT}.
* <p>
* To check whether the bottom scroll indicator is enabled, use the value
* of {@code (getScrollIndicators() & View.SCROLL_INDICATOR_BOTTOM) != 0}.
*
* @return a bitmask representing the enabled scroll indicators
*/
@ScrollIndicators
public int getScrollIndicators();
使用公開而非受保護的存取權
在公開 API 中,一律優先使用 public
,而非 protected
。從長遠來看,受保護的存取權會造成困擾,因為實作人員必須覆寫,才能在預設外部存取權同樣適用的情況下提供公開存取子。
請注意,protected
可見度 不會阻止開發人員呼叫 API,只會稍微增加難度。
實作 equals() 和 hashCode(),或兩者都不實作
如果覆寫其中一個,就必須覆寫另一個。
為資料類別實作 toString()
建議資料類別覆寫 toString()
,協助開發人員偵錯程式碼。
記錄輸出內容是為了瞭解程式行為還是偵錯
決定是否要讓計畫行為取決於導入作業。 舉例來說,UUID.toString() 和 File.toString() 會記錄程式使用的特定格式。如果您只為偵錯公開資訊 (例如 Intent),請從超類別隱含繼承文件。
請勿提供額外資訊
物件的公用 API 應提供與「toString()
」相同的資訊。否則,您會鼓勵開發人員剖析並依賴 toString()
輸出內容,這會阻礙日後的變更。建議您只使用物件的公開 API 實作 toString()
。
避免過度依賴偵錯輸出內容
雖然無法防止開發人員依附於偵錯輸出內容,但只要在物件的 toString()
輸出內容中加入 System.identityHashCode
,兩個不同物件的 toString()
輸出內容就不太可能相同。
@Override
public String toString() {
return getClass().getSimpleName() + "@" + Integer.toHexString(System.identityHashCode(this)) + " {mFoo=" + mFoo + "}";
}
這可有效避免開發人員在物件上編寫類似 assertThat(a.toString()).isEqualTo(b.toString())
的測試斷言。
傳回新建立的物件時,請使用 createFoo
對於會建立傳回值的方法 (例如建構新物件),請使用 create
前置字元,而非 get
或 new
。
如果方法會建立要傳回的物件,請在方法名稱中明確指出。
public FooThing getFooThing() {
return new FooThing();
}
public FooThing createFooThing() {
return new FooThing();
}
接受 File 物件的方法也應接受串流
Android 上的資料儲存位置不一定都是磁碟上的檔案。舉例來說,跨越使用者界線傳遞的內容會以 content://
Uri
表示。如要啟用各種資料來源的處理作業,接受 File
物件的 API 也應接受 InputStream
、OutputStream
或兩者。
public void setDataSource(File file)
public void setDataSource(InputStream stream)
採用原始原始型別,而非方塊化版本
如需傳達遺漏或空值,請考慮使用 -1
、Integer.MAX_VALUE
或 Integer.MIN_VALUE
。
public java.lang.Integer getLength()
public void setLength(java.lang.Integer)
public int getLength()
public void setLength(int value)
避免使用原始型別的類別對等項目,可避免這些類別的記憶體負擔、值的方法存取,以及更重要的自動裝箱作業 (來自原始型別和物件型別之間的轉換)。避免這些行為可節省記憶體和暫時分配,否則可能導致昂貴且更頻繁的垃圾收集。
使用註解來明確指出有效的參數和傳回值
新增開發人員註解,協助釐清各種情況下的允許值。這樣一來,當開發人員提供不正確的值時 (例如,架構需要一組特定常數值,但開發人員傳遞任意 int
),工具就能更輕鬆地提供協助。視情況使用下列任何註解:
是否可為空值
Java API 需要明確的可空值註解,但可空值的概念是 Kotlin 語言的一部分,因此 Kotlin API 絕不應使用可空值註解。
@Nullable
:表示指定的回傳值、參數或欄位可以是空值:
@Nullable
public String getName()
public void setName(@Nullable String name)
@NonNull
:表示指定的回傳值、參數或欄位「不得」為空值。在 Android 中標記項目為 @Nullable
是相對較新的做法,因此 Android 的大部分 API 方法並未持續記錄。因此,我們有「不明、@Nullable
、@NonNull
」三種狀態,這也是 @NonNull
屬於 API 規範的原因:
@NonNull
public String getName()
public void setName(@NonNull String name)
如果是 Android 平台文件,註解方法參數會自動產生「這個值可能為空值」形式的文件,除非參數文件中其他地方明確使用「null」。
現有的「並非真正可為空值」方法:如果 API 中的現有方法沒有已宣告的 @Nullable
註解,則在特定明顯情況下 (例如 findViewById()
),該方法可能會註解 @Nullable
並傳回 null
。對於不想進行空值檢查的開發人員,應新增會擲回 IllegalArgumentException
的同伴 @NotNull requireFoo()
方法。
介面方法:實作介面方法時,新 API 應加入適當的註解,例如 Parcelable.writeToParcel()
(也就是實作類別中的方法應為 writeToParcel(@NonNull Parcel,
int)
,而非 writeToParcel(Parcel, int)
);不過,缺少註解的現有 API 不需「修正」。
強制執行空值
在 Java 中,建議使用 Objects.requireNonNull()
針對 @NonNull
參數執行輸入驗證,並在參數為空值時擲回 NullPointerException
。Kotlin 會自動執行這項作業。
資源
資源 ID:表示特定資源 ID 的整數參數應使用適當的資源類型定義進行註解。除了適用於所有資源的 @AnyRes
之外,每種資源類型都有專屬的註解,例如 @StringRes
、@ColorRes
和 @AnimRes
。例如:
public void setTitle(@StringRes int resId)
常數集的 @IntDef
魔法常數: String
和 int
參數應接收以公開常數表示的有限可能值集,並以 @StringDef
或 @IntDef
適當註解。這些註解可讓您建立新的註解,並像允許的參數 typedef 一樣使用。例如:
/** @hide */
@IntDef(prefix = {"NAVIGATION_MODE_"}, value = {
NAVIGATION_MODE_STANDARD,
NAVIGATION_MODE_LIST,
NAVIGATION_MODE_TABS
})
@Retention(RetentionPolicy.SOURCE)
public @interface NavigationMode {}
public static final int NAVIGATION_MODE_STANDARD = 0;
public static final int NAVIGATION_MODE_LIST = 1;
public static final int NAVIGATION_MODE_TABS = 2;
@NavigationMode
public int getNavigationMode();
public void setNavigationMode(@NavigationMode int mode);
建議使用方法檢查註解參數的有效性,並在參數不屬於 @IntDef
時擲回 IllegalArgumentException
位元遮罩旗標的 @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
常數。您可以加入多個「前置字元」值,系統會自動為所有值發布文件。
SDK 常數的 @SdkConstant
@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";
為覆寫提供相容的空值性
為了確保 API 相容性,覆寫項的 Null 值應與父項目前的 Null 值相容。下表列出相容性期望。簡單來說,覆寫項目只能比覆寫的元素更嚴格或同樣嚴格。
類型 | 家長 | 為我的子女建立帳戶 |
---|---|---|
傳回類型 | 未註解 | 未註解或非空值 |
傳回類型 | 可以為空值 | 可為空值或不可為空值 |
傳回類型 | Nonnull | Nonnull |
有趣的引數 | 未註解 | 未註解或可為空值 |
有趣的引數 | 可以為空值 | 可以為空值 |
有趣的引數 | Nonnull | 可為空值或不可為空值 |
盡可能使用不可為空值的引數 (例如 @NonNull)
方法多載時,請盡量讓所有引數都為非空值。
public void startActivity(@NonNull Component component) { ... }
public void startActivity(@NonNull Component component, @NonNull Bundle options) { ... }
這項規則也適用於多載的屬性設定器。主要引數應為非空值,且清除屬性應以個別方法實作。這可避免「無意義」的呼叫,也就是開發人員必須設定尾端參數,即使這些參數並非必要。
public void setTitleItem(@Nullable IconCompat icon, @ImageMode mode)
public void setTitleItem(@Nullable IconCompat icon, @ImageMode mode, boolean isLoading)
// Nonsense call to clear property
setTitleItem(null, MODE_RAW, false);
public void setTitleItem(@NonNull IconCompat icon, @ImageMode mode)
public void setTitleItem(@NonNull IconCompat icon, @ImageMode mode, boolean isLoading)
public void clearTitleItem()
容器的傳回型別最好不可為空值 (例如 @NonNull)
如果是 Bundle
或 Collection
等容器類型,請傳回空白容器 (如適用,請傳回不可變動的容器)。如果 null
用於區分容器的可用性,請考慮提供個別的布林方法。
@NonNull
public Bundle getExtras() { ... }
get 和 set 配對的可為空值註解必須一致
單一邏輯屬性的取得和設定方法配對,應一律在可空值註解中保持一致。如果未遵循這項準則,Kotlin 的屬性語法就會失效,因此在現有屬性方法中加入不一致的可空值註解,會對 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
) 方法可能會傳回整數狀態碼。
狀態碼應在包含類別中定義為 public static final
欄位,並以 ERROR_
為前置字元,且在 @hide
@IntDef
註解中列舉。
方法名稱開頭應一律使用動詞,而非主詞
方法名稱一律應以動詞開頭 (例如 get
、create
、reload
等),而非您要執行的物件。
public void tableReload() {
mTable.reload();
}
public void reloadTable() {
mTable.reload();
}
偏好使用 Collection 類型,而非陣列做為傳回或參數類型
與陣列相比,泛型集合介面具有多項優勢,包括圍繞唯一性和排序的更強大 API 合約、支援泛型,以及多種方便開發人員使用的便利方法。
基本型別的例外狀況
如果元素是基本型別,請改用陣列,以免自動裝箱的成本。請參閱「改為擷取及傳回原始原始型別,而非裝箱版本」
講求效能的程式碼例外狀況
在特定情況下,如果 API 用於對效能要求嚴格的程式碼 (例如繪圖或其他測量/版面配置/繪製 API),則可使用陣列而非集合,以減少分配作業和記憶體流失。
Kotlin 例外狀況
Kotlin 陣列是不變的,且 Kotlin 語言提供充足的陣列公用程式 API,因此對於要從 Kotlin 存取的 Kotlin API 而言,陣列與 List
和 Collection
相當。
偏好使用 @NonNull 集合
請一律優先使用集合物件的 @NonNull
。傳回空集合時,請使用適當的 Collections.empty
方法,傳回低成本、正確型別且不可變動的集合物件。
在支援型別註解的情況下,請一律優先使用 @NonNull
做為集合元素。
使用陣列而非集合時,也應優先使用 @NonNull
(請參閱前一個項目)。如果擔心物件分配問題,請建立常數並傳遞,畢竟空陣列是不可變動的。例子:
private static final int[] EMPTY_USER_IDS = new int[0];
@NonNull
public int[] getUserIds() {
int [] userIds = mService.getUserIds();
return userIds != null ? userIds : EMPTY_USER_IDS;
}
集合可變動性
Kotlin API 預設應偏好集合的唯讀 (非 Mutable
) 傳回型別,除非 API 合約特別要求可變動的傳回型別。
不過,Java API 預設應偏好使用可變動回傳型別,因為 Android 平台實作的 Java API 尚未提供不可變動集合的便利實作方式。但這項規則不適用於 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 參數型別
如果開發人員可能會在呼叫端建立陣列,只為傳遞多個相同類型的相關參數,建議 Kotlin 和 Java API 都使用 vararg
。
public void setFeatures(Feature[] features) { ... }
// Developer code
setFeatures(new Feature[]{Features.A, Features.B, Features.C});
public void setFeatures(Feature... features) { ... }
// Developer code
setFeatures(Features.A, Features.B, Features.C);
防禦型副本
vararg
參數的 Java 和 Kotlin 實作項目都會編譯為相同的陣列支援位元碼,因此可從 Java 程式碼以可變動陣列呼叫。如果陣列參數會保留至欄位或匿名內部類別,強烈建議 API 設計人員建立陣列參數的防禦性淺層副本。
public void setValues(SomeObject... values) {
this.values = Arrays.copyOf(values, values.length);
}
請注意,建立防禦性副本無法防範初始方法呼叫和副本建立之間的並行修改,也無法防範陣列中物件的突變。
使用集合型別參數或傳回型別提供正確的語意
List<Foo>
是預設選項,但請考慮使用其他類型來提供額外意義:
如果 API 不在意元素順序,且不允許重複或重複沒有意義,請使用
Set<Foo>
。Collection<Foo>,
如果 API 對順序不敏感,且允許重複。
Kotlin 轉換函式
Kotlin 經常使用 .toFoo()
和 .asFoo()
,從現有物件取得不同類型的物件,其中 Foo
是轉換的傳回類型名稱。這與熟悉的 JDK Object.toString()
一致。Kotlin 更進一步,將其用於原始轉換,例如 25.toFloat()
。
名為「轉換」和「已轉換」的轉換之間有顯著差異:.toFoo()
.asFoo()
建立新的獨立物件時,請使用 .toFoo()
與 .toString()
類似,「to」轉換會傳回新的獨立物件。如果之後修改原始物件,新物件不會反映這些變更。同樣地,如果稍後修改 new 物件,old 物件不會反映這些變更。
fun Foo.toBundle(): Bundle = Bundle().apply {
putInt(FOO_VALUE_KEY, value)
}
建立依附包裝函式、裝飾物件或轉換時,請使用 .asFoo()
在 Kotlin 中,轉換是使用 as
關鍵字執行。這反映了介面的變更,但不是身分的變更。在擴充函式中做為前置字元使用時,.asFoo()
會裝飾接收器。原始接收器物件的變動會反映在 asFoo()
傳回的物件中。新 Foo
物件中的突變可能會反映在原始物件中。
fun <T> Flow<T>.asLiveData(): LiveData<T> = liveData {
collect {
emit(it)
}
}
轉換函式應編寫為擴充函式
在接收器和結果類別定義之外編寫轉換函式,可減少型別之間的耦合。理想的轉換只需要原始物件的公開 API 存取權。這項範例證明,開發人員也可以將類似的轉換寫入自己偏好的類型。
擲回適當的特定例外狀況
方法不得擲回一般例外狀況,例如 java.lang.Exception
或 java.lang.Throwable
,而是必須使用適當的特定例外狀況,例如 java.lang.NullPointerException
,讓開發人員處理例外狀況時不會過於籠統。
與直接提供給公開叫用方法的引數無關的錯誤,應擲回 java.lang.IllegalStateException
,而非 java.lang.IllegalArgumentException
或 java.lang.NullPointerException
。
監聽器和回呼
這些是與監聽器和回呼機制所用類別和方法相關的規則。
回呼類別名稱應為單數
請改用 MyObjectCallback
,而非 MyObjectCallbacks
。
回呼方法名稱的格式應為 on
onFooEvent
表示 FooEvent
正在發生,且回呼應採取相應行動。
過去式和現在式應說明時間行為
事件相關回呼方法的命名應指出事件是否已發生或正在發生。
舉例來說,如果是在執行點擊動作後呼叫方法:
public void onClicked()
不過,如果該方法負責執行點擊動作:
public boolean onClick()
註冊回呼
當監聽器或回呼可從物件新增或移除時,相關聯的方法應命名為 add 和 remove 或 register 和 unregister。請與類別或同一套件中其他類別使用的現有慣例保持一致。如果沒有這類先例,請優先使用新增和移除。
涉及註冊或取消註冊回呼的方法應指定回呼類型的完整名稱。
public void addFooCallback(@NonNull FooCallback callback);
public void removeFooCallback(@NonNull FooCallback callback);
public void registerFooCallback(@NonNull FooCallback callback);
public void unregisterFooCallback(@NonNull FooCallback callback);
避免回呼的 Getter
請勿新增 getFooCallback()
方法。在開發人員可能想將現有回呼與自己的替代項目串連在一起的情況下,這會是誘人的逃生出口,但這種做法很脆弱,且會讓元件開發人員難以推斷目前的狀態。例如:
- 開發人員 A 撥打
setFooCallback(a)
- 開發人員 B 呼叫
setFooCallback(new B(getFooCallback()))
- 開發人員 A 希望移除回呼
a
,但不知道B
的類型,且B
的建構方式不允許修改包裝的回呼,因此無法移除。
接受 Executor 來控制回呼的調度
註冊沒有明確執行緒期望的回呼 (幾乎在 UI 工具包以外的任何位置) 時,強烈建議在註冊時加入 Executor
參數,讓開發人員指定要叫用回呼的執行緒。
public void registerFooCallback(
@NonNull @CallbackExecutor Executor executor,
@NonNull FooCallback callback)
根據自選參數的通常規範,即使 Executor
不是參數清單中的最後一個引數,仍可提供省略 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.clearCallingIdentity()
,再叫用應用程式提供的 Executor
上的應用程式回呼。這樣一來,任何使用繫結器身分 (例如 Binder.getCallingUid()
) 進行權限檢查的應用程式程式碼,都會正確將執行的程式碼歸因於應用程式,而不是呼叫應用程式的系統程序。如果 API 使用者需要呼叫端的 UID 或 PID 資訊,這應該是 API 介面的明確部分,而不是根據他們提供的 Executor
執行位置隱含。
API 應支援指定 Executor
。在效能至關重要的情況下,應用程式可能需要立即或與 API 的意見回饋同步執行程式碼。接受 Executor
即可允許這項操作。
防禦性地建立額外的 HandlerThread
或類似於彈跳床的項目,會導致這個理想的用途無法實現。
如果應用程式要在自己的程序中執行耗用大量資源的程式碼,請讓他們這麼做。應用程式開發人員為規避限制而採取的解決方法,長期而言會更難支援。
單一回呼的例外狀況:當回報的事件性質僅支援單一回呼執行個體時,請使用下列樣式:
public void setFooCallback(
@NonNull @CallbackExecutor Executor executor,
@NonNull FooCallback callback)
public void clearFooCallback()
使用 Executor 取代 Handler
過去,Android 的 Handler
是將回呼執行作業重新導向至特定 Looper
執行緒的標準。這項標準已變更為偏好 Executor
,因為大多數應用程式開發人員會管理自己的執行緒集區,因此主執行緒或 UI 執行緒是應用程式可用的唯一 Looper
執行緒。使用 Executor
可讓開發人員控管重複使用的現有/偏好執行環境。
現代並行程式庫 (如 kotlinx.coroutines 或 RxJava) 提供自己的排程機制,可在需要時執行自己的調度,因此請務必提供使用直接執行器 (如 Runnable::run
) 的功能,避免因雙重執行緒跳轉而造成延遲。舉例來說,使用 Handler
發布至 Looper
執行緒需要一個躍點,然後從應用程式的並行架構再進行另一個躍點。
這項規範的例外狀況很少見。常見的例外狀況申訴包括:
我必須使用 Looper
,因為我需要 Looper
才能epoll
活動。
由於在這種情況下無法實現 Executor
的優點,因此這項豁免申請已獲准。
我不希望應用程式碼封鎖執行緒發布事件。如果程式碼是在應用程式程序中執行,通常「不會」核准這類例外狀況要求。應用程式若誤用這項功能,只會對自身造成影響,不會影響整體系統健康狀態。如果應用程式正確使用常見的並行架構,就不會受到額外的延遲處罰。
Handler
與同類別中的其他類似 API 在本機上保持一致。
這項例外狀況要求會視情況核准。偏好新增以 Executor
為基礎的超載,並將 Handler
實作項目遷移至使用新的 Executor
實作項目。(myHandler::post
是有效的 Executor
!) 視類別大小、現有 Handler
方法數量,以及開發人員可能需要一併使用現有 Handler
方法和新方法的可能性而定,或許可以例外新增以 Handler
為基礎的方法。
註冊時的對稱性
如果可以新增或註冊某個項目,也應該可以移除/取消註冊該項目。方法
registerThing(Thing)
應具備相符的
unregisterThing(Thing)
提供要求 ID
如果開發人員重複使用回呼是合理的做法,請提供 ID 物件,將回呼與要求繫結。
class RequestParameters {
public int getId() { ... }
}
class RequestExecutor {
public void executeRequest(
RequestParameters parameters,
Consumer<RequestParameters> onRequestCompletedListener) { ... }
}
多種方法的回呼物件
多種方法的回呼應優先使用 interface
,並在新增至先前發布的介面時使用 default
方法。先前,由於 Java 7 缺少 default
方法,因此這項指南建議使用 abstract class
。
public interface MostlyOptionalCallback {
void onImportantAction();
default void onOptionalInformation() {
// Empty stub, this method is optional.
}
}
模擬非封鎖函式呼叫時,請使用 android.os.OutcomeReceiver
OutcomeReceiver<R,E>
會在成功時回報結果值 R
,否則回報 E : Throwable
,與一般方法呼叫相同。將傳回結果或擲回例外狀況的封鎖方法轉換為非封鎖非同步方法時,請使用 OutcomeReceiver
做為回呼型別:
interface FooType {
// Before:
public FooResult requestFoo(FooRequest request);
// After:
public void requestFooAsync(FooRequest request, Executor executor,
OutcomeReceiver<FooResult, Throwable> callback);
}
以這種方式轉換的非同步方法一律會傳回 void
。requestFoo
會傳回的任何結果,都會改為透過提供的 executor
呼叫 requestFooAsync
的 callback
參數 OutcomeReceiver.onResult
回報。requestFoo
會擲回的任何例外狀況,都會以相同方式回報給 OutcomeReceiver.onError
方法。
使用 OutcomeReceiver
回報非同步方法結果,也能為使用 androidx.core:core-ktx
的 Continuation.asOutcomeReceiver
擴充功能的非同步方法提供 Kotlin suspend fun
包裝函式:
suspend fun FooType.requestFoo(request: FooRequest): FooResult =
suspendCancellableCoroutine { continuation ->
requestFooAsync(request, Runnable::run, continuation.asOutcomeReceiver())
}
這類擴充功能可讓 Kotlin 用戶端呼叫非封鎖的非同步方法,且不必封鎖呼叫執行緒,就像呼叫一般函式一樣方便。這些平台 API 的 1 對 1 擴充功能,可能會與標準版本相容性檢查和考量事項合併,做為 Jetpack 中的 androidx.core:core-ktx
構件提供。詳情請參閱 asOutcomeReceiver 的說明文件,瞭解取消注意事項和範例。
如果非同步方法不符合方法語意,在工作完成時傳回結果或擲回例外狀況,就不應使用 OutcomeReceiver
做為回呼型別。請改用下一節列出的其他選項。
優先使用函式介面,而非建立新的單一抽象方法 (SAM) 型別
API 級別 24 新增了 java.util.function.*
(參考文件) 類型,提供一般 SAM 介面 (例如 Consumer<T>
),適合做為回呼 Lambda 使用。在許多情況下,建立新的 SAM 介面在型別安全或傳達意圖方面價值不高,卻會不必要地擴大 Android API 表面積。
建議您使用這些泛型介面,而非建立新介面:
Runnable
:() -> Unit
Supplier<R>
:() -> R
Consumer<T>
:(T) -> Unit
Function<T,R>
:(T) -> R
Predicate<T>
:(T) -> Boolean
- 參考文件提供更多範例
SAM 參數的放置位置
SAM 參數應放在最後,以便從 Kotlin 慣用語法使用,即使方法因額外參數而過載也一樣。
public void schedule(Runnable runnable)
public void schedule(int delay, Runnable runnable)
文件
這些是 API 公開說明文件 (Javadoc) 的相關規則。
所有公開 API 都必須記錄
所有公開 API 都必須提供充足的說明文件,說明開發人員如何使用 API。假設開發人員使用自動完成功能或瀏覽 API 參考文件時找到這個方法,並從相鄰的 API 介面 (例如同一個類別) 取得最少量的內容。
方法
方法參數和傳回值必須分別使用 @param
和 @return
說明文件註解記錄。Javadoc 內文的格式應如同前面加上「這個方法...」。
如果方法不採用任何參數、沒有特殊考量,且會傳回方法名稱所指的內容,您可以省略 @return
,並撰寫類似下列的說明文件:
/**
* Returns the priority of the thread.
*/
@IntRange(from = 1, to = 10)
public int getPriority() { ... }
一律使用 Javadoc 中的連結
文件應連結至相關常數、方法和其他元素的其他文件。請使用 Javadoc 標記 (例如 @see
和 {@link foo}
),而非純文字字詞。
以下列來源範例來說:
public static final int FOO = 0;
public static final int BAR = 1;
請勿使用原始文字或程式碼字型:
/**
* Sets value to one of FOO or <code>BAR</code>.
*
* @param value the value being set, one of FOO or BAR
*/
public void setValue(int value) { ... }
請改用連結:
/**
* Sets value to one of {@link #FOO} or {@link #BAR}.
*
* @param value the value being set
*/
public void setValue(@ValueType int value) { ... }
請注意,在參數上使用 IntDef
註解 (例如 @ValueType
) 時,系統會自動產生說明文件,指定允許的型別。如要進一步瞭解 IntDef
,請參閱註解指南。
新增 Javadoc 時,請執行 update-api 或 docs 目標
新增 @link
或 @see
標記時,這項規則尤其重要,請務必確認輸出內容符合預期。Javadoc 中的 ERROR 輸出內容通常是因為連結錯誤。update-api
或 docs
Make 目標都會執行這項檢查,但如果您只變更 Javadoc,不需要執行 update-api
目標,docs
目標可能會更快。
使用 {@code foo} 區分 Java 值
請使用 {@code...}
包裝 true
、false
和 null
等 Java 值,與說明文件文字區別。
在 Kotlin 來源中撰寫說明文件時,您可以像使用 Markdown 一樣,用反引號括住程式碼。
@param 和 @return 摘要應為單一句子片段
參數和回傳值摘要應以小寫字元開頭,且只能包含單一句子片段。如有其他資訊超過一句話的長度,請移至方法 Javadoc 主體:
/**
* @param e The element to be appended to the list. This must not be
* null. If the list contains no entries, this element will
* be added at the beginning.
* @return This method returns true on success.
*/
應改為:
/**
* @param e element to be appended to this list, must be non-{@code null}
* @return {@code true} on success, {@code false} otherwise
*/
需要說明的文件註解
說明註解 @hide
和 @removed
為何會從公開 API 中隱藏。
請加入相關操作說明,說明如何替換以 @deprecated
註解標記的 API 元素。
使用 @throws 記錄例外狀況
如果方法擲回已檢查的例外狀況 (例如 IOException
),請使用 @throws
記錄例外狀況。如要讓 Java 用戶端使用以 Kotlin 來源的 API,請使用 @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")
}
}
// ...
如果方法會叫用可能擲回例外狀況的非同步程式碼,請考慮開發人員如何找出並回應這類例外狀況。通常這會將例外狀況轉送至回呼,並記錄接收例外狀況的方法所擲回的例外狀況。除非非同步例外狀況確實是從註解方法重新擲回,否則不應使用 @throws
記錄這類例外狀況。
在文件第一句的結尾加上句號
Doclava 工具會以簡單的方式剖析文件,只要看到句號 (.) 後接空格,就會結束摘要文件 (類別文件頂端快速說明中使用的第一個句子)。這會造成兩個問題:
- 如果簡短文件結尾沒有句號,且該成員已繼承工具擷取的文件,摘要也會擷取這些繼承的文件。舉例來說,請參閱
R.attr
文件中的actionBarTabStyle
,其中已將維度說明新增至簡介。 - 基於相同原因,請避免在第一句中使用「例如」,因為 Doclava 會在「如」之後結束摘要文件。例如,請參閱
View.java
中的TEXT_ALIGNMENT_CENTER
。請注意,Metalava 會在句號後插入不斷行空格,自動修正這項錯誤;不過,請不要一開始就犯下這種錯誤。
將文件格式設為以 HTML 轉譯
Javadoc 會以 HTML 格式呈現,因此請相應地設定這些文件的格式:
換行符號應使用明確的
<p>
標記。請勿加入結尾</p>
標記。請勿使用 ASCII 轉譯清單或表格。
清單應分別使用
<ul>
或<ol>
,表示未排序和已排序。 每個項目都應以<li>
代碼開頭,但不需要結尾</li>
代碼。最後一個項目後必須加上結尾</ul>
或</ol>
標記。表格應使用
<table>
、<tr>
表示資料列,<th>
表示標題,<td>
表示儲存格。所有表格標記都需要相符的結尾標記。您可以在任何標記上使用class="deprecated"
,表示該標記已淘汰。如要建立內嵌程式碼字型,請使用
{@code foo}
。如要建立程式碼區塊,請使用
<pre>
。瀏覽器會剖析
<pre>
區塊內的所有文字,因此請小心使用括號<>
。您可以使用<
和>
HTML 實體逸出這些字元。或者,如果將違規部分包在
{@code foo}
中,也可以在程式碼片段中保留原始方括號<>
。例如:<pre>{@code <manifest>}</pre>
遵循 API 參考資料樣式指南
為確保類別摘要、方法說明、參數說明和其他項目的樣式一致,請按照官方 Java 語言指南的建議操作,詳情請參閱「How to Write Doc Comments for the Javadoc Tool」(如何為 Javadoc 工具撰寫文件註解)。
Android 架構專屬規則
這些規則與 Android 架構內建的 API、模式和資料結構有關 (例如 Bundle
或 Parcelable
)。
意圖建構工具應使用 create*Intent() 模式
意圖的建立者應使用名為 createFooIntent()
的方法。
使用 Bundle,不必建立新的通用資料結構
請避免建立新的通用資料結構,以表示任意鍵到型別值的對應。建議改用 Bundle
。
撰寫平台 API 時,通常會發生這種情況。平台 API 可做為非平台應用程式和服務之間的通訊管道,但平台不會讀取透過管道傳送的資料,且 API 合約可能部分定義於平台外部 (例如在 Jetpack 程式庫中)。
如果平台會讀取資料,請避免使用 Bundle
,並偏好使用強型別資料類別。
Parcelable 實作項目必須有公開的 CREATOR 欄位
可透過 CREATOR
取得 Parcelable 膨脹,而非原始建構函式。如果類別實作 Parcelable
,則其 CREATOR
欄位也必須是公開 API,且採用 Parcel
引數的類別建構函式必須是私有。
將 CharSequence 用於 UI 字串
在使用者介面中顯示字串時,請使用 CharSequence
允許 Spannable
執行個體。
如果只是使用者看不到的鍵或標籤/值,則 String
即可。
避免使用列舉
IntDef
在所有平台 API 中,都必須使用列舉,且在未綁定的程式庫 API 中,也應強烈考慮使用。只有在確定不會新增值時,才使用列舉。
IntDef
的優點:
- 可隨時間新增值
- 如果平台中新增列舉值,導致 Kotlin
when
陳述式不再完整,執行階段可能會失敗。
- 如果平台中新增列舉值,導致 Kotlin
- 執行階段不會使用任何類別或物件,只會使用基本型別
- 雖然 R8 或縮減功能可以避免未組合的程式庫 API 產生這類費用,但這項最佳化措施無法影響平台 API 類別。
列舉的優點
- Java、Kotlin 的慣用語言功能
- 啟用詳盡的切換,
when
陳述式用法- 注意 - 值不得隨時間變更,請參閱先前的清單
- 命名範圍明確且容易搜尋
- 啟用編譯時間驗證
- 舉例來說,Kotlin 中的
when
陳述式會傳回值
- 舉例來說,Kotlin 中的
- 這是可實作介面、擁有靜態輔助程式、公開成員或擴充方法,以及公開欄位的運作類別。
遵循 Android 套件分層階層
android.*
套件階層具有隱含排序,低層級套件不得依附於高層級套件。
避免提及 Google、其他公司及其產品
Android 平台是開放原始碼專案,目標是不受制於任何服務供應商。API 應為一般用途,且系統整合人員或具備必要權限的應用程式都能使用。
Parcelable 實作項目應為最終版本
平台定義的 Parcelable 類別一律會從 framework.jar
載入,因此應用程式嘗試覆寫 Parcelable
實作內容是無效的。
如果傳送應用程式擴充 Parcelable
,接收應用程式就無法使用傳送端的自訂實作項目解壓縮。向後相容性注意事項:如果您的類別過去不是最終類別,但沒有公開可用的建構函式,您仍然可以標示為 final
。
呼叫系統程序的方法應將 RemoteException 重新擲回為 RuntimeException
RemoteException
通常是由內部 AIDL 擲回,表示系統程序已終止,或應用程式嘗試傳送過多資料。在這兩種情況下,公開 API 都應重新擲回 RuntimeException
,避免應用程式持續進行安全性或政策決策。
如果您知道 Binder
呼叫的另一端是系統程序,建議採用下列樣板程式碼:
try {
...
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
針對 API 變更擲回特定例外狀況
公開 API 行為可能會因 API 級別而異,並導致應用程式當機 (例如強制執行新的安全性政策)。
如果 API 需要針對先前有效的要求擲回例外狀況,請擲回新的特定例外狀況,而非一般例外狀況。例如,顯示 ExportedFlagRequired
,而不是 SecurityException
(且 ExportedFlagRequired
可以擴展 SecurityException
)。
這有助於應用程式開發人員和工具偵測 API 行為變化。
實作複製建構函式,而非複製
由於 Object
類別缺少 API 合約,且擴充使用 clone()
的類別時會遇到困難,因此強烈建議不要使用 Java clone()
方法。請改用接受相同類型物件的複製建構函式。
/**
* Constructs a shallow copy of {@code other}.
*/
public Foo(Foo other)
如果類別依賴建構工具進行建構,請考慮新增建構工具副本建構函式,以便修改副本。
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
例項。如果需要,舊版程式碼可以使用 dup() 或 getFileDescriptor() 在 PFD 和 FD 之間轉換。
避免使用奇數大小的數值
請避免直接使用 short
或 byte
值,因為這些值通常會限制您日後發展 API 的方式。
避免使用 BitSet
java.util.BitSet
非常適合實作,但不適合公開 API。這項資料可變動,需要為高頻率方法呼叫分配資源,且無法提供每個位元代表的語意。
如要處理高效能情境,請使用 int
或 long
搭配 @IntDef
。如果成效不佳,請考慮使用 Set<EnumType>
。如為原始二進位資料,請使用 byte[]
。
建議使用 android.net.Uri
android.net.Uri
是 Android API 中 URI 的偏好封裝方式。
請避免使用 java.net.URI
,因為它在剖析 URI 時過於嚴格,且絕不要使用 java.net.URL
,因為其相等定義嚴重損毀。
隱藏標示為 @IntDef、@LongDef 或 @StringDef 的註解
標示為 @IntDef
、@LongDef
或 @StringDef
的註解,表示可傳遞至 API 的一組有效常數。不過,如果將這些常數匯出為 API,編譯器會內嵌常數,而註解的 API 存根 (適用於平台) 或 JAR (適用於程式庫) 中只會保留 (現在無用的) 值。
因此,這些註解的使用情形必須在平台中以 @hide
文件註解標示,或在程式庫中以 @RestrictTo.Scope.LIBRARY)
程式碼註解標示。在這兩種情況下,都必須標示為 @Retention(RetentionPolicy.SOURCE)
,以免出現在 API 存根或 JAR 中。
@RestrictTo(RestrictTo.Scope.LIBRARY)
@Retention(RetentionPolicy.SOURCE)
@IntDef({
STREAM_TYPE_FULL_IMAGE_DATA,
STREAM_TYPE_EXIF_DATA_ONLY,
})
public @interface ExifStreamType {}
建構平台 SDK 和程式庫 AAR 時,工具會擷取註解,並與編譯來源分開封裝。Android Studio 會讀取這個組合格式,並強制執行型別定義。
請勿新增設定供應商金鑰
請勿公開來自 Settings.Global
、Settings.System
或 Settings.Secure
的新金鑰。
請改為在相關類別中加入適當的 getter 和 setter Java API,這通常是「管理員」類別。視需要新增監聽器機制或廣播,將變更通知用戶端。
SettingsProvider
設定與 getter/setter 相比,有許多問題:
- 沒有型別安全。
- 無法以統一方式提供預設值。
- 無法以適當方式自訂權限。
- 舉例來說,您無法使用自訂權限保護設定。
- 無法正確新增自訂邏輯。
- 舉例來說,您無法根據設定 B 的值變更設定 A 的值。
舉例來說:
Settings.Secure.LOCATION_MODE
已存在很長一段時間,但位置資訊團隊已將其淘汰,改用適當的 Java API
LocationManager.isLocationEnabled()
和
MODE_CHANGED_ACTION
廣播,這讓團隊有更大的彈性,API 的語意也更加清楚。
請勿擴充 Activity 和 AsyncTask
AsyncTask
是實作詳細資料。請改為公開接聽程式,或在 AndroidX 中公開 ListenableFuture
API。
Activity
子類別無法組合。延長功能活動時間會導致功能與其他需要使用者執行相同操作的功能不相容。請改用 LifecycleObserver 等工具,透過組合方式達成目的。
使用 Context 的 getUser()
繫結至 Context
的類別 (例如從 Context.getSystemService()
傳回的任何項目),應使用繫結至 Context
的使用者,而非公開以特定使用者為目標的成員。
class FooManager {
Context mContext;
void fooBar() {
mIFooBar.fooBarForUser(mContext.getUser());
}
}
class FooManager {
Context mContext;
Foobar getFoobar() {
// Bad: doesn't appy mContext.getUserId().
mIFooBar.fooBarForUser(Process.myUserHandle());
}
Foobar getFoobar() {
// Also bad: doesn't appy mContext.getUserId().
mIFooBar.fooBar();
}
Foobar getFoobarForUser(UserHandle user) {
mIFooBar.fooBarForUser(user);
}
}
例外狀況:如果方法接受的值不代表單一使用者 (例如 UserHandle.ALL
),則可接受使用者引數。
使用 UserHandle,而非一般整數
建議使用 UserHandle
,確保型別安全,並避免混淆使用者 ID 和 UID。
Foobar getFoobarForUser(UserHandle user);
Foobar getFoobarForUser(int userId);
如果無法避免,代表使用者 ID 的 int
必須以 @UserIdInt
註解。
Foobar getFoobarForUser(@UserIdInt int user);
偏好使用接聽器或回呼來廣播意圖
廣播意圖功能強大,但會導致緊急行為,進而對系統健康狀態造成負面影響,因此應謹慎新增廣播意圖。
以下是我們不建議導入新廣播意圖的具體原因:
傳送廣播時若沒有
FLAG_RECEIVER_REGISTERED_ONLY
標記,系統會強制啟動尚未執行的應用程式。雖然有時這是預期結果,但可能會導致數十個應用程式同時啟動,對系統健康狀態造成負面影響。建議您改用其他策略 (例如JobScheduler
),以便在滿足各種前提條件時,更妥善地進行協調。傳送廣播時,篩選或調整傳送至應用程式內容的功能有限。因此,您很難或無法因應日後的隱私權疑慮,或是根據接收應用程式的目標 SDK 導入行為變更。
由於廣播佇列是共用資源,因此可能會過載,導致事件無法及時傳送。我們發現許多廣播佇列的端對端延遲時間為 10 分鐘以上。
基於上述原因,我們建議新功能考慮使用事件監聽器、回呼或其他設施 (例如 JobScheduler
),而非廣播 Intent。
如果廣播意圖仍是理想的設計,請考慮以下最佳做法:
- 請盡可能使用
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。 - 強烈建議您新增系統層級權限,防止惡意應用程式將
Intent
傳送至開發人員服務。
Kotlin-Java 互通性
如需完整指南清單,請參閱 Android 官方的 Kotlin-Java 互通性指南。我們已將部分指南複製到這個指南,以提高曝光度。
API 瀏覽權限
部分 Kotlin API (例如 suspend fun
s) 並不適合 Java 開發人員使用;不過,請不要嘗試使用 @JvmSynthetic
控制語言專屬的可見度,因為這會對 API 在偵錯工具中的呈現方式產生連帶影響,導致偵錯更加困難。
如需具體指引,請參閱「Kotlin-Java 互通性指南」或「非同步指南」。
伴生物件
Kotlin 會使用 companion object
公開靜態成員。在某些情況下,這些項目會顯示在名為 Companion
的內部類別中,而非顯示在包含的類別中。Companion
類別在 API 文字檔中可能會顯示為空白類別,這是正常現象。
為盡量與 Java 相容,請使用 @JvmField
註解伴生物件的非常數欄位,並使用 @JvmStatic
註解公開函式,直接在包含類別中公開這些欄位和函式。
companion object {
@JvmField val BIG_INTEGER_ONE = BigInteger.ONE
@JvmStatic fun fromPointF(pointf: PointF) {
/* ... */
}
}
Android 平台 API 的演進
本節說明相關政策,包括可對現有 Android API 進行哪些類型的變更,以及如何實作這些變更,盡可能與現有應用程式和程式碼集相容。
二進位檔破壞性變更
避免在最終公開 API 介面中進行破壞性的二進位檔變更。執行 make update-api
時,這類變更通常會引發錯誤,但可能會有 Metalava 的 API 檢查未偵測到的極端情況。如有疑問,請參閱 Eclipse 基金會的「Evolving Java-based APIs」指南,詳細瞭解 Java 中相容的 API 變更類型。隱藏 (例如系統) API 的二進位檔重大變更應遵循淘汰/取代週期。
來源破壞性變更
即使不是二進位檔破壞性變更,我們也不建議進行來源破壞性變更。舉例來說,在現有類別中新增泛型是二進位檔相容的變更,但可能會因繼承或不明確的參照而導致編譯錯誤,因此會破壞來源。執行 make update-api
時,來源重大變更不會引發錯誤,因此您必須謹慎瞭解現有 API 簽章變更的影響。
在某些情況下,為了提升開發人員體驗或程式碼正確性,必須進行來源重大變更。舉例來說,在 Java 來源中新增是否可為空值的註解,可提升與 Kotlin 程式碼的互通性,並降低發生錯誤的機率,但通常需要變更原始碼 (有時是大幅變更)。
私人 API 異動
您隨時可以變更以 @TestApi
註解的 API。
您必須保留以 @SystemApi
註解的 API 三年。您必須按照下列時間表,移除或重構系統 API:
- API y - 已新增
- API y+1 - 淘汰
- 使用
@Deprecated
標記程式碼。 - 新增替代項目,並使用
@deprecated
說明文件註解,在已淘汰程式碼的 Javadoc 中連結至替代項目。 - 在開發週期中,針對內部使用者回報錯誤,告知他們 API 即將淘汰。這有助於驗證替代 API 是否足夠。
- 使用
- API y+2 - 軟性移除
- 使用
@removed
標記程式碼。 - 視需要為指定目標為目前 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)
您必須一併淘汰 XML 中定義且在 Java 中公開的 API,包括 android.R
類別中公開的屬性和可設定樣式的屬性,並提供摘要:
<!-- Attribute whether the accessibility service ...
{@deprecated Not used by the framework}
-->
<attr name="canRequestEnhancedWebAccessibility" format="boolean" />
API 的淘汰時機
淘汰最適合用於避免新程式碼採用 API。
我們也要求您在 API @removed
前將其標示為 @deprecated
,但這無法充分激勵開發人員從已使用的 API 遷移。
淘汰 API 前,請先考量對開發人員的影響。API 淘汰的影響包括:
javac
會在編譯期間發出警告。- 淘汰警告無法全域停用或設為基準,因此使用
-Werror
的開發人員必須個別修正或停用每個已淘汰的 API 用法,才能更新編譯 SDK 版本。 - 無法禁止系統在匯入已淘汰的類別時發出淘汰警告。因此,開發人員必須先內嵌已淘汰類別的每個完整類別名稱,才能更新編譯 SDK 版本。
- 淘汰警告無法全域停用或設為基準,因此使用
d.android.com
的說明文件會顯示淘汰通知。- Android Studio 等 IDE 會在 API 使用位置顯示警告。
- IDE 可能會降低 API 的排名,或從自動完成功能中隱藏 API。
因此,如果 API 遭到淘汰,最重視程式碼健全性的開發人員 (也就是使用 -Werror
的開發人員) 可能會因此不採用新的 SDK。如果開發人員不擔心現有程式碼中的警告,很可能會完全忽略淘汰項目。
如果 SDK 引入大量淘汰項目,這兩種情況都會更加嚴重。
因此,我們建議您只在下列情況下停用 API:
- 我們預計在日後推出的版本中
@remove
這個 API。 - 使用 API 會導致不正確或未定義的行為,我們無法修正,否則會破壞相容性。
當您淘汰 API 並改用新 API 時,強烈建議在 Jetpack 程式庫 (例如 androidx.core
) 中加入對應的相容性 API,簡化新舊裝置的支援作業。
我們不建議在目前和未來版本中,停用運作正常的 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,才能進行軟性移除。移除所有 API 的文件參照,並在軟性移除 API 時使用 @removed <summary>
文件註解。摘要必須包含移除原因,並可納入遷移策略,如「淘汰」一文所述。
軟移除的 API 可以維持原狀,但更重要的是必須保留,以免現有呼叫端在呼叫 API 時當機。在某些情況下,這可能表示要保留行為。
測試涵蓋範圍必須維持不變,但測試內容可能需要因應行為變更而調整。測試仍須驗證現有呼叫者在執行階段不會當機。您可以維持軟移除 API 的行為,但更重要的是,您必須保留該行為,以免現有呼叫端在呼叫 API 時當機。在某些情況下,這可能表示要保留行為。
您必須維持測試涵蓋範圍,但測試內容可能需要變更,以因應行為變更。測試仍須驗證現有呼叫者在執行階段不會當機。
在技術層面,我們會使用 @remove
Javadoc 註解,從 SDK 存根 JAR 和編譯時間類路徑中移除 API,但 API 仍會存在於執行階段類路徑中,與 @hide
API 類似:
/**
* Ringer volume. This is ...
*
* @removed Not functional since API 2.
*/
public static final String VOLUME_RING = ...
從應用程式開發人員的角度來看,API 不會再顯示在自動完成功能中,且參照該 API 的原始碼在 compileSdk
等於或晚於移除該 API 的 SDK 時,將無法編譯;不過,針對較早的 SDK 編譯原始碼時,仍會成功,且參照該 API 的二進位檔仍可正常運作。
部分類別的 API 不得軟性移除。您不得軟性移除特定類別的 API。
抽象方法
您不得軟性移除開發人員可能會擴充的類別中的抽象方法。這樣一來,開發人員就無法在所有 SDK 層級成功擴充類別。
在極少數情況下,如果開發人員從未且無法擴充類別,您仍可軟性移除抽象方法。
硬體移除
硬移除是二進位檔中斷的變更,絕不應出現在公開 API 中。
不建議使用的註解
我們使用 @Discouraged
註解,指出在大多數 (超過 95%) 情況下,不建議使用某個 API。不建議使用的 API 與已淘汰的 API 不同,因為前者存在無法淘汰的狹窄關鍵用途。將 API 標示為不建議使用時,您必須提供說明和替代解決方案:
@Discouraged(message = "Use of this function is discouraged because resource
reflection makes it harder to perform build
optimizations and compile-time verification of code. It
is much more efficient to retrieve resources by
identifier (such as `R.foo.bar`) than by name (such as
`getIdentifier()`)")
public int getIdentifier(String name, String defType, String defPackage) {
return mResourcesImpl.getIdentifier(name, defType, defPackage);
}
您不應新增不建議使用的 API。
現有 API 行為異動
在某些情況下,您可能想變更現有 API 的實作行為。舉例來說,在 Android 7.0 中,我們改善了DropBoxManager
,讓開發人員在嘗試發布過大的事件時,能清楚瞭解無法透過Binder
傳送事件的原因。
不過,為避免現有應用程式發生問題,我們強烈建議您為舊版應用程式保留安全行為。過去,我們是根據應用程式的 ApplicationInfo.targetSdkVersion
來防範這些行為變更,但最近已改為要求使用應用程式相容性架構。以下範例說明如何使用這個新架構實作行為變更:
import android.app.compat.CompatChanges;
import android.compat.annotation.ChangeId;
import android.compat.annotation.EnabledSince;
public class MyClass {
@ChangeId
// This means the change will be enabled for target SDK R and higher.
@EnabledSince(targetSdkVersion=android.os.Build.VERSION_CODES.R)
// Use a bug number as the value, provide extra detail in the bug.
// FOO_NOW_DOES_X will be the change name, and 123456789 the change ID.
static final long FOO_NOW_DOES_X = 123456789L;
public void doFoo() {
if (CompatChanges.isChangeEnabled(FOO_NOW_DOES_X)) {
// do the new thing
} else {
// do the old thing
}
}
}
採用這個應用程式相容性架構設計,開發人員就能在預先發布和 Beta 版期間,暫時停用特定行為變更,以便偵錯應用程式,不必一次調整數十項行為變更。
前瞻相容性
前瞻相容性是一種設計特徵,可讓系統接受適用於後續版本的輸入內容。就 API 設計而言,您必須特別注意初始設計和未來變更,因為開發人員希望編寫一次程式碼、測試一次,就能在任何地方順利執行。
下列原因最常導致 Android 發生向前相容性問題:
- 將新常數新增至先前假設為完整的集合 (例如
@IntDef
或enum
),例如switch
具有會擲回例外狀況的default
。 - 新增對 API 介面中未直接擷取功能的支援 (例如,支援在 XML 中指派
ColorStateList
類型資源,先前僅支援<color>
資源)。 - 放寬執行階段檢查的限制,例如移除舊版中的
requireNotNull()
檢查。
在上述所有情況中,開發人員只會在執行階段發現有問題。更糟的是,他們可能會從現場舊裝置的當機報告中發現問題。
此外,這些情況在技術上都是有效的 API 變更。這些變更不會破壞二進位檔或來源相容性,API Lint 也不會發現任何這類問題。
因此,API 設計人員修改現有類別時,必須格外留意。請自問:「這個變更是否會導致僅針對最新版平台編寫及測試的程式碼,在較舊版本上失敗?」
XML 架構
如果 XML 結構定義是元件間的穩定介面,則必須明確指定該結構定義,並以回溯相容的方式演進,與其他 Android API 類似。舉例來說,XML 元素和屬性的結構必須保留,類似於其他 Android API 介面上的方法和變數。
XML 淘汰
如要淘汰 XML 元素或屬性,可以新增 xs:annotation
標記,但必須遵循典型的 @SystemApi
演進生命週期,繼續支援所有現有的 XML 檔案。
<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 專案可讓您個別更新 Android OS 的子系統 (「Mainline 模組」),不必更新整個系統映像檔。
主線模組必須從核心平台「解除綁定」,也就是說,每個模組與世界其他地方的所有互動都必須使用正式 (公開或系統) API 進行。
主線模組應遵循特定設計模式。本節將說明這些項目。
<Module>FrameworkInitializer 模式
如果主線模組需要公開 @SystemService
類別 (例如 JobScheduler
),請使用下列模式:
從模組公開
<YourModule>FrameworkInitializer
類別。這個類別必須位於$BOOTCLASSPATH
中。範例: StatsFrameworkInitializer使用
@SystemApi(client = MODULE_LIBRARIES)
標記。在其中新增
public static void registerServiceWrappers()
方法。當服務管理員類別需要參照
Context
時,請使用SystemServiceRegistry.registerContextAwareService()
註冊。如果服務管理員類別不需要參照
Context
,請使用SystemServiceRegistry.registerStaticService()
註冊。從
SystemServiceRegistry
的靜態初始設定式呼叫registerServiceWrappers()
方法。
<Module>ServiceManager 模式
一般來說,如要註冊系統服務繫結器物件或取得參照,會使用 ServiceManager
,但主線模組無法使用,因為該物件已隱藏。這個類別已隱藏,因為主線模組不應註冊或參照靜態平台或其他模組公開的系統服務繫結器物件。
主線模組可以改用下列模式,以便註冊及取得模組內實作的繫結器服務參照。
按照 TelephonyServiceManager 的設計,建立
<YourModule>ServiceManager
類別將類別公開為
@SystemApi
。如果只需要從$BOOTCLASSPATH
類別或系統伺服器類別存取,可以使用@SystemApi(client = MODULE_LIBRARIES)
;否則@SystemApi(client = PRIVILEGED_APPS)
即可。這個類別包含:
- 隱藏建構函式,因此只有靜態平台程式碼可以例項化。
- 公用 getter 方法,會傳回特定名稱的
ServiceRegisterer
執行個體。如果您有一個繫結器物件,就需要一個 getter 方法。如果您有兩個,就需要兩個 getter。 - 在
ActivityThread.initializeMainlineModules()
中,對這個類別執行個體化,並傳遞至模組公開的靜態方法。一般來說,您會在FrameworkInitializer
類別中新增靜態@SystemApi(client = MODULE_LIBRARIES)
API,並採用該 API。
這個模式會禁止其他主線模組存取這些 API,因為其他模組無法取得 <YourModule>ServiceManager
的例項,即使 get()
和 register()
API 對這些模組可見也一樣。
以下說明電話服務如何取得電話服務的參照: 程式碼搜尋連結。
如果您在原生程式碼中實作服務繫結器物件,請使用AServiceManager
原生 API。這些 API 對應於 ServiceManager
Java API,但原生 API 會直接公開給主線模組。請勿使用這些方法註冊或參照不屬於模組的繫結器物件。如果您從原生程式碼公開繫結器物件,則 <YourModule>ServiceManager.ServiceRegisterer
不需要 register()
方法。
主系列模組中的權限定義
含有 APK 的 Mainline 模組可以在 APK 中定義 (自訂) 權限,方式與一般 APK 相同。AndroidManifest.xml
如果定義的權限僅供模組內部使用,權限名稱應加上 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
。