カテゴリー: Android

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

KotlinでUnitテストでassertionに何を使うか

KotlinでUnitテストを書く際に、assertionに何を使うかという話。

私の場合特にこだわりがあるわけではないのだが、基本的には多数派に従いたいという気持ちが強い。しかしながら、Kotlinのテストのassertionに使うならこれ一択、みたいなものがない(と思っている)ので、いつも困る。

Javaなら特に何もせずともhamcrestのassertThat(sut.getHoge()).is("fuga")みたいなassertionを使っていればいいのだが、Kotlinの場合はisが予約語であるという問題がある。いちいちisをバッククォートでくくらなければならず美しくない。

knit

https://github.com/ntaro/knit

日本ではいちばん有名なやつだと思われる(DroidKaigiのアプリでも使われていたはずなので)。

sut.getHoge().should be "fuga"みたいな感じで使う。

ただrepositoryを追加しないと組み込めないので私はあまり使っていない。build.gradleに一行追加するだけの話なんだけどね・・・。

expekt

https://github.com/winterbe/expekt

knitに似たsut.getHoge().should.equals("fuga")みたいな感じのAPI。リポジトリの追加が必要ないので、私はこっちをよく使っている。

ただメンテされてないのが玉に瑕。

kotlintest

https://github.com/kotlintest/kotlintest

assertionライブラリではなくて、Spec風のテストを書くためのライブラリ。

ただsut.getHoge() shouldBe "fuga"みたいなassertionが含まれている。

SpekというSpec風のテストを書くためのライブラリがあるが、こちらはJava8でなければ使えないという制約があって、Androidのプロジェクトに適用するのはなんだか難しそうで手を出していない。

kotlintestはネストしたテストを書きやすくて、これいいかもとか思ったこともあったのだけれど、androidTestに組み込むとメソッドカウントがオーバーしてしまうので使えない。

assertk

https://github.com/willowtreeapps/assertk

Android Weeklyで流れてきて知った。AssertJチックなassertionライブラリで、Kotlinで使うならコレのが便利なのだろうか。

assert(sut.getHoge()).isEqualTo("fuga")みたいな感じで使う。

しかし個々最近はshould系ばかり使ってきているので、最初にassertを書かないといけない形式はめんどくさいと感じてしまっている。

AssertJ

http://joel-costigliola.github.io/assertj/

Javaのコードも考慮に入れるならJavaで使えるライブラリを使うのがいいのだろう。

私はJavaだとhamcrest使っとけばいいやな人だったので、Javaでassersionライブラリを何使うかなんて特に気にしたことはなかった。

みんなが使っているものが知りたい

みんなはどれを使っているのだろうか。他にこれが使いやすいみたいなのがあったら教えて欲しい。

私はexpektを使う傾向が強いが、揺らいでいる。

正直なところassertionさえできればなんでもいいのだろうからして、自分が使いやすいものを使えばいいってだけの話なのだろうけれども。

https://discuss.kotlinlang.org/t/what-assertions-library-do-you-use/1809

ここをみると、kotlintestが多いっぽい。

Instant-appを試してみた

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

Instant-Appを試してみた。FlexibleTimerというアプリを作って、Instant-Appに対応させてみたのである。

https://play.google.com/store/apps/details?id=jp.gcreate.product.flexibletimer

Instant-Appというのは、アプリをインストールすることなく使えるようにする仕組みで、今回作ったアプリはhttps://app.gcreate.jp/flexibletimer/にアクセスすれば試すことができる。(Androidからアクセスすれば実行できるはず)

Instant-Appを実行できる環境

今のところAndroid6.0以上の端末であれば動くらしい。

Android6.0(API23)以上の端末であれば、おそらく設定 > Googleの中にInstant Appsという項目があると思う。それを有効にすればInstant-Appが実行できる。将来的にはAPI21以上もサポートされるらしいが、今は23以上が要件。

1つ注意点があって、Instant-Appはどうもどれか1つのアカウントでしか有効にできないみたい。私は2つのアカウントを1つの端末で使っていて、片方のアカウントでInstant-Appを有効にすると、もう片方は無効になってしまう。

このことで何を注意しなければならないかというと、Chromeにログインしているアカウントと、Instant-Appを有効にするアカウントは揃えておけということである。揃えていないとChromeからInstant-Appを提供しているURLにアクセスしても、Instant-Appが実行されないからである。揃えていなくても、例えばSlackで共有されたURLを開けばInstant-Appが実行されるし、Google Playからサイトに移動しても起動するので、Instant-App自体が使えないわけではない。

