アノテーションによるコード検査の改善

lint などのコード インスペクション ツールは、問題の検出やコードの改善に役立ちますが、インスペクション ツールの推測範囲は限定されます。たとえば、Android リソース ID は、文字列、グラフィック、色、その他のリソースタイプの識別に int を使用します。したがって、インスペクション ツールでは、文字列リソースに色がいつ指定されたかはわかりません。つまり、コード インスペクションを使用しても、アプリで適切にレンダリングがされなかったり、アプリを実行できない可能性は残ります。

アノテーションを使用して、lint などのコード インスペクション ツールに情報を提供すると、見落とされやすいコードの問題も検出できるようになります。アノテーションはメタデータ タグとして追加されます。これを変数、パラメータ、戻り値に付加することで、メソッドの戻り値、渡されたパラメータ、ローカル変数やフィールドを検証できます。コード インスペクション ツールでアノテーションを使用すると、null ポインター例外やリソースタイプの不一致などの問題を検出できます。

Android では、Annotations Support Library によって、さまざまなアノテーションがサポートされています。このライブラリには android.support.annotation パッケージからアクセスできます。

プロジェクトへのアノテーションの追加

プロジェクトでアノテーションを有効にするには、ライブラリまたはアプリに support-annotations 依存関係を追加します。追加したすべてのアノテーションは、コード インスペクションまたは lint タスクを実行するときにチェックされます。

Support Annotations Library 依存関係の追加

Support Annotations Library は、Android Supprot Repository の一部です。プロジェクトにアノテーションを追加するには、サポート リポジトリをダウンロードして、build.gradle ファイルに support-annotations 依存関係を追加する必要があります。

  1. [SDK Manager] を開くために、ツールバーで [SDK Manager] を クリックするか、[Tools] > [Android] > [SDK Manager] を選択します。
  2. [SDK Tools] タブをクリックします。
  3. [Support Repository] を展開して [Android Support Repository ] チェックボックスを選択します。
  4. [OK] をクリックします。
  5. ウィザードで操作を進めて、パッケージをインストールします。
  6. build.gradle ファイルの dependencies ブロックに以下の行を追加して、プロジェクトに support-annotations 依存関係を追加します。
     dependencies { compile 'com.android.support:support-annotations:24.2.0' } 
    ダウンロードしたライブラリのバージョンはこれよりも新しい可能性があるため、自身で指定した値がステップ 3 のバージョンと一致していることを確認してください。
  7. ツールバーまたは表示される同期通知で [Sync Now] をクリックします。

アノテーションを独自のライブラリ モジュールで使用する場合、そのアノテーションは、Android Archive(AAR)アーティファクトの一部として、XML 形式で annotations.zip ファイルに組み込まれます。support-annotations 依存関係を追加しても、ライブラリのダウンストリーム ユーザーに対する依存関係は生成されません。

Android plugin for Gradle(com.android.application または com.android.library)ではなく Gradle Java plugin を使用している Gradle モジュールでアノテーションを使用したい場合は、SDK リポジトリを明示的に組み込む必要があります。これは、Android サポート ライブラリが JCenter Java リポジトリでは使用できないためです。

repositories {
   jcenter()
   maven { url '<your-SDK-path>/extras/android/m2repository' }
}

注: appcompat ライブラリを使用している場合は、support-annotations 依存関係を追加する必要はありません。appcompat ライブラリは既にアノテーション ライブラリに依存しているため、アノテーションを利用することができます。

サポート リポジトリに含まれているアノテーションの一覧については、Support Annotations Library リファレンスをご覧ください。または、オートコンプリート機能を使用して、import android.support.annotation. 文で使用可能なオプションを表示することもできます。

コード インスペクションの実行

Android Studio で、アノテーションの検証と自動 lint チェックを含むコード インスペクションを開始するには、メニューバーから [Analyze] > [Inspect Code] を選択します。Android Studio では競合メッセージを表示することで、コードとアノテーションが競合する潜在的な問題があること警告し、有効な解決策を提案します。

