2017/10/16

Android Performance. UI Rendering

レイアウトXMLはどのようなプロセスを経てピクセル情報に変換され, 画面に描画されるのでしょうか?
Androidのパフォーマンスを改善するには, UIレンダリングの仕組みを理解しておく必要があります.

Android Performance. Dropped frameでは画面のアップデートが16ms毎に行われ, これが遅延するとユーザ体験を悪くしてしまうことについて触れました.

アプリが60fpsを維持するためにはMainThreadでの処理を軽くし, 16msごとのリフレッシュレートを逃さないようにしなければなりません.
60fpsを維持できなくする理由はたくさんありますが, 今回はViewの更新とレンダリングパイプラインについて見ていきます.

Layout & Draw

レイアウトXMLがパースされるとレイアウトツリー(ビューヒエラルキー)が作成されます. 描画はルートノードから始まり, ツリーを渡り歩きながらレイアウトと描画が行われます.
複数のビューを持つ親ビュー(ビューグループ)の場合は, 子ビューにいくつかの制約や制限をつけて描画を要求します. 描画の順序は親ビューが先で子ビューが後になるので, 親が子より奥に描画され, 子ビューが親に重なる形で描画されることになります.

ビューのレイアウトにはメジャーとレイアウトのプロセスがあります. 親ビューは子ビューのサイズに依存するので, まずは子ビューのサイズを計測します. 計測が終わると親ビューが全ての子ビューを計算されたサイズで配置していきます. これはビューツリーからトップダウントラバーサルで処理されるため, ビュー階層が浅いほどパフォーマンスが良くなります. ビューのレイアウトが終わるとこれを描画します.

Rasterization

Viewをディスプレイに描画するには, ボタンやテキストをピクセルに変換する必要があります. 例えば, ラスタ形式(ビットマップ, etc.)ではない文字列やボタン, ベクタードロワブルのようなオブジェクトはラスタライズと呼ばれるプロセスでピクセル形式に変換されてから画面に出力されます.
Android3.0以降, レンダリングパイプラインはハードウェアアクセラレーションをサポートしました. ラスタライズはとても時間のかかるプロセスなので, 専用にデザインされたハードウェアユニット(アクセラレータ)で高速に処理されます. これがGPU(Graphics Processing Unit)です.

GPUはポリゴンやテクスチャといったいわゆる画像などのために設計されたハードウェアユニットです. CPUはそういった画像をGPUに供給する役割を果たします. この操作には OpenGL ES のAPIを使って行われています.

ボタンなどのUIオブジェクトを描画したい場合, まずはCPUでポリゴンやテクスチャ情報に変換し, これをGPUに送ってラスタライズします. CPUでポリゴンやテクスチャ情報に変換したり, GPUにこれを入力する処理は高速ではありません.

パフォーマンスのために, これらのオブジェクトに変換する回数を減らすことは効果があります. OpenGL ES のAPIはGPUに入力したオブジェクトをGPU上にそのままキャッシュさせることが可能です. 同じボタンやUIコンポーネントを使う場合は, 単にGPU上に残ったキャッシュを参照すればよいので, 余計なオーバーヘッドが起こりません. レンダリングの性能を最適化するには, GPU上にあるキャッシュを可能な限り長時間保持して, これを再利用するようにすることです.

Display list

標準UIコンポーネントのドロワブルなどはあらかじめGPUに入力されており, これらの描画は効率的に動きます.
しかし, 実際のUIは複雑で, 例えば背景画像といったビットマップはCPUが画像をメモリにロードしてGPUに転送されます. また, ベクタードロワブルはパスを繋げてポリゴンを描画する必要があります.
テキストにいたってはCPUで文字グリフをテクスチャにラスタライズしたあとGPUにこれを入力し, GPUメモリにグリフを参照する領域を描画します.
アニメーションリソースはもっと複雑で, ビジュアルが変わればGPUリソースを1コマ, 1コマ何度も更新しなければなりません.

ハードウェアアクセラレーションが有効である場合, ディスプレイリストを使った新しい描画モデルで描画されます. ディスプレイリストにはGPUレンダリングに必要な情報アセットとOpenGLコマンドリストが格納されていて, 無駄なオーバーヘッドを抑えて効率的に描画することができます.

Draw Phase

ビューが実際にレンダリングされる前に, まずGPUに適した形式に変換するDrawフェーズがあります. これはJavaによるonDrawコマンドで行われますが, Canvasを使ってテッセレートされた複雑なオブジェクトかもしれません.
この変換が終わると, システムによって結果がディスプレイリストとしてキャッシュされます.

Androidではその都度画面全体を再描画することはせず, 更新が必要な領域に絞って描画します. しかし, 多数のビューが無効化(invalidate())されるとDrawフェーズに多くの時間を費やします. あるいはonDrawで非常に複雑なロジックを抱えているかもしれません.

Execute Phase

作成されたディスプレイリストは2Dレンダラーによって実行されます. ディスプレイリストはOpenGL ES APIを使ってドローされます. これによってGPUにデータが送られ, 最終的にピクセルを画面に送ります.
複雑な描画をするカスタムビューでは, OpenGLが描画できるようにコマンドも複雑になる必要があります. 複雑なビューを描画することは2DレンダラーのExecuteフェーズに多くの時間を費やす原因になります.

画面上でUIオブジェクトの位置が変わった場合は, 同じディスプレイリストをもう1度Executeフェーズを実行するだけです. しかし, 画像のビジュアルが変化すると過去のディスプレイリストが無効になるかもしれません. その場合はDrawフェーズでディスプレイリストを再作成して, 再び実行する必要があります. 画像の描画内容が変わるたびにこのプロセスが繰り返されます. このパフォーマンスは画像の複雑さによって変わるため不正確です.

Process

DrawフェーズとExecuteフェーズが終わるとCPUはフレームのレンダリングが完了したことをGPU/グラフィックドライバーに伝えます. このアクションはブロッキングコールであるため, GPUがコマンドを受け付けたことの応答をCPUは待つことになります.
GPUからのコマンド応答が長くなると, このプロセスも長くなります. プロセスが長くなるのは大抵GPUが多くの仕事をしていることが多いです. 多数の複雑なビューの結果, 多くのOpenGLレンダリングコマンドが必要になりGPUの仕事が増えるのです.

16ms / Frame

16msの間に起こるレンダリングパイプラインは次の通りです.

  1. Input(ユーザからの入力)
  2. Animation(アニメーション)
  3. Measure&Layout
  4. Drawing(Draw Phase)
  5. Sync/Upload
  6. Issuing Commands(Execute Phase)
  7. Processing(Process)
  8. Misc

これらの時間はProfile GPU Renderingツールで見ることができます. 下図はフレームごとのレンダリングに要した時間を並べたもので, 緑色の水平線が16msを示すラインです. これを超えるとDropped Frameが発生します.


実際にアプリケーションを作成すると, 16ms/フレーム・60fpsを維持することが大変であることを実感できるでしょう. パフォーマンスを改善するには計測して問題のある箇所を特定することを繰り返すことが重要です.

前回と合わせて, 最低限必要な知識は揃いましたので, アプリのパフォーマンスを悪くしている箇所を特定し, それを改善するアプローチについて次回以降に書きたいと思います.

次回に続く…

2017/10/13

Android Performance. Dropped frame

SystemEvents, Input Events, Application, Service, Alarm, UI Drawingといった多くの処理はMain Thread(UI Thread) で実行されます.
重要なポイントは, 画面は16ミリ秒の間隔で再描画されているということです.

Why 16ms, Why 60fps?

人間は繋がりのある複数枚の絵が十分な速さで連続していると, それがあたかもアニメーションしているかのように錯覚します. パラパラ漫画やアニメGifの原理です.
アニメーションをスムーズに見せるために, どれだけ素早く画像を表示できるかという点が重要で, 滑らかで流れるようなアニメーションには必要不可欠な要素です.

人間の脳がアニメーションしているように感じるためには, 最低でも12fps程度の速度が必要です. これよりも遅いとパラパラ漫画のようなぎこちない見た目になります. 12fpsという速度はアニメーションには見えてもあまりスムーズには映りません.
24fpsは流れるようなアニメーションに見えますが, これはモーションブラーやビジュアルエフェクトの効果によるものです.
60fpsはモーションブラーやエフェクトなしでスムーズに映ります. これ以上のfpsはほぼ感知できない領域です.

注意すべきは人間の目の明敏さで, フレームレートが60fpsから24fpsに落ちると, 途端にアニメーションのスムーズさを欠いたように感じ, よくない印象を与えることになります.

VSYNC

スムーズなアニメーションを実現するためにも, Androidがどのようにして60fpsを実現しているのかを理解しておきましょう. それには2つの用語を理解しておく必要があります.

リフレッシュレート

1秒間に画面を何回リフレッシュできるかの値で, ハードウェアが定めた一定間隔で実行されます.
単位はHz(ヘルツ)で, 例えば60Hzであれば1秒間に60回のリフレッシュが可能です.

フレームレート

GPUが一秒間で幾つのフレームを描画できるかの値です.
単位はfpsで, 例えば60fpsであれば一秒間に60フレームの描画が可能です.

Synchronized

GPUが画像データを出力し, ハードウェアがそれを画面に表示します.
スクリーンの描画は, これを何度も繰り返しているので, GPUとハードウェアはできる限り一緒に働くことが望ましいのですが, リフレッシュレートとフレームレートは同じ頻度で起こることが保証されていません.

フレームレートがリフレッシュレートより早いと, ティアリングという現象が発生します.
これは, GPUが新しいフレームをメモリに上書きしている最中に, 画面がリフレッシュされてしまい, まだ更新中の画像を描画してしまうことで, 画像が崩れる(部分的に古いフレームが残る)現象です. これを解決するのがダブルバッファリングです.

ダブルバッファリングでは, GPUがバックバッファにフレームを描画し, それが終わるとフレームバッファーと呼ばれる領域にコピーします. 画面をリフレッシュするときはこのフレームバッファから取り出してリフレッシュするわけです. これによって古いフレームへの上書きが行われないので, 中途半端に上書きされた状態にはなりません.

ここで注意しないといけないことのは, 画面のリフレッシュ中にバックバッファからフレームバッファへのコピー作業が発生しないようにすることです. そうしないと, 同じ問題が起こります. ここで登場するのがVSYNC(Vertical Synchronization)です.

通常はフレームレートがリフレッシュレートよりも高いことが望ましいです. なぜなら, 画面を読み込むよりもGPUのリフレッシュの方が早くなるからです.
GPUはフレームをバックバッファに載せると, VSYNCによって次の画面リフレッシュまで処理を待つことになります.

