Architecture Componentを触ってみた

とりあえず軽く触ってみた。 codelab Google IOの動画はとりあえず2つ見てみた。 最初のやつは「こんなん作ったでー」という話で、2つ目がその中身を説明っていう感じなので、時間がないなら2つ目だけ見ればいいんじゃないかなと思う。全部英語なので、私の英語力では雰囲気しかわからず時間対効果はあまり良くなかった気がしている。 3つ目もあるのだけど、これはまだ見れていない。タイトルから見るとRoomについての説明なのだろうか。 個人的にArchitecture Componentで気になっていたのは、いかにActivityのライフサイクルに振り回されなくてすむようにできるかという部分だ。つまり、LifecycleOwnerとLiveDataとViewModelについてである。とりあえずその観点で言うと、最初にあげた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を実装するときの悩み

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の実装、みんなはどうやっているのだろう。

Androidプログラミングを学ぶ上で大切だと思うこと

こっちのブログで書くか、別のブログで書くか迷ったのだが、プログラミングの話だしこっちに書こうかなと思う。 書こうと思ったきっかけは、Androidプログラミングを教える仕事を受けたこと。まあ個人的に、私などが人様に何かを教えるなどおこがましいという思いはあったものの、まあ何事もやってみなければわからないということでやってみた。 Android特有の、ライフサイクル周りの話だとかは、まあ知っていないと辛いよねっていうことではあるんだけど、それよりもっと基礎的な、プログラミングを学ぶ姿勢とでも言おうか、今回書きたいのはそこについてだ。Androidプログラミングとタイトルにしているのは、私がAndroidしか知らないから限定しているだけで、たぶん他の言語でも同じなんじゃないかなと思う。 私はずっと一人でAndroidのプログラミングをやってきた。ほぼ独学である。書籍とネットの情報を頼りに黙々とやってきた1。そんなやつが人様にプログラミングを教えるのもおこがましい気がするものの、3ヶ月間教えてみて大事だなと思ったことが3つある。 1つ目は言ってしまえばコミュニケーション能力である。いきなりプログラミング関係ないじゃないかと思われるかもしれないが、これはとても大事である。別に話を弾ませたり、相手の気持ちを慮ったりする能力が必要だというわけではなく2、自分の考えを相手に伝えようと努力できることって、プログラミング関係なしに大事だなと思ったのである。具体的なシチュエーションとしてはエラーを伝える場面でよくそう思った。なんだかよくわからないけれどもこういうエラーが出たんですと、わからないなりに説明しようとする人とそうでない人がいる。エラーの確認の仕方は知っているかどうかの問題なので、初心者だから見込みがないとかそういう話ではない。しかし、わからないなりに説明をしようとする人に、私は伸び代を感じたのである。 2つ目はチャレンジ精神である。とりあえずやってみることが大事だということ。こう書いたら動いた、だったらこう書けば違う動きになるのではないかと仮説を立てて、挑戦してみる姿勢が大事だ。知らなければどうしようもない部分もあるので、教えてもらうことも大事ではあるが、教えてもらうのを待つだけではなく、自ら挑戦する姿勢もまた大事なのであると言いたい。 3つ目は自分で調べる力である。Androidは特にソースコードがすべて公開されているので、なぜそう動くのかはコードを読めば分かる3。ソースコードを読むのでなくても、公式のドキュメントを読んで調べることなら誰でもできるはず4。Androidのプログラムを書いていると、便利なライブラリのお世話になることがとても多いが、そういったライブラリの使い方を、ライブラリのドキュメントやソースコードから調べることは重要だ。 以上3つ。プログラミングを教えているのに、プログラミングの知識、変数がどうのとかJavaがどうのとかとかよりも、もっと基礎的なことの方が大事に思えたことが意外に感じられた。 最後に、これからプログラミングを勉強しようと思っている人は、できるだけ効率的に学びたいなと思っていることだろう。きっと、いい師匠(先生)を見つけるのが効率的なんだろうけれど、どうやって探すのかは私が知りたい。だからそんな効率的に学ぶ方法を探す前に、さっさと何かプログラミングしてみる方が早いと私は思っている。 Androidプログラミングで言えば、とりあえず市販の本でもネットの情報でもなんでいいので、とりあえず書いてあるとおりにやってみるのがよいと思う。書いてある通りにやってうまくいかなければ、誰かに聞くのがいいだろう。 実は、そういう「書いてあるとおりにやってみたけど動かない」という状況は、スキルアップにちょうどいいシチュエーション5なのだけど、初心者はただ辛いだけなので、誰かに聞くのが手っ取り早い。Twitterでつぶやいてみるとか、teratailとかStackoverflowとかで質問してみるとか6。学校の先生がいるなら、先生に質問するのがいいだろう。聞ける人がいるということはとても恵まれたことなので7、そんな環境にいる人は今のうちに有効活用するべきだと思う。 大事だと思った3つのこと。コミュニケーション能力、チャレンジ精神、自分で調べる力。何にでも言えることじゃないかと思うけど、何にでも言えるからこそ大事なんじゃなかろうか。実際、教えていてこの人はプログラムに向いている・向いていないと感じることが何回かあった。それはなぜそう思うのかと考えてみたら、プログラミングの知識よりも本人の姿勢によるところが大きいと思ったのだ。 ネットで公開される知見に頼るところが大きいのでとてもありがたく拝見している ↩ あるにこしたことはないが ↩ 理解できるかはともかく ↩ 理解できるかはともかく ↩ 原因を調べて解決することは、プログラミングをしていく上で何度も通る道であり、そういうハマりを経て人は成長していくのである ↩ 質問する敷居の高さは、teratailの方が低いとは思うが、思い出してほしい。コミュニケーション能力が大切だということを。インターネット上の人々はあなたの状況を理解しているエスパーではないので、聞き方が悪いと答えは返ってこないどころか、逆に傷つくことになるかもしれない。 ↩ 学生という立場は学生という立場を失ってはじめてそのありがたみが分かるのだ ↩

