カテゴリー: Android

Androidのプログラミングについての情報を取り扱っています。

DataBindingを使っていてexecutePendingBindingsを呼び出さないとどうなるか

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

私はfindViewByIdをしなくていいからという理由でDataBindingを使っています。利用するためにbuild.gradleに

dataBinding {
    enabled = true
}

とするだけでいいのも気に入っています。

今回RecyclerViewのViewHolderにDataBindingを適用したときに、executePendingBindings()を呼び出さないことによる弊害がわかったのでご紹介します。

https://developer.android.com/topic/libraries/data-binding/index.html#advanced_binding

ドキュメントにはholder.getBinding().executePendingBindings();と、executePendingBindings()を呼び出すように書いてあります。私はDataBindingを使っていて、このようなメソッドを呼び出したことがなかったので、「なんでいるんだろう?」と疑問に思いました。オブジェクトのバインドはスケジュールされるだけですぐに行われるわけではないと書いてますけど、これまで使わずとも特に問題を感じなかったから、別になくてもいいのではと思ったのです。

私はこんな感じで使ってました。(Adapterのコードの一部ですが)

    @Override
    public void onBindViewHolder(DataBindingViewHolder<ItemHatebuFeedBinding> holder,
                                 int position) {
        ItemHatebuFeedBinding binding = holder.getBinding();
        final HatebuFeedItem  item    = items.get(position);
        binding.setItem(item);
    }

レイアウトファイルはこんな感じです。

<?xml version="1.0" encoding="utf-8"?>
<layout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    >

    <data>
        <variable
            name="item"
            type="jp.gcreate.sample.daggersandbox.model.HatebuFeedItem"
            />
    </data>

    <LinearLayout
        style="@style/RecyclerViewContainer.Clickable"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        >

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="horizontal"
            android:paddingBottom="@dimen/item_padding_with_item"
            >

            <TextView
                android:id="@+id/count"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:paddingRight="@dimen/item_padding_with_item"
                android:text="@{String.valueOf(item.count)}"
                android:textAppearance="@style/TextAppearance.AppCompat.Title"
                android:textColor="@color/red_600"
                />

            <TextView
                android:id="@+id/title"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:text="@{item.title}"
                android:textAppearance="@style/TextAppearance.AppCompat.Subhead"
                android:layout_gravity="fill_horizontal"
                />


        </LinearLayout>

        <TextView
            android:id="@+id/description"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="@{item.description}"
            android:paddingBottom="@dimen/item_padding_with_item"
            />

        <TextView
            android:id="@+id/date"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{item.date}"
            android:textAppearance="@style/TextAppearance.AppCompat.Caption"
            />

    </LinearLayout>
</layout>

executePendingBindings()を呼び出さなくても普通に動作します。下に向かってスクロールする分には何も変なことはありません。しかし、下から上に向かってスクロールすると、時折妙な動き方をします。時折ブレるような挙動をするのです。(ちなみに動画を撮って用意したのですが、ファイルサイズが大きいので貼るのは止めました)

この動きはexecutePendingBindings()を呼び出していると起こりません。なるほど、executePendingBindings()を呼び出さないとこのようなことになるわけですね。

微妙にブレるように感じたのは、RecyclerViewをスクロールして次のViewが要求される→onBindViewHolderが呼び出され、setItem()でオブジェクトをバインドする→Viewが見え始める→バインドしたオブジェクトが実際にViewに描画される→中身によってViewの高さが変わる→アイテムが見え始めてからViewの高さが変わり、表示中のアイテムが動いたようにみえる、という経過を辿っているのでしょう。

下に向かっていく分には、Viewの高さが変わっても伸びた部分は画面外にいくので、特に違和感を感じません。しかし、上に戻っていくときにはViewが見え始めてから高さが変わるため、下に伸びるとそれまで表示していた部分が下に押し出されて、自分がスクロールした分以上にスクロールしたように感じる。もしくは短くなった場合には、スクロールしたのが取り消されて上に引っ張られたかのように感じる。それが違和感の原因でした。

これは各アイテムのViewの高さが一定であれば生じない問題です(高さのズレが生じなくなるため)。この例ではwarp_contentを使っていて、かつ中身の長さがアイテムによって異なっていたために生じました。

これまで特にDataBindingによるタイミングのズレなど気にしたことがなかったのですが、RecyclerViewで使うときには気をつけないといけないんですねぇ。

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のはず。

Crashlyticsのクラッシュレポートをdebugビルドでは行わないようにする

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

新しいアプリを作るときにしかやらないので、いつもやり方を忘れてしまうCrashlyticsのレポート設定のメモ。

デバッグビルドでまでクラッシュレポートが報告されてしまうと、通知がうっとうしいだけですし、手元でスタックトレースが読めるわけですから完全に無駄です。必要なのは基本的にはリリースビルドだけでしょうから、開発中は無効にしてしまうのが便利です。

やることは下記のページのとおりです。

https://docs.fabric.io/android/crashlytics/build-tools.html?gradle#build-tools

まずはレポートを無効にしたいbuildTypeにext.enableCrashlytics = falseを追加します。

この設定はGradleのビルド時に、Crashlyticsで使うIDなんかを生成する処理を行わないようにするもののようです。レポート送信を止めるわけではないので、これを追加しただけでは下記のようなエラーが出てしまいます。

