使用註解提升程式碼檢查效率

使用 Lint 等程式碼檢查工具可協助您找出問題並改善程式碼,但這類工具無法推論出所有問題。舉例來說,Android 資源 ID 會利用 int 識別字串、圖像、顏色和其他資源類型,因此檢查工具無法判斷您是否在應該指定顏色的地方,指定了字串資源。在這種情況下,即便您使用程式碼檢查功能,應用程式仍可能發生轉譯錯誤或完全無法運作。

註解可讓您向 Lint 等程式碼檢查工具提供指示,協助偵測上述較細微的程式碼問題。系統會將註解新增為中繼資料標記,您可以將這類標記附加到變數、參數和回傳值,用於檢查方法回傳值、傳遞的參數、本機變數和各項欄位。與程式碼檢查工具搭配使用時,註解可幫助您偵測問題,例如空值指標例外狀況和資源類型衝突。

Android 透過 Jetpack 註解程式庫支援各種註解。您可以利用 androidx.annotation 套件存取這個程式庫。

注意:如果模組在註解處理工具中有依附元件,您必須使用 Kotlin 的 kaptksp 依附元件設定,或者 Java 的 annotationProcessor 依附元件設定,才能新增該元件。

新增專案註解

如要在專案中啟用註解,請將 androidx.annotation:annotation 依附元件新增至程式庫或應用程式。當您執行程式碼檢查或 lint 工作時,系統會檢查您添加的所有註解。

新增 Jetpack 註解程式庫依附元件

Jetpack 註解程式庫已發布在 Google 的 Maven 存放區中。如要將 Jetpack 註解程式庫新增至專案,請在 build.gradlebuild.gradle.kts 檔案的 dependencies 區塊中加入下列程式碼:

Groovy

dependencies {
    implementation 'androidx.annotation:annotation:1.5.0'
}

Kotlin

dependencies {
    implementation("androidx.annotation:annotation:1.5.0")
}
接著在畫面上出現的工具列或同步通知中,按一下「Sync Now」

如果您在自己的程式庫模組中使用註解,註解會在 annotations.zip 檔案中以 XML 格式包含在 Android ARchive (AAR) 構件內。添加 androidx.annotation 依附元件不會造成程式庫下游使用者的依附元件增加。

注意:如果您使用其他 Jetpack 程式庫,可能不需要新增 androidx.annotation 依附元件。許多其他 Jetpack 程式庫都與註解程式庫存在依附關係,因此您或許已經可以存取註解。

如需 Jetpack 存放區內註解的完整清單,請參閱 Jetpack 註解程式庫參考資料,或使用自動完成功能查看可用的 import androidx.annotation. 陳述式選項。

執行程式碼檢查

如要透過 Android Studio 啟動程式碼檢查 (包括驗證註解及執行自動 Lint 檢查),請在選單中依序選取「Analyze」>「Inspect Code」。如果程式碼與註解發生衝突,Android Studio 會顯示衝突訊息,以標明潛在問題,並建議可行的解決方式。

您也可以使用指令列執行 lint 工作,強制執行註解。雖然這可能有助找出持續整合伺服器的問題,但 lint 工作不會強制執行空值註解 (詳情請見下一節),只有 Android Studio 才會執行。如要進一步瞭解如何啟用及執行 Lint 檢查,請參閱「使用 Lint 檢查項目改善程式碼」。

儘管註解衝突會產生警示,但這些警示並不會妨礙應用程式編譯。

空值註解

空值註解在 Java 程式碼中相當實用,能夠強制規定值是否可為空值。不過,這類註解在 Kotlin 程式碼中較不實用,因為 Kotlin 會在編譯期間以內建規則強制規定值是否可為空值。

如要檢查特定變數、參數或回傳值的空值,請新增 @Nullable@NonNull 註解。@Nullable 註解代表可以是空值的變數、參數或回傳值,而 @NonNull 則表示不得為空值的變數、參數或回傳值。