しかし, 反対にフレームレートがリフレッシュレートよりも低い場合, 例えば30fpsに対して60Hzのディスプレイであった場合, フレームバッファのリフレッシュ作業には, 画面リフレッシュの倍の時間を要するため, 同じフレーム内容で2回ずつリフレッシュすることになります.
問題は, これが断続的に起こった場合です.

十分に早いフレームレートで動作しても, 突然フレームレートが落ちると, ユーザはスムーズなアニメーションに続いて, ぶつ切りになったものを見ることになります.
これらの事象は一般的に ラグ, ジャンク, ヒッチング, スタッター と呼ばれます.

アプリの開発者はこれらの事象を避けなければなりません.
人間の目は明敏で, フレームレートが落ちると, 途端にアニメーションのスムーズさを欠いたように感じ, よくない印象を与えてしまうことを思い出してください.

アプリ開発者が目指すところは 常に60fpsのパフォーマンスを維持すること です.

1000ms / 60frames = 16.666ms/frame

MainThreadでは16msの間隔でUI Drawingイベントが発生します. 60fpsの滑らかなアニメーションを実現するためには16ms間隔の描画が必要になります.

Main Thread(UI Thread)

単一スレッドの処理は逐次実行されるため, 順番に処理されていきます. MainThreadも例外ではありません. UI DrawingイベントもMainThreadで実行されるので, もしあなたの処理が長引くとUI Drawingイベントが遅延し, 次のリフレッシュレートのタイミングを逃してしまい, アニメーションで描画されるはずであったフレームが抜け落ちる ドロップフレーム が発生します.
あなたが書いた処理の後には, 常にUI Drawingイベントが待ち構えていることを忘れないでください.

次回に続きます…

2017/10/10

aapt:attr でリソースファイル数を節約する

layer-listselector など, リソースがまた別のリソースを参照する場合があります.

<selector ...>
  <item android:drawable="@drawable/image01" />
  <item android:drawable="@drawable/image02" />

image01image02 がベクタードロワブルの場合は, 新たに image01.xml, image02.xml と2つのドロワブルリソースを用意する必要があります.

  • selectorlayer-list の定義ファイル
  • image01.xml
  • image02.xml

image01image02 が他リソースでも使われている共通化されたリソースであれば良いのですが, 他では使われず, ここでしか参照されない場合は1つのリソースファイルとしてまとめて定義できた方が管理が楽です.

そうした場合は aapt:attr タグが使えます.

<?xml version="1.0" encoding="utf-8"?>
<selector
  xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:aapt="http://schemas.android.com/aapt">
  <item>
    <aapt:attr name="android:drawable">
      <vector ... >
        <path ... />
      </vector>
    </aapt:attr>
  </item>
</selector>

<aapt:attr> タグで指定したリソースは, aaptによってリソースファイルとして抽出・生成され, name属性名の値は, 親タグの同属性に指定のリソースを設定する動作となります.
この機能は全てのAndroidバージョンで利用できます.

以上です.

2017/10/09

DevFest2017

Android1.5~8.0 Walkthrough のセッションに登壇した際のスライドとスピーカーノートメモ、あと喋った内容の文字起こし.

はじまり。

2017年8月に最新のOS Android8.0 コードネーム Oreoがリリースされました.

アプリを Oreo に最適化するには TargetSdkVersion を 26 に上げる必要があります。
TargetSdkVersion を上げることで、Oreoの新機能を十分に活かすことができます。

ここ数年のアップデートでは システムリソースの消費を抑える DozeやAppStandby、バックグラウンド動作制限などがリリースされています。

これによって、ユーザは端末やアプリを使っていないときの バッテリー消費 を抑えることができます。
その一方で, 開発者は OSの仕様変更に対応する必要があります。

バックグラウンド活動のデザイン原則というものがあります。

  • バックグラウンドの活動を減らすことができないのか?
  • デバイスが充電中の状態になるまで活動を遅らせることができないのか?
  • 他の活動とまとめることができないのか?

といったことを考える必要があります。

8.0で バックグラウンド活動が厳格化されたことで 開発者はこれらと “まじめに” 向き合っていく必要があります。

これらの機能を搭載したOSが市場にどれぐらい流通しているのかをグラフにしました。
一番左のグラフは、下から青がAndroid ヌガー, 緑がマシュマロ, 黄色がロリポップ, 赤がキットカット のシェア率を積み上げたものです。

Dozeは マシュマロ以降のOSに搭載されていますので 市場端末の およそ50% がこれを搭載しています。
Android ヌガーでリリースされた, 一部のBroadcastを無効にするProjectSvelteは 18% です。

この割合は DevelopersサイトのDashboardで公開されている 10月時点でのWorldWideなOSバージョンシェアの数字になります。国内に限定したり、ターゲットユーザ層やminSdkでそもそもサポートしていないOSがあると思いますので、みなさんのサービスと同じ数字にはならない点にご注意ください。

本日は、こういった仕様変更や動作制限の移り変わりを Android 1.5~8.0まで 振り返ります。
時間の都合上、厳選してピックアップしている点はご了承ください。

まず初めは2009年4月リリースのOS1.5 CUPCAKEです.

2009年といえば バラク・オバマ氏が アメリカ合衆国大統領に就任した年 になりますね。
その頃Androidは スクリーンキーボードのサポートやアプリウィジェットプロバイダーをリリースしていました。

リリース:2009年4月 Android1.5 - Api Lv.3

3rd party keyboards… サードパーティ製のキーボードはこの頃からサポート.
Bluetooth A2DP… BluetoothプロファイルのA2DPがサポートされました. 当然まだBLEはサポートされていません.
AppWidgetProvider… アプリウィジェット機能のAPIがリリースされ, 開発者はアプリウィジェットを作成することができるようになりました.

次にリリースされたのが 2009年9月 OS1.6 DONUT です。
Cupcakeでは 320ピクセル x 480ピクセル の解像度のみをサポートしていましたが、Donutからは複数の解像度を扱えるようになりました。
また、バッテリー問題が今よりも はるかに深刻だった時代で, アプリ毎のバッテリー使用量をユーザが確認できる機能などが追加されました。

リリース:2009年9月 Android1.6 - Api Lv.4

Battery usage indicator… アプリごとの消費電力がわかる画面を搭載
当時は電力消費問題が深刻で朝満充電にしても夕方前にはバッテリー切れという状態.

New Android Market UI… 現Google PlayのUIが大幅刷新.
当時のAndroidアプリは簡素なものが多かっただけに, Android Marketの多彩な表現は開発者の目をひくものだった

Text-to-speech engine… 多言語の音声合成エンジンでテキスト読み上げをサポート. ただし日本語は含まれていなかった.

2009年10月 Donutリリースから わずか1ヶ月後には OS2.0 Eclair がリリースされました。
この頃はOSバージョンアップが 今よりも頻繁にあった時代です。
ここでサービス周りのアップデートがありましたので詳しくみてみます。

リリース:2009年10月 Android2.0~2.1 - Api Lv.5~7

Service.setForeground deprecated… Service.setForegroundが非推奨に.
代わりにService.startForegroundを使う必要がある. さらにフォアグラウンドで動作していることをユーザに伝えるためにOngoing Notificationの登録が必須化された.

Key events executed on Key-up… Android2.0はHOMEやBackといったバーチャルキーをサポートするため, ユーザが誤ってキーダウンしてもドラッグすることでキーイベントをキャンセルすることができるように, キーアップでイベント発火されるように変更された.

Multi-touch… マルチタッチがサポートされて, キーボードで素早く文字入力しても抜けることが少なくなりました

その他… Live WallpaperのAPIリリースもこの時.

まず、2.0のタイミングでService.setForegroundメソッドが非推奨になりました。
2.0未満のOSでは フォアグラウンドサービスを開始するのに 通知アイコン が不要でした。

通知アイコンが必須になったのは2.0からで、これによって、ユーザがバックグラウンドで活動しているアプリの存在に気づき、
無用なアプリを停止させることができるようになりました。

また、当時はバックグラウンドの活動に対する制限が緩かったので、バックグラウンドにいるアプリプロセスを片っ端からKillしていくタスクキラー系アプリが バッテリー寿命に効くということで流行りました。
アプリ開発者はそうしたキラー系アプリとも戦っていた時代です。

8.0ではサービスの在り方が大きく変わりました。原則、バックグラウンド状態から新しくサービスを起動できなくなったり、startForegroundServiceで起動する場合には5秒以内にフォアグラウンドへ昇格させないとANRが発生するなど厳格化されました。

バックグラウンド活動まわりで使えるAPIに、ロリポップでリリースされたJobScheduler APIがあります。
これの互換性ライブラリとして FirebaseJobDispatcherが API Lv.9から利用可能です。カバー率はほぼ100%です。
JobSchedulerは ロリポップ から使えるAPIなので 78% の端末で使うことができます。

2010年5月にはOS2.2 Froyoがリリースされました。
音声操作機能や、テザリング機能、GCMの前身にあたる C2DM がリリースされたのもこの時です。

リリース:2010年5月 Android2.2 - Api Lv.8

Install on external storage… アプリのインストール領域に外部ストレージを指定可能になった.

Backup Manager, C2DM… 新しい端末に乗換えした時に便利なアプリデータをクラウドへバックアップ/リストアを実現するAPI Backup Managerがリリース.
アプリはBackup agentを実装することでこれを実現することができる. 現在のバックアップの仕組みとは少し異なる. またGCMやFCMの前身にあたるC2DMもこのOSからサポートされています. C2DMはGCMにリプレースされた時点で非推奨になっています.

JIT compiler… JITコンパイラサポートにより2~5倍高速化. マニフェストに vmSafeMode=false を指定することでJITコンパイラによる最適化を無効化することができます. このオプションは後々AOTコンパイラを無効化するオプションに置きかわります.

その他… PlayServiceはこれ以前のバージョンでは対応していない.

2010年12月には OS2.3 Gingerbreadがリリースされました。
電池が何に使われたかを計測するバッテリー管理機能などが強化されています。

Androidのイースターエッグが搭載されたのもGingerbreadからです。
Gingerbreadでは ゾンビ ジンジャーブレッドマン の絵がイースターエッグで表示されます。
実際、ジンジャーブレッドは ゾンビ な状態になります。

