2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

エムティーアイAdvent Calendar 2021

Day 13

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

Posted at

本記事は エムティーアイ Advent Calendar 2021 の 13 日目の記事です。

はじめに

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

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

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

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

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

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

@Retntion(AnnotationRetention.RUNTIME)
annotation class SampleAnnotation

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

@SampleAnnotation
class AnnotatedClass

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

kotlin-reflect を使うともう少しきれいに記述することができますが、ここでは割愛します。
参考: Idiomatic Kotlin: Annotations and Reflection

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

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

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

YouTube アプリのステータスバー

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

  • アノテーションクラスの作成
  • Fragment を表示する Activity で、Fragment のアタッチ時にステータスバーの色を更新する
  • Fragment にアノテーションをつける
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),
    }
}
StatusBarActivity.kt
supportFragmentManager.addFragmentOnAttachListener { _, fragment ->
    val statusBarAnnotation = fragment::class.java.getAnnotation(StatusBar::class.java)
    if (statusBarAnnotation != null) {
        window.statusBarColor = statusBarAnnotation.color.color
    }
}
RedFragment.kt
@StatusBar(StatusBar.Color.Red)
class RedFragment : Fragment(R.layout.fragment_statusbar) {
    // ...
}

ステータスバー変更

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

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

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

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

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

  • アノテーションクラスを作成する
  • アノテーションに指定したイベントを出力するロガーを作成する
  • Fragment アタッチ時にライフサイクルオブザーバーとして登録する
  • Fragment にアノテーションをつける
LogLifecycle.kt
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
annotation class LogLifecycleEvent(
    val targetEvents: Array<Lifecycle.Event>
)
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}")
        }
    }
}
LifecycleLoggingActivity.kt
val lifecycleLogger = LifecycleLogger()
supportFragmentManager.addFragmentOnAttachListener { _, fragment ->
    fragment.lifecycle.addObserver(lifecycleLogger)
}
@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ログ出力

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

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

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

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 にその値を使ったスクリーンビューとか送るとかもできそうですね。
ただし、リフレクションを利用しているのと最適化もうまく効かなそうなので、パフォーマンスへの影響も多少は出そうな気もしますので自己責任でどうぞ。

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?