This app relies on Crashlytics. Please sign up for access at https://fabric.io/sign_up,
                                                   install an Android build tool and ask a team member to invite you to this app's organization.

開発時にレポート送信自体を止めるためには、アプリ実行時にCrashlytics自体が動かないようにする必要があります。そのためには、Crashlyticsの初期化を行う部分で、下記のように初期化を行います。

        Crashlytics crashlyticsKit = new Crashlytics.Builder()
                .core(new CrashlyticsCore.Builder()
                        .disabled(BuildConfig.DEBUG).build())
                .build();
        Fabric.with(this, crashlyticsKit);

ちなみにdebuggableがfalseなんだけどCrashlyticsのレポートを無効化したい、なんていう場合には、.disabled(BuildConfig.DEBUG || BuildConfig.BUILD_TYPE.equals("test"))のようにしてやればOK。

レポートが送信されないことの確認

アプリの任意の場所でthrow new RuntimeException("Crashlytics report test");と例外を投げてみればOK。

ただし例外を投げる場所に注意が必要です。Crashlyticsの初期化が終わっていないと、そもそもレポートが送信できません。Applicationクラスの外か、ActivityのonCreateの外で例外を投げるようにしないとテストにならないので注意しましょう。

https://docs.fabric.io/android/crashlytics/introduction.html

暗号化について勉強中

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

Androidでユーザデータを暗号化して保護するにはどうしたらいいのでしょうか。

これまで「暗号化のやり方がよくわからないから、機密情報を保存しないようにしよう」と避けてきたのですが、それにも限界があるのかなぁと思って、手を広げてみる気になったのです。

まあ本当の出発点は、ユーザデータの保護ではなく、アプリで扱うデータを秘匿するにはどうしたらいいのだろうだったんですけれど・・・。

例えば、Twitterクライアントを作るとして、そのAPIキーをどうやって秘匿するのかということです。

Androidでこの手のAPIキーを扱う場合、サンプルではJavaのコードに直接書いてありますが、実際に自分でリリースするアプリでもそのままでいいのかという話です。

AndroidではAPKを簡単に抜き出して、しかも簡単にソースコードを確認することもできます。ProGuardで難読化をしても、Javaのコードに埋め込んだ文字列はそのままです。

String api_key = "hogehoge";のapi_keyの部分はProGuardによってaとか意味のない文字に書き換わりますが、”hogehoge”の部分は書き換わりません。(書き換わったらプログラムの動作が変わってしまうので当たり前です)

つまり、やろうと思えばAPIキーは見放題ということになります。

だからここの部分、どうするのがセオリーなんだろうってのから始まって、暗号化すれば秘匿できるのかなと思ってみたのです。

多分、ユーザに見せたくないデータはアプリで保持しないのが正解であって、暗号化は関係ないと思いますが、それでも今まで放置していた暗号化に関して勉強するいい機会ではあるのでそのまま続けております。

どう勉強してるのかって話ですが、https://developer.android.com/training/articles/keystore.htmlを見たり、Android Studioのimport Sampleで取り込めるGoogleのサンプルコードを使ったりしてます。

実際に手を動かしてみて初めて知ったのですが、同じ文字列を同じ暗号化鍵で暗号化しても、得られるバイト列が毎回異なることに驚きました。”abc”を同じ暗号化鍵を使って暗号化したら、毎回123になるというイメージを持っていたものですから、なんで毎回異なるバイト配列になるのか不思議です。しかも異なるバイト配列になるのに、復号化したらちゃんと”abc”に戻るのですからなお不思議です。

これは一体どういう仕組なのか。そもそも暗号化の仕組みを理解せずに使うのは逆に危険なんじゃないのかと思い始め、今度はこんな本を読み始めました。

まあまだ読んでいる最中なんですけど面白いです。

本書には

弱い暗号は暗号化しないより危険である

という考え方が序盤で紹介されています。仕組みを理解せずに暗号化するのもまた良くないと言えるのではないでしょうか。

安定の結城先生なので非常に分かりやすく、楽しく読めそうです。

それにしても、初めはAPIキーを秘匿することがきっかけだったのに、我ながらよく脱線してきたものだなぁと思います。

Contextの使い分けについて私が辿った変遷

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

Application Contextを渡してはいけないという記事を読みました。

http://ytrino.hatenablog.com/entry/2016/05/26/033936

この記事を読んで改めて自分のContextに対する認識があまいことを実感したので、思ったことをまとめてみようと思います。

趣旨は「僕はContextに対してこういうふうに思ってるんだけど、違う・違わないが自信持ててないから誰か突っ込んでくれ」です。たぶんそんなに間違ってないと思うんですけど、相談できるような相手がいないんだ、察しておくれ。

「私もそういう認識です」と言ってもらえれば自信になるし、違う部分があれば指摘をもらって学びの機会になり私のためになります。また、こういう変遷を経て学んできたという情報が初学者の役に立てばなぁなんて思ってもいます。

私のAndroidレベルは初学者というわけでもないですが、Contextにまつわる話を聞くと理解が曖昧でモヤモヤするレベルです。上記記事に倣えばAndroid2級でしょうか。

私は最近でこそライフサイクル考えてContextを使い分けるようになりましたが、ちょっと前まで「困ったらApplication Context渡しとけば問題ない」という考えの人でした。前までというか、今もそういう部分があるんですけどね。だからこそ「Application Contextを渡してはいけない」というのを見て不安になってしまいました。