スマホ向けOSの最新版としての期間が長かったことと、スマホブームが重なったこともあって 一時期は全体の60%を超えるシェアにまでGingerbreadは普及しました。
その後は、2015年にマシュマロがリリースされて、ようやくGingerbreadのシェアが10%を切ったぐらいに ”ゾンビ” な状態でした。

リリース:2010年12月 Android2.3 - Api Lv.9/10

2010年… 東北新幹線全線開業した年.

1touch word selection & copy/paste… テキストのロングプレスで単語が選択されフリー選択モードに移行するようになった.

Improved Power management… アプリがバックグラウンドで消費したCPUタイムをユーザが見られるようになるなどバッテリー管理機能が強化された.

その他… StrictMode搭載. Apache Harmony 6.0ベース化. システムアプリやシステムUIの刷新. Google PlayServiceのサポートはここから.

ここで、パフォーマンスに関する仕様変更についてみてみます。
OS5.0から実行環境がARTに置き換わりましたが、それまではDalvikでした。

OS2.2でJITコンパイラが搭載されたことで CPU使用率の高いコードのパフォーマンスが 最大で5倍改善されました。
OS2.3ではコンカレントGCが採用され、いわゆる”Stop the world”が改善されています。
OS5.0でランタイムがARTに置き換わり、OS7.0ではARTにJITコンパイラが採用されています。

JITコンパイラの採用によってDEXを ジャストインタイム方式で 実行形式にコンバートすればよくなるので、
アプリのインストールやアップデート、OSバージョンアップの時間が大幅に短縮されています。

ランタイムやコンパイラやGCアルゴリズムの違いによってパフォーマンスに差がでる場合もありますので、
ランタイムの違いぐらいは覚えておいて損はないと思います。

ARTはキットカットでも利用できますが オプショナルです。
標準搭載されたのはロリポップ以降ですので 78% の端末に搭載されています。

2011年 2月には OS3.0 Honeycomb がリリースされました。
なかには 黒歴史 という人もいるハニカムですが、重要なアップデートが多くあった OS です。

ActionBar, Fragment, Loader, ハードウェアアクセラレーション, ホログラフィックUIがリリースされています。
ホログラフィックUIはこのスライドデザインのように 黒背景に水色のアクセントカラーをもつテーマで、白背景もバリエーションとしてありましたが、
黒背景が印象的なUIでした。ハニカムは大画面向けのOSで、スマホ向けには配信されていません。

リリース:2011年2月 Android3.0 - Api Lv.11/12/13

New UI design for tablets… Android3.0はタブレットデバイスのような大画面向けのアップデートですが, その内容は後々スマホ向けにも展開され非常に重要なアップデート内容が多く含まれている.

ActionBar, Fragment, Loader… アプリのUI要素にActionBarが導入されました. ActionBarにはMenuキーをエミュレートするオーバーフローメニューが導入されました. また, ActivityをFragmentというサブコンポーネントに分割してMaster-Detail Flowのような柔軟な画面デザインを提供することができる. 開発者は画面の大きさが異なるスマートフォンとタブレット両方で動作するアプリケーションを効率よく作成できるようになります. またActivityやFragmentからの非同期ロードをサポートするLoaderも追加.

Holographic UI… システム全体に新しいUIテーマが適用され, デザインが一新されました. アプリはTheme.Holoを指定することでこれを適用できるようになります. Notificationの表現がリッチになり始めたのもこの頃です.

その他… クリップボードへのコピー&ペースト対応. ハードウェアアクセラレーションサポート.

2011年10月には OS4.0 IceCreamSandwichがリリースされました。
ハニカムの大画面向けUI Frameworkがスマホ向けにも移植され、統一UIフレームワークとなりました。
また、ハードウェアにMenuキーを搭載することが必須でなくなったのもこのタイミングからです。

リリース:2011年10月 Android4.0 - Api Lv.14/15

Unified UI framework… Honeycombで追加されたタブレット向け要素がスマートフォン向けにも引き継がれた. スマホでは画面が小さいことからアクションアイテムがActionBarに収まらない場合, 上下に分割するSplit ActionBarの実装もここから始まります.
ただし, SplitActionBarは現在では非推奨となっています.

MENUボタンがハードウェアに搭載されることは必須ではなくなり, オプションメニューを提供する場合はActionBarにオーバーフローメニューを配置する必要が出てきたのもこのバージョンからです.

2012年 6月には OS4.1 JellyBean がリリースされました。
16ms毎のvsyncやトリプルバッファリングによって、アニメーションやスクロールがより滑らかになりました。
Unicode6対応によって Unicode絵文字にも対応し、また, Google Play Service v1がリリースされたのもこの年です。

リリース:2012年6月 Android4.1~4.3 - Api Lv.16/17/18

Project Butter… 16ms毎のvsyncやグラフィクスのトリプルバッファリングにより, より早く, よりスムーズなユーザ体験を得られるようになりアニメーションやスクロール操作がより滑らかになりました. デバッグツールのsystraceがリリースされたのもこのタイミングです.

Unicode6.0… Unicode6.0絵文字がサポートされたのがこのOSからです. それまでの絵文字はキャリア絵文字でそれぞれ独自の文字コードが割り当てられていましたが, Unicode6.0絵文字がサポートされたことでキャリアを問わず絵文字が使えるようになりました.

Notification styles, GCM … NotificationにBigStyle/InBoxStyle/PictureStyleのスタイルが加わったのがこのバージョン. まだC2DMはGCMにリプレースされた.

その他… 2012年はAndroidMarketがGooglePlayに改名され, Androidアプリ以外のビデオや音楽も扱うストアサービスとして登場した.
GooglePlayServiceライブラリもこの頃にリリースされた. API.18でBluetooth GATTプロファイルに対応も対応した.

Unicode6対応で うれしいことは Unicode絵文字が使えるようになったことですね。
プッシュ文言にUnicode絵文字を使うサービスも増えてきましたが、Unicode絵文字が使えるのはOS4.3からで、それ以前のOSでは文字化けするものがあります。
また, 絵文字に色がついてカラフルになったのはOS4.4からです。

OS5.0では、人間に関わる絵文字はスライドにあるような黄色いキャラクターのグリフに差し替えられました。
OS6.0でUnicode 7と8をサポートし、また「お父さんの絵文字+お母さんの絵文字+子供の絵文字」を
Zero Width Joiner の文字コードで連結すると「家族」の絵文字、1文字に置き換わる仕様にも対応しています。

OS7.0ではUnicode9に対応し、5.0で対応されたnonhuman shapeのキャラクターが”人間”の見た目に戻りました。
絵文字には国や宗教、人種、思想に配慮した仕様になっていて複雑ですが、「human shape」な絵文字と Skin toneの文字コードを繋げることで
絵文字の肌の色を変えることができるようになり、絵文字のバリエーションがグッと増えました。

一応、国内キャリア端末は標準絵文字グリフをキャリア絵文字のグリフで上書きしているので、
OSが同じでもキャリアによって絵文字の見た目に違いがでる問題があることも, ここに付け加えておきます。

それぞれのUnicodeバージョンを搭載している端末の割合はこちらの通りで、
Unicode6が 94%、Unicode 7&8が 50%、Unicode 9が 18%です。

2013年 10月にはOS4.4 Kitkatがリリースされました。
エントリーレベルのデバイスでも動作できるように設計されたOSでストレージアクセスフレームワークが搭載されたのもここからです。
WebViewのアップデートもありましたが そちらは後ほどお話しします。

リリース:2013年10月 Android4.4 - Api Lv.19

Support 512MB RAM device… エントリーレベルのデバイスであっても動作するように設計されたOSで, アプリもActivityManager.isLowRamDevice() APIを使うことで低スペックデバイス向けのコンフィグレーションが可能になりました.

Storage Access Framework… これまで端末内のファイルをユーザに選択させたり, 保存場所を指定させる場合に使われるファイルエクスプローラはOSから提供されていませんでした. ストレージアクセスフレームワークを使うことでユーザに一貫したファイルシステムへの参照方法を提供できるようになりました.

Chromium WebView… WebKitがChromiumベースに差し代わりました. これよりChrome Dev Toolsによるリモートデバッグもサポートされるようになった.

その他… RTLサポートが強化されました. それまではテキストの対応しかなく, リソースを重複して持つ必要がありました.

Android4.4からは, バッテリー消費を抑えるためにアラームの発火タイミングが不正確になります。
4.4以降、どうしても正確なアラームが欲しい場合は AlarmManager の setWindow() か setExact() を使うことになります。

Android6.0は Dozeによるデバイスアイドル状態では アラームの発火が保留されます。
アイドル状態でもアラームを正確に発火させたい場合は setAndAllowWhileIdle() か setExactAndAllowWhileIdle() を使うことになります。

ちなみに、アラームはアプリごとに9分間に1回以上発火はされない仕様です。

2014年10月にはOS5.0 Lollipop がリリースされました。
マテリアルデザインによってUI/UXが大きく変更され、ベクタードロワブル や マルチユーザのサポート、
ARTの標準搭載、CPUの64bitアーキテクチャサポートなど、幅広いアップデート内容になっています。

リリース:2014年10月 Android5.0 - Api Lv.21/22

Material Design, Project Volta… マテリアルデザインの導入でUI/UXが大きく刷新された. RecyclerViewやZ軸, シャドウの概念もここから.
また, バッテリー消費を抑えて電池持ちを改善するプロジェクトProject Voltaが明らかにされました. ジョブスケジューラの機能が提供されたことにより, アプリの動作が最適化されバッテリー消費を抑えることに貢献しています.

Overview, Notification, Multi-user… OverviewはこれまでRecentsと呼ばれていた”最近使ったアプリーケション一覧”の機能に相当するものです.
従来は使ったアプリケーションのリストが並ぶだけでしたが, ここに複数のActivityをドキュメントとして追加することができるようになり, マルチタスクにも使えるようになりました. また, Notificationにはプライオリティやカテゴリの概念が追加され, 重要な通知がヘッドアップ表示されるようになったのもこの頃です.

64bit, ART… また, パフォーマンス改善も行われ, ARTランタイム対応や64bit対応もここから始まりました.

その他… Chromium WebViewがPlayStoreからアップデートできるようになった. AndroidHttpClientのメンテナンスが終了・廃止されURLConnectionの使用が必須に. API Lv20はAndroid Wear向けのAPI Lvとして割り当てられた. またディベロッパープレビュー版という提供方法が始まったのもここから.

WebView周りの変更についてみてみると、4.3までのWebViewは WebKit上で動作していましたが, 4.4以降はChromium上で動作します。
5.0では Google Play経由で アップデート可能になり、WebViewのセキュリティパッチが素早くユーザに届けられるようになりました。
WebViewに依存したアプリの開発者は WebViewのアップデート頻度が高くなったので注意する必要があります。