舉例來說,如果您將包含空值的本機變數當做參數傳遞至方法,且該參數帶有 @NonNull 註解,那麼在建構程式碼時,系統會產生警示,標出非空值衝突。此外,如果並未先檢查結果是否為空值,就嘗試對標有 @Nullable 的方法參照結果,則會產生空值警示。只有在每次使用方法都必須明確執行空值檢查時,才應將 @Nullable 用於該方法的回傳值。

下列範例會將 @NonNull 註解附加至 Java contextattrs 參數,以確認傳遞的參數值並非空值,也會檢查 Java onCreateView() 方法本身不會傳回空值。Kotlin 範例程式碼中並未使用 @NonNull 註解,因為該註解會在指定不可為空值的類型時自動添加至產生的位元碼:

Kotlin

import androidx.annotation.NonNull
...
    /** Add support for inflating the <fragment> tag. **/
    override fun onCreateView(
            name: String?,
            context: Context,
            attrs: AttributeSet
    ): View? {
        ...
    }
...

Java

import androidx.annotation.NonNull;
...
    /** Add support for inflating the <fragment> tag. **/
    @NonNull
    @Override
    public View onCreateView(String name, @NonNull Context context,
      @NonNull AttributeSet attrs) {
      ...
      }
...

分析是否可為空值

Android Studio 能執行是否可為空值的分析,以在程式碼中自動推論並插入空值註解。這項分析會掃描程式碼中所有方法階層的契約,偵測下列項目:

  • 可以傳回空值的呼叫方法。
  • 不應傳回空值的方法。
  • 可以是空值的變數,例如欄位、本機變數和參數。
  • 不得為空值的變數,例如欄位、本機變數和參數。

分析工具隨後會在偵測到上述項目的位置,自動插入適當的空值註解。

如要在 Android Studio 中分析是否可為空值,請依序選取「Analyze」>「Infer Nullity」。Android Studio 會將 Android @Nullable@NonNull 註解插入在程式碼中偵測到上述項目的位置。執行空值分析後,建議您驗證插入的註解。

注意:新增空值註解時,自動完成功能可能會建議使用 IntelliJ @Nullable@NotNull 註解,而非 Android 空值註解,也可能自動匯入相應程式庫。不過,Android Studio Lint 檢查工具只會尋找 Android 空值註解。驗證註解時,請確認專案使用的是 Android 空值註解,這樣 Lint 檢查工具才能確實在檢查程式碼期間傳送通知給您。

資源註解

驗證資源類型很有幫助,因為 Android 參照可繪項目字串等資源後,系統會將這些參照項目當做整數傳遞。

如果程式碼預期參數會參照特定的資源類型 (例如 String),這類程式碼可能會傳遞至預期的 int 參照類型,但實際上仍可參照不同類型的資源,例如 R.string 資源。

舉例來說,您可以新增 @StringRes 註解來檢查資源參數是否包含 R.string 參照,如下所示:

Kotlin

abstract fun setTitle(@StringRes resId: Int)

Java

public abstract void setTitle(@StringRes int resId)

在程式碼檢查期間,如果參數中未傳遞 R.string 參照,註解就會產生警示。

您也可使用相同的註解格式新增 @DrawableRes@DimenRes@ColorRes@InterpolatorRes 等其他資源類型的註解,這些註解會在程式碼檢查期間執行。

如果參數支援多種資源類型,您可以在該參數中加入多個資源類型註解。請使用 @AnyRes,標明附有註解的參數可以是任何類型的 R 資源。

雖然您可以使用 @ColorRes 指定某參數應為顏色資源,但系統不會將 RRGGBBAARRGGBB 格式的顏色整數視為顏色資源。請改用 @ColorInt 註解,表明參數必須是顏色整數。如果不正確的程式碼將 android.R.color.black 這類的顏色資源 ID 傳遞至附有註解的方法,而不是傳遞顏色整數,建構工具就會標記該程式碼。

