月別: 2016年9月

Android Studio2.2でレイアウトエディタを開いた際にTextで開くようにする

この記事は最終更新から3ヶ月以上が経過しています。情報が古い可能性があります。

layout.xmlを開いた際に、デフォルトではDesignタブで開かれると思います。これをTextに変更する方法です。

Android Studio 2.1まではレイアウトのPreview画面に歯車アイコンがあって、Prefer XML EditorにチェックをつければOKでしたが、Android Studio 2.2のPreview画面にはそのようなものが見当たりません。

Android Studio 2.2からは、設定画面から変更するようです。

レイアウトエディタをTextで開く

Preference > Editor > Layout Editorで設定できます。

Daggerを使ってSingletonにする仕組み

この記事は最終更新から3ヶ月以上が経過しています。情報が古い可能性があります。

ものすごいあほうなことを書いているかもしれませんが、そのときはご指摘ください。

Daggerを使って依存性を注入する際に、アプリ内でSingletonになるようにすることあるじゃないですか。

@Singleton
@Component(modules = AppModule.class)
public interface AppComponent {
    void inject(MainActivity activity);
}

@Module
public class AppModule {
    private Context context;
    public AppModule(Context context) {
        this.context = context;
    }

    @Provides
    @Singleton
    public SomeClass provideSomeClass() {
        return new SomeClass().initializeWithDefault();
    }
}

みたいに、SomeClassがアプリ内でシングルトンになるようにすると。

今までずっと、@Singletonって指定してるから実現できてるんだと思っておりました。実際には違います。これはそもそもAppComponent自体がアプリ内でシングルトンになっていなければ実現されません。

このAppComponentはApplicationクラスを拡張して、そこで初期化してるから@Singletonという指定が効くのです。このAppComponentを、ActivityのonCreateで初期化していたらシングルトンにはなりません。AppComponentインスタンスの中ではSomeClassのインスタンスは一度生成されたら使いまわされますが、AppComponentのインスタンスが複数生まれてしまえば生成されるSomeClassもAppComponentのインスタンスの数と同じだけ増えていくことになります。

そして@Singletonは別に@Singletonでなくてもいいのです。自分でスコープを作って、例えば

@AppScope
@Component(modules = AppModule.class)
public interface AppComponent {
}

@Module
public class AppModule {
    @Provides
    @AppScope
    public SomeClass provideSomeClass() {
    }
}

としても結果は同じです。Componentにつけたスコープ名の中でインスタンスを使いまわすっていう感じになるわけです。

だから@Singletonつけてるからシングルトンになるわけではないのです。AppComponentのインスタンスがアプリ内で1つだからこそ、シングルトンにできているわけです。

ここがあやふやなままだったので、Daggerよく分からん状態だったのですが、これで一歩前進できます。

カスタムViewが想定通りに描画されているかテストする

この記事は最終更新から3ヶ月以上が経過しています。情報が古い可能性があります。

カスタムViewを作って、しかもそれがCanvasを使って描画するようなものだった場合、どうやって動作確認をしていますか?

私はこれまで実機で動かして、目視で確認していました。Viewの見た目なので目視で確認するしかないんですけどね。それを手動でやっていました。

しかしつい先日、手動での確認が難しい案件に出くわしました。それは端末のセンサーの値を読み取って、その値にあわせてカスタムViewの描画が変わるようなものでした。これは手動で確認したくとも難しいです。

例えば心拍数を元に描画が変わるカスタムViewを想像してみてください。心拍数が120を超えたら特殊な表示を行う仕様だと思ってください。実機でそれを確認しようと思ったら、心拍数を上げるべく毎回運動しなきゃいけない、なんてことになるわけです。

そういったViewの描画、見た目の確認がしたい。こういうの、みんなどうやってテストしているのだろう。それが今回の出発点です。

サンプルプロジェクトをGitHubに置いてみたので良かったら見てみてください。~~というよりコードの解説はこの記事では一切ありませんので、GitHubでみてください。~~

やり方書かないのもあれなので、追記しました。

サンプルについて

TextViewの周りを線でデコレーションするカスタムViewがテスト対象です。どこを描画するかを指定してinvalidate()すると、TextViewの周りに線が描画されます。onDrawメソッドをオーバーライドして、Canvasを使って線を描いています。

今回はこの描画がちゃんとできるかを確認する、というそんなテストです。

スクリーンショットを撮って確認しよう

Viewの描画を確認したいわけですから、ユニットテストでは確認できません。

そこでまず思いついたのが、スクリーンショットを撮って、その画像で確認できたらいいんじゃないかというものでした。以前にEspresso+Spoonで自動的にスクリーンショットを撮るテストの話を見たのを覚えていたので、これを使えばいけそうと考えました。

問題が2つ

しかしSpoonを使ってスクショを撮るには、WRITE_EXTERNAL_STORAGEパーミッションが必要になります。プロダクト側で必要なら問題ありませんが、そうでない場合はテストのためだけに不要なパーミッションを追加することになります。できればそれは避けたい。