Firebase Crashを使ってみた

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 ルートのbuild.gradleのclasspassに`'com.google.firebase:firebase-plugins:1.0.5'`を追加 app/build.gradleに`apply plugin: 'com.google.firebase.firebase-crash'`を追加 Firebase Consoleからプロジェクトの設定→サービスアカウント→クラッシュレポートから、新しい秘密鍵の生成を行いダウンロードする(jsonファイル) ダウンロードしたファイルへのパスを`FirebaseServiceAccountFilePath`というプロパティに記述する1 `./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()を使えば原因を特定するのに有効な情報を付け足せるのだろうが、どういう情報を付け足せば原因把握に役立つのかはいまいち分からない。 gradle.propertiesなどで指定すれば良い。プロジェクトルートに秘密鍵のファイルを配置したのであれば、`FirebaseServiceAccountFilePath=../<秘密鍵のファイル名>`という感じ。 ↩

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

コードから動的に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()だと無視されることも、このあたりが関係しているのかもしれない。 ちなみに引数3つのコンストラクタの第3引数はdefStyleAttrで、`R.styleable.xxx`を指定するためのものであり、styleを渡したところで適用されない。 ↩ LayoutInflaterがViewをinflateするときにこの方法でinflateしている。 ↩ https://github.com/DroidKaigi/conference-app-2017/pull/401/commits/1812e77a4e3cb598e94714cf12cd83b01d716c79#diff-13d7c85c29370a83d0d27462c1d57f2aR76 ↩

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

BottomNavigationViewを使ってみようかなと思ったときに、ふと「Snackbarはどこに表示されるのが正しいのか」ということを疑問に思った。ガイドラインではBottomNavigationViewの上からSnackbarが現れるようにするということが書いてあった。 https://material.io/guidelines/components/bottom-navigation.html#bottom-navigation-specs Elevation的にはSnackbarがBottomNavigationViewより下にあるので、「下に配置する」というべきなんで、上から現れると表現するのも誤解がありそうな気がして気持ち悪い。 実装 挙動はわかったが、ではそれをどうやって実装すればよいのかという話になると、これがややこしい。いろいろ探し回ったが、こちらのサイトを参考にするのが良さそうな感じであった。 https://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を隠す。 このあたりからこちらのサイトを参考にしだす。 https://sakebook.hatenablog.com/entry/2017/02/12/032501 私はAppbarLayoutが隠れている比率を計算して、同じ比率だけBNVを隠すという実装を行った。最初はAppbarLayoutのBehaviorを真似しようと思ったが、ややこしかったので途中で諦めた。 ちなみにAppbarLayoutを動かさないで、この仕様を取り入れたいという場合は、onNestedScrollなどを使って自分で隠すようにする必要があるだろう。 この実装にしたのはその手動計算が面倒くさかったというのもある。 やり方としては custom behaviorで`layoutDependsOn`を使いAppbarLayoutに依存するように宣言 `onDependentViewChanged`でAppbarLayoutがどれだけ隠れているかを計算する 同メソッド内でBNVの`setTranslationY`を使ってBNVを隠す やっていることはこれだけである。 SnackbarをBNVの上に表示する これが一番苦労した。参考にしたサイトでは、Snackbar表示中はBNVを動かさない、というやり方での実装だった。私の場合はSnackbar表示中だろうとBNVは動くし、それに合わせてSnackbarも動く。 custom behaviorで`oayoutDependsOn`を使いSnackbar.SnackbarLayoutに依存するよう宣言 `onDependentViewChanged`でSnackbarが出現したことをフラグで持つ Snackbar表示中は、`onNestedPreScroll`でSnackbarのpaddingを更新する `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で動作確認している。バージョンによって挙動が変わると思うので、注意してほしい。