執行緒註解

執行緒註解會檢查方法是否從特定類型的執行緒呼叫。以下是支援的執行緒註解:

建構工具會將 @MainThread@UiThread 註解視為可互換,所以您可以透過 @MainThread 方法呼叫 @UiThread 方法,反之亦然。不過,如果是在各執行緒檢視區塊不同的系統應用程式當中,UI 執行緒和主要執行緒就可能不同。因此,您應該為與應用程式檢視區塊階層相關聯的方法加上 @UiThread 註解,並且只為與應用程式生命週期相關聯的方法加上 @MainThread 註解。

若類別中所有方法都具有相同的執行緒要求,您可以將單一執行緒註解新增至該類別,驗證類別內的所有方法都是透過相同執行緒來呼叫。

執行緒註解的常見用途為:驗證附有 @WorkerThread 註解的方法或類別只透過適當背景執行緒來呼叫。

值限制註解

使用 @IntRange@FloatRange@Size 註解,即可驗證傳遞的參數值。如果想將註解套用到使用者可能弄錯範圍的參數,@IntRange@FloatRange 最為實用。

@IntRange 註解會驗證整數或長參數值是否在指定範圍內。以下範例表示 alpha 參數必須包含介於 0 至 255 的整數值:

Kotlin

fun setAlpha(@IntRange(from = 0, to = 255) alpha: Int) { ... }

Java

public void setAlpha(@IntRange(from=0,to=255) int alpha) { ... }

@FloatRange 註解會檢查浮點值或雙參數值是否在指定的浮點值範圍內。以下範例表示 alpha 參數必須包含介於 0.0 至 1.0 的浮點值:

Kotlin

fun setAlpha(@FloatRange(from = 0.0, to = 1.0) alpha: Float) {...}

Java

public void setAlpha(@FloatRange(from=0.0, to=1.0) float alpha) {...}

@Size 註解會檢查集合/陣列的大小或字串的長度。您可以使用 @Size 註解驗證下列特質:

  • 大小下限,例如 @Size(min=2)
  • 大小上限,例如 @Size(max=2)
  • 實際大小,例如 @Size(2)
  • 大小必須是某個數字的倍數,例如 @Size(multiple=2)

舉例來說,@Size(min=1) 會檢查集合是否並未包含任何內容,而 @Size(3) 則會驗證陣列正好含有三個值。

以下範例表示 location 陣列至少必須包含一個元素:

Kotlin

fun getLocation(button: View, @Size(min=1) location: IntArray) {
    button.getLocationOnScreen(location)
}

Java

void getLocation(View button, @Size(min=1) int[] location) {
    button.getLocationOnScreen(location);
}

權限註解

使用 @RequiresPermission 註解,即可驗證方法呼叫端的權限。如果想檢查有效權限清單中的單一權限,請使用 anyOf 屬性。如要檢查一組權限,請使用 allOf 屬性。以下範例會為 setWallpaper() 方法加上註解,指明方法呼叫端必須具有 permission.SET_WALLPAPERS 權限:

Kotlin

@RequiresPermission(Manifest.permission.SET_WALLPAPER)
@Throws(IOException::class)
abstract fun setWallpaper(bitmap: Bitmap)

Java

@RequiresPermission(Manifest.permission.SET_WALLPAPER)
public abstract void setWallpaper(Bitmap bitmap) throws IOException;

以下範例要求 copyImageFile() 方法的呼叫端必須具有外部儲存空間的讀取權限,以及所複製圖片內位置中繼資料的讀取權限:

Kotlin

@RequiresPermission(allOf = [
    Manifest.permission.READ_EXTERNAL_STORAGE,
    Manifest.permission.ACCESS_MEDIA_LOCATION
])
fun copyImageFile(dest: String, source: String) {
    ...
}

Java

@RequiresPermission(allOf = {
    Manifest.permission.READ_EXTERNAL_STORAGE,
    Manifest.permission.ACCESS_MEDIA_LOCATION})