用意したURL(これはアプリを作成する際にAndroidManifest.xmlに定義する)にアクセスすれば、Instant-App用のAPKをGoogle Playからロードしてきて、インストールすることなくアプリが実行される。

URLは実際にアクセス可能でなければならない。つまり、Webサイトを所有していない場合には使えないということになる。所有していない場合はFirebase Hostingを利用してねというQAがある。(Stackoverflow

Instant-Appへの対応の仕方

今のところAndroid Studio3.0を使わなければInstant-App対応は不可能である。理由としては、3.0でなければInstant-Appを作成するために使うcom.android.featureプラグインが認識できないからだ。

まだ3.0は正式バージョンではないので、急いで対応しないとならないというものではないと思う。

既存のアプリをInstant-Appへ対応させるのは、そんなに難しい手順が必要なわけではない。たぶん、3.0でビルドできなくなったとか、gradleプラグインが対応してなくて動かないとか、そういった種類のトラブルに対応するほうがはるかに大変なだけだと思う。

細かいやり方はドキュメントを見てもらえばそんなに難しくはないと思う。

ちなみにcodelabで既存のアプリをInstant-App対応させる方法が一通り学べる。

はまったポイント

foreground serviceが動かない

Instant-AppではLong-running background serviceはサポートされていないが、foreground serviceは使うことができる。ことになっているが、現時点ではまだ対応されていないらしい。

Stackoverflow

Architecture Componentが動かない

私の場合LiveDataを使おうとしていたのだが、Instant-Appでは使えなかった。エラーは起きないが、LiveDataの変更が受け取れなかった。

https://issuetracker.google.com/issues/38493434

Content Providerの仕組みで初期化してるから、Content Providerが使えないInstant-Appでは動かないのではないのかということらしい。

しょうがないので、RxJava2のBehaviorSubjectに置き換えて対応した。

App Linksの設定

私はApp Linksにこれまで対応したことがなかったので、Android StudioのApp Links Assistant任せでやっていたのだが、App Linksについて、仕組みとか対応の仕方とかをちゃんと調べてからやったほうがハマりが少ないのではないかと思う。

特にAndroidManifest.xmlの記述に関してはちゃんとドキュメントに書いてあるので、App Links Assistantに任せるのではなく、ドキュメントを見ながら設定するように。ビルドすること自体は問題なくできるが、Google PlayにAPKをアップロードしたときにはじめて弾かれるので、設定ミスに気づくのが遅くなってしまった。

https://developer.android.com/topic/instant-apps/prepare.html#default-url

私の場合、1つのintent-filterにhttpsとhttpの両方を設定すること、<meta-data>タグでdefault-urlの設定をすることが漏れていて、無駄な時間を過ごしてしまった。

サイト側の設定

私の場合はhttps://app.gcreate.jp/flexibletimer/というURLの、Webサイト側の設定をどうするかという話である。

必須なのはhttps対応することと、有効なドメインを持つこと、後は該当サイトにアクセスできるようにWebサーバを用意することだ。

Instant-AppのためにはApp Linksの対応が必要なので、https://developer.android.com/training/app-links/index.htmlを確認しながら設定すると良い。

Instant-AppのAPKはGoogle Playに置くだけであり、サイト側ではドメイン直下に.well-known/assetlinks.jsonファイルを配置するだけである。

ただこのApp Linksの設定は、Instant-Appの実行に関してはあまり意味は無いのだと思う。これはあくまでApp Linksの設定だから、インストールしたアプリがある状態で、指定したURLにアクセスしたときにアプリが起動するようにするための設定だと思うので。

しかしそうすると、なぜ指定したURLにアクセスするとInstant-App用のAPKがGoogle Playから読み込まれるのかがよくわからない。

仕組みがよくわからないので、どう設定したらどう動くのかが知りたくて、今回試してみたわけである。

とりあえず、用意したURLにアクセスすればGoogle PlayにアップロードしたInstant-AppのAPKが読み込まれてアプリが実行されるという仕組みのようで、Webサイト側は特別な設定は必要なかった(App Links用のjsonファイルを用意するくらい)。

現状ではcanaryバージョンのAndroid Studio3.0を使うので、Instant-Appがどうのというより、Android Studioのバグに振り回されることのほうが多かった気がする。

Instant-Appは利用者同士がコンテンツを共有するようなタイプのアプリの場合、特に効果的だと思う。むしろ今回私が作ったような、アプリ単体で完結してしまうようなアプリの場合、Instant-Appである意味はあんまりないと思った。アプリ内でやり取りされるコンテンツを、アプリをインストールしていないユーザに対して簡単にアクセスできるようにするのがInstant-Appの強みだと思う。

Architecture Componentを触ってみた

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

とりあえず軽く触ってみた。

codelab

Google IOの動画はとりあえず2つ見てみた。

最初のやつは「こんなん作ったでー」という話で、2つ目がその中身を説明っていう感じなので、時間がないなら2つ目だけ見ればいいんじゃないかなと思う。全部英語なので、私の英語力では雰囲気しかわからず時間対効果はあまり良くなかった気がしている。

3つ目もあるのだけど、これはまだ見れていない。タイトルから見るとRoomについての説明なのだろうか。

個人的にArchitecture Componentで気になっていたのは、いかにActivityのライフサイクルに振り回されなくてすむようにできるかという部分だ。つまり、LifecycleOwnerLiveDataViewModelについてである。とりあえずその観点で言うと、最初にあげたcodelabを触れば、雰囲気はわかった。2つ目の動画でだいたいスタンスがわかったような気がしている。

たぶん2つ目の動画で話していたと思うのだけど、このArchitecture Componentはすでに個々の開発者がそれぞれの工夫でActivityなどのライフサイクルによる呪縛を回避している手段を置き換えるためのものではないという話が、個人的にはしっくり来た。すでにライフサイクルとの付き合い方がうまくできている人は、別にそれでいいと。

ただ、Androidをこれから学ぼうとする人にとっては、Android特有のライフサイクルにまつわるあれこれは、学習していく上でつまづきやすいポイントで、さらにそれを回避するためのライブラリの使い方を学ぼうとすると余計にややこしくなってしまう。そこで、これからAndroidを学んでいく人にとって、とっつきやすいシンプルな仕組みを用意したよ、というのがArchitecture Componentということらしい。

私は最近ではDaggerやRxJavaを使って、Activityにはデータを持たせない、単に表示するだけのものとして扱うようにしてアプリを作るようにしている。そういった方法ですでにうまいこと回せているなら、無理して移行する必要はないのだろう。すでにうまいことやっている人にとっては、ちょっと触れば雰囲気がつかめるだろうから、とりあえずcodelabだけ触って雰囲気を掴んでおいて、1.0が出るのを待つくらいのスタンスでいいんじゃないかなと思う。

以下は触ってみた感想。

LifecycleOwnerとは

ざっくりした理解で言うと、ActivityとかFragmentとかServiceとか、Android特有のライフサイクルをもってるオブジェクトのことという認識でいる。

LiveDataの購読を行う際に引数に指定してやったり、ViewModelの生成・取得を行う際に引数に渡したりするのに出てくる。

LiveDataの購読に関しては、LifecycleOwner(Activityとか)のライフサイクルにあわせて自動的に購読解除してくれるらしい。つまり、いちいち自分でunsubscribe/disposeとかしたりしなくていいっていうこと。

またActivity等のライフサイクルにあわせた処理を行うのに利用したりできるっぽい。(codelabではLocationManagerから位置情報を受け取るクラスを作って、Activityのライフサイクルにあわせてセンサーの登録と解除を行うのに使っていた)

LiveData

UIに表示したりする実際のデータ。DataBindingでいうObservable<Hoge>みたいなものだし、RxJavaでいうObservable<Hoge>みたいなもの。

その実態は最新の値を保持してよしなに通知してくれるデータホルダー。RxJavaみたいなストリームではないよということだ。またスレッドの概念も持ってないので、バックグラウンドで処理してメインスレッドで通知みたいなことはしない。

LiveDataの更新はメインスレッドでないとできないみたいで、別スレッドからLiveData.setValue()したら落ちた。別スレッドから値を更新したい場合は、postValue()を使うらしい。

ActivityなどのViewは、このLiveDataを購読して、変更を受け取ったらUIを更新することだけ考えるような作りにするのが良いのだろう。

RxJavaでいうBehaviorSubjectみたいな動きをするなぁという印象を持った。

ViewModel

画面回転してもライフサイクルが継続してくれるもの。これが最初からあればAndroidアプリ開発はもっと楽になっていただろうと思う。

今までActivityにもたせていた状態やロジックを、全部こっちに持ってくればうまいことできると思う。

Activity.finish()を呼び出したらViewModelのライフサイクルも終了する(ViewModel::onCleard()が呼び出される)。

画面を回転させた場合はそのまま以前の状態を引き継いだものが、再生成されたActivityに渡ってくる。ただ、あくまでonConfigurationChangeで破棄されないだけなので、ホーム画面に一度移動した後にOSによって終了されたりした場合(Activityを保持しないが有効になってたりした場合)はViewModelのライフサイクルも終了してしまう。

データの永続化は別途考えないといけない。

軽く触ってみて

RxJavaなどの知識を持っているからか、割りとすんなり使えそうな気がしている。

それを抜きにしてもそんなにややこしくないと思うので、Androidをこれから学ぼうという人はとりあえずArchitecture Componentの使い方を学ぶと、シュッと入門できていいんじゃないだろうか。

触る前は「なんかいろいろあってややこしそうだな」とか思っていたのだが、触ってみると思いの外それぞれ独立していて、使いたいコンポーネントだけ利用すればいいという意味がよくわかった。

私個人としては、ViewModelはすぐにでも使いたい。Daggerを使って似たようなことをやっていたけれども、ViewModelを使うほうが楽だ。LiveDataはもうちょっと調べて、DataBindingとの組み合わせ方を掴んだら置き換えるかもしれない。

スワイプで削除できるRecyclerViewを実装するときの悩み

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

RecyclerViewを使うときに必ず実装するであろうRecyclerAdapter。List<Hoge>をRecyclerViewに表示するのに使う。

単にリストを表示するだけならあまり迷わないのだが、プラスアルファの処理を行う必要が出てきたときに私はよく悩む。例えば、現在進行形でもにょっているのが、リストのアイテムををスワイプしたらそのアイテムを削除したいというケース。とりあえず実装して動いてはいるのだが、削除に関するコントロール処理をどこに書くのが適切なのだろうかという疑問に対する明快な解を持ち合わせていない。

最近触っていないけど、FilteredHatebuというアプリでは削除に関する処理をPresenterに担わせた。Adapterは単にList<Hoge>とRecyclerViewの橋渡しをするだけというシンプルな作りだ。

一方で、現在作っているアプリではAdapterで削除に関する処理を行っている。この2つの違いがなぜ生まれたかというと、AdapterがList<Hoge>を持っているかどうかという問題に行き着く気がする。

RecyclerViewやListViewを使うとき、ネットで見かけるコードではAdapterにList<Hoge>を持たせるものをよく見かける。コンストラクタを使って渡すなり、セッターを使うなりして、AdapterにList<Hoge>をセットしてやる手法だ。単に表示するだけならこれで問題はないのだが、削除に関する処理を行おうとすると混乱し始める。

削除処理はList<Hoge>のアイテムを削除する処理を内包する。RecyclerViewの2番めのアイテムがスワイプされたら、List<Hoge>の2番めのHogeを削除しないといけない。ではその削除を実行するのは、AdapterなのかそれともActivityなのか、それとももっと他のもの(例えばPresenter)なのかがよくわからない。

List<Hoge>の操作が必要なのだから、List<Hoge>を管理しているものが削除すれば良い。となったときに、AdapterがList<Hoge>を持っていることが多いので、そのままAdapterに削除処理を実装することが多いのである。

削除可能なRecyclerViewの実装について、ベストプラクティスが知りたい。そして知りたいと思ったときに、ふと「そもそもAdapterにList<Hoge>を持たせるのはどうなんだろうか」と疑問に感じたのである。

私の理解では、AdapterはList<Hoge>とRecyclerViewの橋渡しをするもの、つまりHogeクラスを表示するためのViewに変換するのがその責務という認識だ。その認識からすると、AdapterにList<Hoge>をもたせて削除に関する処理が加わっている今作っているAdapterは、AdapterではなくてControllerになってる気がする。

そんなことを考えていると、そもそもRecyclerViewでアイテムをスワイプして削除させるのが間違っているのではないかという気分にもなってくる。別にAdapterにどれだけの責務をもたせるかは、開発者のさじ加減であって、個人の好きなようにしたらいいのかもしれない。

そんな堂々巡りのはて、まあ動けばいいかという結論に落ち着く。削除可能なRecyclerViewの実装、みんなはどうやっているのだろう。

Firebase Crashを使ってみた

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

Firebase Crash Reportingを使ってみた。今まではCrashlyticsを使っていたのだが、最近はFirebaseをアプリに組み込むことが多いので、クラッシュレポートもFirebaseでやってみようかなというのがことの始まり。

導入手順的に考えると、Firebase Crash Reportingはとても簡単。Firebaseを使うプロジェクトであれば、dependenciesに'com.google.firebase:firebase-crash:<VERSION>'を追加するだけで終わり。これだけでアプリがクラッシュしたら勝手にレポートをあげてくれる。

Firebaseを使う設定に関しても、Android Studioに組み込まれているFirebaseのツール(?)を使えばいとも簡単に使えるようになるので、導入の敷居はCrashlyticsに比べるととても楽である。

一方で、Crashlyticsと比較すると面倒くさいポイントもいくつかあって、単純に乗り換えればいいやという話でもなさそうなのが悩ましい。

mapping.txtのアップロード

ProGuardをかける場合に難読化されたスタックトレースを解読するため、mapping.txtのアップロードが必要になる。Crashlyticsの場合、設定が必要だが自動的にアップロードを行ってくれる。

一方でFirebase Crash Reportingは自分でFirebase Consoleにアップロードしなければならない。gradleタスクでアップロードするための方法が用意されてはいるが、Crashlyticsと比較すると「自動アップロード」とはいえない。初期導入が簡単な反面、ProGuardのmapping.txtをアップロードする設定を行う手間がある。

https://firebase.google.com/docs/crash/android

  1. ルートのbuild.gradleのclasspassに'com.google.firebase:firebase-plugins:1.0.5'を追加
  2. app/build.gradleにapply plugin: 'com.google.firebase.firebase-crash'を追加
  3. Firebase Consoleからプロジェクトの設定→サービスアカウント→クラッシュレポートから、新しい秘密鍵の生成を行いダウンロードする(jsonファイル)
  4. ダウンロードしたファイルへのパスをFirebaseServiceAccountFilePathというプロパティに記述する1
  5. ./gradlew :app:firebaseUploadReleaseProguardMappingを実行してアップロード(buildVariantなどによってタスク名は変わる)

firebaseUploadXXXというタスクを実行しないといけないので、そのままだと確実に忘れそう。assembleReleaseを実行したらこのタスクも実行するように指定できたらなぁと思ったのだけど、やり方がわからなかった。

そして依存させるなら、assembleReleaseよりもapkをGoogle Playにアップロードするタスク(自動化しているなら)に依存させるのが良さそうではある。

debugビルドでアップロードしてほしくない問題

Firebase Crashは特に何もしなくとも、アプリがクラッシュすればスタックトレースをアップロードしてくれる。カスタムApplicationクラスに初期化処理を書いて・・・なんてことすら必要ない。ContentProviderの初期化の仕組みを使ってライブラリ側で勝手に初期化しているとかなんとか見た気がする。ある意味便利ではあるが、一方で不便なところもある。それは、クラッシュレポートを送信させない手段が存在していないところである(たぶんない)。

CrashlyticsはカスタムApplicationで初期化をする必要があり、ここで例えばデバッグビルド中は送信しないようにしたり設定できる。Firebase Crashにはそういうのはないっぽい。そもそも自分で初期化しないし、送信を停止するようなメソッドも見当たらない。

これはFirebase CrashをreleaseCompileで組み込めば一応回避は可能である。

一方で、プライバシーポリシーの問題というか、ユーザの許可を得ずにクラッシュ情報を収集してよいのかという問題があると思う。このあたりの法的問題に、他の開発者さんはどう対処しているのか私は知らないが、個人を特定する情報は含まれていないとしても、例えばユーザにクラッシュレポートを送信しないような選択肢を提供したいときに、Firebase Crashではそれができないということになる。クラッシュレポートについてオプトアウトできるようにしてあるアプリがあるのかと言われるとよくわからないけれども。まあもし対処する必要が出てきたとしたら、きっとしれっと無効にできるようにアップデートされるのかもしれない。

logとreport

Firebase Crashは基本的には組み込めばそれで終わりな感じで、後は任意のタイミングでFirebaseCrash.log()とかFirebaseCrash.report()などを使ってクラッシュ時の情報を付け加えるくらいしかやることはない。

log()はクラッシュレポートにイベントとして情報を追加することができるものである。

report()は例えばtry~catchでcatchした例外のスタックトレースを送信するのに使う。

私はどちらもうまいこと使いこなせる自信がない。今までもクラッシュレポート見ても、一体どういう状況で発生しているのかよく分からなくて対応ができなかったことがよくある。log()を使えば原因を特定するのに有効な情報を付け足せるのだろうが、どういう情報を付け足せば原因把握に役立つのかはいまいち分からない。


  1. gradle.propertiesなどで指定すれば良い。プロジェクトルートに秘密鍵のファイルを配置したのであれば、FirebaseServiceAccountFilePath=../<秘密鍵のファイル名>という感じ。 

コードから生成したViewにstyleを適用してもLayoutParamsについては無視される

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

コードから動的にViewを生成したい時がある。そしてそのとき見た目をカスタマイズしたいなんてときがある。もちろんsetBackground()とかsetPadding()を呼び出して設定することは可能であるが、どうせならXMLでやるときのようにstyleを適用したい、なんて場面があるだろう。・・・私にはあった。

さてそんなときに、どうやったらJavaのコードでnewしたTextViewにstyleを適用できるのだろうか、という話。2つ方法があって、どちらもコンストラクタでstyleを指定する。

1つはViewのコンストラクタに引数を4つとるものを使う方法。new TextView(context, null, 0, R.style.some_style);という感じでTextViewを生成する。ただし引数4つのコンストラクタはAPI21からしか存在しないので注意1

もう1つはContextThemeWrapperを使う方法。new TextView(new ContextThemeWrapper(context, R.style.some_style));という感じで生成する。こちらも同じくJavaコードから生成したViewに、styleを適用することができる2。droidkaigiのアプリにコントリビュートしたら学べた方法3

ただし、どっちの方法でstyleを適用しようとも、LayoutParamsに関する設定だけは無視されることに気をつけたい。私はstyleにandroid:layout_marginを指定していたのだが、Javaのコードから生成した場合、marginが無視された。

レイアウトXMLでstyleを適用した場合は、marginも含めてstyleが適用される。しかしコードからだと適用されない。これはコードからstyleを適用した場合、コンストラクタでViewを生成した時点ではViewがLayoutParamsを持たないからだと思われる。

そもそもLayoutParamsはView自身が使う情報ではなく、そのViewを配置するViewGroupが利用する情報になる。コードから生成した場合、このLayoutParamsは親レイアウトにaddView()を行った時点で設定される。もちろんsetLayoutParams()を呼び出すことで事前に設定することも可能だが、コンストラクタを呼び出しただけでは生成されないことがポイント。つまり、View自身に関わるpaddingなどの情報はstyleの適用で設定されるけれども、LayoutParamsに関する情報は設定されないということである。

コンストラクタでLayoutParamsも持たせればいいのにと思うかもしれないが(私も思ったが)、どのViewGroupに配置されるのかがわからないので、コンストラクタの時点でLayoutParamsを設定することは無駄なのだと思う。プログラマが事前にどのViewGroupに配置するか決めているのであれば、setLayoutParams()を使えということなのだろう。

そもそもstyleにLayoutParamsに関する情報をもたせることが間違いなのかもしれない。今まで特に気にせずにstyleでLayoutParamsに関する情報を持たせていたが、実は推奨されないやり方だったのだろうか。

ちなみに、今回の出来事ではじめて知ったのだが、XMLでandroid:layout_xxxとなるのがLayoutParamsらしい。どれがLayoutParamsなのかわからないじゃないかとか思ったけど、自明だった。

ちなみにViewクラスにはsetStyle()みたいなメソッドは存在しない。よく考えてみると、XMLファイルでstyleを適用する際に、なぜかstyleだけはnamespaceがつかない。基本的にViewに要素を書くときは、android:xxxだったりapp:xxxといった感じで頭に必ずnamespaceをつけるのに、直接style="xxx"と書く。つまりstyleの適用だけは特殊な扱いなのだろう。

setContentView()からだとstyleに書いたLayoutParamsが有効になるが、addView()だと無視されることも、このあたりが関係しているのかもしれない。


  1. ちなみに引数3つのコンストラクタの第3引数はdefStyleAttrで、R.styleable.xxxを指定するためのものであり、styleを渡したところで適用されない。 
  2. LayoutInflaterがViewをinflateするときにこの方法でinflateしている。 
  3. https://github.com/DroidKaigi/conference-app-2017/pull/401/commits/1812e77a4e3cb598e94714cf12cd83b01d716c79#diff-13d7c85c29370a83d0d27462c1d57f2aR76 

BottomNavigationViewの上にSnackbarが表示されるようにしつつFABも連動して動くようにする

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

BottomNavigationViewを使ってみようかなと思ったときに、ふと「Snackbarはどこに表示されるのが正しいのか」ということを疑問に思った。ガイドラインではBottomNavigationViewの上からSnackbarが現れるようにするということが書いてあった。

https://material.io/guidelines/components/bottom-navigation.html#bottom-navigation-specs

Elevation的にはSnackbarがBottomNavigationViewより下にあるので、「下に配置する」というべきなんで、上から現れると表現するのも誤解がありそうな気がして気持ち悪い。

実装

挙動はわかったが、ではそれをどうやって実装すればよいのかという話になると、これがややこしい。いろいろ探し回ったが、こちらのサイトを参考にするのが良さそうな感じであった。

http://sakebook.hatenablog.com/entry/2017/02/12/032501

結論から言うとCoordinatorLayout.Behaviorを継承して、カスタムビヘイビアを使って実装するしかないようだ。今のところは。それとも、もしかしたら、私が見つけられなかっただけで、もっと簡単な方法があるのかもしれない。

FABを追加した場合はどうするのか

BottomNavigationViewを配置して、さらにFABも一緒に配置したい場合はどうするのか。

つまりこういう動きをしたい、ということである。

デモ

  • SnackbarはBNVの上辺から現れる
  • FABはSnackbarを避ける
  • SnackbarはBNVの動きに合わせて動く=FABも連動して動く
  • FABはBNVも避ける
  • BNVはスクロールに合わせて隠れる(Appbarが隠れるのと連動する)

単純にSnackbarがBNVの上辺から出現してくれれば(SnackbarがBNVを避けてくれれば)ことは簡単なのだが、そういう設定にたどり着くことができず、最終的にcustom behaviorでゴリ押しした。

コードはGitHubにあげておいた。

どうやったか

FABがBNVを避ける

これは原理をいまだ理解していないのだが、BNVにapp:insetEdge="bottom"を加えることでFABがBNVを避けるようになる。

これに気づくまでが非常に長くて、ここで俺の苦労を聞いてくれと言いたいところだが割愛する。とりあえず、FABがSnackbarを避けるのはBehaviorによるものではなかったというのが今回の作業で得られたもっとも大きな収穫かもしれない。

insetEdgeの挙動に詳しい人、もしくは詳しく解説したブログ記事なんかをご存じの方は教えて欲しい。

BNVを隠す

スクロールに合わせてBNVを隠す。

このあたりからこちらのサイトを参考にしだす。

http://sakebook.hatenablog.com/entry/2017/02/12/032501

私はAppbarLayoutが隠れている比率を計算して、同じ比率だけBNVを隠すという実装を行った。最初はAppbarLayoutのBehaviorを真似しようと思ったが、ややこしかったので途中で諦めた。

ちなみにAppbarLayoutを動かさないで、この仕様を取り入れたいという場合は、onNestedScrollなどを使って自分で隠すようにする必要があるだろう。

この実装にしたのはその手動計算が面倒くさかったというのもある。

やり方としては

  1. custom behaviorでlayoutDependsOnを使いAppbarLayoutに依存するように宣言
  2. onDependentViewChangedでAppbarLayoutがどれだけ隠れているかを計算する
  3. 同メソッド内でBNVのsetTranslationYを使ってBNVを隠す

やっていることはこれだけである。

SnackbarをBNVの上に表示する

これが一番苦労した。参考にしたサイトでは、Snackbar表示中はBNVを動かさない、というやり方での実装だった。私の場合はSnackbar表示中だろうとBNVは動くし、それに合わせてSnackbarも動く。

  1. custom behaviorでoayoutDependsOnを使いSnackbar.SnackbarLayoutに依存するよう宣言
  2. onDependentViewChangedでSnackbarが出現したことをフラグで持つ
  3. Snackbar表示中は、onNestedPreScrollでSnackbarのpaddingを更新する
  4. onDependentViewRemovedでSnackbarが消えたらフラグをクリアする

なぜonDependentViewChangedのみでやらないのかというと、このメソッドはSnackbarがニョキッと動いている最中は呼ばれるのだが、完全に表示されてSnackbarが停止した状態では呼び出されない。そのため、Snackbarが停止している間にBNVを動かすと、その間はSnackbarが置いてけぼりになってしまうからだ。

BNVの動きに連動してSnackbarのpaddingを更新しなければならないので、こんな変な実装になってしまった。

BNVのbehaviorがSnackbarの動きを制御するという若干の気持ち悪さがあるが、他に方法を思いつかなかった。

insetEdgeをうまく使えばもっと簡単なのでは?

と思っていろいろ試したのだけど、結局良くわからなかったのでこのような実装になった。

insetEdgeのinsetが何のことかよくわかっていない。似たようなやつにdodgeInsetEdgeなるものもある。dodgeInsetEdge="bottom"を設定したら、画面上部に向かってViewが飛んでいって、呪いの館を思い出した。

insetEdgeの使い方を詳しく解説しているサイトをご存知だったら教えて欲しい。

コードの全体(といっても、重要なのはcustom behaviorだけ)はGitHubにあるので参照してほしい。

ちなみにこのコードはsupport library 25.3.1で動作確認している。バージョンによって挙動が変わると思うので、注意してほしい。

setLayoutParamsには親のViewGroupに属するLayoutParamsをセットする

基本的に私はViewをJavaのコードで生成することは稀なので、適当にやっていたのだけれど。

RelativeLayout parent = new RelativeLayout(context);
TextView hope = new TextView(context);
parent.addView(hoge);

みたいな感じでコードから生成したとき、このViewのwidth/heightにmatch_parentとかwrap_contentを設定するのに、LayoutParamsを使うことになると思う。

parent.setLayoutParams(new RelativeLayout.LayoutParams(RelativeLayout.MATCH_PARENT, RelativeLayout.WRAP_CONTENT));
hoge.setLayoutParams(new RelativeLayout.LayoutParams(RelativeLayout.MATCH_PARENT, RelativeLayout.WRAP_CONTENT));

みたいに。そこでLayoutParamsを指定するのに、なぜいちいちRelativeLayoutとかクラスを指定しているのだろうと、毎度のこと不思議に思っていた。別にViewGroupでもいいんじゃないのかとか思いながら、基本的にはAndroid Studioの補完によって出てきたものをそのまま使っていた。

とりあえずViewGroup指定しとけばオッケーだと思っていたのだが、どうもそれはオッケーではなかったらしい。たまたま今まで問題に出くわしていなかっただけだった。

親のViewGroupのLayoutParamsを使わなければならない

まあ大抵の場合、ViewGroupを使っておけば問題ないのだろう。LinearLayoutとかRelativeLayoutとかFrameLayoutとかにコードから生成したViewを追加していくだろうから。しかしバージョンによってはそれではうまく動かないことがあるらしいということがわかった。

私が出くわしたのは、ListViewに表示するViewをコードから生成したときに、ViewGroup.LayoutParamsを使うとクラッシュするという症状だった。ちなみにAndroid7で確認したら問題なく動いていたので、普通にスルーしていた。4.4で確認したらクラッシュした。

ListViewもViewGroupを継承しているのだからViewGroupでも問題ない気がするのに・・・。

ちなみにこれはAbsListView.LayoutParamsを使うことで、4.4でも7でもクラッシュしなくなくなった。

この問題から学んだことは、LayoutParamsは親のViewGroupに属するLayoutParamsを使わないといけないのだということである。LayoutParamsいっぱいあって適当に選んでいたが、今後はちゃんと指針が持てたという意味で、よい経験ができた。

ActivityScopeを使ってActivityクラスごとにシングルトンになるようにした話

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

まずはじめに。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の経験値積んでから挑戦しようと思っております。

http://frogermcs.github.io/activities-multibinding-in-dagger-2/

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

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

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.getInstance(config);
        realm.beginTransaction();
        realm.copyToRealmOrUpdate(new UriFilter(insert));
        realm.commitTransaction();
        realm.close();
    }