コマンドラインで lint タスクを実行して、アノテーションを適用することもできます。lint タスクは継続的インテグレーション サーバーでの問題報告に役立つ場合がありますが、nullness アノテーションを適用しないことに注意してください(Android Studio のみが適用します)。lint インスペクションの有効化と実行の詳細については、lint によるコードの改善をご覧ください。

アノテーションが競合すると警告が発生しますが、この警告が出てもアプリのコンパイルは可能です。

Nullness アノテーション

@Nullable アノテーションと @NonNull アノテーションを追加して、指定された変数、パラメータ、戻り値が null になり得るかをチェックします。@Nullable アノテーションは、null にできる変数、パラメータ、戻り値を示し、@NonNull アノテーションは、null にできない変数、パラメータ、戻り値を示します。

たとえば、null 値を含むローカル変数がパラメータとしてメソッドに渡され、そのパラメータに @NonNull アノテーションが付いている場合、コードをビルドすると、非 null 競合を示す警告が出ます。また、@Nullable で指定されたメソッドの結果を null かどうか最初にチェックせずに参照しようとすると、nullness 警告が出ます。メソッドを使用するたびに明示的に null チェックを実施する必要がある場合に限り、メソッドの戻り値に @Nullable を使用してください。

以下の例では、context パラメータと attrs パラメータに @NonNull アノテーションが付いており、渡されるパラメータの値が null 以外かどうかチェックしています。onCreateView() 自身の戻り値が null 以外かどうかもチェックします。

import android.support.annotation.NonNull;
...

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

null 可能性分析

Android Studio では、自動で推測をして、nullness アノテーションをコード内に挿入する null 可能性分析がサポートされています。null 可能性分析では、コードのメソッド階層全体のコントラクトをスキャンして次のものを検出します。

  • null を返す可能性がある呼び出しメソッド
  • null を返すべきでないメソッド
  • null になる可能性があるフィールド、ローカル変数、パラメータなどの変数
  • null 値を保持することがないフィールド、ローカル変数、パラメータなどの変数

この分析を行うと、検出された位置に適切な null アノテーションが自動的に挿入されます。

Android Studio から null 可能性分析を実行するには、[Analyze] > [Infer Nullity] を選択します。Android Studio では、コード内の検出された位置に Android の @Nullable アノテーションと @NonNull アノテーションを挿入します。null 分析を実行した後は、挿入されたアノテーションの検証を行うことをおすすめします。

注: nullness アノテーションの追加時には、オートコンプリート機能によって、Android null アノテーションではなく、IntelliJ @Nullable アノテーションと @NotNull アノテーションが提示され、対応するライブラリが自動的にインポートされる場合があります。ただし、Android Studio Lint チェッカーでは、Android null アノテーションのみを検索します。アノテーションを検証する場合は、プロジェクトで Android null アノテーションを使用していることを確認してください。これにより、コード インスペクション時に Lint チェッカーから適切な通知が出されるようになります。

リソース アノテーション

Android では、ドローアブル リソースや文字列リソースなどのリソースへの参照は整数として渡されるため、リソースタイプの検証が有用になります。コードに含まれるパラメータが、ドローアブルなどの特定のタイプのリソースを参照すると想定される場合は、期待する参照型 int が渡されます。ただし、コードで実際に参照するのは、R.string リソースなどの別のタイプのリソースです。

たとえば、以下のように @StringRes アノテーションを追加すると、リソース パラメータに R.string 参照が含まれるかどうかチェックできます。

public abstract void setTitle(@StringRes int resId) { … }

コードのインスペクション時に R.string 参照がパラメータに渡されていない場合は、アノテーションによって警告が出ます。

@DrawableRes@DimenRes@ColorRes@InterpolatorRes などの他のリソースタイプ向けのアノテーションも、同じアノテーション形式で追加することができ、コード インスペクション時に実行されます。パラメータで複数のリソースタイプをサポートする場合は、特定のパラメータにこれらのアノテーションを複数指定することができます。アノテーションを付けたパラメータが、どのタイプの R リソースであってもよいことを示すには、@AnyRes を使用します。