渡してはいけないの意味

Application Contextを渡してはいけないというのは、どういう意味なのでしょうか。

  1. アプリがクラッシュするから渡してはいけない
  2. 表示がおかしくなるから渡してはいけない
  3. 渡しても動作上は問題ないが、コードの見た目上・意味的に美しくないので渡してはいけない

実はApplication Contextをなるべく使おうというのも、Application Contextを渡してはいけないというのも、あるコンテキストにおいては間違ってないんじゃないかなと思います。それぞれ違うコンテキストで話しているので、そこを混同すると混乱してしまいます。

前者は「メモリリークを回避する」という文脈での話で、Activityのリークを防ぐにはApplication Contextを使えばいいのは正しい(はず)です。

一方後者はthemeの適用の観点での話で、Application Contextでは意図した動きにならない・変にハマることがあるからApplication Contextを安易に使ってはいけないという話で、これはこれで正しいわけです。

どっちも正しいのですが、残念ながらコードを書いていてContextを要求された時に、私たちは何らかのContextを渡さないと先に勧めません。何を渡せばいいかは「ケースバイケースなんで一概にどれを使えとは言えない」んで、Contextの要求に対しては、それぞれ適切なContextを渡す必要があります。

ではどのようにしたら、何を使うのが適切かを判断できるのでしょうか。ライフサイクルを考えろということでしょうか。そのライフサイクルはどうやって判断するんでしょうか。

私はその判断を、今はソースコードを読むことで行っています。Contextを要求するメソッドが、クラスが、いったい渡されたContextをどのように利用しているのか。それをもって何を渡すか考えています。

Contextについて私の辿った道

Androidを学び始めた当初は、Contextなんて意識していませんでした。本やサンプルコードの写経する分には意識しないですみますから。

しかし写経から脱し、自分でコードを書き始めるとそうもいきません。こういう処理を実装したいな→このメソッド使えばなんとかなりそう→Contextが必要らしい。はたとキーを打つ手が止まります。

そのメソッドを使うにはContextを渡すしかない。渡さないと先に進まない。とりあえずthisって書いたら動いた。よし、これで先へ進める。なんていうのが、Androidを学び始めた人が辿る道ではないでしょうか。私はそうでした。

学び始めの頃はActivityからなんかすることが多いので、this、すなわちActivityを渡していればとりあえずは動いてくれました。私はそうして「ああ、ContextっていうのはActivityを渡せばいいのだな」と認識するようになりました。

しかしActivity Contextを渡してしまうとメモリリークが発生するぞという話を耳にします。そこで登場するのがApplication Context。getApplicationContext()を使えばメモリリーク対策になるという話を聞いて、「ああ、じゃあとりあえずApplication Contextを渡しておけば問題ないのか」となりました。

しかし今度はApplication Contextを渡すと想定通りにViewが表示されないことがあるという。無思考にApplication Contextを渡すのもどうやらダメらしい。

一周回ってきてしまいました。もう一体どうしろと。このメソッドContext要求してくるのに僕は一体何を渡せばいいんだ。

私のContextとのつきあい方

そんな私が多少なりとも指針を持てるようになったのは、一言で言ってしまえば慣れてきたからなんだと思います。

とりあえずコード書いて、動かしてみて。それを繰り返すうちに慣れてきて。ソースコードを読むようになって「多分こうやれば大きく間違ってはいないはず」という、そんな指針を持てるようになりました。

Contextを要求されたら何を渡すか。重要なのは考え方、つきあい方だと思います。これが今回の記事の本題です。

私はまずライフサイクルを考えるより前に、Contextを要求するメソッドのソースコードを確認しに行きます。そいつがContextへの参照を保持するのか、それともContextを使ってなんかするだけなのか。それを確認するのです。

Contextへの参照を保持するなら、Activity Contextを渡していいのか、ライフサイクルを考えなければなりません。ライフサイクルを考えるというよりは、Activityとともに役目を終えるのか、そうでないのかで判断していますが、正直このあたりは曖昧です。

該当のメソッドが単にStringリソースを読みだすのに使っているとかだったら、別にActivity Contextを使っても問題ないですよね。そもそもActivityがリークするのは、Activityへの参照を持ち、渡した先のオブジェクトがActivityより長生きする場合だからです。

Contextへの参照を持たないのであれば、別にどっちを使おうが問題ないので、好きな方使えばいいじゃんって感じですが、Activityから呼び出すのであれば、わざわざApplication Context渡すのも大げさなので、Activity Contextでいいかと思います。

Androidと付き合っていくうちに、なんとくこんな感じに行き着きました。それでもメモリリークが怖いなら、Contextに何を使えばいいか迷うよりも、それでメモリリークするのかを確認する方に力を注いだほうが良いと思います。

でも、独学でやってるとこれで本当に問題ないのか確証が持てないので、いつもどこかで不安です。誰かに聞く機会もなくてつらい。

見えないメモリリークの影

そもそもContextについて意識し始めるキッカケは、私の場合はActivityのメモリリークです。

このメモリリークも私にとってはContext以上によく分からない存在でした。最初のうちはメモリリークしているかどうかをどうやって確認すればいいのかがわからなかったからです。確認方法が分からないから、メモリリークしないコードを書かなきゃという気持ちだけが肥大化していました。

