[Android] 実行時アノテーションによる処理例


本記事は エムティーアイ [Advent Calendar 2021](https://qiita.com/advent-calendar/2021/mti) の 13 日目の記事です。

## はじめに

独自のアノテーションクラスを作成し、アノテーションに応じた処理を実行する例を紹介します。
あまり実用性はないので、こんなこともできるんだくらいの感覚で読んでいただけると幸いです。

記事中で説明するサンプルアプリはこちら。

https://github.com/pps5/annotation-sample

## 実行時のアノテーション取得方法

アノテーションは Retention が定められています。

- Kotlin: [AnnotationRetention](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.annotation/-annotation-retention/)
- Java: [RetentionPolicy](https://docs.oracle.com/javase/jp/8/docs/api/java/lang/annotation/RetentionPolicy.html)

実行時に読み取るためには `RUNTIME` を指定する必要があります。

Kotlin ではデフォルトで `RUNTIME` ですが、本記事のサンプルではすべて明示的に指定しています。

```kotlin
@Retntion(AnnotationRetention.RUNTIME)
annotation class SampleAnnotation
```

Java クラス経由で取得するには以下のようにします。

```kotlin
@SampleAnnotation
class AnnotatedClass

val annotatedClass = AnnotatedClass()
val sampleAnnotation: SampleAnnotation? = annotatedClass::class.java.getAnnotation(SampleAnnotation::class.java)
```

kotlin-reflect を使うともう少しきれいに記述することができますが、ここでは割愛します。
参考: [Idiomatic Kotlin: Annotations and Reflection](https://medium.com/tompee/kotlin-annotations-and-reflection-caa0d73bf39b)

## ステータスバーの色を変える

1 つ目の例は実行時にアノテーションで指定した値に色を変える実装です。

ステータスバーは画面最上部にある、時刻や通知表示をしているエリアです:

![YouTube アプリのステータスバー](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2381799/3b9d7c87-4d77-a7af-d817-c8bccbb0d152.png)

やることは以下の 3 つです。

- アノテーションクラスの作成
- Fragment を表示する Activity で、Fragment のアタッチ時にステータスバーの色を更新する
- Fragment にアノテーションをつける

```kotlin:StatusBar.kt
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
annotation class StatusBar(
    val color: Color,
) {

    enum class Color(val color: Int) {
        Red(android.graphics.Color.RED),
        Blue(android.graphics.Color.BLUE),
    }
}
```

```kotlin:StatusBarActivity.kt
supportFragmentManager.addFragmentOnAttachListener { _, fragment ->
    val statusBarAnnotation = fragment::class.java.getAnnotation(StatusBar::class.java)
    if (statusBarAnnotation != null) {
        window.statusBarColor = statusBarAnnotation.color.color
    }
}
```

```kotlin:RedFragment.kt
@StatusBar(StatusBar.Color.Red)
class RedFragment : Fragment(R.layout.fragment_statusbar) {
    // ...
}
```

![ステータスバー変更](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2381799/41981282-a894-e637-198f-1217829341c3.gif)


今回は FragmentTransaction 内で `TRANSIT_FRAGMENT_OPEN` を指定しているため、それほど違和感なく切り替わっています。

アタッチされるタイミングで表示が切り替わるため、Fragment 遷移時のアニメーションの長さや種類によってはあまりきれいに表示されないため注意が必要です。


## Fragment ライフサイクルのログ出力

今度はデバッグビルドのときに Fragment のライフサイクルをログに出力する例です。

Fragment ごとに出力するログを指定できるようにします。

- アノテーションクラスを作成する
- アノテーションに指定したイベントを出力するロガーを作成する
- Fragment アタッチ時にライフサイクルオブザーバーとして登録する
- Fragment にアノテーションをつける

```kotlin:LogLifecycle.kt
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
annotation class LogLifecycleEvent(
    val targetEvents: Array<Lifecycle.Event>
)
```

```kotlin:LifecycleLogger.kt
class LifecycleLogger : LifecycleEventObserver {

    override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
        if (!BuildConfig.DEBUG) {
            return
        }

        val targetEvents = source::class.java
            .getAnnotation(LogLifecycleEvent::class.java)
            ?.targetEvents
            .orEmpty()

        if (targetEvents.contains(Lifecycle.Event.ON_ANY) ||
            targetEvents.contains(event)
        ) {
            Log.d(source::class.java.canonicalName, "lifecycle event: ${event.name}")
        }
    }
}
```

```kotlin:LifecycleLoggingActivity.kt
val lifecycleLogger = LifecycleLogger()
supportFragmentManager.addFragmentOnAttachListener { _, fragment ->
    fragment.lifecycle.addObserver(lifecycleLogger)
}
```

```kotlin
@LogLifecycleEvent(targetEvents = [Lifecycle.Event.ON_ANY])
class AllEventFragment : Fragment(R.layout.fragment_logging) {
    // ...
}

@LogLifecycleEvent(targetEvents = [Lifecycle.Event.ON_START, Lifecycle.Event.ON_STOP])
class OnStartAndOnStopFragment : Fragment(R.layout.fragment_logging) {
    // ...
}
```

![Lifecycleログ出力](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2381799/3cbf6c95-8694-e2a9-8e63-5c924f31f8cb.gif)

ログのクラス名とイベント名を取得すると

- AllEventFragment ではすべてのイベント
- OnStartAndStopFragment では ON_START と ON_STOP のみ

が出力されており、たしかにアノテーションで指定したとおりです。

```plaintext:Logcat
 D/io.github.pps5.annotationsample.logging.AllEventFragment: lifecycle event: ON_CREATE
 D/io.github.pps5.annotationsample.logging.AllEventFragment: lifecycle event: ON_START
 D/io.github.pps5.annotationsample.logging.AllEventFragment: lifecycle event: ON_RESUME
 D/io.github.pps5.annotationsample.logging.AllEventFragment: lifecycle event: ON_PAUSE
 D/io.github.pps5.annotationsample.logging.AllEventFragment: lifecycle event: ON_STOP
 D/io.github.pps5.annotationsample.logging.OnStartAndOnStopFragment: lifecycle event: ON_START
 D/io.github.pps5.annotationsample.logging.AllEventFragment: lifecycle event: ON_DESTROY
 D/io.github.pps5.annotationsample.logging.OnStartAndOnStopFragment: lifecycle event: ON_STOP
 D/io.github.pps5.annotationsample.logging.AllEventFragment: lifecycle event: ON_CREATE
 D/io.github.pps5.annotationsample.logging.AllEventFragment: lifecycle event: ON_START
 D/io.github.pps5.annotationsample.logging.AllEventFragment: lifecycle event: ON_RESUME
 D/io.github.pps5.annotationsample.logging.AllEventFragment: lifecycle event: ON_PAUSE
 D/io.github.pps5.annotationsample.logging.AllEventFragment: lifecycle event: ON_STOP
 D/io.github.pps5.annotationsample.logging.AllEventFragment: lifecycle event: ON_DESTROY
```

## おわりに

今回は紹介していないですが、アノテーションでスクリーン名を指定して、OnResume のタイミングで Firebase Analytics にその値を使ったスクリーンビューとか送るとかもできそうですね。
ただし、リフレクションを利用しているのと最適化もうまく効かなそうなので、パフォーマンスへの影響も多少は出そうな気もしますので自己責任でどうぞ。