@ColorRes を使用すると、パラメータをカラーリソースとして指定できますが、カラー整数(RRGGBB 形式または AARRGGBB 形式)は、カラーリソースとして認識されません。パラメータがカラー整数であることを示すには、@ColorInt アノテーションを使用します。コードで、カラー整数ではなく、android.R.color.black などのカラーリソース ID をアノテーション付きメソッドに渡していると、ビルドツールで不適切な処理であると判断されて警告が出ます。

スレッド アノテーション

スレッド アノテーションは、メソッドが特定のタイプのスレッドから呼び出されているかどうかチェックします。以下のスレッド アノテーションがサポートされます。

注: ビルドツールでは @MainThread アノテーションと @UiThread アノテーションを入れ替え可能なものとして扱います。そのため、@MainThread メソッドから @UiThread メソッドを呼び出すことも、その逆も可能です。ただし、異なるスレッドに複数のビューがあるシステムアプリの場合、UI スレッドはメインスレッドと異なることがあります。したがって、アプリのビュー階層に関連付けられているメソッドには @UiThread のアノテーションを付け、アプリのライフサイクルに関連付けられているメソッドにのみ @MainThread のアノテーションを付ける必要があります。

1 つのクラスに含まれるすべてのメソッドで同じスレッド要件を共有している場合は、そのクラスに 1 つのスレッド アノテーションを追加することで、クラス内のすべてのメソッドが同じタイプのスレッドから呼び出されることを検証できます。

スレッド アノテーションは、AsyncTask クラスにおけるメソッドのオーバーライドの検証に使用するのが一般的です。これは、AsyncTask クラスがバックグラウンドで実行され、UI スレッド上でのみ結果を公開するためです。

値制約アノテーション

渡されたパラメータの値を検証するには、@IntRange@FloatRange、および @Size アノテーションを使用します。@IntRange および @FloatRange は、ユーザーが誤った範囲を設定している可能性のあるパラメータに適用すると、最も効力を発揮します。

@IntRange アノテーションは、integer または long パラメータ値が指定された範囲内にあるかどうかを検証します。以下の例では、alpha パラメータに 0 から 255 までの整数値が含まれるようにしています。

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

@FloatRange アノテーションは、float または double パラメータ値が指定された浮動少数点値の範囲内にあるかどうかをチェックします。以下の例では、alpha パラメータに 0.0 から 1.0 までの浮動少数点値が含まれるようにしています。

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) は配列にちょうど 3 つの値が含まれているかどうか検証します。以下の例では、location 配列に 1 つ以上の要素が含まれるようにしています。

int[] location = new int[3];
button.getLocationOnScreen(@Size(min=1) location);

パーミッション アノテーション

メソッドの呼び出し元のパーミッションを検証するには @RequiresPermission アノテーションを使用します。有効なパーミッションのリストのうち、いずれか 1 つのパーミッションがあるかどうか確認するには、anyOf 属性を使用します。一連のパーミッションがあるかどうか確認するには、allOf 属性を使用します。以下の例では、setWallpaper() メソッドにアノテーションを付けて、このメソッドの呼び出し元に permission.SET_WALLPAPERS パーミッションがあることを保証しています。

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

この例では、copyFile() メソッドの呼び出し元に、外部ストレージへの読み取りおよび書き込みパーミッションの両方が付与されていることを条件付けています。

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

インテントのパーミッションの場合、インテント アクション名を定義する文字列フィールドに対して、パーミッション要件を指定します。

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

読み取りアクセスと書き込みアクセス向けに別々のパーミッションを必要とするコンテンツ プロバイダのパーミッションの場合は、@RequiresPermission.Read アノテーションまたは @RequiresPermission.Write アノテーションに、それぞれのパーミッション要件を含めます。

@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) メソッドでは、このメソッドに渡されるインテントに間接パーミッションを使用しています。

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

