BeagleでAndroidアプリにデバッグメニューを仕込む

# なぜデバッグメニューを仕込むのか

アプリを作っている時

**コードを修正→ビルド→端末上でポチポチしてテスト**

というサイクルを繰り返しますが、特に開発中は少しだけ設定を変えてテストしたいケースが多々あります。

例えばこんなとき...

```
* APIサーバーを切り替えたい
* 一部のAPIだけモックを使いたい
* 違うユーザーでログインしたい
* ユーザー状態をプレミアムユーザーに切り替えたい
* 何かしらのフラグを立てたい
```

これらの条件を変えるためだけにコードを書き換えてビルドし直してを繰り返すと時間がもったいないです。

ということでアプリを再ビルドすることなく簡単に設定を変更できるようにデバッグメニューを作ってみましょう。

# デバッグメニューライブラリ Beagle

今回はデバッグメニューを簡単に作ることができる[Beagle](https://github.com/pandulapeter/beagle)というライブラリを使います。

![Imgur](https://imgur.com/hrzf2z0.png)

どんなことができるのか手っ取り早く知りたい人はデモアプリが用意されているので、そちらをポチポチしてみるのがいいです！

[Beagle Showcase - Debug menu library demo - Apps on Google Play](https://play.google.com/store/apps/details?id=com.pandulapeter.beagle)

![Imgur](https://imgur.com/6D2xRhi.png)


# サンプル

サンプルとして、2つのユースケースを考えます。

1. APIサーバーを切り替えたい (dev/qa/prod)
1. ユーザーIDを変更したい

完成するとこんな感じ。さっそく作っていきます。

| ホーム | デバッグメニュー |
| --- | --- |
| ![Imgur](https://imgur.com/dxLRBV8.png)| ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/56635/6daac93c-46ce-75a7-b0ec-7864fa9b8f4d.png)|


## 下準備

Beagleを使う前にまずは下準備から。

アプリ再起動で設定した値が揮発してほしくないので、SharedPreferenceに永続化するために以下のようなクラスを用意しました。

```kotlin
class PersistDataAccessor(
    context: Context,
    private val props: DataProps
) {
    private val prefs = context.getSharedPreferences("debug_menu", Context.MODE_PRIVATE)

    fun getValue(): String = prefs.getString(props.key, props.defaultValue)!!

    fun setValue(value: String) = prefs.edit().putString(props.key, value).apply()
}

enum class DataProps(
    val key: String,
    val defaultValue: String
) {
    API_HOST(
        key = "api_host",
        defaultValue = ""
    ),
    USER_ID(
        key = "user_id",
        defaultValue = ""
    )
}
```

次にデバッグ時とデバッグ時以外で値の取得元を変えるために、以下のクラスを用意します。

``` kotlin
class ConfigurableString(
    private val accessor: PersistDataAccessor,
    private val defaultValue: String
) {
    fun getValue(): String = when {
        BuildConfig.DEBUG -> accessor.getValue()
        else -> defaultValue
    }

    fun setValue(value: String) = accessor.setValue(value)
}
```

次にデバッグメニューから設定をどのように書き換えるのかを定義します。今回はAPIホストは選択肢から選び、ユーザーIDは手入力することを想定しています。

```kotlin
enum class ConfigurableStringDefinition(
    val displayName: String,
    val type: Type,
    private val props: DataProps,
    private val defaultValue: String
) {
    API_HOST(
        "APIホスト",
        Type.Options(
            listOf(
                Option("dev", "dev.api.example.com"),
                Option("qa", "qa.api.example.com"),
                Option("prod", "api.example.com"),
            )
        ),
        DataProps.API_HOST,
        BuildConfig.API_HOST
    ),
    USER_ID(
        "ユーザーID",
        Type.ManualInput,
        DataProps.USER_ID,
        "user_id"
    );

    sealed class Type {
        object ManualInput : Type()
        data class Options(val options: List<Option>) : Type()
    }

    data class Option(val id: String, val value: String)

    fun create(context: Context): ConfigurableString =
        ConfigurableString(PersistDataAccessor(context, props), defaultValue)
}
```

## デバッグメニューの組み立て

依存を追加します。

```gradle:プロジェクトのbuild.gradle
repositories {
    google()
    jcenter()
+   maven { url "https://jitpack.io" }
}
```

```gradle:appモジュールのbuild.gradle
dependencies {
+   def beagle_version = '2.4.0'
+   implementation "com.github.pandulapeter.beagle:ui-bottom-sheet:$beagle_version"
}
```


`Beagle#initialize()`を呼んでBeagleを初期化します。
次に`Beagle#set()`でモジュールをセットします。

ここで言うモジュールとは、デバッグメニューの部品のことです。

```kotlin
object DebugMenu {
    fun setup(application: Application) {
        Beagle.initialize(application)

        Beagle.set(
            *ConfigurableStringDefinition.values().map { definition ->
                module(application.applicationContext, definition)
            }.toTypedArray()
        )
    }
}
```

Beagleでは豊富な種類のモジュールが用意されています。
今回は値を手入力できる`TextInputModule`とラジオボタンから1つだけ値を選択する`SingleSelectionModule`を使用しています。

```kotlin
object DebugMenu {
    private fun module(
        context: Context,
        definition: ConfigurableStringDefinition
    ): Module<*> = when (definition.type) {
        ConfigurableStringDefinition.Type.ManualInput -> {
            val configurable = definition.create(context)
            TextInputModule(
                text = Text::CharSequence,
                initialValue = configurable.getValue(),
                onValueChanged = configurable::setValue,
                areRealTimeUpdatesEnabled = false
            )
        }
        is ConfigurableStringDefinition.Type.Options -> {
            val configurable = definition.create(context)
            val (options) = definition.type
            val currentValue = configurable.getValue()
            val initialItem = options.find { it.value == currentValue }

            SingleSelectionListModule(
                title = definition.displayName,
                items = options.map { ListItem(it.id, it.value) },
                initiallySelectedItemId = initialItem?.id,
                onSelectionChanged = { option ->
                    if (option != null) {
                        configurable.setValue(option.value)
                    }
                }
            )
        }
    }
```

最後に`DebugMenu#setup`を`Application`の`onCreate()`内で呼び出せばOKです。

```kotlin
class MyApplication : Application() {
    override fun onCreate() {
        super.onCreate()
        DebugMenu.setup(this)
    }
}
```

## 結果

実行するとこんな感じで動きます。
わかりやすさのために「デバッグメニューを開く」ボタンを配置していますが、端末をひと振りすることでもデバッグメニューを表示することができます。

![beagle-sample-01.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/56635/a09841ca-e7c4-dff5-dbfb-b090f7d7d0c1.gif)


# サンプルのリポジトリ

完全なコードはこちらを参照してください。

https://github.com/maxfie1d/beagle-sample

# 参考リンク

* [pandulapeter/beagle: A smart, reliable, and highly customizable debug menu library for Android apps that supports screen recording, network activity logging, and many other useful features.](https://github.com/pandulapeter/beagle) 
* [Beagle — A smart and reliable companion library for debugging your Android apps](https://medium.com/halcyon-mobile/beagle-a-smart-and-reliable-companion-library-for-debugging-your-android-apps-40bb00da3007)

---

この記事を読んで「面白かった」「学びがあった」と思っていただけた方、よろしければ Twitter や facebook、はてなブックマークにてコメントをして頂けると嬉しいです。
また [DeNA 公式 Twitter アカウント @DeNAxTech](https://twitter.com/DeNAxTech) では、 Blog記事だけでなく色々な勉強会での登壇資料も発信してます。こちらもぜひフォローして頂けると嬉しいです。
<a href="https://twitter.com/DeNAxTech?ref_src=twsrc%5Etfw" class="twitter-follow-button" data-size="large" data-lang="ja" data-show-count="false">Follow @DeNAxTech</a><script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>