7.0 以降は Chrome APKから WebViewを提供する機能が搭載されています。これによって、メモリ消費が改善されました。
8.0では アプリのWebView がマルチプロセスモードで実行されます。ウェブコンテンツはアプリのプロセスとは別の独立したプロセスで処理されるので、
セキュリティが強化されています。

また、ここに記載していませんが Chrome custom Tab の機能がOS4.1以降で利用できるようになっています。

Chromium版は 93%の端末 に搭載されていて, WebViewを個別にアップデート可能な端末は 78% です。

2015年 10月にはOS6.0 マシュマロがリリースされました。
このあたりからアプリの挙動を変えるアップデートが目立つようになりました。
RuntimePermission, Doze, AppStandbyなどです。

リリース:2015年10月 Android6.0 - Api Lv.23

RuntimePermission… パーミッションモデルに大きな変更が入りました. ユーザはアプリのパーミッションを管理できるようになり, 好きなタイミングで権限を付与/剥奪できるようになります. また, アプリインストール時にパーミッション許可を求めることはせず, アプリの任意のタイミングでユーザにパーミッション付与を求めるようになります.

Doze, App Standby… 電源に接続していない状態で, 一定時間端末を画面オフで放置していた場合にスリープ状態を維持するDozeや, アプリが長時間アイドル状態であった場合にアプリのネットワークアクセスが無効になり, 同期とジョブが保留されるようになりました.

AutoBackup, Do not disturb… アプリデータが自動でGoogle Driveへバックアップできるようになりました. 追加のコードは必要ありません. バックアップを無効にする場合はマニフェストに1行無効にするフラグを定義します. また, Do not disturbモードもこのバージョンからです.

その他… Apache HTTP clientが削除. OpenSSLからBoringSSLに移行. TextSelectionもそれまでの編集モードからpopup windowでアクションを選択するUIに変更されました.

6.0のDoze機能は, 画面OFF かつ 充電中ではない場合 かつ 端末をほとんど動かさない静止状態 にし続けると、
CPU と ネットワーク通信 を一時保留して バッテリーの寿命を延ばす 省電力機能が働きます。

7.0ではDoze状態になる条件が緩和されて、端末が静止していなくてもDoze状態に入ります。
これによって、ポケットにスマホを入れて持ち歩いているような状況でも バッテリー消費を抑えることができるようになりました。

Dozeがリリースされたのは Android マシュマロ以降なので 50%の端末 がこれを搭載しています。

2016年8月には OS7.0 Nougat がリリースされました。
マルチウィンドウやRAMの使用量を削減するProject Svelteによって一部のブロードキャストが廃止されました。
また、ランチャーアイコンにまつわる変更もあります。

リリース:2016年8月 Android7.0/71 - Api Lv.24/25

Multi-window, Screen zoom… スマートフォンやタブレットで画面を分割して2つのアプリを並べて利用できるようになり, AndroidTVではピクチャーインピクチャーがサポートがサポートされました. また視力が低いユーザ向けの補助機能としてスクリーンズームが搭載され, 端末の画面密度設定が変更可能になりました.

Doze2, File security… 従来はDozeモードに突入するためには端末が静止状態である必要がありましたが, Doze2ではこの制限がなくなりました.
また, プライベートディレクトリのアクセス権限が厳格化され, 他アプリにファイルを直接読み書きさせることができなくなりました. これに伴いfileスキームのURIを含むIntentを共有しようとするとセキュリティ例外が投げられるようになっています.

Project Svelte… アプリのバックグラウンド実行を最適化することでRAMの使用量を削減する取り組み. CONNECTIVITY_ACTION、ACTION_NEW_PICTURE、ACTION_NEW_VIDEOの暗黙的なブロードキャストが削除されました. これらのブロードキャストは複数のアプリが同時に起動するため, メモリが逼迫しシステムのパフォーマンスを低下させる要因になるためです.

その他… 3DレンダリングAPIのvulkanがプラットフォームに統合, データセーバ機能の搭載, WebViewがChrome APKから提供される, VRサポート, App Shortcutなど. またこのタイミングでApache HarmonyベースからOpenJDKベースに移行された.

OS4.3 では 一般的なサイズよりも大きく アプリアイコンを表示するランチャーアプリに対応するため,
端末の抽象解像度ではなく、リクエストされたサイズに応じてリソースを返す mipmapリソースがサポートされました。

OS7.1では アプリアイコンを丸く表示するランチャーアプリが増えたため, アプリから丸いアイコンを提供するRound Iconリソースが追加されています。
OS8.0では さらに元のデザインを崩すことなく、自由にアプリアイコンの形を変えることができるAdaptive Iconがサポートされています。

そして 2017年8月の Android8.0 現在に至ります。

リリース:2017年8月 Android8.0 - Api Lv.26

Background execution limits… バックグランドによる動作が大きく制限されました。サービスを開始してもアプリがバックグラウンドに遷移するとサービスは自動で停止されます。
バックグランドからサービス開始したい場合はContext.startForegroundServiceメソッドをコールし, 5秒以内にフォアグラウンドサービスに昇格させる必要があります。
また、暗黙的なブロードキャストも制限されはじめ、JobShcedulerへの移行が推奨されています。

Notification dots, XML font… アプリの通知がランチャーアイコンにドットで表現されるようになったり, フォントをリソースとして扱えるXMLフォントの機能が導入されました。

Alert windows… システムウィンドウより上にアラートウィンドウを表示できなくなりました。アプリはTYPE_APPLICATION_OVERLAYウィンドウを使うことができます。

その他… HttpsURLConnectionが古いTSLバージョンへフォールバックする動作をやめる, WebViewのマルチプロセスモード実行, ANDROID-IDの処理方法変更, クリッカブルなViewがデフォルトでフォーカス可能に変更, スマホ/タブレットでのピクチャーインピクチャーモード対応, AppShortcutの改善, アダプティブアイコン, 最大アスペクト比, マルティディスプレイ, JobScheduler改善など

Android8.0 ではバックグラウンドで実行する動作を制限していますが、多くの場合 ジョブスケジューラに置き換えることができます。

これは ディベロッパーサイトの Intelligent Job-Scheduling というページからの引用で、
 ジョブを賢くスケジューリングすることで、バッテリ寿命といったシステム状態とともに、アプリのパフォーマンスも向上できます。
と書かれています。

バッテリー寿命というのは、モバイルユーザ体験の重要なポイントです。

Android を よりスマートで、より早く、よりパワフルなプラットフォームに仕上げるには、OSのバージョンアップだけではなく、
アプリの最適化によるバージョンアップも必要不可欠です。

OSに最適化する作業は大変ですが、プラットフォームにも デバイスにも また, ユーザにも優しいアプリ開発を心がけたいものです。

発表は以上ですが、本日紹介した内容は時間の都合上、細かな内容は省いています。
みなさんのサービスに関わる部分で気になるものがありましたら、これらのページを参考にしてみてください。



2017/07/25

VisibleForTestingとRestrictTo

昨日, メッセージの表示頻度を簡単に調整できるライブラリdenbunをリリースしました.

Denbun

初めてのライブラリリリースなので色々と学びがありました.
本稿ではVisibleForTestingRestrictToアノテーションについて書き留めます.

VisibleForTesting

フィールドやメソッドのスコープはできるだけ狭くすることが大切ですが, テスタビリティを確保するためにやむなくスコープを広くとる場合があります.
VisibleForTestingは, スコープをテスタビリティのために広く定義していることを明示します.

例えば, Denbunライブラリでは情報の永続化先であるSharedPreferenceとのI/Oをフックできるようにしてテスタビリティを確保しています.

@VisibleForTesting(otherwise = PACKAGE_PRIVATE)
public DenbunConfig daoProvider(@NonNull Dao.Provider provider) { ... }

このメソッドはプロダクションコードではPackage Privateスコープで扱われることを想定し, テストコードではPublicスコープで扱われることを想定しています.
そのため本来あるべきスコープはPackage Privateなのですが, テスタビリティのためにPublicとしています.

メソッドが本来あるべきスコープはVisibleForTestingアノテーションのotherwiseパラメータに指定します.
こうすることで, プロダクションコードにおいてPackage Privateスコープ外からアクセスしてきた場合にインスペクションによる警告が表示されるようになります.

ただし, このアノテーションはクラスファイルに影響を及ぼすものではないので, インスペクションの警告を無視して無理やり要素にアクセスすることは可能です.

VisibleForTestingの真価は, このアノテーションで指定された要素をプロダクションコードで呼び出すとインスペクションの警告によって使い方が間違っていることを教えてくれるところにあります.
これは, javadocにコメントを残す対応よりもはるかに効果的で簡単です. また, 利用側に実装者の意図をインスペクションを通して伝えることができるので利用側にとっても嬉しい機能です. 実際のライブラリ開発では手軽に導入できてアクセス制御で悩むことも減るのでとても便利に使えます.

ただ, 実際にはアクセス制御できていないので, APIを公開することが致命的であるケースにおいてはイミュータブルインタフェースをかませるなどの対応が必要です. (そのようなケースはあまり思い浮かびませんが, セキュリティが必要なSDKなどでは該当しそうです)

RestrictTo

次にRestrictToアノテーションです. これはテストのために用意されたメソッドであることを明示するものです.
VisibleForTestingはテスタビリティのための”スコープ”に着目しているので, そのメソッド自体は想定されるスコープ内であればプロダクションコードで呼ばれることが許されています.
例えば, VisibleForTesting(otherwise = private)なメソッドであればプロダクションコードでもクラス内(privateスコープ内)からの呼び出しが想定されているということです.

一方で, RestrictToはメソッド自体の存在に着目しています.
RestrictTo(TEST)であれば, テストコードからの呼び出しのみを想定しており, プロダクションコードでの呼び出しは想定されません. RestrictTo(LIBRARY)であれば, ライブラリ内での用途に限った要素であることを明示しています.
これはライブラリを作る側としてはとても強力です. これもVisibleForTestingと同じく, 呼び出し側が想定外の呼び出しを行なった場合にインスペクションの警告を表示します.

例えば, Denbunライブラリでは, DenbunBoxの初期化は一度しか行えず, 2回目以降はno-opになるよう実装されています.
しかし, UnitTestをする際にテストケースごとにDenbunBoxを再初期化したくなる場合も想定して, DenbunBoxの状態をリセットするreset()メソッドを用意しています.