間接パーミッションを使用する場合、ビルドツールではデータフロー分析を実行して、メソッドに渡された引数に @RequiresPermission アノテーションがあるかどうかチェックします。次に、パラメータの既存のアノテーションをメソッド自体に適用します。startActivity(Intent) の例では、適切なパーミッションのないインテントがメソッドに渡されると、Intent クラスのアノテーションによって、startActivity(Intent) の使用が無効であることを示す警告が出されます(図 1 )。

図 1. startActivity(Intent) メソッドの間接パーミッション アノテーションによって生成される警告

ビルドツールは、Intent クラスの対応するインテント アクション名のアノテーションから、startActivity(Intent) に関する警告を生成します。

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

メソッドのパラメータにアノテーションを付けるときに、必要に応じて、@RequiresPermission.Read@RequiresPermission.Write の両方またはいずれか一方の代わりに @RequiresPermission を使用することができます。ただし、間接パーミッションでは、 @RequiresPermission は、読み取りパーミッション アノテーションと書き込みパーミッション アノテーションのどちらとも併用できません。

戻り値アノテーション

メソッドの結果または戻り値が実際に使用されているかどうかを検証するには @CheckResult アノテーションを使用します。すべての非 void メソッドに @CheckResult アノテーションを付けるのではなく、複雑なメソッドの結果を明確にするためにアノテーションを追加します。たとえば、経験の浅い Java デベロッパーは、<String>.trim() が元の文字列から空白を削除するものだと勘違いすることがあります。メソッドに @CheckResult アノテーションを付けると、呼び出し元で <String>.trim() の戻り値を一切使用していない場合に警告が出ます。

以下の例では、checkPermissions() メソッドにアノテーションを付けて、このメソッドの戻り値が実際に参照されるようにしています。さらに、デベロッパーに提示する代用メソッドとして enforcePermission() メソッドを指定しています。

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

CallSuper アノテーション

オーバーライドするメソッドで、メソッドの super を呼び出しているか検証するには、@CallSuper アノテーションを使用します。以下の例では、オーバーライドするメソッドの実装で super.onCreate() を呼び出すように、onCreate() メソッドにアノテーションを付けています。

@CallSuper
protected void onCreate(Bundle savedInstanceState) {
}

Typedef アノテーション

@IntDef アノテーションと @StringDef アノテーションを使用すると、int および string セットの列挙型アノテーションを作成して、他の型のコード参照を検証できます。Typedef アノテーションでは、特定のパラメータ、戻り値、フィールド参照が特定の定数セットを参照することを保証します。さらに、コード補完によって使用できる定数が自動的に提示されるようになります。

Typedef アノテーションでは @interface を使用して、列挙型のアノテーション タイプを新たに宣言します。@IntDef アノテーションと @StringDef アノテーションを @Retention と使うと、新しいアノテーションを作成できます。これらは列挙型を定義するために必要です。@Retention(RetentionPolicy.SOURCE) アノテーションは、コンパイラに .class ファイルに列挙型アノテーションのデータを保存しないように通知します。

以下の例は、メソッド パラメータとして渡される値が、定義済みの定数のいずれか 1 つを参照するように、アノテーションを作成する手順を示しています。

import android.support.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 属性を使用したアノテーションを定義できます。以下の例では、有効な DISPLAY_ 定数のリストを使用した DisplayOptions アノテーションが作成されています。

import android.support.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 {}

...

アノテーション フラグを使用したコードのビルドで、修飾パラメータまたは戻り値が有効なパターンを参照していない場合は、警告が出ます。

コード アクセシビリティ アノテーション

メソッド、クラス、フィールドのアクセシビリティを示すには、@VisibleForTesting アノテーションと @Keep アノテーションを使用します。

@VisibleForTesting アノテーションは、コードをテストしやすくするために、コードのブロックに必要以上に可視性を持たせていることを明示します。

コードがビルド時に圧縮されても、@Keep アノテーションの付いているエレメントは削除されません。通常、このアノテーションをリフレクションからアクセスされるメソッドとクラスに付加して、コードが未使用であるとコンパイラで判断されないようにします。