public static final void copyImageFile(String dest, String source) {
    //...
}

如要取得意圖的權限,請在定義意圖動作名稱的字串欄位中加入權限要求:

Kotlin

@RequiresPermission(android.Manifest.permission.BLUETOOTH)
const val ACTION_REQUEST_DISCOVERABLE = "android.bluetooth.adapter.action.REQUEST_DISCOVERABLE"

Java

@RequiresPermission(android.Manifest.permission.BLUETOOTH)
public static final String ACTION_REQUEST_DISCOVERABLE =
            "android.bluetooth.adapter.action.REQUEST_DISCOVERABLE";

如果內容供應器需要個別的讀取與寫入權限,請將各項權限要求納入 @RequiresPermission.Read@RequiresPermission.Write 註解中:

Kotlin

@RequiresPermission.Read(RequiresPermission(READ_HISTORY_BOOKMARKS))
@RequiresPermission.Write(RequiresPermission(WRITE_HISTORY_BOOKMARKS))
val BOOKMARKS_URI = Uri.parse("content://browser/bookmarks")

Java

@RequiresPermission.Read(@RequiresPermission(READ_HISTORY_BOOKMARKS))
@RequiresPermission.Write(@RequiresPermission(WRITE_HISTORY_BOOKMARKS))
public static final Uri BOOKMARKS_URI = Uri.parse("content://browser/bookmarks");

間接權限

如果權限取決於提供給方法參數的特定值,請在參數本身中使用 @RequiresPermission,不要列出特定權限。舉例來說,startActivity(Intent) 方法會依據傳遞至該方法的意圖,使用間接權限:

Kotlin

abstract fun startActivity(@RequiresPermission intent: Intent, bundle: Bundle?)

Java

public abstract void startActivity(@RequiresPermission Intent intent, @Nullable Bundle)

當您使用間接權限時,建構工具會執行資料流程分析,檢查傳遞至方法的引數是否有任何 @RequiresPermission 註解,然後對方法本身強制執行參數中現有的所有註解。在 startActivity(Intent) 範例中,假使將沒有適當權限的意圖傳遞至方法,Intent 類別中的註解會導致 startActivity(Intent) 出現無效警示,如圖 1 所示

圖 1. 透過 startActivity(Intent) 方法的間接權限註解產生的警示。

建構工具針對 Intent 類別中相應意圖動作名稱的註解,在 startActivity(Intent) 上產生警示:

Kotlin

@RequiresPermission(Manifest.permission.CALL_PHONE)
const val ACTION_CALL = "android.intent.action.CALL"

Java

@RequiresPermission(Manifest.permission.CALL_PHONE)
public static final String ACTION_CALL = "android.intent.action.CALL";

如有需要,您可以在為方法參數加上註解時,將 @RequiresPermission.Read@RequiresPermission.Write 替換成 @RequiresPermission。不過,就間接權限而言,@RequiresPermission 不得搭配讀取或寫入權限註解使用。

回傳值註解

使用 @CheckResult 註解,即可驗證是否確實使用方法的結果或回傳值。請勿為每個非空值方法加上 @CheckResult 註解,而是在想針對容易混淆的方法說明結果時,才添加此註解。

舉例來說,Java 新手開發人員經常誤以為 <String>.trim() 會從原始字串中移除空白字元。為這個方法將上 @CheckResult 註解可標記 <String>.trim() 的使用行為,在此情況下,呼叫端不會對該方法的回傳值執行任何操作。

下列範例會為 checkPermissions() 方法加上註解,檢查此方法的回傳值是否確實成為參照目標,而且會指定 enforcePermission() 方法做為建議開發人員採用的替代方法:

Kotlin

@CheckResult(suggest = "#enforcePermission(String,int,int,String)")
abstract fun checkPermission(permission: String, pid: Int, uid: Int): Int

Java