@RestrictTo(TESTS) public static void reset() { ... }

このメソッドは, ライブラリ内部および, プロダクションコードからの呼び出しも想定していません. テストに限定した利用を想定したものです.

ライブラリを作る際には, こういったアノテーションも活用して, 利用する側に作り手の意図を明示するのも大切だなと感じました.

以上です.

Denbunライブラリでメッセージの表示頻度を調整する

tl;dr

はじめに

モバイルプラットフォームでは, ユーザ向けに何かしらのメッセージを表示することがよくあります.
それは, イベントの発生を知らせるものであったり, ユーザのアクションが完了したことを知らせるものであったり, エラーの発生を知らせるものであったりと様々です.
これらのメッセージは重要なものですが, 中には退屈と思われてしまうものもあります.

  • ユーザがアプリケーションの振る舞いを学習するのに重要なメッセージが, アプリケーションを使い慣れた後になっては, ただのお節介なメッセージになってしまうケース
  • 毎回閉じるだけの”お知らせダイアログ”といった類のもの
  • コンテンツの削除確認といった誤操作防止目的のもの
  • Backキーを押した際の「アプリケーションを終了しますか?」なもの

ユーザを退屈させないためにも, メッセージの表示頻度を調節することが重要です.

Denbun

メッセージの表示頻度を調整するためのアプローチはいくつかあります.

  • ダイアログに「今後表示しない」チェックボックスをつけてユーザ主動でダイアログ表示をやめさせる方法
  • 一度しか表示しないような回数限定メッセージ
  • 一週間のうち決まった曜日にだけ表示する定期的なメッセージ など…

これらのアプローチをとるためには, 表示設定や表示回数といった内容を永続化して都度, 表示頻度を調整する必要があります.
そこで, メッセージの前回表示時間や表示回数といった情報を保存し, 表示頻度の調整をサポートするDenbunライブラリをリリースしました.



このライブラリは, 次のようなメッセージ通知を実現したい場合に有効です.

  • 「今後表示しない」 オプション付きメッセージ
  • N回だけ表示するメッセージ
  • 定期的に表示するメッセージ(1週間に1回の頻度で表示. 月曜日に1回だけ表示. etc.)
  • N回表示した後は, n時間経過するまで表示しないメッセージ

メッセージの表現系(Dialog, Toast, Snackbar, etc.)は問いません.
このライブラリは, メッセージの前回表示時間や表示回数をSharedPreferenceに保存しており, これらの情報を駆使して”今, メッセージを表示すべきかどうか” を判断することで, メッセージの表示頻度を調整します.

使い方

まず初めに, Application.onCreateなどで, DenbunBoxを初期化します.
DenbunBoxはこのライブラリの起点となる重要なクラスです.

DenbunBox.init(new DenbunConfig(this));

DenbunBoxの初期化が終わったら, メッセージを表現するDenbunインスタンスを取得します.
メッセージの表示頻度の調節はこのDenbunインスタンスを通して行います.

Denbun msg = DenbunBox.get(ID);

Denbunインスタンスのshow()を呼び出すことで, 表示時間や表示回数の情報が更新され永続化されます.

Denbun msg = DenbunBox.get(ID);
msg.shown();

メッセージの最適な表示頻度はメッセージ毎に異なりますので, Denbunインスタンスを取得する際に最適な表示頻度を算出できるFrequency Adjusterを指定します.
例えば, 下記の例は1回限りのメッセージ通知を実現する例です.

// This message is displayed only once.
Denbun msg = DenbunBox.get(ID, new CountAdjuster(1));
...
msg.isShowable(); // true
msg.shown();
msg.isShowable(); // false

あるいは, メッセージを直接的に今後表示しなくすることも可能です.

Denbun msg = DenbunBox.get(ID);
msg.suppress(true);

メッセージによっては表示頻度の計算が複雑になるものもあるでしょうから, Frequency Adjusterは自前のものを実装してDenbunBox.getに指定することもできます.

実際にDialogやToastを表示する際には, DenbunインスタンスのisShowable()の値を確認してから表示すると決められた頻度でメッセージを表示することができます.

テスタビリティ

Denbunライブラリを使ったコードをテストしたい場合は下記が参考になります.
DenbunConfigにはDenbunライブラリとSharedPreferenceのI/Oを取り持つDAOのgetter/setterが用意されています(このメソッドは@VisibleForTestingです)

DenbunConfig conf = new DenbunConfig(app);

// spy original DaoProvider
Dao.Provider origin = conf.daoProvider();
conf.daoProvider(pref -> (spyDao = spy(origin.create(pref))));
DenbunBox.init(conf);

DenbunBox.find(ID).shown();
verify(spyDao, times(1)).update(any());

おわりに

Denbunライブラリを使い始めるには次の一文をbuild.gradleに追記するだけです.
<latest version>には最新のライブラリバージョンを指定してください.

compile 'com.yuki312:denbun:<latest version>'

近々v1.0.0をリリース予定です.
PRやIssueがあればGitHubの方に登録していただけると幸いです.

以上です.

2017/07/20

Intentの共有先一覧から自アプリを除外する

他アプリ起動周りでちょっとハマったのでメモ.

テキストやURIを暗黙Intentで共有する場合, 自アプリがそれに反応するintent-filterを持っていると, ActivityChooserに表示候補として含まれてしまう場合があります.
自アプリで捌きたくないから他アプリに共有しているのに, そのリストに自アプリが載っているのはよろしくない.
ということで, Intentは投げるけれどActivityChooserに自アプリを含めない方法を探りました.

TL;DR

  • createChooser, ChooserActivityまわりの挙動がOSバージョンで異なっている
  • API LV.23 前後でPackageManager.MATCH_DEFAULT_ONLYの振る舞いが変わる
  • API LV.23 前後でActivity選択ダイアログのレイアウトが変わる
  • 結論queryIntentActivitiesからの自前ダイアログ生成のが楽そう

シンプルにqueryIntentActivitiesIntent.createChooserを組み合わせればできるだろうと思っていたのですが, 古いOSで確認したところ意図した通りに動きませんでした.
で, 古いOSでの動作もサポートすべく, 色々検討した結果を残しておきます.

createChooser

Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(uri));
int flag = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M ? PackageManager.MATCH_ALL
    : PackageManager.MATCH_DEFAULT_ONLY;
List<ResolveInfo> launchers 
  = context.getPackageManager().queryIntentActivities(intent, flag); // *a

// 自アプリを起動対象から除外する
List<Intent> intents = new ArrayList<>();
for (ResolveInfo app : launchers) {
  if (context.getPackageName().equals(app.activityInfo.packageName)) {
    continue;
  }
  Intent target = new Intent(intent);
  target.setPackage(app.activityInfo.packageName);
  intents.add(target);
}

if (intents.isEmpty()) {
  // 起動対象のアプリが見つからなかった
} else {
  // createChooserの第一引数のIntentに反応できるアプリが存在しない場合は EXTRA_INITIAL_INTENTS
  // の指定が無視されるため, 必ず反応できるIntentを設定する目的でremove(0)を指定する.
  Intent chooser = Intent.createChooser(intents.remove(0), title); // *1
  chooser.putExtra(Intent.EXTRA_INITIAL_INTENTS, intents.toArray(new Parcelable[0])); // *1
  context.startActivity(chooser);
}

ポイントは *1 の部分で, 下記のコードではAPI Lv.23未満だとうまく動作しませんでした.

  Intent chooser = Intent.createChooser(new Intent(), title); // *1
  chooser.putExtra(Intent.EXTRA_INITIAL_INTENTS, intents.toArray(new Parcelable[0])); // *2

EXTRA_INITIAL_INTENTSに目的のIntentを設定すればうまくいきそうなものですが, API Lv.23未満だと *1 の第一引数Intentに反応できるActivityの数が0であった場合に EXTRA_INITIAL_INTENTS が無視される挙動になります(つまりActivityNotFound)
API Lv.23以上ではEXTRA_INITIAL_INTENTSが評価されます.

API Lv.23未満でcreateChooserの第一引数に渡すIntentは, 少なくとも1つ以上のActivityが反応できる必要があるので下記のようなコードになりました.

Intent chooser = Intent.createChooser(intents.remove(0), title); // *1
chooser.putExtra(Intent.EXTRA_INITIAL_INTENTS, intents.toArray(new Parcelable[0])); // *2

MATCH_ALL

*a で, PackageManager.MATCH_DEFAULT_ONLY はAPI Lv.23から挙動が変わっています.
API Lv.23未満だと, Category.DEFAULTに反応するActivityを抽出するものでしたが,
API Lv.23以上だと, 「既定で開く」設定されたActivityがある場合はそのActivityしか返却されなくなりました. API Lv.23以上でAPI Lv.23未満と同じ挙動にするためにはAPI LV.23から追加されたPackageManager.MATCH_ALLを指定する必要があります.

int flag = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M ? PackageManager.MATCH_ALL
    : PackageManager.MATCH_DEFAULT_ONLY;
List<ResolveInfo> launchers 
  = context.getPackageManager().queryIntentActivities(intent, flag); // *a

より便利にいくなら, API Lv.23以上でもMATCH_DEFAULT_ONLYResolveInfoを拾って, 「既定で開く」設定が自アプリになっていなければそのまま起動, 自アプリであれば上記の処理を実行するとすればいけそうです.

