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

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クラスごとにシングルトンになるようにした話

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

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

カスタム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

USBデバッグONだと動かないようにする理由が知りたい

USBデバッグがONになっていると動かないアプリがあるらしい。銀行系アプリで動かないらしいという話は聞いたことがあったのだが、私は使っていないから特になんとも思っていなかった。まあセキュリティ的な問題でしょうがないんだろうな程度だ。

しかし自分が使っているアプリが今後USBデバッグON状態では動きませんと言われてしまった。自分の影響のあるところで話が出てくるとちょっと困る。

Read full post gblog_arrow_right

Google Playの課金周りの処理はよく分からない

私はスマホにアカウントを2つ登録して使っている。メインのアカウント(以降Aと呼称)とゲーム用のアカウント(以降Bと呼称)って言う感じで分けているのだ。

で、そうやってアカウントを2つ使っていると、ゲーム内課金を行うときにちょっと困ったことが起こる。課金するアカウントにどっちが選ばれるか問題である。

Read full post gblog_arrow_right

ランタイムパーミッションの対応をするとActivityがマッチョになるのなんとかならんかな

Android6.0からアプリのインストール時ではなく、パーミッションを必要となる操作を行う前に許可を求める形式になった。これ自体はとてもよいこと。インストール時にやたら権限を要求するアプリがなんだか怖くてインストールすらしなかったのが、これのおかげでとりあえず試して見れるようになったのはとてもよい。

Read full post gblog_arrow_right