@CheckResult(suggest="#enforcePermission(String,int,int,String)")
public abstract int checkPermission(@NonNull String permission, int pid, int uid);

CallSuper 註解

使用 @CallSuper 註解,即可驗證覆寫方法是否呼叫該方法的 super 實作。

以下範例會為 onCreate() 方法加上註解,確保所有覆寫方法實作都呼叫 super.onCreate()

Kotlin

@CallSuper
override fun onCreate(savedInstanceState: Bundle?) {
}

Java

@CallSuper
protected void onCreate(Bundle savedInstanceState) {
}

Typedef 註解

Typedef 註解會檢查特定參數、回傳值或欄位是否參照一組特定常數,也能讓程式碼完成功能自動提供可用常數。

使用 @IntDef@StringDef 註解,即可建立整數和字串組合的列舉註解,用於驗證其他類型的程式碼參照。

Typedef 註解會使用 @interface 宣告新的列舉註解類型。@IntDef@StringDef@Retention 是定義列舉類型所需的註解,而且可用來加註新註解。@Retention(RetentionPolicy.SOURCE) 註解會指示編譯器不要將列舉註解資料儲存在 .class 檔案中。

以下範例列出建立註解的步驟,此註解會檢查做為方法參數傳遞的值是否參照其中一個已定義的常數:

Kotlin

import androidx.annotation.IntDef
//...
// Define the list of accepted constants and declare the NavigationMode annotation.
@Retention(AnnotationRetention.SOURCE)
@IntDef(NAVIGATION_MODE_STANDARD, NAVIGATION_MODE_LIST, NAVIGATION_MODE_TABS)
annotation class NavigationMode

// Declare the constants.
const val NAVIGATION_MODE_STANDARD = 0
const val NAVIGATION_MODE_LIST = 1
const val NAVIGATION_MODE_TABS = 2

abstract class ActionBar {

    // Decorate the target methods with the annotation.
    // Attach the annotation.
    @get:NavigationMode
    @setparam:NavigationMode
    abstract var navigationMode: Int

}

Java

import androidx.annotation.IntDef;
//...
public abstract class ActionBar {
    //...
    // Define the list of accepted constants and declare the NavigationMode annotation.
    @Retention(RetentionPolicy.SOURCE)
    @IntDef({NAVIGATION_MODE_STANDARD, NAVIGATION_MODE_LIST, NAVIGATION_MODE_TABS})
    public @interface NavigationMode {}

    // Declare the constants.
    public static final int NAVIGATION_MODE_STANDARD = 0;
    public static final int NAVIGATION_MODE_LIST = 1;
    public static final int NAVIGATION_MODE_TABS = 2;

    // Decorate the target methods with the annotation.
    @NavigationMode
    public abstract int getNavigationMode();

    // Attach the annotation.
    public abstract void setNavigationMode(@NavigationMode int mode);
}

當您編寫上述程式碼時,如果 mode 參數並未參照其中一個已定義的常數 (NAVIGATION_MODE_STANDARDNAVIGATION_MODE_LISTNAVIGATION_MODE_TABS),就會產生警示。

若一併使用 @IntDef@IntRange,即表示整數可以是一組指定常數,或是某個範圍內的值。

啟用將常數與旗標結合的功能

如果使用者能夠將可用常數與旗標 (例如 |&^ 等等) 結合,您可以定義內含 flag 屬性的註解,檢查參數或回傳值是否參照有效的模式。

以下範例會建立 DisplayOptions 註解,內含有效 DISPLAY_ 常數的清單:

Kotlin

import androidx.annotation.IntDef
...

@IntDef(flag = true, value = [
    DISPLAY_USE_LOGO,
    DISPLAY_SHOW_HOME,
    DISPLAY_HOME_AS_UP,
    DISPLAY_SHOW_TITLE,
    DISPLAY_SHOW_CUSTOM
])
@Retention(AnnotationRetention.SOURCE)
annotation class DisplayOptions
...