この処理でうまくいきましたが, デバイスによってはシェアダイアログのレイアウトが下記のように残念な結果に :(

動作をみる限りでは, createChooserに渡したIntentが1行目に並び, EXTRA_INITIAL_INTENTSに渡したIntentが2行目に並んでいる様子.
これを解決するならシェアダイアログを自前で組む必要がありそうです.
(あるいはAPI Lv.23ではcreateChooserの第一引数にどのActivityにもマッチしないnew Intent()といったIntentを指定するなど…)

API Lv.24からEXTRA_EXCLUDE_COMPONENTSなる定数も追加されているので, API Lv.24以上はこれを使えということかもしれませんが, こんなことにOSバージョン分岐させるのも面倒なので, 手っ取り早くやるならqueryIntentActivitiesからの自前ダイアログ作成が安定しているという結論に落ち着きました.

以上です.

2017/07/10

Replace Dialog to BottomSheet

従来はコンテンツを他アプリへ共有する際などにダイアログUIが使われていましたが,
昨今では, マテリアルデザインのModal bottom sheetsで説明されているように, ボトムシートUIにするのが一般的です.

ボトムシートを実装するにはいくつか方法がありますが, 既存のダイアログをボトムシートに変更したいだけであれば, AppCompatDialogを継承したBottomSheetDialogFragment/BottomSheetDialogを使うだけで比較的容易に対応できます.

// 継承元をDialogFragmentからBottomSheetDialogFragmentに変更
// public class MyDialogFragment extends DialogFragment
public class MyDialogFragment extends BottomSheetDialogFragment {

...

  @Override public Dialog onCreateDialog(Bundle savedInstanceState) {
    ...
    View view = binding.getRoot();
    MyBottomSheetDialog bottomSheet = new MyBottomSheetDialog(getContext());
    bottomSheet.setContentView(view);

    // ボトムシートダイアログを返却する
    return bottomSheet;
  }
}

ボトムシートの幅はスクリーンサイズに合わせて最大幅を調節することが推奨されています.

Screen width Minimum distance from screen edge (in increments) Minimum sheet width (in increments)
960dp 1 increment 6 increments
1280dp 2 increments 8 increments
1440dp 3 increments 9 increments

BottomSheetDialogの横幅を決めるためには, ダイアログの場合と同じくウィンドウの幅を調整する必要があります.
ウィンドウの幅はBottomSheetDialogのコンストラクタで指定することができます.

private static class ShareBottomSheetDialog extends BottomSheetDialog {
  @Override protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    // 横画面などでボトムシートが間延びしないように最大幅を設ける
    Optional.ofNullable(getWindow())
      .ifPresent(window -> window.setLayout(
        Math.min(displayWidth, maxWidth), 
        ViewGroup.LayoutParams.MATCH_PARENT);    

また, ボトムシート自体をどこまで引き出した状態で表示するかをpeekHeightを使って指定できます. peekHeightBottomSheetBehaviorで指定することができます.

bottomSheet.setContentView(view);

// 横画面などでもシェアアイコンが表示されるようにダイアログの高さ(peek)を確保する
BottomSheetBehavior behavior 
  = BottomSheetBehavior.from((View) view.getParent());
behavior.setPeekHeight(height);

以上です.

2017/04/28

DaggerのAndroid拡張を導入する(v2.11-rc1)

Dagger 2.11-rc1

Dagger2.10でdagger.androidモジュールがリリースされました.
本稿ではDagger2.10と2.11でリリースされたdagger.androidモジュールの使い方について簡単に紹介したいと思います.

本題へ入る前に, Dagger2.11では当然, 歴代のバージョンで追加されてきた機能を土台にしています.
Daggerを触ったことがない人は Android: Dagger2 を.
Subcomponentを使ったことがない人はAndroid: Dagger2 - Subcomponent vs. dependenciesを.
マルチバインディングを使ったことがない人はDagger2. MultibindingでComponentを綺麗に仕上げるを一度読んでから本稿に戻ってくると理解しやすいと思います.

また今回紹介するコードのリポジトリは下記に公開してあります.
Dagger2.11正式リリースタイミングでも更新していくので, よろしければ ⭐️ をお願いします :)

YukiMatsumura/AndroidDaggerSample

Dagger(Dependency Injection)を最大限に活かせるのは, 依存オブジェクトをDagger自身が生成して, 依存性を満たすようにデザインすることでしょう. しかし, AndroidはActivityやFragmentといったOSが生成・管理するオブジェクトがあり, Daggerが全てを生成・管理することができません.
そうした場合, 次のようにフィールドインジェクションを使って依存性を満たすことになります.

public class MainActivity extends Activity {
  @Inject Hoge hoge;

  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    // 必ず最初に実行すること!
    ((App) getContext().getApplicationContext())
        .getApplicationComponent()
        .newActivityComponentBuilder()
        .activity(this)
        .build()
        .inject(this);
    // ...
  }
}

これにはいくつかの問題があります.

  1. まず, ActivityやFragment, Service, ContentProviderといったOS管理のクラスへインジェクションする数だけコピペコードが出来上がり, メンテナンス性を悪くします.
  2. そしてなにより, クラスが依存性を注入するオブジェクト(ComponentやModules)のことについてそれぞれのクラスが知っている必要があるため, Dependency Injectionのコア原則を破っています.

今回紹介するdagger.androidモジュールを導入すると, これらの問題を解決することができます.

NOTE:
android.daggerモジュールはまだBetaバージョンのため今後変更される可能性があります.  
今でもクラス名がリネームされるなどしているため, 他でコードを参考にされる場合はdaggerのバージョンに注意する必要があります.  

本稿では現時点で最新のリリースバージョンDagger2.11-rc1を対象にしています.  
StableのDagger2.10からの変更点もありますので, Dagger2.10を使う場合は変更点にご注意ください.  

Dagger2.10 -> 2.11の変更点:
 - New API: @ContributesAndroidInjector simplifies the usage of dagger.android
 - All HasDispatching*Injectors are renamed to Has*Injector. They also return an AndroidInjector instead of a DispatchingAndroidInjector
 - Added DaggerApplication and DaggerContentProvider

リネーム情報はGitHubのリリースページに記載されています.  
https://github.com/google/dagger/releases

依存ライブラリの追加

まずはDagger2.11のライブラリを追加しないとはじまりません.
build.gradleのdependenciesに次のライブラリを追加します.

  // Core dependencies
  compile 'com.google.dagger:dagger:2.11-rc1'
  annotationProcessor 'com.google.dagger:dagger-compiler:2.11-rc1'

  // Android dependencies
  compile 'com.google.dagger:dagger-android:2.11-rc1'
  annotationProcessor 'com.google.dagger:dagger-android-processor:2.11-rc1'

  // Require if use android support libs.
  compile 'com.google.dagger:dagger-android-support:2.11-rc1'

dagger-android-*なモジュールがDaggerのAndroid拡張です.
プロジェクトでサポートライブラリを使用している場合はdagger-android-supportも必要です.

余談ですが, 手元の環境ではfindbugsのdependencyでコンフリクトが起きたので, 合わせて解消しています.

エラー:
Error:Conflict with dependency 'com.google.code.findbugs:jsr305' in project ':app'. Resolved versions for app (3.0.1) and test app (2.0.1) differ. See http://g.co/androidstudio/app-test-app-conflict for details.

解決: espresso-coreの依存モジュールからjsr305をexcludeしておく
  androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
    exclude group: 'com.android.support', module: 'support-annotations'
    exclude group: 'com.google.code.findbugs', module: 'jsr305'
  })

Daggerのライブラリを取得したらComponent, Moduleを作成していきましょう.

ActivityのComponent/Module作成

順を追って必要なオブジェクトを作って行きます. まずはMainActivityに紐づくMainComponentの定義から.

MainComponentはこのあと作るアプリケーションコンポーネントのサブコンポーネントとして定義するので@Subcomponentアノテーションをつけます.
さらに, コンポーネントビルダー@Subcomponent.Builderを同じく宣言します.

package com.yuki312.androiddaggersample;

import dagger.Subcomponent;
import dagger.android.AndroidInjector;

@Subcomponent
public interface MainComponent extends AndroidInjector<MainActivity> {
  @Subcomponent.Builder
  abstract class Builder extends AndroidInjector.Builder<MainActivity> {
  }
}

MainComponentにはAndroidInjectorインタフェースを継承させます.
AndroidInjectorはAndroidのコアコンポーネント(Activity, Fragment, Service, BroadcastReceiver, ContentProvider)に依存性を注入するメソッドinject(T)を定義したインタフェースです.

次にMainModuleを定義します.

import android.app.Activity;
import dagger.Binds;
import dagger.Module;
import dagger.android.ActivityKey;
import dagger.android.AndroidInjector;
import dagger.multibindings.IntoMap;

@Module
public abstract class MainModule {

  @Binds @IntoMap @ActivityKey(MainActivity.class)
  abstract AndroidInjector.Factory<? extends Activity> bindInjectorFactory(
      MainComponent.Builder builder);
}

@ActivityKeyでのMainActivity.class指定は, 後ほど説明する適切なAndroidInjector.Builderを選択するための型情報に必要なものです.
Androidの各コアコンポーネント専用のInjectorを生成するファクトリをここで指定します. AndroidInjectorについては後ほど説明します.

続いてアプリケーションクラス用のAppModule.

package com.yuki312.androiddaggersample;

import dagger.Module;

@Module(subcomponents = { MainComponent.class })
public class AppModule {
}

そしてAppComponent.

package com.yuki312.androiddaggersample;

import dagger.Component;
import dagger.android.AndroidInjector;
import dagger.android.support.AndroidSupportInjectionModule;

@Component(modules = { AndroidSupportInjectionModule.class, AppModule.class, MainModule.class })
public interface AppComponent extends AndroidInjector<App> {

  @Component.Builder
  abstract class Builder extends AndroidInjector.Builder<App> {
  }
}

modules={...}にはインジェクションモジュールを含める必要があります.
インジェクションモジュールには次の種類が用意されています.

  • AndroidInjectionModule.class(サポートライブラリを使わない場合)
  • AndroidSupportInjectionModule.class(サポートライブラリを使う場合)

インジェクションモジュールには, AndroidのコアコンポーネントにinjectするComponent/SubComponentのファクトリクラスであるAndroidInjector.Factoryを値に持つMapがAndroidコアコンポーネント毎に定義されており, それぞれのインスタンスはマルチバイインディングの仕組みで構築されています.

@Module
public abstract class AndroidInjectionModule {
 @Multibinds
  abstract Map<Class<? extends Activity>, AndroidInjector.Factory<? extends Activity>>
      activityInjectorFactories();

  @Multibinds
  abstract Map<Class<? extends Fragment>, AndroidInjector.Factory<? extends Fragment>>
      fragmentInjectorFactories();

  @Multibinds
  abstract Map<Class<? extends Service>, AndroidInjector.Factory<? extends Service>>
      serviceInjectorFactories();
 ...

AndroidInjectionModule, AndroidSupportInjectionModuleAndroidInjector.Factoryの管理に必要であることがわかります.
アプリケーション全体に渡るコアコンポーネントを管理するため, 基本的にはApplicationスコープのコンポーネントで管理することになります.
AppComponentにはビルダーAndroidInjector.Builderも忘れずに定義しておきます.

DaggerApplication

次にApplicationクラスの定義です.
Applicationクラスには各Androidコアコンポーネント用のAndroidInjectorを定義する必要があります.
AndroidInjectorはActivityやFragmentといったコアコンポーネントに依存性を注入するためのインジェクター用のインタフェースです.
コアコンポーネント用のインジェクターには次のものがあります.

