FilteredHatebuにCIを導入してみました。
CI上での設定も必要なので、これだけで導入できるわけではありませんが、CI導入のための変更はこんな感じです。エミュレータが必要なEspressoでのテストがこけないよう、テストコードを修正した変更も含まれています。
そもそもこのアプリをpublicなリポジトリでやっているのは、実はCIを導入してみたかったからという理由がありました。
最初はとりあえずCIを導入してみようというだけで作業を始めたのですが、結局よく目にするTravisCI、CircleCI、Werckerの3つを試してみました。
CIを使うにあたっては、publicなリポジトリで運用する、無料でできる範囲でやる、あまりにややこしいことはやらない(できればそのCI単体で完結させる、ただしSlackへの通知は除く)という3点を念頭にやってみました。
各CIサービスを使ってみた感想 TravisCIは秘匿情報を比較的安全に運用しやすい CircleCIはテストレポートを確認しやすい Werckerは圧倒的にビルドが早い publicなリポジトリでの運用という観点で、秘匿情報(署名ファイルなど)の取扱、テストレポートの確認手段、ビルド時間の長さ、導入難易度について私の主観でまとめてみるとこんな感じです。
CIサービス 秘匿情報 テストレポート ビルド時間 導入難易度 TravisCI ○ × × ○ CircleCI × ○ × ○ Wercker △ △ ○ × 秘匿情報については後述。 テストレポートは、./gradlew connectedAndroidTestなどを実行した後に生成されるHTML形式のレポートを確認できるかということです。CircleCIはCIサービス上でHTMLファイルにアクセスすることが可能です。Werckerは直接見れませんがファイルをダウンロードできます。Travisはそもそも見れません。どこか外部のストレージサービスでも使って、ファイルをアップロードするしかないようです。
ビルド時間はWerckerの圧勝です。TravisCIもCircleCIもSDKのアップデートに時間を食われるため、一度のビルドに18分くらいかかります。Werckerビルド環境をDockerで構築してしまえるので、SDKのアップデートが発生しません。初回を除けばだいたい6分くらいで済んでいます。圧倒的な早さです。
導入難易度については、TravisCIとCircleCIはどちらもあまり大差はないと思います。ドキュメントも日本語情報も充実しています。設定もYAMLで記述するだけですから、CIサービスごとの方言はあるもののそうハマるものでもありません(CircleCIは微妙にドキュメントが現状に追いついていない部分があって惑わされたりもしましたけど)。
一方でWerckerはドキュメントが他2つと比較して充実しているわけではありません(他2つと比較すると分かりにくいと感じました)。さらにDockerの知識が必要にもなるので、導入までにかかった時間は一番長かったです。(逆にDockerの知識があって、Androidの環境を構築するのが簡単にできてしまう人であれば、Wercker使うのが一番ラクかもしれません)
3つのサービスを使ってみましたが、それぞれ一長一短で、このサービスが最強といえないもどかしさがありました。ただprivateリポジトリで使うなら、Werckerが一番いい気がします(早さと設定の自由度が魅力)。
秘匿情報の取扱 困ったのは、署名ファイル(release.keystore)をCIでどう扱うのかという問題です。
どのCIサービスでも同じですが、release.keystoreなどの秘匿情報をCIでも扱えるようにするためには、そのファイルをリポジトリに含めるか、もしくはインターネット経由でアクセスできるどこかに別途公開しておくかしかありません。
前者の方法はpublicなリポジトリでは使えません。privateリポジトリであれば問題ないのでしょうが、私のケースでは採用できませんでした。
かといって後者のどこか別の場所に置くという方法も、適切な場所が思いつきませんでした。誰でもアクセスできるような場所に置くことはできませんし、適切なアクセス制限がかけられる置き場所となると、選択肢はそう多くはないと思います。
どのCIサービスでも、privateな情報についてはCI側で環境変数を利用することができます。publicなリポジトリで運用しても、パスワードなどが見えないように配慮することができます。TravisとWerckerは環境変数にセキュアな項目にする設定があるので、その点いくらか安心です。ただしCircleCIは、publicなプロジェクトではパスワードとか漏れたら困る情報を環境変数に入れるなと注意書きがありました。
しかし環境変数で扱えるのは文字列です。署名に使うrelease.keystoreというバイナリファイルを環境変数で扱うことはできません。
私が試した3つのサービスで、唯一秘匿情報ファイルの取扱がCIサービス単体で解決できるのはTravisだけです。解決できると言っても、暗号化してリポジトリに含めるという方法ですけれど。
他のサービスでは別途ストレージサービスを利用するなどして、そこからファイルをとってくるという手法を使わなければなりません。
エミュレータを使うテストは鬼門 今回CIを導入したプロジェクトでは、Espressoを使ったテストを行っています。手元の実機では安定していても、CIで実行すると失敗ばかりで非常に困りました。
Espressoテストレコーダーは万能ではない ローカルの実機だと問題ないのにCI上だとエラーが起こりました。例えばこんなコード。
- ViewInteraction appCompatTextView = onView( - allOf(withId(android.R.id.text1), withText("test.com/"), - childAtPosition( - allOf(withClassName( - is("com.android.internal.app.AlertController$RecycleListView")), - withParent(withClassName(is("android.widget.FrameLayout")))), - 0), - isDisplayed())); - appCompatTextView.perform(click()); + onData(anything()) + .atPosition(0) + .perform(click()); onData~にコードを書き換えるとCI上でも問題なく動くようになりました。Espressoテストレコーダーは便利ですが、こればかりに頼る訳にはいかないという教訓です。
DataBindingを使っていると起こるエラー java.lang.IllegalAccessError: Class ref in pre-verified class resolved to unexpected implementation at android.databinding.DataBindingUtil.<clinit>(DataBindingUtil.java:31)という謎のエラーが発生しました。
最初に導入していたTravisでぶち当たった問題で、Android Studioでテストを実行する分には問題ないのに、TravisとWerckerではコケました。Werckerは利用するDockerイメージによるものだとは思います。CircleCIは特に問題になりませんでした。
https://code.google.com/p/android/issues/detail?id=182715
どういうエラーなんだかよく把握していないのですが、DataBindingを使っていて、かつテスト対象のActivityでDataBindingを使っていると発生するようです。
#31に回避策が書いてあります。
エミュレータの起動を待つ処理 CIでエミュレータを使ったテストを行うためには、エミュレータが起動するのを待たなければなりません。TravisCIにもCircleCIにも、エミュレータの起動を待つためのスクリプトが用意されています。Werckerを使う場合はそんな便利なコマンドは用意されていないので、自分でシェルスクリプトを書く必要があります。
しかしこれがまあ安定しない。
まずはじめに。Dagger2の話をしていますが、きちんと理解しているわけではないので間違った内容があるかもしれません。鵜呑みにしないでください。
この記事で言ってることのサンプルコードはGutHubで公開しています。
この記事の要旨は「MainActivityの中でシングルトンを実現したい(した)」ということです。
Dagger2を知ったキッカケ 私がDagger2を知ったきっかけはdroidkaigi2016です。その時からずっと腑に落ちなかったのがActivityScopeの存在です。ActivityScopeがわからないというか、子コンポーネントをわざわざ作る意義がわからなかったのです。
droidkaigi2016のコードを見よう見まねでDagger2を使ったアプリを作ったのですが、そのアプリではほぼすべての依存性をAppModuleに定義してありました。そのためAppModuleだけがやたらと肥大化し、ActivityModuleには何も定義されていないような状態で、ActivityScopeを作っている意味がまったくありませんでした。
結果そのアプリでは、Dagger2を依存性の充足のために用いるのではなく、シングルトンパターンを使うことなくアプリ内でインスタンスが1つになるようにするための道具として使っている状態でした。
ActivityScopeでやりたかったこと 私は、例えば端末の画面が回転しても、同じMainActivityであれば常に同じコントローラなりPresenterなりがセットされるようにしたいと思っていました。そうすれば非同期処理を引き継ぐためにアレコレする煩雑さから解放されます。
それを実現するために子コンポーネントを区切ってActivityScopeを作ってるんだろうと考えていたのですが、実際の挙動はそうはなりません。ActivityModuleで@ActivityScopeなんて指定したところで、画面回転したら注入されるのは異なるインスタンスです。
(これはActivityComponentをActivityのonCreateで初期化して、Activityがそのインスタンスを保持していることに原因がありましたが、詳細は後述)
そもそもActivityのライフサイクルは非常に短命で、初心者がまず躓くポイントとして挙げられるほどに感覚値とずれたものです。画面回転しただけでインスタンスが変わる。同じMainActivityなのに。同じMainActivityが表示されてるのに、実は内部では異なるインスタンスのものなんですというのがややこしいポイントです。
私はずっとActivityScopeを使えば、同じMainActivityなら常に同じインスタンスを注入できるようになるんじゃないかなと思っていました。でもそれができない。それが私の「Dagger2よく分からん」の原因の1つでした。
Dagger2がインスタンスをシングルトンのように扱うことが出来る仕組み そもそもDagger2でインスタンスを使いまわせるのは、スコープをアノテーションで指定しているからではありません。ApplicationModuleで@Singletonを指定したインスタンスが常に同一であるのは、アプリ内で同じApplicationComponentを参照しているからできていることです。
ApplicationComponentはApplicationクラスを拡張した独自のクラスに保持して、そこにアクセスしていると思います。例えばこれを、(普通そんなことはしませんが)ActivityのonCreateでApplicationComponentを生成するようにしたらどうなるでしょう。画面回転によるActivityの再生成が起こる度に@Singletonとしたインスタンスであろうが毎回異なるインスタンスが注入されることになります。つまり@Singletonをつけてるからシングルトンになるわけではないということです。
ActivityのonCreateでApplicationComponentを生成した場合、同じActivityのインスタンス内ではシングルトンにできます。例えばそのActivityでViewPagerを使っていて、Fragmentを複数内包しているとしましょう。そのFragmentたちはActivityのもつApplicationComponent(ややこしい)にアクセスすることで、@Singletonで指定したインスタンスを使いまわすことが出来ます。
ApplicationクラスでActivityComponentのインスタンスを保持 同じActivityクラスでActivityComponentを使いまわせるようにするためには、Activityより長いライフサイクルを持つものにComponentのインスタンスを保持してもらう他ありません。
私はとりあえずApplicationクラスにActivityクラス名をキーとしたHashMapを持たせて管理させるようにしました。CustomApplication.classのソースコード
こうすることで、例えばFilterEditActivity.classでは、画面回転でActivityのインスタンスが変わろうと、常に同じActivityComponentを取得でき、FilterEditAcitivy内で常に同じPresenterが使えるようになります。
デメリット ほんとうの意味でのActivityのライフサイクルと異なるわけなので、逆にわかりづらくなっている気がしないでもありません。ActivityScopeといいながらその生存期間はApplicationと同じになってしまっています。
Fragmentを使う場合に更に混乱します。実際にサンプルのプロジェクトでは、Activity+ViewPager+Fragmentを使う部分でややこしいことになっています。
Activityの場合はActivityクラスで識別すればいいのですが、ViewPager内のFragmentはクラス名で識別することが出来ません(同じクラスでも中身が異なるため)。
またActivityContextを注入したい場合に困ります。まず間違っても@ActivityScopeで定義してはいけません。それをやると一番最初に生成されたActivityのインスタンスが使いまわされることになってしまいます。ただスコープをつけなくても、Componentが参照しているActivityContextは最初に作成されたActivityのインスタンスとなってしまうので、スコープをつけないだけでも足りません。
このサンプルプロジェクトでは、苦肉の策としてApplicationクラスにActivityModuleのインスタンスも一緒に管理させるようにしています。ActivityModuleがもつContext(Activity)を更新するためです。
しかしそうやったところで、ActivityScopeで使いまわしたい何らかのインスタンスに、ActivityContextを持たせなければならない場合はどうしようもありません。サンプルプロジェクトでは幸いActivityContextに依存するものがないのでなんとかなっていますが、将来的には不明です。
変な依存を産んでしまっている気がしないでもない 依存性をなくすためのDagger2で、逆に変な依存を産んでしまっているような気がしないでもありません。
ただ、個人的にActivityScopeに持っていたモヤモヤが晴れたことと、Activityクラスごとにシングルトンというのが実現できてよかったと思っています。
ここまで書いておきながら言うのもなんですが、Activityクラスごとにシングルトンにするということは、Application内でシングルトンということと考えて、素直にApplicationScopeで定義したほうがいいのかもしれません。実際このサンプルプロジェクトでも、やっぱりActivityComponentの存在意義があまりないように思います(ApplicationComponentだけあれば事足りるような状態)。
Moduleの肥大化に対しても、役割ごとにモジュールを分けるという方法で対策しているので、ActivityComponent自体をなくしてしまったほうがスッキリするような気もしています。
やっぱりActivityComponentを分ける意義が分かってないです。何かいいことがあるから分けてるんですよね・・・?
英語の記事ですがこちらの記事も参考になるかもしれません。たぶん同じようなことができて、かつスマートな実装なんだと思います。私にはややこしくてよく理解できないので、もうちょっとDagger2の経験値積んでから挑戦しようと思っております。
https://frogermcs.github.io/activities-multibinding-in-dagger-2/
勉強がてらホッテントリリーダーを作ってみました。ソースコードはGitHubで公開しています。
アプリも公開中です。
自分の勉強のためというのが目的のアプリです。最初はDagger2に慣れるために適当に遊んでいたのですが(その名残が微妙に残っている)、それをちゃんとしたアプリに落とし込んだときに使いこなせるのかという不安がありました。そこでアプリとして動くものを作ろうと考え、じゃあいっそいろいろなライブラリを使いながら勉強しようと、このような形になりました。
とりあえずアプリとして動くところまではできたので、Google Playで公開してみました。アプリ名をもうちょっとひねろうかと思ったのですが、思いつかなかったのでそのままな名前をしております。
公開している部分にはまだ含まれていませんが、Dagger2でモジュールを差し替えて通信をモックしたり、テストコードを加えたりといい勉強になっています。そのあたりもそのうち公開できたらなと思っています。
Dagger以外にもRetrofitをはじめて使ってみたり、いい勉強になっている気がします。
テスト周りとかCIの勉強も出来たらなぁと考えています。
Realmを使ってみました。ちなみに私は、今まではGreenDAOとAndroid Ormaしか使ったことがありません。
とりあえずCRUD操作のやり方をつかもうとテストを書いてみました。テストの書き方が根本的に間違っている可能性が無きにしもあらずですが、こんな感じで作りました。
public class FilterDataSourceRealmTest { private static RealmConfiguration config; private static FilterDataSourceRealm sut; @BeforeClass public static void initializeTest() { config = new RealmConfiguration.Builder() .name("test_realm") .deleteRealmIfMigrationNeeded() .build(); sut = new FilterDataSourceRealm(config); } @Before public void setUp() { Realm.deleteRealm(config); } @After public void tearDown() { Realm.deleteRealm(config); } @Test public void insertFilter() throws Exception { final CountDownLatch latch = new CountDownLatch(1); sut.insertFilter("test.com/"); sut.getFilter("test.com/") .subscribe(new Action1<UriFilter>() { @Override public void call(UriFilter uriFilter) { assertThat(uriFilter.getFilter(), is("test.com/")); latch.countDown(); } }); latch.await(2, TimeUnit.SECONDS); } } テスト対象のコード(一部抜粋)はこんな感じです。
public class FilterDataSourceRealm implements FilterDataSource { private RealmConfiguration config; public FilterDataSourceRealm(RealmConfiguration config) { this.config = config; } @Override public void insertFilter(String insert) { Realm realm = Realm.
私は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で使うときには気をつけないといけないんですねぇ。
以前、Viewの描画をテストするためのリポジトリを作りました。記事はこれです。
Viewが想定通り描画されているか確認するため、Spoonを使ってスクリーンショットを撮るようにしました。GitHubにあげたコードでは、TextViewの周りに枠線を描画するCustomViewを作成し、その枠線が描画されるかを確認するというものでした。
しかしSpoonで撮ったスクリーンショットでは、右側と下側に描画されるはずの線が表示されていません。実機で動かすと描画されているのですが、スクリーンショット上では見えない。
Spoonのバグなんじゃないかななんて最初は思っていたのですが、調べてみると原因は違うところにありました。いえ、Spoonのせいではないということはわかったのですが、じゃあなぜそうなるのかというところが分からないので困っている状態です。
Spoonのスクリーンショットで線が描画されない理由は、右と下の線が画面外に描画されてしまっているからです。
CustomViewは右側・下側に描画する位置を、onDrawメソッドの引数で渡ってくるCanvasのサイズ(canvas.getWidth()とcanvas.getHeight())を使って描画しています。
実機で実行した場合、ここに渡ってくるCanvasのサイズは、CustomViewと同じサイズになっているようなので、TextViewの周りに枠線が描画されます。
一方で、androidTestで実行した場合、このcanvas.getWidth()で得られる数値は、想定したものよりはるかに大きい数値になります。数値の大きさから察するに、画面全体と同じ大きさになっているような気がします。
実機で実行した場合:
10-05 17:35:09.972 1992-1992/jp.gcreate.sample.viewdrawingtest.uiTest D/test: canvas:android.view.GLES20RecordingCanvas@30073153, height:96, width:983 10-05 17:35:15.412 1992-1992/jp.gcreate.sample.viewdrawingtest.uiTest D/test: canvas:android.view.GLES20RecordingCanvas@2a498faf, height:96, width:983 androidTestで実行した場合:
10-05 17:37:37.955 3888-3888/jp.gcreate.sample.viewdrawingtest.uiTest D/test: canvas:android.view.GLES20RecordingCanvas@375e49fb, height:1436, width:983 10-05 17:37:37.982 3888-3888/jp.gcreate.sample.viewdrawingtest.uiTest D/test: canvas:android.graphics.Canvas@1cad5ead, height:1919, width:1079 androidTestで実行すると、渡ってくるCanvasが実機の場合と異なるようです。
onDrawメソッドで渡されるCanvasとは一体何なのかという点についても、私はよく分かっていないのですが、androidTestで実行されるtest用のAPK(この場合app-UiTest-debug-androidTest.apk)が何をやっているのかもよく分からなくなってきました。
androidTestを実行すると、実機上に画面が表示され、テストコードに書いた動きが実行されていくので、それは全てtest用のAPKで実行されているのだとばかり思っていました。しかしそう考えると、実機で表示されている画面では枠線が描画されているのに、Spoonで撮影したスクリーンショットには映っていないことの理由が説明できません。
そんなことを考えていると、Instrumentation Testとは一体何なのかがよく分からなくなってきました。
タイトルが分かりにくいんですが、こちらの画像をご覧ください。
画像の例ではアプリのパッケージ名がjp.gcrete.sample.daggersandboxで、そこからさらにapiとかdiとかのパッケージに分化してます。
Android Studio2.2にしてから、なぜかそのサブパッケージの部分が単にapiではなく、jp.gcreate.sample.daggersandbox.apiと省略されずに表示されていました。
Layout EditorのようにAndroid Studioの設定でそうなっているのかとも思いましたが、該当するような設定項目はありませんでした。
なんでだろうなと思って探してみたところ、issueが立ってました。
https://code.google.com/p/android/issues/detail?id=223389
https://code.google.com/p/android/issues/detail?id=222914
どうもDataBindingを有効にすると発生するそうです。実際、この画像のプロジェクトでもDataBindingを使っており、これをfalseに変更すると普段通りの表示になりました。
DataBindingを使っている人のみ影響を受けるみたいです。
最近はfindViewByIdを使わなくて済むからくらいの軽い理由で、DataBindingを多用しているので早く直ってほしいです。
まあProjectウィンドウが見づらくてなんか気持ち悪いってだけなんですけどね。
Android Studio 2.2.1で直ったみたいです。
layout.xmlを開いた際に、デフォルトではDesignタブで開かれると思います。これをTextに変更する方法です。
Android Studio 2.1まではレイアウトのPreview画面に歯車アイコンがあって、Prefer XML EditorにチェックをつければOKでしたが、Android Studio 2.2のPreview画面にはそのようなものが見当たりません。
Android Studio 2.2からは、設定画面から変更するようです。
Preference > Editor > Layout Editorで設定できます。
ものすごいあほうなことを書いているかもしれませんが、そのときはご指摘ください。
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を作って、しかもそれが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で外部から操作してただけなんですね。
https://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に変更して、手動でディレクトリを作成します。(何か他にいい方法知ってれば教えてください)
Page 8 of 19