Save Actionsというプラグインに感動した話

DroidKaigi2017の会場には行かなかったけれども、参加した人や公開されたスライドなんかは一通りチェックしている。そんな中でこちらのスライドでSave Actionsなるプラグインを知る。 少し幸せになる技術 Android Studioで保存時にフォーマットを自動で揃える Save Actions – JetBrains Plugin Repository Save Actions – GitHub Android Studioでビルドしたりファイルの保存が行われた際に、自動的にoptimize importやreformat codeを実行してくれるプラグインである。 ファイルの保存時にrefomart codeを実行する方法として、cmd+sにマクロを割り当てて行うという方法は以前から知っていたのだが、私は導入していなかった。というのも、私はAndroid Studioを使っていてただの一度も自分からファイルの保存を行ったことがないからである。そもそもcmd+Sを押す習慣がなかったのだ。それだったら気がついたときにalt+cmd+lでreformat codeを実行するのと大差ないなと思って導入しなかった。なのでreformat codeを実行することもよく忘れていた。(droidkaigi2017のリポジトリにフォーマットしてないコードをプッシュしたりしていた、申し訳なかった) しかしこのSave Actionsプラグインに出会えたことで、今後そんなミスは起こさないだろう。 このSave Actionsは、Android StudioのPreference > Pluginsからは検索することができない。作者の方がAndroid Studioによる動作確認が取れていないのがその理由らしい。 導入するにあたっては、JetBrainsのPlugin Repositoryからzipファイルをダウンロードしてきて直接インストールする必要がある。 ちなみに、今のところAndroid Studioだからきちんと動作をしないという事象には出くわしていない。 素晴らしいなと感じたのが、ファイルが自動保存されるタイミングやビルド(デバッグ実行)したタイミングで、自動的にreformat codeが走ってくれること。しかもそのSave Actionsのreformat codeで修正された部分は、自分が修正したコードとは異なる色で表示される(新規が緑、変更が青、Save Actionsによるreformat分はグレーみたいな感じで色分けされる)。あくまでAndroid Studioのエディタ上での表示の話なので、コミットする際に別れてくれるわけではないけれども。 なお、Save Actionsを導入する場合は、必ずFile path exclusionsに.gradleファイルを追加する必要がある。 https://github.com/dubreuia/intellij-plugin-save-actions/issues/51 Gradleファイルをいじる際に、キーをタイプするたびにreformat codeなどが走るというバグがあるからだ。 個人的にはプラグインのバグというより、IntelliJ系IDEの仕様なのではないのかと思っている。gradleファイルはspaceを追加するだけでも「syncしろ」と言ってくるので、このあたりの動きが関連しているようにも思う。ともあれ、当座のところはgradleファイルをSave Actionsの対象外にすることでしのげる。 ともあれ、個人的にとても便利なプラグインだと思う。あまりに感動して作者にお礼のツイートを送ってしまうほど、ドンピシャなプラグインだった。 少し幸せになる技術というスライドで紹介されていたが、私は少しどころではなくかなり幸せになれた。ありがたい。

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いっぱいあって適当に選んでいたが、今後はちゃんと指針が持てたという意味で、よい経験ができた。

CIを導入してみた

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を使う場合はそんな便利なコマンドは用意されていないので、自分でシェルスクリプトを書く必要があります。 しかしこれがまあ安定しない。
Read full post gblog_arrow_right

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

まずはじめに。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/