Help us understand the problem. What is going on with this article?

コードを変更せずにデバッグメニューでAndroidアプリの動作を変更する

More than 1 year has passed since last update.

Shibuya.apk #20 の資料とCyberAgent Developers Advent Calendar 2017の9日目です。
今日はAbemaTVで利用しているデバッグメニューの仕組みについて紹介したいと思います。

背景

アプリのデバッグやQAによるテスト中にはバグ修正などさまざまな作業があります。
よくあるのが、『めったに出ないレビューを訴求するダイアログをデバッグ中は毎回出したい』『デバッグ中にサーバー環境を切り替えたいという』などという一時的な変更があります。
そのために作業を中断し、ブランチを切り替え、編集して動作を確認し、ビルドし直してapkを渡して、作業に戻っているとあっという間に時間が過ぎてしまいます。

そういうタイミングでよくやる方法としてはデバッグメニューを実装して、
デバッグ時にデバッグメニューで動作を変更できるようにして、テストを楽にします。
例えばデバッグ時のみ通知を表示し、そこからデバッグメニューの画面に遷移して、そこで変更できるようにするイメージです。

デバッグメニューの実装

例えば、以下の様な場合でチュートリアルを毎回出したい場合、みなさんはデバッグメニューでの変更を実装する時どのように実装するでしょうか?

チュートリアル判定ロジックのコード例:

    if (isShowTutorialTiming()) {
        showTutorial()
  }


private fun isShowTutorialTiming(): Boolean {
    // 次のタイミングまで表示しない
    if (System.currentTimeMillis() < pref.getNextTutorialtiming()) {
        return false;
    }
    // 3回しか表示しない
    if (3 < pref.getTutorialCount()) {
        return false;
    }
    return true;
}

一つの方法としてはデバッグメニューを作ってPreferenceなど必要となるものを変更して、見れるようにするというものがあります。

pref.setNextTutorialtiming(currentMills + 100000)
pref.setTutorialCount(0)

その場合は1度は表示できますが、何度も表示したりはできません。何度も変更し直す必要が出てきます。
また全てのものがPreferenceのように変更できるとは限りません。

もう一つの方法としてデバッグ時だけisShowTutorialTimingの動作を変更してしまうというものがあります。
この方法の場合はプロダクションのコードに手を入れていくことになり、デバッグ用のコードをプロダクションにたくさん入れて汚してしまいます。

動作を変更できるようにしたコード例:

        if (isShowTutorialTiming()) {
            showTutorial()
        }
    }

    fun isShowTutorialTiming(): Boolean {
        // ******ここから追加******
        if (BuildConfig.DEBUG) { 
            if(pref.isAlwaysShowTutorialTiming()){
                return true;
            }
        }
        // ******ここまで追加******
        // 次のタイミングまで表示しない
        if (System.currentTimeMillis() < pref.getNextTutorialtiming()) {
            return false;
        }
        // 3回しか表示しない
        if (3 < pref.getTutorialCount()) {
            return false;
        }
        return true;
    }

AspectJ

ところで、アスペクト指向プログラミングをご存知でしょうか?
"アスペクト指向プログラミングは、オブジェクト指向ではうまく分離できない特徴を「アスペクト」とみなし、アスペクト記述言語をもちいて分離して記述することでプログラムに柔軟性をもたせようとする試み。"だそうです。
つまりデバッグで値を変えたいというのをアスペクトとして分離できたら良さそうに見えます。
それをするのがAspectJです。AspectJは"AspectJは、Javaに対するアスペクト指向プログラミングのための拡張。"だそうです。(Wikipediaより)
例えば、AspectJではon〜で始まるメソッドを実行する前にログを出力するというのが以下のコードだけで書くことができます。これはJavaのクラスファイルのバイトコードにウィービング(Weaving)、処理を織り込むことで実装されています。

AspectJのコード例:

@Aspect
public class AspectExample {
    @Before("execution(* on*(..))")
    public void before() {
        System.out.println("before !!");
    }
}

AspectJを用いたアプローチ

同様にデバッグのための処理はちらばってしまうので、アスペクトとして分離できたら良さそうです。
AspectJを使ってこれを分離してみましょう。
@DebugReturnアノテーションを作成して、それをつけてあげています。

@DebugReturnのみをつけたコード例:

if(isShowTutorialTiming()) {
    showTutorial()
}

