はてなブックマークのホッテントリリーダーを作った

勉強がてらホッテントリリーダーを作ってみました。ソースコードはGitHubで公開しています。 アプリも公開中です。 自分の勉強のためというのが目的のアプリです。最初はDagger2に慣れるために適当に遊んでいたのですが(その名残が微妙に残っている)、それをちゃんとしたアプリに落とし込んだときに使いこなせるのかという不安がありました。そこでアプリとして動くものを作ろうと考え、じゃあいっそいろいろなライブラリを使いながら勉強しようと、このような形になりました。 とりあえずアプリとして動くところまではできたので、Google Playで公開してみました。アプリ名をもうちょっとひねろうかと思ったのですが、思いつかなかったのでそのままな名前をしております。 公開している部分にはまだ含まれていませんが、Dagger2でモジュールを差し替えて通信をモックしたり、テストコードを加えたりといい勉強になっています。そのあたりもそのうち公開できたらなと思っています。 Dagger以外にもRetrofitをはじめて使ってみたり、いい勉強になっている気がします。 テスト周りとかCIの勉強も出来たらなぁと考えています。

Realmのテストのやり方を知りたい

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.
Read full post gblog_arrow_right

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

私は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で使うときには気をつけないといけないんですねぇ。

Instrumentation Testで生成されるAPKは何をしているのだろう

以前、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とは一体何なのかがよく分からなくなってきました。

Android Studio2.2でProject Windowのパッケージ名表示が省略されない

タイトルが分かりにくいんですが、こちらの画像をご覧ください。 画像の例ではアプリのパッケージ名が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で直ったみたいです。

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

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にする仕組み

ものすごいあほうなことを書いているかもしれませんが、そのときはご指摘ください。 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が想定通りに描画されているかテストする

カスタム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に変更して、手動でディレクトリを作成します。(何か他にいい方法知ってれば教えてください)
Read full post gblog_arrow_right