また、スクショはActivityを起動してそれを撮影することになるわけですが、実際に対象のViewを表示するActivityがテストに適した作りになっているとは限りません。

例えばこのサンプルプロジェクトでも、MainActivityを使ってテストできなくもありません。Espressoを使ってボタンを押すようにすれば、カスタムViewの描画は切り替わります。しかしこのMainActivityの仕様だと、カスタムViewの上と下に線を描画した状態をテストできません。

つまり、実際に使うActivityとは別にテストのためだけのActivityが欲しいわけです。

ではそんなActivityをプロダクションに混ぜるのかという話になりますが、それも避けたい。

テスト用のProduct Flavorsを用意する

そこでテスト用のプロダクトフレーバーを作成することでこれを回避しました。これもあまりスマートなやり方ではなく、できれば避けたかったのですが仕方ありません。

debugビルドにだけテスト用のパーミッション、Activityを含めるという方法もなくはないのですが、プロダクトフレーバーで切り分けてしまったほうが潔いかなと思ったのです。

テスト用のAndroidManifestとActivityさえ用意できれば、後は簡単です。

余談、androidTestに専用Activityを作ればいいんじゃないかという考え

ちなみに私は最初、androidTest配下にテスト用のActivityを追加して、それ経由でテストすればいいんじゃないかと考えました。しかしそれはうまくいきません。

なぜなら、androidTestに配置したコードはテスト用のAPKにコンパイルされるからです。

私は今までずっと勘違いしていました。androidTestに書いたテストを実行したら、mainに配置してるテスト対象コードにテストコードを追加したAPKが作成されて、それでテストが実行されてるんだと思ってました。どうもそうではなくて、普通のAPKを単にテスト用APKで外部から操作してただけなんですね。

http://stackoverflow.com/questions/27826935/android-test-only-permissions-with-gradle

作り方

まずproductFlavorを追加します。サンプルでは普段使うやつをDefault、Viewのテスト用のものをUiTestとしました。ここではUiTestを追加するとして書いていますので、適宜読み替えてください。

まずapp/build.gradleにproductFlavorの設定を追加します。applicationIdSuffixはお好みで。

android {
    productFlavors {
        Default {
        }
        UiTest {
            applicationIdSuffix ".uiTest"
        }
    }
    // そのままだとUiTestReleaseもbuildVariantに追加されてしまうので、それに対処
    android.variantFilter { variant ->
        if(variant.buildType.name.equals('release')
                && variant.getFlavors().get(0).name.equals('UiTest')) {
            variant.setIgnore(true);
        }
    }
}

EspressoとSpoonのセットアップ

Espresso

Spoon

プロジェクトルートのbuild.gradleに追記。

        classpath 'com.stanfy.spoon:spoon-gradle-plugin:1.2.2'

app/build.gradleに追記。

apply plugin: 'spoon'


android {
    defaultConfig {
        // 追加しないと多分テストがうまく走ってくれないと思います。
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
}

dependencies {
    androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
        exclude group: 'com.android.support', module: 'support-annotations'
    })
    androidTestCompile('com.android.support.test:runner:0.5', {
        exclude group: 'com.android.support', module: 'support-annotations'
    })
    androidTestCompile 'com.squareup.spoon:spoon-client:1.6.4'
}

プロダクトフレーバー用のディレクトリを作成

プロジェクトツールウィンドウのスコープをProjectに変更して、手動でディレクトリを作成します。(何か他にいい方法知ってれば教えてください)

<project root>
+-app
  +-src
    +-androidUiTest
    | +-java
    |   +-<your package>
    |      +-Viewの描画確認とスクショを撮るコードをここに配置
    +-UiTest
      +- AndroidManifest.xml
      +-java
      | +-<your package>
      |   +-Viewの描画確認のためのActivityを配置
      +-res // layout.xmlが必要なら作る

AndroidManifest.xmlに書くこと

  • WRITE_EXTERNAL_STORAGEの追加
  • 追加したActivityの宣言

サンプルコードを見てもらえば分かりますが、ビルド時にmainにおいてあるAndroidManifest.xmlとマージしてくれるので、UiTestで必要な分だけ書けばOKです。

あとはテストコードを書くだけ

別にアサーションは必要ないし、Espresso使っていると言ってもViewの操作をするわけでもないので(それをしなくていいようにテスト用のActivityを用意している)、テストコード書くのは超簡単なはず。

レポートの生成

  1. buildVariantをUiTestDebugに変更
  2. ターミナルで./gradlew :app:assembleUiTestDebug
  3. ターミナルで./gradlew :app:assembleUiTestDebugAndroidTest
  4. ターミナルで./gradlew :app:spoonUiTestDebugAndroidTest
  5. app/build/spoon/UiTestにレポートが生成される
  6. index.htmlをブラウザで開く

ターミナルでコマンドを打つか、もしくはgradleツールウィンドウから該当のタスクをダブルクリックとかでもOKのはず。