  • HasActivityInjector
  • HasFragmentInjector,
  • HasServiceInjector,
  • HasBroadcastReceiverInjector,
  • HasContentProviderInjector
  • HasSupportFragmentInjector(dagger-android-support)

それぞれのインタフェースには各コアコンポーネント専用のインジェクターを返すメソッドが定義されているわけですが, Applicationクラスでこれら全てのインジェクターを実装するのは面倒なので, Dagger2.11ではDaggerApplicationクラスが提供されました.

  • dagger.android.DaggerApplication(サポートライブラリを使わない場合)
  • dagger.android.support.DaggerApplication(サポートライブラリを使う場合)

Dagger2.11-rc1ではサポートライブラリ対応/非対応でクラス名が同じなのでextendsする際には注意が必要です.
また, DaggerApplicationはApplication用のインジェクターを返すapplicationInjectorをabstractメソッドとして定義してあるので, これをオーバーライドしておきます.
これで, Applicationクラスへのフィールドインジェクションもサポートされます.

package com.yuki312.androiddaggersample;

import dagger.android.AndroidInjector;
import dagger.android.support.DaggerApplication;

public class App extends DaggerApplication {

  @Override protected AndroidInjector<? extends DaggerApplication> applicationInjector() {
    return DaggerAppComponent.builder().create(this);
  }
}

仕上げ

最後の仕上げにMainActivityでフィールドインジェクションを実装しましょう.

package com.yuki312.androiddaggersample;

...
import dagger.android.AndroidInjection;

public class MainActivity extends AppCompatActivity {

  ...

  @Override protected void onCreate(Bundle savedInstanceState) {
    AndroidInjection.inject(this);
    super.onCreate(savedInstanceState);
    ...
  }
}

AndroidInjection.inject(this);. たったこれだけです! 簡単ですね:)
従来のComponentやModuleの指定が現れないのでDependency Injectionの原則にも忠実です.

おまけ

dagger-android-support は何者か

dagger.androidの肝はAndroidコアコンポーネントへのインジェクションサポートです.
今回登場した HasSupportFragmentInjector, AndroidSupportInjectionModule, dagger.android.support.DaggerApplicationが主にサポートライブラリ向けのクラスになります.
これらの中身を覗くと, android.support.v4.app.Fragmentのためのバインディングマップであったり, インジェクターであったりの処理が定義されています.
つまり, サポートライブラリのFragmentを使ったinjectionをサポートするためにこれらのライブラリが必要になってきます.
サポートライブラリのFragmentを使わないのであれば必ずしも必要というわけではなさそうですね.

コアコンポーネントのInjectorはどうやって選ばれる?

ActivityやFragmentといったコアコンポーネントのインジェクターはAndroidInjectionModuleに定義されたAndroidInjector.Factoryから生成することができますが, これが設定されているマルチバインディングで構築されたMapからファクトリインスタンスを取り出す操作はDispatchingAndroidInjectorが行なっています.
DispatchingAndroidInjectorはDaggerが生成するオブジェクトであるためアプリケーション側から直接触ることはないと思いますが, dagger.androidの内部動作を把握するには押さえておく必要のあるクラスです.

ContentProviderInjectorとApplicationInjector

