月別: 2017年4月

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

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

こっちのブログで書くか、別のブログで書くか迷ったのだが、プログラミングの話だしこっちに書こうかなと思う。

書こうと思ったきっかけは、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つのこと。コミュニケーション能力、チャレンジ精神、自分で調べる力。何にでも言えることじゃないかと思うけど、何にでも言えるからこそ大事なんじゃなかろうか。実際、教えていてこの人はプログラムに向いている・向いていないと感じることが何回かあった。それはなぜそう思うのかと考えてみたら、プログラミングの知識よりも本人の姿勢によるところが大きいと思ったのだ。


  1. ネットで公開される知見に頼るところが大きいのでとてもありがたく拝見している 
  2. あるにこしたことはないが 
  3. 理解できるかはともかく 
  4. 理解できるかはともかく 
  5. 原因を調べて解決することは、プログラミングをしていく上で何度も通る道であり、そういうハマりを経て人は成長していくのである 
  6. 質問する敷居の高さは、teratailの方が低いとは思うが、思い出してほしい。コミュニケーション能力が大切だということを。インターネット上の人々はあなたの状況を理解しているエスパーではないので、聞き方が悪いと答えは返ってこないどころか、逆に傷つくことになるかもしれない。 
  7. 学生という立場は学生という立場を失ってはじめてそのありがたみが分かるのだ 

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で動作確認している。バージョンによって挙動が変わると思うので、注意してほしい。