このテストコードはandroidTestに配置して実機で実行します(Instrumentation Test)。

テストのたびにデータをまっさらにするため、@Before@AfterRealm.deleteRealm()を呼び出しています。

FilterDataSourceRealmはDaggerを使ってシングルトンで運用します。初期化時にRealmConfigrationを与えて、Realmのインスタンスは各メソッドの中でインスタンス生成&closeを行うようになっています。

で、このテストコードでテストを行うと、テストメソッド内でRealm関連の操作が失敗した場合にjava.lang.IllegalStateException: It's not allowed to delete the file associated with an open Realm. Remember to close() all the instances of the Realm before deleting its file: /data/data/jp.gcreate.sample.daggersandbox/files/test_realmというエラーが出ます。Realmへの操作で何らかのエラーが生じたんでしょうが、出て来るエラーは「close忘れてるぞ」になります。

例外の意味は分かります。Realm.deleteRealm()を実行するときにcloseしてないRealmのインスタンスが存在してはいけないということです。ですが、この場合のエラーの本質はClose忘れではなく、Realmの操作が失敗していることです。私はその原因が何なのか知りたいわけです。insertFilterが失敗したからcloseが行われず、その結果@Afterで実行しようとしたRealm.deleteRealm()が失敗しているわけですから。

実装中はこの「close忘れ」エラーが出るたびに、@Before@Afterの処理をコメントアウトし、本当のエラーの原因を確認していました。すると実際にどこで失敗しているのかエラーメッセージが教えてくれました。

テストメソッドの度にRealmのファイルを消すというのが愚策なんですかねぇ・・・。でも消さないとテストの実行順で登録データが異なることになってテストにならないし。

公式サンプルにPowerMockを使ったテストがありましたが、PowerMockよくわからないのと、Mockじゃなくて実際に書き込みとかしたかったのでこんな形のテストを書いてみたのですが、テスト失敗のログがまったく役に立たなくて苦労しました。

Realmもテストも詳しいわけではないので、いろいろ勘違いしている部分があるのかもしれません。