Androidの仕組み上, アプリケーションプロセスがCygoteからforkされて開始される際, ContentProviderの初期化はApplicationの初期化より早いです.
つまり, ActivityやBroadcastReceiver, Serviceなど他のコアコンポーネントと唯一異なってContentProviderのonCreate時にはまだApplicationクラスが初期化(onCreate)されていない可能性があります.
DaggerApplicationクラスを覗くとこの辺りをどう解決しているのかをうかがい知ることができます.

  // injectIfNecessary is called here but not on the other *Injector() methods because it is the
  // only one that should be called (in AndroidInjection.inject(ContentProvider)) before
  // Application.onCreate()
  @Override
  public AndroidInjector<ContentProvider> contentProviderInjector() {
  ...


  /**
   * Lazily injects the {@link DaggerApplication}'s members. Injection cannot be performed in {@link
   * Application#onCreate()} since {@link android.content.ContentProvider}s' {@link
   * android.content.ContentProvider#onCreate() onCreate()} method will be called first and might
   * need injected members on the application. Injection is not performed in the the constructor, as
   * that may result in members-injection methods being called before the constructor has completed,
   * allowing for a partially-constructed instance to escape.
   */
  private void injectIfNecessary() {
    if (needToInject) {

この他にも, コアコンポーネントのComponent/Module定義を簡略化できる@ContributesAndroidInjectorや, コアコンポーネントインスタンスをパラメータにとるProviderメソッドの提供方法などもありますが, 本稿では割愛します.

ひとまず, dagger.androidパッケージがどのようなものになる予定なのか, 本稿で大まかにでも掴めたようでしたら幸いです.
rcがとれて, Dagger2.11が正式リリースされたタイミングで俯瞰図なども描きたいと思います.

以上です.

2017/04/27

DataBindingでViewのtagにenumを設定する

DataBindingを使えばViewのtagフィールドに好きなオブジェクトを差し込めるので↓のような実装を試してみました.
固定長リストをlayout.xmlで定義する際にenumを設定すれば, onClickリスナーでそれを取り出して使うことができます.

キャストする箇所がアレですが,,

<layout>
<data>
 <import type="hoge.foo.Type"/>
</data>

<LinearLayout
  ...
  >
    <TextView
      ...
      android:tag="@{Type.A}" />

    <TextView
      ...
      android:tag="@{Type.B}" />

    <TextView
      ...
      android:tag="@{Type.C}" />

    <TextView
      ...
      android:tag="@{Type.D}" />
</LinearLayout>
</layou>
@Override public void onClick(View v) {
  Object tag = v.getTag();
  if (tag == null || !(tag instanceof Type)) return;

  Type type = Type.class.cast(tag);
  ...
}

以上です.

2017/04/25

レイアウトのサイズ指定で足し算する

Viewのサイズを指定する時に, (A)のサイズと(B)のサイズの和を指定したい場合があります.
AとB, どちらもアプリで定義しているサイズであれば, その和を新たなdimensとして定義することもできますが,
例えば “アクションバーの高さ + 8dp” など, 片方がアプリの管理下にない場合はdimensで定義することができなくなります.
コード上で指定することもできますが, レイアウトの問題はレイアウトXMLで完結させたいところ.
理想としては下記のような指定ができれば良いのですが, Androidではこれができません.

<View
  android:paddingTop="?attr/actionBarSize + 8dp" />

そこで, DataBindingの”式”を使えばそれっぽく書くことができます.

<layout>
 <data>
  <import type="hoge.foo.Dimens"/>
  <import type="hoge.foo.Dimens.ActionBar"/>
 </data>

 <View
   android:paddingTop="@{ActionBar.height(context) + Dimens.dpToPx(context, 8)}"
public final class Dimens {
  @Px public static int dpToPx(Context c, int dp) {
    DisplayMetrics metrics = c.getResources().getDisplayMetrics();
    return (int) (TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, metrics) + 0.5f);
  }

  public static class  ActionBar  {
    @Px public static int height(Context c) {
      TypedValue tv = new TypedValue();
      if (c.getTheme().resolveAttribute(android.R.attr.actionBarSize, tv, true)) {
        return TypedValue.complexToDimensionPixelSize(tv.data,
            c.getResources().getDisplayMetrics());
      }
      return 0;
    }
  }

以上です.

2017/04/16

モバイルアプリ開発エキスパート養成読本

この度「モバイルアプリ開発エキスパート養成読本」をご恵贈頂きました. ありがとうございます.

ご恵贈頂いたことに加えて, この本の執筆メンバーでもある@shihochanさん, @ogaclejapanさんからの頼みとあっては, 何も書かないというわけにはいきませんので, 拙文ではありますが色々と本稿で書かせて頂きます.
*この本はAndroidとiOSをカバーしていますが, 私はAndroiderなので本稿ではAndroidに関する部分に絞っています.

この本ではリアクティブプログラミング, ビルドバリアント, DI, ユニットテストや運用に役立つツールが紹介されており, 流行り廃りの早いライブラリの類ではなく, 息の長いアプリを開発する上で押さえておくべきポイントがほどよくまとまっています. 本稿ではこの本で取り上げられている内容をいくつかピックアップし, この本を執筆されていたであろう時点から今時点までの間で動きのあった技術情報などを加味して色々と書いていきます.

リアクティブプログラミング

この本では, リアクティブプログラミングについての概念的な部分やRxJavaと, RxJava2の変更点について書かれています.
Android Studio 2.4 Preview 6でJava8構文サポートのアップデートがリリースされ, RxJavaを使う際には是非とも導入したいラムダ式が標準でサポートされるようになるのももうすぐです.

ビルドバリアント

この本では, アプリケーションの開発版・リリース版, 無料版・課金版など, apkのビルドモードや複数バージョンを効率よく作成できるビルドバリアントの仕組みが紹介されています.
ビルドバリアントを使い始めると様々なビルドタイプとプロダクトフレーバーの組み合わせを作りたくなります.
その時はフレーバーディメンションが便利です.

DI

この本では, DIフレームワークのDagger2が紹介されています.
新しいDagger2.10ではdagger.androidパッケージが追加され, AndroidでよりシンプルにDIできるようになりました.

ユニットテスト

この本では, ユニットテストやUIテストの方法についても紹介されています.
リアクティブプログラミングの章でも紹介されていますが, RxJavaをプロジェクトで使っている場合, ObservableのスケジューラをTestSchedulerImmediateSchedulerに差し替えたくなるケースがよくあります.
次のTestRuleを用意することでテストメソッド毎のスケジューラ切り替えを楽にできます.

package hoge.foo;

import com.android.annotations.NonNull;
import java.util.Objects;
import org.junit.rules.TestRule;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;
import rx.Scheduler;
import rx.android.plugins.RxAndroidPlugins;
import rx.android.plugins.RxAndroidSchedulersHook;
import rx.android.schedulers.AndroidSchedulers;
import rx.plugins.RxJavaHooks;
import rx.schedulers.Schedulers;
import rx.schedulers.TestScheduler;

/**
 * RobolectricおよびJUnitテストにおけるScheduler各種をImmediateSchedulerとするルール.
 * スケジューラを差し替えたい場合はコンストラクタに指定する.
 *
 * @see SchedulerWith
 * @see SuppressSchedulerRule
 */
public class RxSchedulerRule implements TestRule {

  private final Scheduler defaultScheduler;
  private Scheduler scheduler;

  public static RxSchedulerRule immediate() {
    return new RxSchedulerRule(Schedulers.immediate());
  }

  public static RxSchedulerRule test() {
    return new RxSchedulerRule(Schedulers.test());
  }

  public RxSchedulerRule(@NonNull Scheduler scheduler) {
    this.defaultScheduler = Objects.requireNonNull(scheduler);
    this.scheduler = defaultScheduler;
  }

  /**
   * 現在設定されているスケジューラを取得
   */
  public Scheduler scheduler() {
    return scheduler;
  }

  /**
   * 現在設定されているスケジューラを{@link TestScheduler}にキャストして取得
   *
   * @throws ClassCastException 現在設定されているスケジューラが{@link TestScheduler}にキャストできない場合
   */
  public TestScheduler testScheduler() {
    return (TestScheduler) scheduler;
  }

  @Override public Statement apply(Statement base, Description description) {
    return new Statement() {
      @Override public void evaluate() throws Throwable {
        resetSchedulers();

        // スケジューラの上書き抑止
        if (description.getAnnotation(SuppressSchedulerRule.class) != null) {
          base.evaluate();
          return;
        }

        // 個別に指定されたスケジューラがあればそちらを優先
        SchedulerWith annotation = description.getAnnotation(SchedulerWith.class);
        scheduler = annotation != null ? annotation.value().scheduler : defaultScheduler;

        RxAndroidPlugins.getInstance().registerSchedulersHook(new RxAndroidSchedulersHook() {
          @Override public Scheduler getMainThreadScheduler() {
            return scheduler;
          }
        });
        RxJavaHooks.setOnComputationScheduler(s -> scheduler);
        RxJavaHooks.setOnIOScheduler(s -> scheduler);
        RxJavaHooks.setOnNewThreadScheduler(s -> scheduler);

        try {
          base.evaluate();
        } finally {
          resetSchedulers();
        }
      }

      private void resetSchedulers() {
        RxJavaHooks.reset();
        AndroidSchedulers.reset();
        RxAndroidPlugins.getInstance().reset();
      }
    };
  }
}
package hoge.foo;

import rx.Scheduler;
import rx.schedulers.Schedulers;

/**
 * テストケースで実行するスケジューラの種類.
 *
 * @see SchedulerWith
 */
public enum SchedulerType {

  /**
   * {@link Schedulers#immediate()}
   */
  IMMEDIATE(Schedulers.immediate()),

  /**
   * {@link Schedulers#io()}
   */
  IO(Schedulers.io()),

  /**
   * {@link Schedulers#computation()} ()}
   */
  COMPUTATION(Schedulers.computation()),

  /**
   * {@link Schedulers#newThread()}
   */
  NEW_THREAD(Schedulers.newThread()),

  /**
   * {@link Schedulers#test()}
   */
  TEST(Schedulers.test());

  public final Scheduler scheduler;

  SchedulerType(Scheduler scheduler) {
    this.scheduler = scheduler;
  }
}
package hoge.foo;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import rx.schedulers.Schedulers;

/**
 * {@link RxSchedulerRule}で指定したスケジューラとは異なるスケジューラをテストケースで指定できる.
 * これは, テストケース全体としては{@link Schedulers#immediate()}を使用するテストルールを採用しているものの,
 * 一部のテストケースでのみ{@link Schedulers#test()}を使用したい場合などに使用される.
 *
 * <code>
 *
 * @Rule public RxSchedulerRule rxSchedulerRule = new RxSchedulerRule();
 * @SchedulerWith(SchedulerType.TEST)
 * @Test public void testcase() throws Exception { ... }
 * </code>
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface SchedulerWith {

  /**
   * このテストケースでのスケジューラを指定.
   * 指定がない場合は{@link SchedulerType#IMMEDIATE}が指定される.
   */
  SchedulerType value() default SchedulerType.IMMEDIATE;
}
package hoge.foo;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * {@link RxSchedulerRule}によるスケジューラの上書きを抑止できる.
 * このアノテーションが付与されたテストケースではスケジューラがテストルールによって上書きされない.
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface SuppressSchedulerRule {
}

本稿では, モバイルアプリ開発エキスパート養成読本で取り上げられている技術のごく一部をピックアップしてみました.
この本ではiOSも扱っており, Androidと同様にアプリを開発する上で押さえておきたいポイントがまとめられています.

「”とりあえず動くAndroidアプリ”を作ってきたけれど, 最新の技術を取り入れてワンランク上のアプリ開発がしたい」「いまどきのAndroidアプリ開発事情を知りたい」そんな方に向けて, 私はこの本をおすすめします.

以上.

2017/03/25

ConstraintLayout

勘所

ConstraintLayoutは名前の通り, 制約によってレイアウトを組むものです.
今までのLinearLayoutやRelativeLayoutのようにViewの配置によるレイアウトから考え方を変えて, レイアウトを制約で定義することによってレスポンシブUIのような柔軟なUIや複雑なレイアウトをよりフラットなViewヒエラルキで実現できるようになり, パフォーマンスの向上が期待できます.

ConstraintLayoutについては下記のDeveloperサイトにまとめられています.
導入方法まで含めた動画も公開されています.

Build a Responsive UI with ConstraintLayout
https://developer.android.com/training/constraint-layout/index.html

Layout Editorの使い方.
https://developer.android.com/studio/write/layout-editor.html

本稿はドキュメントを読むだけではわからなかった箇所+簡易な基礎説明を主に載せています.

Horizontal / Vertical constraint

ConstraintLayoutにおけるViewのポジションは水平 and 垂直方向の制約(Constraint)を定義することで指定します. ConstraintLayoutにおいてViewは水平方向と垂直方向の制約両方を定義しなければなりません.

制約はRelativeLayout`の相対位置指定に一見似ていますが, RelativeLayoutがView自体のポジションを定義するのに対して, ConstraintLayoutの制約はViewのルール(制約)を決めるだけであり, Viewのサイズ指定には別のパラメータが用意されています.

従来のレイアウトより高次な”制約”という概念がConstraintLayoutには追加されており, これがよりレスポンシブなUIを作成する助けとなっています.
下記のイメージはImageViewの左辺/下辺が親レイアウトに, ImageViewの上辺/右辺がTextViewに整列する”制約”を追加した例です.

ImageViewが制約の範囲内でレイアウトされているのがわかります.
さらに, Viewのポジションを決定する要素として”バイアス”が追加されました. これは決められた制約の中でViewの位置を決めるものです.

ConstraintLayoutのlayout_width / height

ConstraintLayoutでは従来のmatch_parentが廃止されました.
Viewのサイズ(width/height)指定には次の3つの考え方があります.

  • Wrap Content
  • Match Constraints
  • Fixed

Wrap Content

Viewコンテンツに必要な分だけの領域をサイズとします. 従来のそれと同じ効果のものです.
XMLでの指定も同じです android:layout_width="wrap_content"

Match Constraints

制約のルールを満たす範囲内で指定できる最大限の領域をサイズとします.
以前のmatch_parentは廃止され, かわりにMatch Constraintsを指定することになります.

例えば, 画面の端から端までのViewサイズを定義するのであれば, 親レイアウトの両端に制約を追加してMatch Constraintsを指定すれば完成です.
制約のルールを満たす範囲内で指定できる最大限の領域をサイズとするわけです.

ちなみに, match_constraintsという定義値はありません. Viewのサイズが0(dp)である場合, ConstraintsLayoutがMatch Constraintsとして扱います.
そのため, レイアウトXML上はandroid:layout_width="0dp"となります.

これに加えて, Match Constraintsに限りwidth or heightをもう一方のheight or widthとのratio(比率)で指定することができます(比率はwidth:heightの順)

例えば, widthをheightの2倍にしたい場合は次のように指定します.

android:layout_width="0dp"
android:layout_height="100dp"
app:layout_constraintDimensionRatio="w,2:1"

あるいは, 次のように操作します.

heightは固定値で, widthにはMatch Constraint(0dp)を指定しています. さらに, widthのサイズ比率の制約を定義するapp:layout_constraintDimensionRatioを指定します.
例では, ここに"w,2:1"と定義しています. この値の意味は次の通りです.

`w`(width)のサイズを 2(width) : 1(height) の比率で指定する

heightが100dpで指定されているので, 結果的にwidthは200dpになります.
widthを固定サイズとしてheightをratio指定することも可能です. その場合は

android:layout_width="100dp"
android:layout_height="0dp"
app:layout_constraintDimensionRatio="h,2:1"

といった具合になります. 上記の例だとwidthが100dpなので, 2(width) : 1(height) の割合で結果的にheightは50dpのサイズになります.

もし, widthとheight両方にMatch Constraint(0dp)が指定されている場合, ratioはwidthを基準とするのか, あるいはheightを基準とするのかを選択することになります.

親レイアウトの上下左右の両端に制約をもたせたMatch ConstraintなImageViewでratioを指定する例をみてみます.

まず, Match Constraintがwidth/height両方に指定されていますので親レイアウトいっぱいにViewが広がります.
その後, heightのサイズを 1:1 とするratioが指定されますが, 親レイアウトのwidthがheightより大きいので, 親レイアウトの高さに収まらなくなっています.
次に, widthのサイズを 1:1 とするratioに切り替えられます. これによってheightはMatch Constraintにより親レイアウトの高さに収まり, widthはheightと同じサイズ(ratio 1:1)が適用されているのがわかります.
最後には, 親レイアウト(画面)のwidth/hight比率と近似の 16:9 に設定されています. ratioを適用するのは短辺にあたる height が指定されています.
レイアウトXMLには次のように定義されます.

android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintDimensionRatio="h,16:9"

Fixed

名前の通り, 固定値でサイズを指定するものです.
指定方法も従来と変わりありません. android:layout_width="48dp"

Text Baseline alignment

テキストのベースラインでViewのポジションを調整したい場合はベースライン制約を追加します.

Guideline

垂直または水平なガイドライン(基準線)を定義することができます.
これによって, 親レイアウトの端ではなく, 特定の余白を上下左右に設けたレイアウトの定義が楽になります.

タブレットのような大画面時に左右余白をもたせたい場合など, ガイドラインの位置を調整するだけで, これを制約とするViewの相対位置が変化するためより直感的なレイアウトを組むことができます.

layout_goneMargin

ConstraintLayoutではViewのVisibilityがGONEに設定されても, 他の制約が崩れないよう, サイズ0のViewがいるかのように振る舞います.

もし, 依存先のViewが存在しなくなった場合のマージンを別で指定したい場合は, layout_goneMarginStart / Top / End / Bottomを使うことができます.

以上です.