そんな中で「とりあえずApplication Context渡しておけばライフサイクルの関係でActivityがリークすることはない」という1つの指針は、私にとっては心強かったです。

実際にはApplication Contextを渡すからメモリリークしなくなるわけではないというのは、今であれば分かります。しかしはじめの頃はそんな違いも分からなくて、そんなことよりContextとして何を渡せばいいのかという指針があることがありがたかったのです。

これに関しては、メモリリークしているかどうかを確認する方法を学ぶ方が大切だと、今であれば思いますけどね。当初は確認方法がわからないけど、メモリリークしたら困るからApplication Context渡しとけっていう感じでコード書いてました。

初学者に向けてステップアップのために

初学者がもっとも優先すべきはとりあえず先に進むことだと思います。動くアプリを作ることです。そしてAndroidに慣れることです。

Contextを要求するメソッドに対して、何らかのContextを渡す。Activity Context渡すのかApplication Context渡すのか迷うと思います。最初のうちはとりあえずどっちか渡して動いてればいいと思います。動けば正義です。迷って足を止めるより、いっぱい作ってAndroidに慣れる方が大事だと思います。

慣れてきたらContextを渡すときに「こいつはActivityより長生きする奴なのかな」と考えましょう。そんなときはソースコードを読むことが助けになると思います。

Contextを要求するメソッドは、渡されたContextをどのように使っているのか。そいつもContextが必要なメソッドを呼び出すために必要としているのか。それともContextへの参照を保持するのか。

Contextに何を使うべきなのかはケースバイケースで一概には言えません。Activity Contextじゃないとダメな場合もあれば、Application Contextの方が適切な場合もあり、はたまたどっち渡しても問題ない場合だってあります。・・・なんでこんなややこしいことになってるんでしょうね?

とりあえず、動けば正義でトライアンドエラーを繰り返してAndroidに慣れる。慣れてきたらソースコードを読んでみて、Contextがどう使われているかを気にしてみる。そうやってステップアップしていくしかありません。

私のContextとの現在のつきあい方

長くなったので最後にまとめ。

私はActivity Contextを基本的に使うようにしていて、利用先でContextへの参照を持つ場合にはApplication Contextの利用を考える、というような感じでやってます。

Activity Context使っているのはThemeのためとかじゃなくて、単にgetApplicationContext()を書くと長くなるからとかそんな理由です。明らかにActivityのライフサイクルより長生きするオブジェクトに対しては、Application Context渡していますけれども。

正直な所、メモリリークをおそれてContextにどれを使うか悩むより、メモリリークの確認の仕方を調べて、リークしていないかを確認することに力入れたほうが建設的なんじゃないかなと思います。

何を使うのが適切か迷ってまごまごする前に、どう使われているのかをまず確認する。Contextの使い分けに関してはこれが私の指針なんですが、そんなに大きく間違ってはないですよね?

Kotlin使ってみて感じる便利さと葛藤

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

最近アプリやAndroid Studio用プラグインを作るのにKotlinを使っています。

始めたばかりの頃は「Javaだとああ書くんだけど、Kotlinだとどう書けばいいんだ」ということが多かったです。Javaでまともに書けないのにKotlinに手を出すのは早いんじゃないかとも思っていました。

しかし少しずつ試していくと、Kotlinの便利な部分が分かってきてきました。

私の場合、「Kotlinで書き始めたんだけどやっぱ使い方よくわからないからJavaに戻そう」とう場面が初期の頃はよくありました。はじめはKotlinで書いていたけど、やっぱりJavaで実装しようとという感じです。

そんなときに、「Javaに戻すのめんどくせえ」と感じる部分があって、そこで改めて「Kotlinってやっぱ便利やなぁ」なんて実感しました。

それからというもの、Kotlinの比重が徐々に増えてきて、今では逆にJavaで書く方が面倒くさいと感じるようになってしまいました。

一方で、Kotlinが無敵というわけではありません。Annotation Processingを使うライブラリがKotlinだとうまく使えないことがあったり(基本的には大丈夫ですが、Javaで書けば動くコードがKotlinで同じように書くと動かないことがあったりします)、Javaと比べるとコード補完が遅かったり、Kotlinを使うことで感じるストレスもあります。

ですが、不便さを差し置いてもKotlinで書いた方がすっきり書けるのはやっぱり快適だと思っています。

テストコードから始めるといいかも

どなたかの記事で、Kotlinはテストコードから導入してみたらどうかという記事を読みました。私もいい方法だと思います。Kotlinの便利さを実感するためではなく、どう書くかを知るのにちょうどいいと思います。

私もJUnitでのユニットテストをKotlinで書いています。テスト対象をKotlinで書いてるからとか、セミコロンつけなくてもいいから、とかそんな理由です。ユニットテストについてはKotlinが便利だからという理由はあまりないかもしれません。