@DebugReturn
fun isShowTutorialTiming() :Bool {
    // 次のタイミングまで表示しない
    if (System.currenTimeMills() < pref.getNextTutorialtiming()){
        return false;
    }
    // 3回しか表示しない
    if (3 < pref.getTutorialCount()){
        return false;
    }
    return true;
}

そしてdebug/のビルドバリアントに以下のファイルを置いてあげます。

AspectJのコード例:

@Aspect
public class DebugAspect {
    @Around("execution(* *.*(..)) && @annotation(com.github.takahirom.DebugReturn)")
    public Object debugReturnMethod(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        return DebugPreference.getInstance().isDebugShowTutorialTiming();
    }
}

これだけのコードで振る舞いを自由に変更できるようになりました!!

Debug Alter

ただこの実装をしていくのは辛いと思いますし、AspectJを入れるのもけっこう大変だったので、Gradleプラグインとしてこの部分をライブラリ化しました。DebugAlterという名前にしました。スターしてください。
https://github.com/takahirom/debug-alter

使い方

実装のイメージとしては以下のような形になります。といってもわかりにくいと思うので、コードで説明していきます。

返り値を変更したいところで@DebugReturnを使うことができます。リリースに入るのはアノテーションのみとなります。
以下ではスナックバーを出すかどうかの判定とスナックバーのテキストを取得して表示しています。

app/src/main/java/com/../MainActivity.kt

    class MainActivity : AppCompatActivity() {
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)

            val fab = findViewById<FloatingActionButton>(R.id.fab)
            fab.setOnClickListener { view ->
                if (isSnackbarShowTiming()) {
                    Snackbar.make(view, getSnackbarText(), Snackbar.LENGTH_LONG)
                            .setAction("Action", null).show()
                }
            }
        }

        @DebugReturn
        fun isSnackbarShowTiming(): Boolean {
            return false
        }

        @DebugReturn
        fun getSnackbarText(): String {
            return "bad"
        }

@DebugReturnをつけたメソッドを設定するために、Debug用のアプリケーションクラスを作成します。

app/src/debug/AndroidManifest.xml

    <application
        android:name=".DebugApp"
        tools:replace="android:name"
        />

そして以下のようにDebugAlterItemを使って@DebugReturnで変更したいところを設定していきます。
@DebugReturnのメソッドが呼び出されたときにDebugAlterItem#isAlter()が呼び出され、falseを返すと何も変えずに@DebugReturn
がついたメソッドを呼び出し、trueを返すとDebugAlterItem#get()が呼び出されます。ここではSharedPreferenceを使って設定しています。

app/src/debug/java/.../DebugApp.kt

// Extends your main Application classs
class DebugApp : App() {

    override fun onCreate() {
        super.onCreate()

        val preference = PreferenceManager.getDefaultSharedPreferences(this)
        val items = arrayListOf<DebugAlterItem<*>>(
                object : DebugAlterItem<String>("getSnackbarText") {
                    override fun isAlter(): Boolean = preference.contains(key)
                    override fun get(): String? = preference.getString(key, null)
                },
                object : DebugAlterItem<Boolean>("isSnackbarShowTiming") {
                    override fun isAlter(): Boolean = preference.contains(key)
                    override fun get(): Boolean? = preference.getBoolean(key, false)
                })

        DebugAlter.getInstance().setItems(items)

これで設定できました。
あとはこのSharedPreferenceを使って、デバッグメニューを実装してあげればうまく動いてくれるはずです!
具体的なdependenciesなどのbuid.gradleの書き方などは以下に書いてあります。
https://github.com/takahirom/debug-alter

まとめ

  • デバッグメニューを実装する時にAspectJを使うとプロダクションのコードを汚さずに実装していけます。
  • 手軽にやるならDebugAlterを使ってみてください。(実装としてはかなり薄いです)
  • 以下のリポジトリをスターしてください。 
    https://github.com/takahirom/debug-alter
takahirom
Google Developer Expert for Android
cyberagent
サイバーエージェントは「21世紀を代表する会社を創る」をビジョンに掲げ、インターネットテレビ局「AbemaTV」の運営や国内トップシェアを誇るインターネット広告事業を展開しています。インターネット産業の変化に合わせ新規事業を生み出しながら事業拡大を続けています。
http://www.cyberagent.co.jp/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした