本頁面旨在提供開發人員指南,協助他們瞭解 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、Socket、FlatBuffers 或任何其他非 Java 和非 NDK API 介面。不過,Android 中的大部分 ID,都位於 AIDL 中,因此本頁將著重於 AIDL。
產生的 AIDL 類別不符合 API 樣式指南規定 (例如,無法使用超載),且 AIDL 工具並未明確設計用於維持語言 API 相容性,因此您無法將這些類別嵌入公用 API。
請改為在 AIDL 介面上方新增公用 API 層,即使它最初是淺層包裝函式也一樣。
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);
}
}
對於非 Android 平台的 Binder
介面 (例如 Google Play 服務匯出的服務介面,供應用程式使用),需要穩定、已發布且有版本的 IPC 介面,這表示要改進介面本身的難度會大幅提高。不過,如果有需要,建議您在其中加入包裝函式層,以便符合其他 API 規範,並讓您更輕鬆地使用相同的公開 API 來處理新版 IPC 介面。
請勿在公用 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
。Manager 類別會與系統服務互動,並且是單一互動點。不需要自訂,因此請將其宣告為 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
}
}
}
單一例項與 singleton 的差異在於,開發人員可以建立 SingleInstance
的假版本,並使用自己的 Dependency Injection 架構來管理實作,而無須建立包裝函式,或者程式庫可以在 -testing
構件中提供自己的假版本。
釋放資源的類別應實作 AutoCloseable
透過 close
、release
、destroy
或類似方法釋放資源的類別,應實作 java.lang.AutoCloseable
,讓開發人員在使用 try-with-resources
區塊時自動清理這些資源。
避免在 android.* 中引入新的 View 子類別。
請勿在平台公用 API (即 android.*
) 中導入直接或間接繼承自 android.view.View
的新類別。
Android 的 UI 工具包現在採取「先 Compose」策略。平台公開的新 UI 功能應以較低階的 API 公開,以便在 Jetpack 程式庫中為開發人員實作 Jetpack Compose,以及視圖為基礎的 UI 元件 (選用)。在程式庫中提供這些元件,可在平台功能無法使用時,提供回溯導入的實作機會。
欄位
這些規則是關於類別上的公開欄位。
不要公開原始欄位
Java 類別不應直接公開欄位。欄位應為私有欄位,且只能透過公開的 getter 和 setter 存取,無論這些欄位是否為最終欄位皆然。
少數例外狀況包括基本資料結構,在這種情況下,您不需要強化指定或擷取欄位的行為。在這種情況下,請使用標準變數命名慣例命名欄位,例如 Point.x
和 Point.y
。
Kotlin 類別可公開屬性。
公開的欄位應標示為「final」
強烈建議您不要使用原始欄位 (@see 不要公開原始欄位)。但在極少數情況下,欄位會以公開欄位的形式公開,請將該欄位標示為 final
。
請勿公開內部欄位
請勿在公開 API 中參照內部欄位名稱。
public int mFlags;
使用 public 而非 protected
常數
以下是關於公開常數的規則。
標記常數不得重疊 int 或 long 值
Flags 表示可組合成某個聯合值的位元。如果不是這種情況,請勿呼叫變數或常數 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 開放原始碼專案使用的。
意圖動作和額外項目,以及 Bundle 項目,應使用其定義的套件名稱來命名空間。
package android.foo.bar {
public static final String ACTION_BAZ = "android.foo.bar.action.BAZ"
public static final String EXTRA_BAZ = "android.foo.bar.extra.BAZ"
}
使用 public 而非 protected
使用一致的字首
相關常數都應以相同的前置字串開頭。舉例來說,如果要使用旗標值的常數集合:
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 使用常數的標準前置字串
使用一致的資源名稱
公開 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 的形式公開。不過,如果必須公開,則公開版面配置和可繪項目的名稱必須使用 under_score 命名慣例,例如 layout/simple_list_item_1.xml
或 drawable/title_bar_tall.xml
。
當常數可能變更時,請將其設為動態變數
編譯器可能會內嵌常數值,因此維持相同的值會視為 API 合約的一部分。如果 MIN_FOO
或 MAX_FOO
常數的值日後可能會變更,建議改為使用動態方法。
CameraManager.MAX_CAMERAS
CameraManager.getMaxCameras()
考慮回呼的前瞻相容性
在未來 API 版本中定義的常數,對指定舊版 API 的應用程式而言是未知的。因此,傳送至應用程式的常數應考量應用程式的目標 API 版本,並將較新的常數對應至一致的值。請考慮下列情境:
假設的 SDK 來源:
// Added in API level 22
public static final int STATUS_SUCCESS = 1;
public static final int STATUS_FAILURE = 2;
// Added in API level 23
public static final int STATUS_FAILURE_RETRY = 3;
// Added in API level 26
public static final int STATUS_FAILURE_ABORT = 4;
使用 targetSdkVersion="22"
的假設應用程式:
if (result == STATUS_FAILURE) {
// Oh no!
} else {
// Success!
}
在本例中,應用程式是在 API 級別 22 的限制下設計,並做出了 (相對) 合理的假設,即只有兩種可能的狀態。不過,如果應用程式收到新新增的 STATUS_FAILURE_RETRY
,則會將此視為成功。
傳回常數的方法可透過限制輸出內容,讓輸出內容與應用程式指定的 API 級別相符,從而安全地處理這類情況:
private int mapResultForTargetSdk(Context context, int result) {
int targetSdkVersion = context.getApplicationInfo().targetSdkVersion;
if (targetSdkVersion < 26) {
if (result == STATUS_FAILURE_ABORT) {
return STATUS_FAILURE;
}
if (targetSdkVersion < 23) {
if (result == STATUS_FAILURE_RETRY) {
return STATUS_FAILURE;
}
}
}
return result;
}
開發人員無法預測常數清單是否會在未來變更。如果您使用 UNKNOWN
或 UNSPECIFIED
常數定義 API,而該常數看起來像是萬用常數,開發人員就會假設在編寫應用程式時,已發布的常數是完整的。如果您不想設定這項預期值,請重新考慮是否應為 API 使用萬用常數。
此外,程式庫無法從應用程式中分離出自己的 targetSdkVersion
,而且透過程式庫程式碼處理 targetSdkVersion
行為變更的過程複雜且容易出錯。
整數或字串常數
如果值的命名空間無法在套件外擴充,請使用整數常數和 @IntDef
。如果命名空間是共用,或可由套件外部的程式碼延伸,請使用字串常數。
資料類別
資料類別代表一組不可變動的屬性,並提供一組小型且明確定義的實用程式函式,用於與該資料互動。
請勿在公用 Kotlin API 中使用 data class
,因為 Kotlin 編譯器無法保證產生的程式碼的語言 API 或二進位相容性。請改為手動實作必要的函式。
建立例項
在 Java 中,如果屬性不多,資料類別應提供建構函式;如果屬性很多,則應使用 Builder
模式。
在 Kotlin 中,無論屬性數量為何,資料類別都應提供含有預設引數的建構函式。在指定 Java 用戶端時,提供建構工具也可能有助於在 Kotlin 中定義的資料類別。
修改和複製
如果需要修改資料,請提供具有複製建構函式的 Builder
類別 (Java),或是傳回新物件的 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);
例外狀況:
如果時間長度是專門套用於逾時值,則適合使用「timeout」。
類型為 java.time.Instant
的「time」適合用於表示特定時間點,而非時間長度。
以原始值表示時間長度或時間的方法應以時間單位命名,並使用 long
接受或傳回持續時間做為原始值的方法,應在方法名稱後加上關聯的時間單位 (例如 Millis
、Nanos
、Seconds
),以便保留未經修飾的名稱,以便與 java.time.Duration
搭配使用。請參閱「時間」。
方法也應適當地加上單位和時間基底的註解:
@CurrentTimeMillisLong
:值為非負時間戳記,以自 1970-01-01T00:00:00Z 起的毫秒數量測。@CurrentTimeSecondsLong
:值為非負時間戳記,以自 1970-01-01T00:00:00Z 起的秒數為單位。@DurationMillisLong
:值為以毫秒為單位的非負時間長度。@ElapsedRealtimeLong
:值是SystemClock.elapsedRealtime()
時間基準中的非負時間戳記。@UptimeMillisLong
:值是SystemClock.uptimeMillis()
時間基準中的非負時間戳記。
原始時間參數或傳回值應使用 long
,而非 int
。
ValueAnimator.setDuration(@DurationMillisLong long);
ValueAnimator.setDurationNanos(long);
表示時間單位的方法應使用非縮寫的縮寫單位名稱
public void setIntervalNs(long intervalNs);
public void setTimeoutUs(long timeoutUs);
public void setIntervalNanos(long intervalNanos);
public void setTimeoutMicros(long timeoutMicros);
為長時間引數加上註解
平台包含多個註解,可為 long
類型的時間單位提供更強的型別:
@CurrentTimeMillisLong
:值為非負時間戳記,以1970-01-01T00:00:00Z
起算的毫秒數為單位,因此以System.currentTimeMillis()
時間基準計算。@CurrentTimeSecondsLong
:值為非負時間戳記,以1970-01-01T00:00:00Z
起算的秒數為單位。@DurationMillisLong
:值為以毫秒為單位的非負時間長度。@ElapsedRealtimeLong
:值是SystemClock#elapsedRealtime()
時間基準中的非負時間戳記。@UptimeMillisLong
:值是SystemClock#uptimeMillis()
時間基準中的非負時間戳記。
測量單位
對於表示時間以外的所有測量單位方法,建議使用駝峰式大寫的 SI 單位前置字元。
public long[] getFrequenciesKhz();
public float getStreamVolumeDb();
將選用參數放在超載的結尾
如果您有含選用參數的方法超載,請將這些參數放在最後,並與其他參數保持一致的順序:
public int doFoo(boolean flag);
public int doFoo(int id, boolean flag);
public int doFoo(boolean flag);
public int doFoo(boolean flag, int id);
為選用引數新增超載時,簡單方法的行為應與為更複雜方法提供預設引數時的行為完全相同。
推論:如果方法是多型別的,除了新增選用引數或接受不同類型的引數以外,請勿超載方法。如果超載的方法執行的動作與原始方法截然不同,請為其命名。
含有預設參數的方法必須使用 @JvmOverloads 註解 (僅限 Kotlin)
含有預設參數的方法和建構函式必須加上 @JvmOverloads
註解,才能維持二進位程式相容性。
如需詳細資訊,請參閱官方 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);
另請參閱:在超載中將選用參數放在最後
建構工具
建議使用建構工具模式建立複雜的 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);
}
建構工具 setter 可接受選用屬性的 @Nullable 引數
通常在第二級輸入內容中使用可空值會比較簡單,特別是在 Kotlin 中,因為 Kotlin 會使用預設引數,而非建構工具和超載。
此外,@Nullable
Setter 會與其 Getter 相符,且 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.
*/
建構函式可為可變屬性提供建構函式,這些屬性可在已建構的類別中使用
如果您的類別具有可變動屬性,且需要 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);
建構工具不應有 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()
方法,以便傳回建構物件的例項。
Builder build() 方法必須傳回 @NonNull 物件
建構函式的 build()
方法應會傳回已建構物件的非空值例項。如果因參數無效而無法建立物件,則可將驗證作業延後至建構方法,並應擲回 IllegalStateException
。
不要公開內部鎖定
公開 API 中的 synchronized
方法不應使用 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;
將 set
/is
用於 Java 存取子方法,或將 is
用於 Java 欄位,即可將這些項目用作 Kotlin 的屬性:
obj.isVisible = true
obj.isFactoryResetProtectionEnabled = false
if (!obj.isAvailable) return
屬性和存取方法通常應使用正面命名,例如 Enabled
而非 Disabled
。使用負面術語會顛倒 true
和 false
的意思,導致更難推論行為。
// Passing false here is a double-negative.
void setFactoryResetProtectionDisabled(boolean disabled);
如果布林值是用來描述屬性的包含或擁有權,您可以使用 has 而非 is;不過,這會不與 Kotlin 屬性語法搭配運作:
// Transient state is an indirect property used to track state
// related to the object. The object is not transient; rather,
// the object "has" transient state associated with it:
void setHasTransientState(boolean hasTransientState);
boolean hasTransientState();
以下是一些可能更適合的替代前置詞,包括「can」和「should」:
// "Can" describes a behavior that the object may provide,
// and here is more concise than setRecordingEnabled or
// setRecordingAllowed. The object "can" record:
void setCanRecord(boolean canRecord);
boolean canRecord();
// "Should" describes a hint or property that is not strictly
// enforced, and here is more explicit than setFitWidthEnabled.
// The object "should" fit width:
void setShouldFitWidth(boolean shouldFitWidth);
boolean shouldFitWidth();
切換行為或功能的方法可能會使用 is 前置字元和 Enabled 後置字元:
// "Enabled" describes the availability of a property, and is
// more appropriate here than "can use" or "should use" the
// property:
void setWiFiRoamingSettingEnabled(boolean enabled)
boolean isWiFiRoamingSettingEnabled()
同樣地,表示對其他行為或功能的依附性方法可能會使用 is 前置字串和 Supported 或 Required 後置字串:
// "Supported" describes whether this API would work on devices that support
// multiple users. The API "supports" multi-user:
void setMultiUserSupported(boolean supported)
boolean isMultiUserSupported()
// "Required" describes whether this API depends on devices that support
// multiple users. The API "requires" multi-user:
void setMultiUserRequired(boolean required)
boolean isMultiUserRequired()
一般來說,方法名稱應以問題的形式書寫,並由傳回值回答。
Kotlin 屬性方法
針對類別屬性 var foo: Foo
,Kotlin 會使用一致的規則產生 get
/set
方法:在 getter 前方加上 get
,並將第一個字元大寫;在 setter 前方加上 set
,並將第一個字元大寫。屬性宣告會分別產生名為 public Foo getFoo()
和 public void setFoo(Foo foo)
的方法。
如果屬性為 Boolean
類型,則在名稱產生時會套用額外規則:如果屬性名稱開頭為 is
,則 getter 方法名稱不會加上 get
,而是直接使用屬性名稱。因此,建議您為 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
您應提供兩種 setter 方法:一個方法會採用完整位元字串,並覆寫所有現有的標記;另一個方法則會採用自訂位元遮罩,提供更彈性的設定。
/**
* Sets the state of all scroll indicators.
* <p>
* See {@link #setScrollIndicators(int, int)} for usage information.
*
* @param indicators a bitmask of indicators that should be enabled, or
* {@code 0} to disable all indicators
* @see #setScrollIndicators(int, int)
* @see #getScrollIndicators()
*/
public void setScrollIndicators(@ScrollIndicators int indicators);
/**
* Sets the state of the scroll indicators specified by the mask. To change
* all scroll indicators at once, see {@link #setScrollIndicators(int)}.
* <p>
* When a scroll indicator is enabled, it will be displayed if the view
* can scroll in the direction of the indicator.
* <p>
* Multiple indicator types may be enabled or disabled by passing the
* logical OR of the specified types. If multiple types are specified, they
* will all be set to the same enabled state.
* <p>
* For example, to enable the top scroll indicator:
* {@code setScrollIndicators(SCROLL_INDICATOR_TOP, SCROLL_INDICATOR_TOP)}
* <p>
* To disable the top scroll indicator:
* {@code setScrollIndicators(0, SCROLL_INDICATOR_TOP)}
*
* @param indicators a bitmask of values to set; may be a single flag,
* the logical OR of multiple flags, or 0 to clear
* @param mask a bitmask indicating which indicator flags to modify
* @see #setScrollIndicators(int)
* @see #getScrollIndicators()
*/
public void setScrollIndicators(@ScrollIndicators int indicators, @ScrollIndicators int mask);
Getter
您應提供一個 getter,以便取得完整位元遮罩。
/**
* Returns a bitmask representing the enabled scroll indicators.
* <p>
* For example, if the top and left scroll indicators are enabled and all
* other indicators are disabled, the return value will be
* {@code View.SCROLL_INDICATOR_TOP | View.SCROLL_INDICATOR_LEFT}.
* <p>
* To check whether the bottom scroll indicator is enabled, use the value
* of {@code (getScrollIndicators() & View.SCROLL_INDICATOR_BOTTOM) != 0}.
*
* @return a bitmask representing the enabled scroll indicators
*/
@ScrollIndicators
public int getScrollIndicators();
使用 public 而非 protected
在公用 API 中,請一律優先使用 public
而非 protected
。長期來說,受保護的存取權會變得相當麻煩,因為在預設情況下,外部存取權就足以提供公用存取器,因此實作者必須覆寫。
請注意,protected
可見度不會阻止開發人員呼叫 API,只會讓呼叫變得稍微惱人。
實作 equals() 和 hashCode() 或兩者皆不實作
如果您覆寫其中一個,就必須覆寫另一個。
為資料類別實作 toString()
建議資料類別覆寫 toString()
,以便開發人員對程式碼進行偵錯。
記錄輸出內容是用於程式行為還是偵錯
決定是否要讓程式行為依賴您的實作方式。舉例來說,UUID.toString() 和 File.toString() 會記錄程式的特定格式。如果您只會公開用於偵錯的資訊 (例如 Intent),請從超類別繼承文件。
請勿加入額外資訊
toString()
提供的所有資訊,也應可透過物件的公開 API 取得。否則,您會鼓勵開發人員剖析並依賴 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
:表示指定的傳回值、參數或欄位「不能」為空值。將項目標示為 @Nullable
對 Android 來說相對較新,因此 Android 的大部分 API 方法並未一致記錄。因此,我們有「unknown, @Nullable
, @NonNull
」的三態狀態,這也是 @NonNull
為 API 規範的一部分的原因:
@NonNull
public String getName()
public void setName(@NonNull String name)
針對 Android 平台文件,註解方法參數會自動產生「此值可能為空值」的文件格式,除非「空值」在參數文件的其他位置明確使用。
現有的「不完全可為空值」方法:如果 API 中現有的未宣告 @Nullable
註解的方法可以在特定明顯情況下 (例如 findViewById()
) 傳回 null
,則可加上 @Nullable
註解。如果開發人員不想進行空值檢查,則應新增擲回 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 相容性,覆寫值的空值應與父項的目前空值相容。下表列出相容性預期。簡單來說,覆寫值應只比所覆寫的元素更嚴格,或與其相同。
類型 | 家長 | 為我的子女建立帳戶 |
---|---|---|
傳回類型 | 未標註 | 未註解或非空值 |
傳回類型 | 可以為空值 | 可為空值或不得為空值 |
傳回類型 | Nonnull | Nonnull |
有趣的引數 | 未標註 | 未註解或可為空值 |
有趣的引數 | 可以為空值 | 可以為空值 |
有趣的引數 | Nonnull | 可為空值或不得為空值 |
盡可能使用非空值 (例如 @NonNull) 引數
方法超載時,請盡量讓所有引數皆為非空值。
public void startActivity(@NonNull Component component) { ... }
public void startActivity(@NonNull Component component, @NonNull Bundle options) { ... }
這項規則也適用於超載的屬性 setter。主要引數應為非空值,清除資源應實作為個別方法。這可避免「無意義」的呼叫,也就是開發人員必須設定尾隨參數,即使不必要也要設定。
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 組合的可空性註解必須一致
單一邏輯屬性的 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
。
如果動作可能因非同步更新狀態或開發人員無法控制的情況而失敗,則 Setter 或動作 (例如 perform
) 方法可能會傳回整數狀態碼。
狀態碼應在包含類別中定義為 public static final
欄位,並在前面加上 ERROR_
,然後在 @hide
@IntDef
註解中列舉。
方法名稱開頭應一律使用動詞,而非主詞
方法名稱一律應以動詞開頭 (例如 get
、create
、reload
等),而非您要操作的物件。
public void tableReload() {
mTable.reload();
}
public void reloadTable() {
mTable.reload();
}
偏好使用集合 類型,而非陣列做為傳回或參數類型
與陣列相比,泛型型別集合介面提供多項優勢,包括更強大的 API 合約,可針對唯一性和排序提供支援、支援泛型,以及多種方便開發人員使用的便利方法。
基本類型的例外狀況
如果元素是基本元素,請改用陣列,以免產生自動裝箱的成本。請參閱「取用並傳回原始原始物件,而非封裝版本」
效能敏感程式碼的例外狀況
在某些情況下,如果 API 用於效能敏感的程式碼 (例如圖形或其他測量/版面配置/繪圖 API),建議使用陣列而非集合,以減少配置和記憶體流失。
Kotlin 例外
Kotlin 陣列是固定不變的,而 Kotlin 語言提供大量與陣列相關的公用程式 API,因此陣列與 List
和 Collection
同樣,可用於從 Kotlin 存取的 Kotlin API。
建議使用 @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 應預設偏好可變動的傳回類型,因為 Java API 的 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);
}
請注意,建立防禦性副本並不會提供任何保護措施,無法防止在初始方法呼叫和副本建立作業之間發生並行修改,也無法防止陣列中所含物件的突變。
使用集合類型參數或傳回類型提供正確的語意
List<Foo>
是預設選項,但您可以考慮使用其他類型來提供額外意義:
如果 API 不區分元素順序,且不允許重複或重複元素沒有意義,請使用
Set<Foo>
。Collection<Foo>,
如果您的 API 不區分順序,且允許重複項目。
Kotlin 轉換函式
Kotlin 經常使用 .toFoo()
和 .asFoo()
,從現有物件取得不同類型的物件,其中 Foo
是轉換的傳回類型名稱。這與熟悉的 JDK Object.toString()
一致。Kotlin 進一步將此功能用於原始轉換,例如 25.toFloat()
。
.toFoo()
和 .asFoo()
之間的轉換名稱差異相當重要:
建立新的獨立物件時,請使用 .toFoo()
與 .toString()
一樣,「to」轉換會傳回新的獨立物件。如果原始物件稍後經過修改,新物件就不會反映這些變更。同樣地,如果「新」物件稍後經過修改,則「舊」物件不會反映這些變更。
fun Foo.toBundle(): Bundle = Bundle().apply {
putInt(FOO_VALUE_KEY, value)
}
建立依附包裝函式、修飾物件或轉換時,請使用 .asFoo()
在 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
是為了允許對包裝的回呼進行這類修改而建構的。
接受執行緒,以便控制回呼調度
當您註冊沒有明確執行緒預期的回呼 (幾乎是 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 時,應用程式處理端的傳入繫結器物件實作 必須在應用程式提供的 Executor
上呼叫應用程式的回呼之前,先呼叫 Binder.clearCallingIdentity()
。如此一來,任何使用繫結器身分 (例如 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
回報非同步方法結果時,也會為非同步方法提供 Kotlin suspend fun
包裝函式,以便使用 androidx.core:core-ktx
的 Continuation.asOutcomeReceiver
擴充功能:
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 主體格式化,就像在前面加上「This method...」一樣。
如果方法不採用任何參數、沒有特殊考量,且傳回方法名稱所述的內容,您可以省略 @return
,並撰寫類似以下的文件:
/**
* Returns the priority of the thread.
*/
@IntRange(from = 1, to = 10)
public int getPriority() { ... }
一律在 Javadoc 中使用連結
文件應連結至其他相關常數、方法和其他元素的文件。請使用 Javadoc 標記 (例如 @see
和 {@link foo}
),而非純文字字詞。
以下是原始碼範例:
public static final int FOO = 0;
public static final int BAR = 1;
請勿使用原始文字或程式碼字型:
/**
* Sets value to one of FOO or <code>BAR</code>.
*
* @param value the value being set, one of FOO or BAR
*/
public void setValue(int value) { ... }
請改用連結:
/**
* Sets value to one of {@link #FOO} or {@link #BAR}.
*
* @param value the value being set
*/
public void setValue(@ValueType int value) { ... }
請注意,在參數上使用 IntDef
註解 (例如 @ValueType
) 時,系統會自動產生說明文件,指出允許的類型。如要進一步瞭解 IntDef
,請參閱註解指南。
新增 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
*/
Google 文件註解需要說明
說明為何註解 @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
,其中會說明已新增至摘要的維度。 - 同樣地,請避免在第一個句子中使用「e.g.」,因為 Doclava 會在「g.」後結束簡介說明文件。例如,請參閱
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 語言指南的建議操作,詳情請參閱「如何為 Javadoc 工具撰寫文件註解」。
Android 架構專屬規則
這些規則是關於 API、模式和資料結構,這些元素與 Android 架構中內建的 API 和行為 (例如 Bundle
或 Parcelable
) 相關。
意圖建構工具應使用 create*Intent() 模式
意圖的創作者應使用名為 createFooIntent()
的方法。
使用 Bundle 而非建立新的通用資料結構
請避免建立新的通用資料結構,用來代表任意鍵與類型值的對應關係。建議您改用 Bundle
。
這類問題通常會在撰寫平台 API 時發生,這些 API 可做為非平台應用程式和服務之間的通訊管道,平台不會讀取透過管道傳送的資料,且 API 合約可能在平台外部 (例如 Jetpack 程式庫) 部分定義。
如果平台會讀取資料,請避免使用 Bundle
,並優先使用強類型資料類別。
Parcelable 實作必須有公開的 CREATOR 欄位
透過 CREATOR
公開可分割的版面配置,而非原始建構函式。如果類別實作 Parcelable
,則其 CREATOR
欄位也必須是公開 API,且使用 Parcel
引數的類別建構函式必須是私有。
使用 CharSequence 做為 UI 字串
當字串顯示在使用者介面中時,請使用 CharSequence
允許 Spannable
例項。
如果只是使用者看不到的鍵或其他標籤或值,String
就沒問題。
避免使用 Enums
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,這類別通常是「manager」類別。新增監聽器機制或廣播,以便視需要通知用戶端變更。
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 而非純 int
建議使用 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.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)
註解。 - 說明開發人員必須在
AndroidManifest.xml
中新增<intent-filter>
,才能接收平台的意圖。 - 強烈建議您新增系統層級權限,避免惡意應用程式將
Intent
傳送至開發人員服務。
Kotlin-Java 互通性
如需完整指南清單,請參閱官方 Android Kotlin-Java 互通性指南。我們已將特定指南複製到這份指南,以提高曝光度。
API 瀏覽權限
某些 Kotlin API (例如 suspend fun
) 並非供 Java 開發人員使用;不過,請勿嘗試使用 @JvmSynthetic
控制特定語言的可見度,因為這會對偵錯工具中 API 的呈現方式產生連帶效果,導致偵錯作業更加困難。
如需具體指引,請參閱 Kotlin-Java 互通性指南或 非同步指南。
伴生物件
Kotlin 會使用 companion object
公開靜態成員。在某些情況下,這些會在 Java 中顯示在名為 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 Foundation 的「Evolving Java-based APIs」指南,進一步瞭解哪些類型的 API 變更在 Java 中相容。隱藏式 (例如系統) 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 可能會讓最關心程式碼健康的開發人員 (使用 -Werror
的開發人員) 不願採用新的 SDK。如果開發人員不關心現有程式碼中的警告,可能會完全忽略淘汰項目。
引入大量淘汰項目的 SDK 會使這兩種情況惡化。
因此,我們建議您僅在下列情況下淘汰 API:
- 我們預計在日後的版本中
@remove
該 API。 - API 使用方式會導致不正確或未定義的行為,如果要修正這類行為,就必須犧牲相容性。
當您淘汰 API 並以新 API 取代時,我們強烈建議在 androidx.core
等 Jetpack 程式庫中新增對應的相容性 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,但在執行階段類別路徑中仍會存在,類似於 @hide
API:
/**
* Ringer volume. This is ...
*
* @removed Not functional since API 2.
*/
public static final String VOLUME_RING = ...
從應用程式開發人員的角度來看,當 compileSdk
等於或晚於移除 API 的 SDK 時,API 就不會再出現在自動完成功能中,而參照 API 的原始碼也不會編譯;不過,參照 API 的舊版 SDK 和二進位檔的原始碼仍可繼續編譯。
某些類別的 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 作業系統的子系統 (「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 模式
通常,為了註冊系統服務 Binder 物件或取得對這些物件的參照,您會使用 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()
方法。
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
。