ユニットテストにおいてKotlinが便利だと思うのは、バッククオート(`)で囲むことで、メソッド名やクラス名を数字から始めることができたり、途中に空白を含めることができたりすることでしょうか。

私はテスト名に日本語を使うことが多いです。そしてそのときに、メソッド名に使える文字に制約があるのが微妙に困ります。

例えば各月の最終日を求めるメソッドのテストをするのに「4月の場合は30日を返す」というテスト名にしたくてもできません。Javaではメソッド名を数字から始めることができないからです。だからこんなときは「月に4月を指定したら30日を返す」という感じのメソッド名にするのですが、これが微妙なストレスになります。(頭に「月を」つけるだけやんという感じですが、微妙にストレス感じるんですよこれ)

Kotlinではこの制約に煩わされることがありません。メソッド名をバッククオートで囲めば、数字から始めようが、途中に空白を挟もうが問題ないのです。

    @Test fun `2つの時刻の差を求める`(){
        val time1 = LocalDateTime.of(2014, 1, 1, 23, 58, 30)
        val time2 = LocalDateTime.of(2014, 1, 2,  0,  4, 30)
        val actual = Duration.between(time1,time2).seconds
        assertThat(actual, `is`(360L))
    }

なにそれ気持ち悪いと思われるかもしれませんが、これはれっきとしたKotlinの仕様です。

Grammar – SimpleName

Javaのメソッド名規約によってテストメソッド名を考えるのが面倒くさいなぁと感じている人は私だけではないと思いたい。

一方でKotlinでユニットテスト書けば便利なことばかりではありません。例えばassertThatなどを使おうとするとimport文を手書きで書かないと認識してくれないのが不便です。(私の環境の問題なのかもしれません。JavaだとALT+Enterでimportできるんですけどね・・・)

セミコロンつけなくてもいい

単純なことですが、Kotlinは文末にセミコロンをつけなくてもいいのです。

これが便利・・・と言いたいところですが、私は半々かなぁと感じています。

確かにいちいちセミコロンつけなくてもいいので楽です。たまにJavaでコードを書くときに、しょっちゅうセミコロンつけ忘れます。それくらいには快適です。

一方セミコロンが不要なせいで、メソッドチェーンするときに私は微妙にストレスを感じます。

Javaだとメソッドチェーンするときに改行をするとインデントを一段深くしてくれます。

しかしKotlinでは改行した時に文末なのか次の文に移るのかが判別不能なので、インデントを深くしてくれたりしません。これが毎回微妙にめんどうくさいです。私はいつもドットを打ってカーソル戻して改行するという方法をとって回避しています。

多分Reformat Code(Cmd+Alt+l)を使うのが楽なんでしょうけども。みんなどうしてるんだろう・・・。

文字列の扱いが便利

Strings

Javaだと文字列に変数を埋め込もうと思うと、+演算子で連結しなければなりません。もしくはString.format()を使うかですね。

Kotlinだとそんな面倒くさいことをせずとも、文字列中に変数を埋め込めるので便利です。こんな感じに書けるわけです。

val hoge = "num is $num"

埋め込む変数が多くなればなるほど、これはとても便利になります。デバッグのために変数の中身を文字列として出力して確認することがよくあると思いますが、そんなときに特に楽だと思います。

複数行に渡る文字列も、"""で囲むことでそのまま文字列として扱うことができます。ただしこれを使うと、インデントによる空白も文字列に含まれてしまうので、使いドコロが難しい気もします。

配列操作が便利

例えば配列の要素の中から最大値を取得しようと思ったら、Javaだとこんな感じになるでしょう。

int[] array = {1,20,3,40,5,16,7};
int max = 0;
for (int num : array) {
    max = Math.max(num, max);
}

Kotlinだとこうです。

val array = arrayOf(1,20,3,40,5,16,7)
val max = array.max()

わざわざループを回したりしなくてすんで、非常にすっきりします。

他にも、例えば配列のインデックスもあわせて使いたい場合は、.forEachIndexed{index, data ->//処理}なんて書けます。for文を書く頻度がだいぶ少なくなります。

一方で、配列の宣言がいまだに慣れません。

arrayOf()arrayListOf()listOf()mutableListOf()などなど、いろいろ種類があっていつも混乱します。JavaのListを引数に取るメソッドをKotlinから利用するときなど、引数の型が違うという感じでAndroid Studioに怒られるのがしょっちゅうです。

拡張関数が便利

Kotlinの便利さを実感するのがこの拡張関数です。

例えばorg.threeten.bp.LocalDateTimeとjava.sql.Timestampの変換をしようと思うと、DateTimeUtilsクラスを利用すればできます。

val localDateTime = LocalDateTime.now()
val timestamp = DateTimeUtils.toSqlTimestamp(localDateTime)

でもこれ、LocalDateTimeクラスに直接Timestampへの変換を行うメソッドがあったらもっとスマートに書けます。そしてKotlinなら拡張関数を利用することでそれが実現できます。

例えばUtil.ktというファイルを作成して、そこにこんな関数を定義します。

fun LocalDateTime.toTimestamp() = DateTimeUtils.toSqlTimestamp(this)

するとval timestamp = localDateTime.toTimestamp()と書けるようになるのです。

Javaだとユーティリティクラスを作成して、staticメソッドでやっていたようなことを、Kotlinだとずっとスマートに実装することができます。これに手を出し始めるとJavaには戻れないですね。

ただ多用すると知らない人からみたら「なんぞこれ」というコードになってしまいそうです。自分で作って自分で使う分には便利で気持ちいいのですが、チームで使うときにはまた違う感想になるのかもしれません。

まとめ

私自身はKotlin便利で、全部Kotlinで書けたら楽だなぁとは思っているんですが、一方で素直にJavaで書いた方が楽じゃないかと思うときも良くあります。

書き上がったソースコードはすっきりしているものの、書いてる最中はコード補完が遅くてストレス感じます。単純に私のマシンスペックの問題なのかもしれませんが(3年前に買ったMacbook Airなのでいい加減買い換えたい)、固まってイライラすることも少なく無いです。たまにJavaで書くとコード補完が早くて快適に感じます。

KotlinはJavaと100%互換で、基本的にはKotlinかJavaかを意識しなくてもいいのですが、それでもやっぱりJavaとの境界は意識せざるを得ません。Javaのことを意識して書かざるをえないのであれば、はじめからJavaで書いた方が楽という部分はあると思います。

また、Android StudioのInstant Runとの関係かもしれませんが、変更したソースコードが反映されずにデバッグに余計な手間がかかることがたまにあるのもストレスです。毎回起こるわけでもないのがまた微妙なところです。

素直にJavaで実装したほうが楽なのかもしれないけれども、一方でKotlinの便利さを知ってしまったがゆえにJavaで書くのも面倒くさいという葛藤を抱いております。ちょっと前までは「フルKotlinが一番便利でいい」とこだわろうとしていたのですが、今はKotlinとJavaをいい感じに併用していく柔軟性が大事なのかなと考えを改めているところです。

画面をタッチして線を描く お絵かきアプリを作るための第一歩

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

今回の記事のサンプルコードは、GitHubで公開しています。

お絵かきアプリを作ろうと思って格闘中です。とりあえず線を描くだけでも学びがいろいろあったのでまとめておこうと思います。

Pathを使って描画するとカクカクする問題

線を描くにはPathを使うのがオーソドックスのようですが、何も考えずにパスを使った描画を行うと、線がカクカクしてしまいます。(サンプルコードのPathPaintView)

path.lineTo(e.getX(), e.getY());
drawCanvas.drawPath(path, paint);

path.lineToによる描画

これはなぜ起こるのでしょうか。

MotionEventが配信される間隔の問題

その理由はまず線をPathではなく点で描画してみると分かります。(DotPaintView)

drawCanvas.drawPoint(e.getX(), e.getY(), paint);

drawPointによる描画

描画される点がまばらになっています。このドットはonTouch()が呼ばれるタイミングで描画されています。このドットの間隔がタッチイベントがViewに伝えられているタイミングだということです。これはスクリーンをタッチした情報が、逐一間断なくonTouch()に渡されているわけではないことを意味しています。

Historical情報を利用する

ではドットとドットの間のタッチイベントの情報は失われているのかというと、決してそうではありません。onTouchに渡されるMotionEventには、MotionEventが配信されていない時に生じた座標を保持しています。

その情報はMotionEvent.getHistoricalX()などで取得することができます。これを利用すれば、MotionEventの情報をより精細に取得することができます。(HistoricalDotPaintView)

int history = e.getHistorySize();
for (int h = 0; h < history; h++){
drawCanvas.drawPoint(e.getHistoricalX(h), e.getHistoricalY(h), paint);
}
drawCanvas.drawPoint(e.getX(), e.getY(), paint);

getHistoricalによる描画

ドットの間隔が狭まりました。指をゆっくり動かせばキレイな線が描画できます。しかしこのHistorical情報にも限度があり、指を少しでも早く動かすとやはり間隔が空いてしまいます。

Historical情報を使ってPathによる描画を行う

Historical情報を利用すれば、精度の高い座標情報を取得できることが分かりました。この座標情報をPathによる描画で利用してみます。(HistoricalPathPaintView)

int history = e.getHistorySize();
for (int h = 0; h < history; h++){
path.lineTo(e.getHistoricalX(h), e.getHistoricalY(h));
}
path.lineTo(e.getX(), e.getY());
drawCanvas.drawPath(path, paint);

Device 2016 02 13 163849

単にpath.lineTo(x, y)で描画した時に比べると随分なめらかになりました。しかし、高速で動かしたらやっぱりカクカクしてしまうのは避けられません。なぜならpath.lineTo()による描画は、HistoricalDotPaintViewで描画した点と点の間を直線で結んでいるにすぎないからです。

これを解決するには、点と点の間をなめらかな曲線で結べば解決できそうです。

ベジェ曲線を利用する

ベジェ曲線によりスムーズな線をひく方法はいろいろ考えられるでしょう。1つの方法としてこんなやり方ができます。(BezierPathPaintView)

private void onTouchMove(MotionEvent e){
float midX = (previousX + e.getX()) / 2;
float midY = (previousY + e.getY()) / 2;
path.quadTo(previousX, previousY, midX, midY);
previousX = e.getX();
previousY = e.getY();
}

自分で作っておきながら分かりやすく説明できないのですが、この処理のポイントは3つです。

  • 前回のMotionEventで配信された座標点を記憶すること
  • 前回の座標と今回の座標の中間点を計算すること
  • 前回の座標を調整点とする、前回の中間点から今回の中間点までの2次ベジェ曲線を描く

この方法では、正確にタッチした通りの線が描けるわけではないのですが、比較的簡単な処理でカクカクしない線を描くことができます。

ベジェ曲線による描画

Historical情報を利用してやれば、更に精度の高い線が描けるでしょう。

ちなみにpath.quadTo(x1, y1, x2, y2)は、(x1,y1)の座標が制御点で、後半の(x2,y2)の座標が終端になります。始点はpathが持っている最後の座標になります。つまり前に描画したpath.quadToの終点が次の描画の始点になるということです。

サンプルはGitHub

今回の記事のサンプルコードは、GitHubで公開しています。

Android Studio 2.0 beta 4を使って作っています。古いバージョンのAndroid Studioを利用している場合は、build.gradleのclasspath 'com.android.tools.build:gradle:2.0.0-beta4'をお使いの環境に合わせて修正すれば動くと思います。

単に線を描くだけでもなかなか奥が深いです。

タッチイベントについて

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

端末の画面をタッチした情報はMotionEventとしてActivityやViewに通知されます。

MotionEventはさまざまな情報を持っています。

MotionEvent – Android Developers

  • アクション(触れたのか、動かしたのか、離したのか)
  • ポインタの数(何本の指で触っているのか)
  • タッチした座標

これらは全てポインタごと別々に識別されていて、全てポインタのインデックスでアクセスすることが出来ます。(ポインタのIDではありません)

その辺りをごっちゃにしてハマった結果、Stackoverflowに投稿した質問がこちらです。Androidでマルチタッチ時のポインターIDを検出する方法(ちなみに投稿後に勘違いが原因であることに気づいた)

ポインタインデックス

ポインタのインデックスは必ず0から始まり、getPointerCount() - 1まで割り振られます。

例えば2本の指でタッチしている場合、getPointerCount()は2を返します。1本目の指がポインタインデックス0で、2本目がインデックス1となります。

さらにこの状態で1本目の指を離すと、2本目の指のインデックスが0に変わります。

指を離す順番によってインデックスはころころ変わるため、特定のポインタを識別するのには使えません。

例えば人差し指、中指、薬指を使ったタップを考えましょう。途中で人差し指、薬指は離したり触れたりしているとします。しかし常に中指はつけたままにして、これをトラッキングしたいとします。この場合にはポインタインデックスを使うことは出来ません。

特定のポインタを識別するにはポインタIDを利用します。

ポインタID

一度タッチするとポインタにはIDが割り当てられ、そのIDは指を離すまで変わりません。

上記の例で言うと、中指を画面から離さないかぎり中指を示すポインタのIDは常に同じです。

一方で注意しなければいけないのは、座標を取得したりするメソッドの引数はポインタインデックスであるということです。

ポインタはIDで識別するけど、そのポインタの情報を取得するために必要なのはポインタインデックスです。

そのため、特定のポインタIDの座標を取得したりするには、findPointerIndex()メソッドを使って、IDからポインタインデックスを引き出す必要があります。

インデックスとIDの違い

ポインタインデックスは常に0から始まり、他のポインタが増減する度に再割当てされます。一方でポインタを識別するIDは、指が触れたときに割り振られ画面に触れている限りその値は変わりません。

例えばこんな感じになります。

インデックス0 ID0 人差し指
インデックス1 ID1 中指
インデックス2 ID2 薬指
 ↓この状態で人差し指を離す
インデックス0 ID1 中指
インデックス1 ID2 薬指
 ↓人差し指でタッチする
インデックス0 ID0 人差し指
インデックス1 ID1 中指
インデックス2 ID2 薬指

ポインタIDとポインタインデックスの値は、指を押した順番と反対に離す分には一致したままですが、押した順番とは異なる離し方をすると値がズレます。

ヒストリー

タッチイベントはリアルタイムに配信されるわけではありません。

開発者向けオプションでポインタの位置を表示するようにすると、ポインタの軌跡がそのまま表示されますが、onTouchEvent()にMotionEventが配信される間隔はマチマチです。例えばgetX()で取得できる座標は飛び飛びになってしまいます。

手書きの文字を描画しようと思うと、getX()メソッドだけを使っていると、描画処理の分MotionEventが配信される間隔が空いてしまい、描画できる線がカクカクしてしまうことでしょう。

しかしちゃんとMotionEventには、前回onTouchEventに配信されてから今回配信されるまでの間に記録している情報が格納されて配信されています。

getHisorySize()を使うことで、以前のonTouchEventが呼ばれてから今回のイベントが呼ばれるまでに、いくつのイベントを保持しているかが分かります。

ヒストリー情報を使ってポインタの情報を取得するには、getHistoricalX()といったメソッドを利用することになります。

アクション

タッチイベントの種類(触れたのか、離したのか、動かしたのか)はgetAction()で取得できます。

しかしgetAction()で取得できる情報は、ポインタのインデックスとポインタごとのアクションがごちゃまぜになった情報になります。例えば2本指同時押しだとgetAction()では261という数字が返ります。ちなみに1本でタッチすれば0です。

これはgetAction()がアクションの発生したポインタインデックスと、ポインタインデックスごとのアクションを全てまとめた値を取得するメソッドだからです。

getActionIndex()でアクションが発生したポインタのインデックスが分かります。

getAcitonMasked()は動作を表す純粋なアクションだけを返します。

つまりタップ(MotionEvent.ACTION_DOWN)を検出したい場合、マルチタッチを考慮するとgetActionMasked()を使う必要があるということです。getAciton()では二本指での同時押しを検出できない可能性があります。

座標

座標はgetX()でX座標、getY()でY座標を取得できます。

引数にポインタインデックスを渡すことで、指定したポインタインデックスの示す座標を取得できます。引数を省略した場合には、インデックス0の座標が取得できます。

getRawX()getRawY()と、getX()getY()の違いは、どこを基準とした座標数値が取得できるかです。

getRawX()などは座標の補正を行わない、端末のスクリーン上の座標を示します。スクリーンの左上をX=0,Y=0とした座標になります。Raw座標はポインタインデックス0のものしか取得できないみたいです。

対してgetX()はMotionEventを受け取るViewの左上をX=0,Y=0とした座標に変換されます。

サイズ

getSize()でサイズが取得できます。

このサイズは何かというと、多分タッチパネルが認識しているタッチの範囲とでも言いましょうか、指のサイズみたいなイメージです。

指の触れる範囲を増やしていくとサイズも大きくなります。

圧力

getPressure()で圧力を取得できます。

感圧式のタッチパネルならそのまま圧力(どれくらいの強さで押しているのか)が分かるのだと思います。

静電気を検出する静電容量方式タッチパネルでも値は変動しますが、純粋な意味での圧力を示しているわけではありません。指の触れている範囲が大きくなれば圧力も大きくなるみたいです。

ツールタイプ

指で触れているのか、スタイラスなのかというのが、getToolType()を使うことで検出できます。

しかしこの情報でスタイラスを識別するには、当然ながらスタイラスが端末に「自分はスタイラスである」と情報を送信している必要があります。

スタイラスを識別する万能メソッドではないことは注意が必要でしょう。少なくとも端末とペアリングするタイプのスタイラスでないと、ダメだと思います。

試していませんが、Bluetoothのマウスを端末にペアリングして使うと、これでマウスのポインタが識別できるのかもしれません。

タッチイベントを確認するサンプル

ActivityであればonTouchEventをオーバーライドすればタッチイベントを受け取ることが出来ます。例えばこんなコードを利用することでタッチイベントを確認することが出来ます。

public class MainActivity extends AppCompatActivity {
    private TextView textView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        textView = (TextView) findViewById(R.id.text);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int action = event.getAction();
        int actionMask = event.getActionMasked();
        int actionIndex = event.getActionIndex();
        int count = event.getPointerCount();
        String text = "ACTION:" + action + " INDEX:" + actionIndex + "(id:" + event.getPointerId(actionIndex)
                + ") MASK:" + actionMask + "\n"
                + " pointer count:" + count + " x:" + event.getX() + " y:" + event.getY() + "\n";
        for (int i = 0; i < count; i++) {
            int id = event.getPointerId(i);
            text += " pointer index:" + i + " pointer id:" + id
                    + " x:" + event.getX(i)
                    + " y:" + event.getY(i)
                    + "\n";
        }
        textView.setText(text);
        return true;
    }
}

Android Testing Codelabやってみた

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

Android Testing CodelabはEspressoなどのテストツールの使い方を学べるサンプルです。

全篇Englishですが、大体雰囲気でわかるレベルだと思います。

このレッスンをやれば、アプリ開発におけるテストツールの使い方、

  • JUnitを使ったユニットテスト(Andoridの端末を必要としないテスト)
  • Espressoを使ったUIテスト(実機orエミュレータを必要とするテスト)

が学べます。

ちょっとお得だなと思ったのが、アプリがMVPパターンで作られていることです。テストツールの使い方の勉強のついでに、MVPパターンも学べるなんて一石二鳥だな、なんて思ったのでちょっとやってみました。

思っていたよりもざっくりとした解説なので、雰囲気をつかめるものくらいに考えるといいと思います。

それでもテストのやり方よく分かっていない私からすると、学びの多いレッスンでした。

パッケージを機能で分けるやり方もある

このサンプルではパッケージをレイヤーごとではなく機能ごとに分けてありました。(機能ごとというよりはActivityごとに近い分け方だと思いましたが)

私はこれまでずっと、ModelはModelパッケージに、というレイヤーごとにパッケージを分けていましたが、「機能ごとに分けた方が見やすくていいだろ」と書かれていて目からうろこでした。

ProductFlavorを使ったクラスの切り替え

テストのためにモック用のクラスに差し替えるやり方の解説があります。

XMLのリソースファイル(strings.xmlなど)は異なるFlavorで同一のリソース名が存在した場合、Flavorのものが優先されmainで定義したリソースは上書きされます。

一方でJavaのクラスだと挙動が異なり、同一名のクラスが存在するとエラーになります。そのため、全てのFlavor共通で利用するクラスだけをmainに配置し、切り替えが必要なクラスはFlavorのディレクトリに配置するという工夫が必要になります。

モックを利用したJUnitのテスト

Mockitoを使ったJUnit4のテストのやり方が勉強になりました。これは単純に私がMockitoの使い方がよく分かっていなかったからですが。

@Mockアノテーションに寄る初期化とか、ArgumentCaptorの使い方とか。

EspressoによるUIテスト

Espresso-IntentsによるIntentのモック方法、Espresso-Contribを使ったナビゲーションドロワーのUIテストなどが紹介されています。

Espressoテストの章になると、一部修正が必要な部分がありましたが、それもまた勉強になりました。(EditTextへ文字入力をエミュレートした後は、croseKeyboard()しないとエラーになるとか、上へボタンを参照する部分が英語以外の環境だとエラーになるとか)