Java

import androidx.annotation.IntDef;
...

@IntDef(flag=true, value={
        DISPLAY_USE_LOGO,
        DISPLAY_SHOW_HOME,
        DISPLAY_HOME_AS_UP,
        DISPLAY_SHOW_TITLE,
        DISPLAY_SHOW_CUSTOM
})
@Retention(RetentionPolicy.SOURCE)
public @interface DisplayOptions {}

...

當您使用註解旗標編寫程式碼時,如果裝飾的參數或回傳值並未參照有效模式,就會產生警示。

保留註解

@Keep 註解可確保在建構時縮小程式碼不會移除帶註釋的類別或方法。這類註解通常會加入因反映而存取的方法和類別,以防止編譯器將程式碼視為未使用。

注意:使用 @Keep 註解的類別和方法一律會出現在應用程式 APK 中,即使您從未在應用程式的邏輯中參照這些類別和方法。

為了讓應用程式保持小巧,請考慮是否有必要保留應用程式中的每個 @Keep 註解。如果您是以反射機制存取加註的類別或方法,請在 ProGuard 規則中使用 -if 條件式,指定發出反射呼叫的類別。

如要進一步瞭解如何壓縮程式碼,以及如何指定不應移除的程式碼,請參閱「縮減、模糊處理及最佳化應用程式」。

程式碼瀏覽權限註解

使用下列註解,即可表明方法、類別、欄位或套件等特定程式碼片段的瀏覽權限。

顯示程式碼以供測試

@VisibleForTesting 註解表示,相較於平常所需的瀏覽權限,加註的方法會採用較寬鬆的權限設定,方便相關人員測試。此註解包含選用的 otherwise 引數;當您不必為了測試而顯示方法時,就能使用這個引數指定該方法的瀏覽權限。Lint 會使用 otherwise 引數,強制執行預期的瀏覽權限。

在以下範例中,myMethod() 通常是 private,但在測試期間為 package-private。根據 VisibleForTesting.PRIVATE 標示,如果在 private 存取權允許的情況以外 (例如透過另一個編譯單元) 呼叫此方法,Lint 會顯示訊息。

Kotlin

@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
fun myMethod() {
    ...
}

Java

@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
void myMethod() { ... }

您也可以指定 @VisibleForTesting(otherwise = VisibleForTesting.NONE),表明方法僅用於測試。這份表單與使用 @RestrictTo(TESTS) 相同。都能執行相同的 Lint 檢查。

限制 API

@RestrictTo 註解表示,所加註 API (套件、類別或方法) 的存取權會受到限制。詳細說明如下:

子類別

您可以使用註解表單 @RestrictTo(RestrictTo.Scope.SUBCLASSES),限定 API 僅能存取子類別。

在此情況下,只有擴充已加註類別的類別才能存取此 API。Java protected 修飾符的限制不夠嚴格,因為這個修飾符允許透過同一套件中不相關的類別進行存取。此外,由於您一律無法將先前處於 protected 的已覆寫方法設為 public,因此有時您會希望將方法保持 public 狀態以便日後靈活使用,但需要提供一則指示,表明該類別只能在類別內部或子類別中使用。

程式庫

您可以使用 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) 註解形式,僅允許程式庫存取 API。

在此情況下,只有程式庫的程式碼才能存取加註的 API。如此一來,您不僅可以在所需的任何套件階層中整理程式碼,還能在一組相關程式庫之間共用程式碼。Jetpack 程式庫已經可以採用此做法,這類程式庫包含許多不開放外部使用的實作程式碼,但程式碼必須處於 public 狀態,才能各個互補的 Jetpack 程式庫之間共用。

測試

您可以使用註解表單 @RestrictTo(RestrictTo.Scope.TESTS),避免其他開發人員存取您的測試 API。

僅有測試程式碼才能存取加註的 API。這樣可以避免其他開發人員使用 API 進行您打算僅用於測試目的的開發。