2015/05/31

デバイス認証 - Confirm Credential

はじめに

caution!
本稿はAndroid M Preview向けに限られた内容です.
本稿の一部または全ては既存のAndroid versionやAndroid M正式版およびそれ以降のAndroid versionにおいて有効ではない可能性があります.

本稿の一部はAuthenticationの内容を参考としています. より正確な情報を得たい場合はこちらもご参考ください.
また, 本稿の説明で例示しているソースコードの完全版はGitHub上に公開しています.

Introduction

あなたのアプリケーションでDevice Credential, つまりデバイス認証機能をより簡単に使用できるAPIをAndroid M Previewは提案している.

ユーザが特定のアクションを起こす前にその端末で使用されているデバイス認証機能を利用してユーザ認証を行うことができる.
デバイス認証にはPIN, Pattern, Passwordといった認証方法が用意されている. 使用される認証形式については端末に設定されている認証形式, つまりその時端末に設定されているセキュリティロックの解除方式に左右される.
この機能を使用することで, ユーザはアプリ固有のパスワードといった認証情報を個別に覚えておく必要がなくなる.

Confirm Credentials

Confirm Credential

アプリケーションがデバイス認証をより簡単に, よりセキュアに使用できるAPIがいくつか追加された.
アプリケーションはデバイス認証状態を確認し, 必要に応じて認証画面を呼び出すことができる. アプリケーションはデバイスの認証画面をIntentで呼び出し, その結果をonActivityResultの形で得る.

デバイス認証にはタイムアウトの概念が設けられている. これは一度ユーザがあなたのアプリケーションでデバイス認証を行ったあと, 指定時間の間は認証がパスされる仕組みだ.

指定時間はデバイス認証を解除するセキュリティ鍵(共通鍵暗号方式)の有効時間として設定され, 新たに設けられたKeyGenParameterSpec.setUserAuthenticationValidityDurationSeconds()のAPIとKeyGeneratorまたはKeyPairGeneratorを使用して実現できる.

アプリケーションはユーザ体験を損ねないようデバイス認証が過度に表示されないように注意しなければならない.
先述のセキュリティ鍵をもってデバイス認証解除を試み, もしタイムアウトを満了しているようであれば
createConfirmDeviceCredentialIntent() メソッドで再度デバイス認証を行うこと.
認証の結果はstartActivityForResult(Intent, int)で返却されRESULT_OK(認証成功)かどうかをチェックするだけでよい.

デバイス認証をパスすることはデバイス操作を許可されたユーザであることを意味するが, 操作しているユーザが必ずしもデバイスオーナーである保証はない. また, デバイス認証は必ずしもユーザが設定しているとは限らない. この場合createConfirmDeviceCredentialIntent()の結果としてnullが返却される.
事前にユーザがデバイス認証の設定を行っているかを確認したい場合はKeygardManager.isKeyguardSecure()を使用できる.

if (!mKeyguardManager.isKeyguardSecure()) {
  // User hasn't set up a lock screen.
  Toast.makeText(this,
      "Security lock has not been set up.", Toast.LENGTH_LONG).show();
  // button.setEnabled(false);
}

あなたのアプリケーションにおいて特定の操作に必要とされる認証強度がデバイス認証で満足いくものであるかどうかは上記を踏まえて考える必要がある.

Coding!

Check Keyguard secure

デバイス認証機能を使用する場合, まずは端末で当該機能がONになっているかを確認する必要がある.
この機能が有効になっていないと後のセキュリティ鍵を生成することができない.

protected void onCreate(Bundle savedInstanceState) {
  ...
  if (!mKeyguardManager.isKeyguardSecure()) {
    // User hasn't set up a lock screen.
    textView.setText("セキュリティロックを設定してください");
    return;
  }

デバイス認証機能が有効であることを確認した上で, Android Key Store Systemからセキュリティ鍵(対照鍵)を作成する.
セキュリティ鍵を生成する際に有効期間も設定しておく. これがデバイス認証を再度ユーザへ求めるまでの指定時間となる.

  private void createKey() {
    try {
      KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
      keyStore.load(null);
      KeyGenerator keyGenerator = KeyGenerator.getInstance(
          KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore");

      // セキュリティ鍵を生成する
      keyGenerator.init(new KeyGenParameterSpec.Builder(KEY_NAME,
          KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
          .setBlockModes(KeyProperties.BLOCK_MODE_CBC)
          // デバイス認証機能を必須化. デバイス認証機能がOFFの場合はセキュリティ例外が発生する
          .setUserAuthenticationRequired(true)
          // セキュリティ鍵の有効期間を設定する
          .setUserAuthenticationValidityDurationSeconds(AUTHENTICATION_DURATION_SECONDS)
          .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
          .build());
      keyGenerator.generateKey();
    } catch (InvalidAlgorithmParameterException
        | CertificateException | NoSuchAlgorithmException | KeyStoreException
        | IOException | NoSuchProviderException e) {
      throw new RuntimeException("鍵の生成に失敗", e);
    }
  }

デバイス認証が必要かどうか, あるいはセキュリティ鍵の有効期間内かどうかは, ここで作成したセキュリティ鍵の復号化で判断できる.
アプリ起動時等にデバイス認証機能のOn/Offを確認し, セキュリティ鍵を生成した後にユーザがデバイス認証機能をOffにするケースも考慮する必要がある.

  private void tryEncrypt() {
    try {
      KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
      keyStore.load(null);
      SecretKey secretKey = (SecretKey) keyStore.getKey(KEY_NAME, null);

      // 鍵の復号化が成功するのはデバイス認証の指定時間30秒以内である場合に限られる
      Cipher cipher = Cipher.getInstance(
          KeyProperties.KEY_ALGORITHM_AES + "/" + KeyProperties.BLOCK_MODE_CBC + "/"
              + KeyProperties.ENCRYPTION_PADDING_PKCS7);
      cipher.init(Cipher.ENCRYPT_MODE, secretKey);
      cipher.doFinal(SECRET_BYTE_ARRAY);

      // 鍵の有効期間内(認証済み)であればデバイス認証をパスする
      showAlreadyAuthenticated();
    } catch (UserNotAuthenticatedException e) {
      // デバイス認証機能にまだパスしていない
      showAuthenticationScreen();
    } catch (KeyPermanentlyInvalidatedException e) {
      // 鍵が生成されたあとにデバイス認証機能をoffにされた場合に発生する
      textView.setText("作成されていた鍵が無効になりました. 再度実行してください.");
    } catch (BadPaddingException | IllegalBlockSizeException | KeyStoreException |
        CertificateException | UnrecoverableKeyException | IOException
        | NoSuchPaddingException | NoSuchAlgorithmException | InvalidKeyException e) {
      throw new RuntimeException(e);
    }
  }

Launch Security lock screen

デバイス認証にパスしていない状態であればcreateConfirmDeviceCredentialIntent()で認証画面を呼び出せる.
引数にはそれぞれ画面のタイトルと説明文を指定でき, 認証画面の文言をカスタマイズすることができる.

  private void showAuthenticationScreen() {
    // デバイス認証画面のタイトルと説明文は変更することが可能
    Intent intent = mKeyguardManager.createConfirmDeviceCredentialIntent(null, null);
    if (intent != null) {
      startActivityForResult(intent, REQUEST_CODE_CONFIRM_DEVICE_CREDENTIALS);
    }
  }

  @Override
  protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    if (requestCode == REQUEST_CODE_CONFIRM_DEVICE_CREDENTIALS) {
      // Challenge completed, proceed with using cipher
      if (resultCode == RESULT_OK) {
        showConfirmation();
      } else {
        // The user canceled or didn’t complete the lock screen
        // operation. Go to error/cancellation flow.
      }
    }
  }

Confirm Credential security lock

前回のデバイス認証から指定時間の範囲内であれば鍵の復号化が成功する. この場合はユーザに再度認証を求める必要はない. ユーザへの過度な認証要求を投げないように注意すること.

Confirm Credential already enable

以上.

新しいパーミッションモデル - Runtime Permission

はじめに.

caution!
本稿はAndroid M Preview向けに限られた内容です.
本稿の一部または全てはM Preview以前のAndroid versionやM正式版で有効ではない可能性があります.

本稿の一部はPermissionsの内容を参考としています. より正確な情報を得たい場合はこちらもご参考ください.
また, 本稿の説明で例示しているソースコードの完全版はGitHub上に公開しています.

New permission model

Android Mでは新しいApp permission modelが提案されている.
従来モデルではアプリケーションが使用するパーミッションのリストをインストール時にユーザへ提示し, これらの使用許可を一度に取得する必要があった.
従来モデルでは一見不要と思えるパーミッションがリストされ, マルウェアと誤解されるケースもあり, アプリケーションのインストールを阻害する1つの障壁となっていた.

新しいApp permission modelの導入により, アプリはインストール時に必要最低限のパーミッションをユーザに求め, アプリケーションをインストール後, 必要となったタイミングでユーザにパーミッションの使用許可を求めることが可能になった.

アプリケーションの付加的な機能として提供されるものであればインストール時にパーミッションの許可を求めず, それを実行する時になって初めてパーミッションの使用許可を得れば良い.
たとえば, Google Analyticsでトラッキング情報の収集をユーザが求めないケースを考える.

従来モデルであれば, アプリケーションはGoogle Analyticsを使用するために下記のパーミッションの使用許可を得る必要があった. 例えユーザがGoogle Analyticsの使用に同意しなかったとしてもだ.

  <uses-permission android:name="android.permission.INTERNET"/>
  <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>

アプリケーションはGoogle Analytics以外でこれらのパーミッションが不要であったとしても, それを前もってユーザに理解してもらうのは困難だ. むしろ情報が抜き取られるのではないかという不信感をINTERNETパーミッションが与えているかもしれない.

新しいApp permission modelではアプリケーションのインストール時にこれらのパーミッションの使用許可を得る必要がなくなる.
代わりにそれらの機能を必要とする時, つまりアプリがインストールされた後, その機能をユーザが使用する際にパーミッションの使用許可を求める形に変えることができる.

ユーザがGoogle Analyticsの使用に同意しない場合(つまりINTERNETパーミッションの使用を拒否した場合), アプリケーションのインストールを諦める必要はない. 代わりにGoogle Analytics機能の使用のみを諦め, アプリケーションはそれ以外の機能を提供すれば良いのだ.

2015.06.25 訂正
INTERNETパーミッションはこちらに記載されている通り, PROTECTION_NORMALとなるため, Runtime Permissionのそれとは異なりインストール時に付与されるパーミッションです. 上の例は誤った内容であるためここに訂正します. (ご指摘いただいた@tao_gaku様ありがとうございました.)

対照的に, 許可を得られなければアプリケーションの価値を十分に提供できないようなパーミッションであればインストール時にまとめて取得しておいたほうが良いだろう. パーミッションの使用許可を得られなかったばかりにアプリケーションがまともに動作しないようであればユーザはあなたのアプリに低い評価をつけるかもしれない.

for M Preview Apps

M Preview向けに作成されたアプリ, つまりTargetSdkVersionがM Preview以上であるアプリは新しいApp Permission modelをベースに作成されているものとしてシステムは振る舞う.

Permission settings

M Previewではユーザがアプリケーションのパーミッションをいつでも剥奪できるようにパーミッションに関する設定画面を設けている.
Settings>Apps>Advanced>App permissionsからパーミッションの設定画面を起動できる.
アプリケーションは一度使用許可を得たパーミッションであっても, 次に実行するときにはそれが剥奪されている可能性を考慮しておく必要がある.

for System applications

通常のサイクルとは異なり, System applicationはインストール時にAndroid Manifestに定義されたパーミッションをユーザの確認なしにシステムによって許可される.
ただし, System applicationであっても例外なくユーザが設定アプリケーションから個別にPermissionを剥奪することが可能であることを忘れてはいけない.

for Signature protection

Protection levelがsignatureであるパーミッションは, 同パーミッションを持つアプリケーションが既に存在する場合, ユーザの確認なしにシステムによって許可される.
ただし, これについてもSystem applicationと同じく設定アプリケーションから個別にパーミッションの剥奪が可能であることに注意する必要がある.

for legacy applications

アプリケーションがM向けに作られたものではなく(targetSdkVerison < M), かつ新しいApp Permission Model上で動作する場合, こちらも同様に設定アプリケーションから個別にパーミッションを剥奪することが可能となっている.

ただし, M向けに作られたアプリケーションは新しいApp Permission Modelに対応していないことが予想されるため, システムはその場合に”アプリケーションが正常に動作しない可能性がある”旨のダイアログを表示してパーミッションの剥奪を行う.

Legacy app permission deny

レガシーなアプリケーションがパーミッションを剥奪された場合, パーミッションが必要なAPIを実行すると必ずセキュリティーパーミッションが発生するとは限らない. 代わりに空のデータが返却されることもあるだろうし, エラーを意味するコードが返されるかもしれないし, 予期しない動作となる場合もある.
例えば, カレンダー情報の検索に必要なクエリをパーミッションの使用許可なしで実行した場合, クエリの結果は”空”を返す.

for Communication

アプリケーションが写真を撮るケースにおいて, アプリケーションがandroid.permission.CAMERAのパーミッションをユーザにリクエストし, これの使用許可を 得た上でCamera APIを使用して写真を撮影する場合. このアプローチではアプリケーションにパーミッションの必要範囲が閉じており, 操作においても同様である.

異なって, Camera APIを使用せずACTION_IMAGE_CAPTURE Intentを使って写真の撮影を試みる場合, もしあなたのアプリケーションがACTION_IMAGE_CAPTURE Intentをサポートしている場合, さらにあなたのアプリケーションがまだandroid.permission.CAMERAのパーミッション使用許可を得ていない場合, あなたのアプリケーションはandroid.permission.CAMERAの使用許可をユーザに求めるが, この時に連携元のアプリケーションが持つパーミッションが適切かどうかの確認も怠ってはならない.

Coding!

Manifest

M向けに作成されるアプリは必ず新しいApp Permission Modelに対応しておく必要がある.
AndroidManifestにはアプリに必要なパーミッションを宣言しておき, 実行時に使用許可をとればよいパーミッションであれば必要になってから許可をとる仕組みを実装する.

M Previewで新しいPermission Modelをテストするにはbuild.gradleで次を設定する.

  • compileSdkVersion is set to ‘android-MNC’
  • minSdkVersion is set to ‘MNC’

Preview SDKでは古いプラットフォームでテストできないことも明示しておく.

  • targetSdkVersion is set to ‘MNC’

M Preview版では<uses-permission>を補完する要素として新たに<uses-permission-sdk-m>を宣言できる. <uses-permission-sdk-m>はMより古いOSを搭載した端末では無視され<uses-permission>の宣言がされていないものとして扱われる. そのため古いOS上ではアプリケーションをインストールする際に<uses-permission-sdk-m>で定義されたパーミッションの使用許可は表示されず, これの使用許可も与えられない.

パーミッションを必要とする場合, 古いOSでは<uses-permission>で宣言しておく必要がある. あなたのアプリケーションが古いOSで動作するのに必要なパーミッションは<uses-permission>で宣言しておく必要がある. ただし, M以降でのみ必要となるパーミッションは<uses-permission-sdk-m>で宣言する選択肢を得られる.

注意すべきは, <uses-permission>で宣言されたパーミッションであってもユーザはこれらのパーミッションを剥奪できるということである. M以降の端末をターゲットとするのであれば<uses-permission-sdk-m>, <uses-permission>双方で定義されたパーミッションが剥奪されている状況を考慮して実装しておく必要がある.

Check and Request Permissions

新しいApp Permission Modelに対応した端末であるかどうかを確認するにはBuild.VERSION.CODENAMEでOSのコードネームを確認する. M Previewである場合は"MNC"が返却される.

  private boolean isMNC() {
    // TODO: In the Android M Preview release, checking if the platform is M is done through the codename, 
    // not the version code.Once the API has been finalised, the following check
    // should be used:
    // return Build.VERSION.SDK_INT == Build.VERSION_CODES.MNC

    return "MNC".equals(Build.VERSION.CODENAME);
  }

アプリケーションがパーミッションの使用許可を得ているかどうかの確認にはContext.checkSelfPermission(permission_name)を使用する.
カメラの使用許可を確認するにはContext.checkSelfPermission(Manifest.permission.CAMERA)で, もし使用許可を持っていない場合はrequestPermissions()で許可を得る.

public void onClick(View v) {
  // 自アプリにカメラの使用許可があるか確認
  if (!hasSelfPermission(MainActivity.this, permission.CAMERA)) {
    // 使用許可がない場合はこれをリクエストする
    requestPermissions(new String[]{permission.CAMERA}, REQUEST_CODE);
  } else {
    // 使用許可がある場合はカメラを起動する
    launchCamera();
  }
}

private boolean hasSelfPermission(Activity activity, String permission) {
  return activity.checkSelfPermission(permission) == PackageManager.PERMISSION_GRANTED;
}

requestPermissionsに指定するパーミッションはAndroidManifestであらかじめ<uses-permission-sdk-m>あるいは<uses-permission>で宣言されている必要がある. AndroidManifestで宣言されていないパーミッションをリクエストすることはできない.

requestPermissionsでパーミッションをリクエストするとシステムはパーミッションの使用許可をユーザにたずねるダイアログを表示する.

Allow Camera Permission

ここでの結果はActivity.onRequestPermissionsResultで得ることができる.

@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
  if (requestCode == REQUEST_CODE
      && permission.CAMERA.equals(permissions[0])
      && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
    launchCamera();
  }
  super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}

Testing

新しいApp Permission Modelのために新たなADBコマンドがいくつか追加されている.

Install with permissions

adb installコマンドへ新たに-gオプションが追加された. この引数ありでインストールされたアプリケーションはAndroidManifestで宣言されている全てのパーミッションの使用が許可された状態でインストールされる.

$ adb install -g <path_to_apk>

Grant and revoke permissions

package manager (pm)コマンドにアプリケーションのパーミッションを付与/剥奪するコマンドが追加された.

# Permission Grant
$ adb pm grant <package_name> <grant_permission_name>

# Permission Revoke
$ adb pm revoke <package_name> <revoke_permission_name>

Best Practices

新しいApp Permission modelでは, あなたのアプリケーションがパーミッションを求める度にユーザの操作を中断する. これはユーザ体験を阻害する要因にもなるため, できる限りその頻度を抑える努力をすべきである.

例えば, あなたのアプリケーションでカメラで撮影した画像を取得するためにCAMERAパーミッションの使用許可を得てCamera APIを実装するのではなく, MediaStore.ACTION_IMAGE_CAPTURE のIntentによるアプリ連携でこれを実装することができる.

また, 一度に大量のパーミッション使用許可を求めるべきではない. パーミッションは必要となった時に求めるように努め, ユーザを唖然とさせないようにすること.
例えば, あなたがフォトグラフィアプリケーションを作成している場合, アプリケーションを起動した際にCameraパーミッションを求めるようにすべきだが, 撮影した画像をContactsデータを参照してShareする機能を有していた場合に, Contacts参照のパーミッション使用許可までアプリ起動時に取得してはならない.
後者のパーミッションはユーザのShareアクションまで待って, そこで初めてパーミッションの使用許可を得るのが常套手段である.

Many permission allowing

Explain why you need permissions

パーミッションの使用許可を得るダイアログには, あなたのアプリケーションがどのような理由でそのパーミッションの使用許可を求めているのかの説明文は表示されない. たとえばフォトグラフィアプリケーションが突然ローケーションサービスの使用許可を求めるとユーザはなぜフォトグラフィアプリケーションにロケーションサービスのパーミッションが必要であるのか困惑し不安になる. これは良いユーザ体験を生まない.

フォトグラフィアプリケーションは画像に埋め込まれたgeotagを解釈し, よりよいユーザ体験を提供することをユーザに説明する必要があるかもしれない. それはrequestPermissions()を呼び出す前に済ませておいたほうがよい.
例えばアプリケーションのチュートリアルにそれを埋め込んでおく方法がある. チュートリアルで機能を説明し, その時点でパーミッションの使用許可を取得しておく方法も有効だ. あるいはそこでShareのデモンストレーションをやってしまってもよいかもしれない. ユーザはあなたのアプリケーションが不必要にパーミッションの使用許可を求めていないことを理解し安心できるだろう.
もちろん, 全てのユーザが忠実にチュートリアルを読むとは限らず, また一度許可したとしても後からこれを剥奪される可能性も残っているため, Shareを実行する際にはパーミッションのチェックをいつも通り実施する必要はある.

以上.

2015/05/30

Google Cloud Message 3.0 クイックスタート

参考

Get Project

Google公式のQuickStartアプリで簡単に始めることができる.
サンプルプロジェクトは下記から取得する.

$ git clone https://github.com/googlesamples/google-services.git

cloneしたファイルにあるgoogle-services/android/gcmがGCM3のサンプル.

Get Configuration file

GCM3.0を使用するためにConfiguration fileの追加が必要.
Configuration fileを得るためのウィザードをGoogleが用意してくれている.
Configuration Fileの取得には作成済みのAndroid Appを求められるが手軽に試すための’Default Demo App’を選択すればAndroid Appを持っていなくても始めることができる.
本稿はApp name:’Default Demo App’, package name: ‘gcm.play.android.samples.com.gcmquickstart’として進める.
ともかくウィザードに従ってconfiguration fileを取得する.
[Choose and configure services]>[ENABLE GOOGLE CLOUD MESSAGING]>[Generate configuration files]>
[Download google-services.json]でconfiguration fileを取得できる.
ダウンロード画面で表示される “Server API Key” と “Sender ID” は覚えておく.

Implementing GCM Client on Android

前述のgoogle-servicesを流用すれば実装の必要はないので本章はskipできる.

既存アプリにGCM3を適用したい場合は下記の要件に注意.
Googleはクライアントコードの作成においてGoogleCloudMessagingAPIの使用とAndroid Studio+Gradleの使用を推奨している.

またAndroidのGCM clientへの要件としては次のものがある.

  • Android2.2 or higer(need Google Play Store application. )
  • GCMの新機能を使用したい場合はAndroid2.3 or higher
  • 3.0以前はGoogleアカウントの設定が必須であったが, 4.0.4以降はGoogleアカウント設定が必須ではなくなった.
    4.0.4以降はGoogleへのログインが必要になるケースがある.

GCMはクライアントとサーバ両方実装することを推奨している. サーバの実装についてはAbout GCM Connection Serverを参照.
クライアントコードについてはQuick Start Sampleが参考になる.

Caution!
既存のコードがWakefulBroadcastReceiverを使っているならGCMReceiverとGcmListenerServiceに置き換えることを推奨する.

  • AndroidManifest.xmlではGcmBroadcastReceiverを “com.google.android.gms.gcm.GcmReceiver”に置き換え, サービスの継承元をIntentServiceからGcmListenerServiceに置き換える.
  • BroadcastReceiverの実装コードを削除する.
  • IntentServiceのコードをGcmListenerServiceのコードに置き換える.

Add the configuration file

ダウンロードしたconfiguration fileをappモジュール直下に配置する.

Run Application

Configuration fileを追加したならばGCM Quickstartのアプリケーションを実行する.
GCM Quickstartアプリが起動し, DeviceのRegistrationが完了すると”Token retrived and sent to server”のメッセージが表示される.

GCM Qucickstart

Sent Google Cloud Message

Quickstartアプリに同梱されているgcmsenderを使ってGCMの動作を確認できる.
gcmsenderモジュールにあるgcm.play.android.samples.com.gcmsender.GcmSenderクラスに定義されているAPI_KEYを”Get Configuration file”で取得したAPI_KEYに置き換える.

public class GcmSender {
  // public static final String API_KEY = "API_KEY";
  public static final String API_KEY = "AIxxxxxxx";

GcmSenderは下記のコマンドで実行できる. argsに好きなメッセージを指定して実行すればデバイスにメッセージが届く.

$ ./gradlew :gcmsender:run -Pargs="hello GCM3."

以上.

Android M Preview 環境の準備

はじめに.

M Preview環境のセットアップには下記サイトに従って進めるとよい.
http://developer.android.com/preview/setup-sdk.html

本稿は上記サイトをベースにいくつか加筆・修正したものとなる.

Get Android Studio 1.3

M Previewを試すにはAndroid Studio 1.3の環境を用意する必要がある.

caution!
Android Studio1.3はCanary previewで動作が不安定な状況.
そのためマスター環境とは別で環境構築することが推奨されます.

Android Studio 1.3 previewをインストールするには

  1. Android Studioをダウンロード
  2. [Preferences]から, [File]>[Settings]>[Appearance&Behavior]>[System Settings]>[Update]
  3. UpdateパネルにあるAutomatically check updates for:でCanary Channelを選択.

これで再度Android Studioのアップデートをチェックすると1.3 previewがhitするのでこれを適用する .

Get the Preview SDK

Preview SDKが必要なためこれをダウンロードする.

  1. Android SDK Managerを起動.
  2. Platformセクションから Android MNC Previewを選択
  3. Toolsセクションから最新のSDK ToolsとPlatform-toolsとBuild-toolsを選択・インストール
  4. Android Studioに戻り, [Preferences]>[Appearance & Behavior]>[System Settings]>[Android SDK]にあるSDK PlatformタブにAndroid MNC Previewの項目があればOK.

Create or Update Project

プロジェクトを新規作成するのであれば

  • デバイスに”Phone and Tablet”を選択
  • SDKとしてminSDKに”API 22+: Android M (Preview)”を選択する.

既存プロジェクトをアップデートするならbuild.gradleをそれぞれ下記に変更する.

  • compileSdkVersion is set to ‘android-MNC’
  • minSdkVersion is set to ‘MNC’
  • targetSdkVersion is set to ‘MNC’

もし, Android Studio 1.2からAndroid Studio 1.3 previewにアップデートしたならば,
project rootにあるbuild.gradleでAndroid Gradleに”1.3.0-beta1”を改めて指定する必要がある.

buildscript{
  dependencies {
    classpath 'com.android.tools.build:gradle:1.3.0-beta1'

Set up for Testing

M PreviewのアプリケーションをテストするにはM Previewのイメージが焼かれたデバイスが必要. あるいはエミュレータでテストできる.

実際の端末でテストする

Nexus5, 6, 9, TVにM Preview system imageを焼けばよい.
Downloads page

  • Nexus 5 (GSM/LTE) “hammerhead” device system image
  • Nexus 6 “shamu” device system image
  • Nexus 9 (Wi-Fi) “volantis” device system image
  • Nexus Player (Android TV) “fugu” device system image

Preview imageを焼くのは自己責任となる. また端末のデータはすべて消去されるので知識を持って実施すること.

エミュレータ

M PreviewのエミュレータがリリースされているのでAndroid SDK Managerでエミュレータ環境一式(System image etc.)をダウンロードしておく.
Android Virtual Device ManagerからM Previewのイメージを作成.

  • Device: Nexus 5, Nexus 6, Nexus 9, or Nexus Player
  • Target: Android M (Preview) - API Level M

All done! let’s start M Preview!

2015/05/29

Google I/O 2015 概要まとめ

Keynote

参考リンク

Gizmode http://www.gizmodo.jp/2015/05/google_io_2015.html
Google I/O 2015 official https://events.google.com/io2015/

Devivce

  • “メールの検索が遅すぎる”そこからGMailプロダクトが始まった. GMap, YouTubeも同じく.
  • さらに2つのプロダクトを作成した.
    • Chrome. ブラウザが遅くセーフティではなかったからだ.
    • Android. フラグメンテーション化され, ユーザ体験も開発も困難であったからだ.
  • これらのプロダクトは世界中に供給されている.
    • GoogleSearch 1B+/ YouTube 1B+/ GMap 1B+/ Gmail 0.9B/ Android 1B+/ Chrome 1B+
  • I/O 2015では主に2つのことを話す.
    • Fitst is how we are evolving out computing platform.
      NOT jut for mobile, but beyond mobile for a multiscreen world.
    • Second is how google core to out mission of organizing the world’s information
      is really evolving the mobile experience for uses.
  • 10億台以上のデバイスを出荷してきたが, 世界のデバイス状況をビジュアライゼーションした.
  • https://github.com/YukiMatsumura/Documents/blob/master/Image/GoogleIO2015_device_ships.png
  • dot1つ1つがActivityなデバイスを表現している.
    • Blue: high end mode. galaxy S6 etc…
    • Orange: 新興マーケットでエントリーレベルのデバイス.
  • Android Device
    • 500 carrier. 4K models.
    • Android wear. 7 models
    • Android auto, 35 brands
    • Android tv, sony, sharp ship. popular in europe.
    • Chromecast 17M sold, cast button touches 1.5B, 20K cast apps.

Chromecast

  • iOS support.

Android M Preview

  • 主に不具合の改修と品質改善
  • App Permission(old AppOpt).
    • application install時に許可を求めるスタンスを変える.
    • Permissionが必要な機能を使用するタイミングで, Permissionの許可をユーザに求める(Runtime Permissions)
    • 納得できないpermissionは拒否できる.
    • Permissionを使用しているアプリの逆引き機能.
  • Chrome custom Tabs
    • WebのBig contentを使うにはWebView or Browser連携が便利である.
    • Browserはloginやsession管理を簡単にするが自アプリの制御下におけない
    • Chrome custom Tabsがこのジレンマを解消する
      • Chromeがコンテンツをfetch
      • Chromeの戻るボタンでアプリに戻ることができる.
      • 保存済みパスワード情報情報の利用?
  • App link

    • URIから直接Application内部にシームレスにジャンプする
    • 実装
      Application: Manifestに intent-filter android:autoVerify=”true”, and data shceme and host.
      Web Servers: namespace/package_name/sha256_fingerprint のJSONファイルを配置
      (Googleクローラによるverifyを待つ必要もあったはず)
  • Android Pay

    • secure, sipmle, choice.
    • 既存のクレジットカードを使用出来る. クレカ番号は店に知らされません.
    • Android Payを使用するかBanking appをつかうかはユーザ選択可能.
  • Fingerprint
    • Mでは指紋認証をネイティブサポート
    • デバイスのアンロック, Play Store purchaseに使われる.
    • クレジットカードとFingerprintを端末上で登録しておく.
    • Fingerprintでpurchaseのauthrizeが可能.
    • Fingerprint APIはアプリに公開されている
  • Doze
    • パワーマネージメントのための新機能は”Doze”と呼ばれる.
    • タブレットなどナイトテーブル上に放置(Dozing)されている場合, データの同期(通信)を抑止(deep sleep state)する.
    • バッテリー寿命は約2倍に増加
  • USB type-C support.
    • 急速充電.
    • Mobile をバッテリーチャージャ(電源供給側)とすることも出来る.
    • USBモード選択としてCharging or Power supply(他) のモード選択ができる
  • M APIs
    • 数百のAPI変更がある
    • Easy word selection and “Floating clipboard toolbar”.
    • アプリに加えて”People”へのダイレクト共有(Single click.)
  • M Preview Release
    • Nexus5, 6, 9 and player.

Wear

  • 1500を超えるwatch face.
  • always on
    • 常時点灯かつ省電力モード
    • always on modeでは数秒経つとスクリーンオフではなくより電力消費量を抑えられるBlack&White modeに移行する
      (watch faceでは秒針が消えているように見える.)
    • Android Wear: Always-On Apps https://youtu.be/7m6Z9d0fDaM
  • wrist gesture
    • 手首をひねることでリストスクロールが可能に.
  • emoji
    • フリーハンドで絵文字を描き, 絵文字候補を選択・送信可能に.
  • wear application
    • 4000+ のandroid wear向けアプリケーションがアップされている

IoT

  • (TODO: IoT話題の導入部.未記載)
  • Brillo

    • IoT OS
    • Derived Android. Minimal footprint.
    • arch: Hardware/Kernel/HAL/Connectivity(Wifi,BLE,etc.) and Device Administration(Analytics, update, crash reports, etc.).
    • Connectivity 機能をAndroidが提供する
    • Brillo developer preview release Q3 2015.
  • Weave

    • WeaveはIoTデバイス間を接続し対話するためのコミュニケーションレイヤー
    • 互いに理解し共有し合うための共通言語
    • IoTデバイス(Brillo device)同士で対話するためだけのものではない.
    • CloudやPhoneも対話の対象となる
    • weaveは標準のスキーマを持つ.
    • JSON likeなメッセージフォーマット
    • Brillo deviceは”写真を取る”, “ドアをアンロックする”といった表現をこれで表現・解釈する.
    • Support Cross Platform
    • IoTをMobileなどのデバイスで検出, セットアップしてすぐに対話できる.
    • Weave full stack release Q4 2015

Mission, Deep Neural Networks.

  • Google’s core mission
    • Organize the world’s information and make it universally accessible and useful.
  • ユーザが求めているものをより正確に理解する.
  • 機械学習により膨大なデータから質問を理解し画像データを解析し答えを導き出す.
  • このテクノロジは”Deep Neural Network”と呼ばれる
  • Deep neural networkはヒエラルキの形態をとる
  • 第一階層ではラインやエッジ、影、形を理解する
  • 第二階層では耳, 足, 手といったパーツを理解する
  • 最終階層では全体を理解する
  • 2013年の誤認識率は23%だが, 2015年は8%.

Google Now

  • Google Nowのアシストを実現する3つ
    • Understand context, Bring you answers proactively, Help you take action.
  • Context
    • 何をアシストすべきか, 状況を理解する
    • 会話の中の”それ”が何なのかを理解する
  • Answers
    • 積極的にContextに基く答えを提供する.
    • 付近のガススタンドをおすすめしたり興味のあるスポーツ試合の状況を提案する.
    • これはGoogleが世界中にある約1Bものエンティティを理解できているため実現できる.
  • Action
    • Now on Tap
      • M でリリースされる新機能
      • Contextを理解する.
      • 映画に関するメールを見ながらNowを起動するとContextを理解して映画に関する情報を回答
      • 晩御飯に誘われたなら美味しいディナーのお店をおすすめする.
      • 音楽を聞きながら「彼の本名は?」でアーティストの本名を回答
      • ホームを長タップで起動するか, “Ok Google”.
  • APIの公開はなし

Photos

Android M Developer Preview & Tools

http://android-developers.blogspot.jp/2015/05/android-m-developer-preview-tools.html

  • AndroidStudio v1.3 Preview
    • Support C/C++ Debug.
    • Support NDK
    • build speed up
    • Data binding support
    • Integrated testing support
    • Vector darawables support
    • New Annotation support
      • @WorkerThread: これでアノテートされたメソッド上でUI操作をするとWarning.
      • @Size(max = n): 上限値を超えを検知した場合にWarning
      • @Size(n): サイズが2でないことを検知した場合にWarning
      • @IntRange (from = m, to=n): m〜nのレンジに収まっていないことを検知した場合にWarning. alpha値の指定などで便利.
      • @FloatRange: @IntRangeと同様.
      • @CheckResult: 呼び出し元が戻り値の確認をしていない場合にWarning
      • @CallSuper: Super methodの呼び出しをしていない場合にWarning
      • @ColorInt: argsコードでない場合にWarning
      • @RequiresPermission: methodの実行にpermissionが必要で, それを満たしていない場合にWarning
      • QuickFixでpermissionコードをAndroidManifestに追加するか
        AndroidMがtargetであればRuntime PermissionとしてPermission Checkコードを生成できる.
  • Plymer 1.0 release.
  • Cloud Test Lab
    • アプリ自動テストのためのクラウド型テストプラットフォーム.
    • screen videoとログでレポートする
    • Firebase買収
    • Cloud Messaging
    • 70B messeges sent daily, 600K+ apps.
    • Support Android, iOS and Chrome.
    • Google Play
    • Developers pages support.

What’s New in Android

  • M Previews schedule. Prev1.MAY, Prev2.JUNE, Prev3.JULY, Final.Q3
  • App Permission
    • application install時にパーミッションを求める画面はユーザインストールの第一の障壁となる
    • Runtime permissions in M
      • application install時にパーミッションを求める必要がなくなる.
      • カメラをつかいたいユーザだけ使えばいい. そういうユーザにだけカメラの使用許可をとればいい.
    • Use controls
      • アプリが持っているパーミッション一覧画面
      • 特定のパーミッションを持っているアプリ一覧画面
    • Permission dealされたらどうするのが良いか(practice)
      • App targeting M can: can ask for any permission at any time.
      • Legacy apps will: get all permission at install time, as before
        つまりインストール時の障壁は残る.
      • User can:
      • deny any permissions upon request
      • deny any permissions at any later time - even legacy apps
        古いアプリ(not M target apps)でもpermission denyできる. その場合はダイアログで
        ちゃんと動かないかもよ通知がされる.
      • Test!
  • FingerPrint
    • FingerprintManager.authenticate()
      verify that authrized user is present your app controls all UI.
      Fingerprintのセンサーを使用して認証用UIと認証確認をアプリ側で実装する.
    • KeyguardManager.createConfirmDeviceCredentialIntent()
      Present lock screen to user startActivityForResult(), check for RESULT_OK
      アプリでUI他を実装したくない場合はこちらを使う. システム設定に従ってパターン, PIN,
      サポートされていればFingerprint認証の一連作業をシステムに委譲できる.
    • Sample code
  • Android Backup
    • targetSdk M以上でアプリデータのバックアップが標準ONである.
    • xml resourceでバックアップするスキームを定義できる(include/exclude)
      • AndroidManifest.xml <application android:fullBackupContent="@xml/backupscheme"
      • res/xml/backupscheme.xml <full-backup-content><exclude domain="[database|file]" path="...">(or <include>)
  • GCM Network Manager
    • Like JobScheduler
  • Data Binding
    • データバインド関連のボイラープレートコードを排除する.
    • layout.xmlにモデルの情報を直接していすることでデータバインドを実現する
<layout>
  <data>
    <variable name="item" type="hoge.hoge.StoreItem" />
  </data>
  <FrameLayout ...>
    <ImageView ... android:src="@{storeItem.image}" />
  </FrameLayout>
</layout>
// build.gradle
dependencies {
   classpath 'com.android.databinding:dataBinder:1.0-rc0'
}
apply plugin: 'com.android.databinding'
  • layout.xmlの中でonClickListenerのコールバックメソッド(名前は自由!)を直接指定できる.
  • この機能の良し悪しは意見を募っている状態.
    • Android Design support library
  • Material designの実装をサポートする
    • CoordinatorLayout
    • Snackbar
    • TabLayout
    • NavigationView
    • TextInputLayout
    • Toolbar with Motion
    • FAB
  • その他UI変更点
    • RecyclerView ItemTouchHelper
      • Swipe-to-dismiss
      • Drag & Drop
    • WebView
      • PostMessage
      • WebViewClient
      • WebSettings.setOffscreenPreRaster()
  • Icon in Notifications
    • Notification.Builder.setSmallIcon(Icon)サポート.
    • Icon ic = Icon.createWithBitmap(iconBitmap)
    • setSmallIconにBitmapを受け渡し可能.
    • Icon with Bitmap.
  • Text Selection
    • Floating palette with Action items. set ActionMode.TYPE_FLOATING
    • Floating Toolbar.
    • Text Selectionの上部にフローティング状態のAction(ToolBar)が表示される.
  • Direct Share
    • meta-dataにChooserTargetを構築する自前Serviceを指定.
    • 自前ServiceでChooserTargetを構築する(Peopleの追加など)
<activity ...>
  ...
  </intent-filter>
  <meta-data android:name="android.service.chooser.chooser_target_service"
      android:value=".MyService" />
</activity>
<service android:name=".MyService"
  android:permission="android.permission.BIND_CHOOSER_TARGET_SERVICE">
  <intent-filter>
    <action android:name="android.service.chooser.ChooserTargetService" />
  </intent-filter>
</service>
public class MyService extends ChooserTargetService {
  @Override
  public List<ChooserTarget> onGetChooserTargets(ComponentName name, IntentFilter filter) {
    // ...
  • New Stylus API Support in M
  • External Storage
    • permanent storageの採用
    • pathのハードコードはNG
    • No new APIs!
    • For preview testing.
    • Preview imageで試したい場合は外部USBデバイスを繋いでコマンド発行しテストする
adb root && sleep 2
adb shell setprop persist.fw.force_adoptable 1
adb reboot

What’s New in Android Development Tools

  • Top Requested Developer Features
    1. Design Tools
    2. Faster Emulator
    3. NDK Support
    4. App Testing Support
  • Vector Image
    • SVGファイルからのインポートで.xml drawableの自動生成をサポート
  • Gradle 1.3/2.4 build faster
    • 10proj x 25variants
      • 1.2->1.3 で倍以上の高速化
    • 400 variants
      • 1.2->1.3 で約10倍の高速化
  • Testing in the Android Developer Workflow
    1. Android Studio. setup projects with tests & infrastructure
    2. Android Testing Library. UI Test & Unit Test
    3. Cloud Test Lab. Testing on matrix of devices (virtual & real)
    4. Google Play Testing. Google Play App Testing & Validation.
  • Debugger
    • Debugging modeでVariableのIntegerに対して View as > Android Type Integerで
      • @IntDefの定義名を解決できる
      • ColorコードであればARGB形式に変換し色をプレビューする
      • resource idであればResource名を解決できる
  • Captures ViewでMAT likeなメモリ解析が可能に. ビュジュアルかされたメモリアロケート状況など.
  • Android StudioでGoogle AccountにloginしておくことでGoogle Sign-inやAnalytics, AdMobといったモジュールの導入からテンプレート作成までサポート.
  • SDK ManagerからPlatformなどをダウンロードする機能はAndroidStudioに統合される.
  • Themeファイルの編集でサンプルコンポーネント上でstyle指定の確認が可能に.
  • Material Designのデザインもエディタから数ステップを踏むことで可能に.
  • Release plan.
    • v1.3: Feature Release
    • v1.4 Polish Late-Summer 2015
    • v1.5 Feature Release Fall 2015

Material Now

  • Material Design guildlineを更新.
    • update FAB animation.
    • Adaptive UI includes responsive layout guidance, grids, breakpoints, and patterns
    • Empty states contains guidance for states when regular content can’t be shown
    • Launch screens includes branding and placeholder UI content
    • Navigation Additional significant content updates include:
      • Elevation and shadows has been renamed from “Objects in 3D space,”
        adding new details about elevation changes
      • Authentic motion describes easing terms in more detail
  • Material Design Lib release.
    • FAB: FABをlayout.xmlに追加するのみで実装可能
    • CardListView: Shadow効果付きの実装
    • NavigationDrawer: Menuタグを追加するだけで細かなGridなどのdp値も実装されている
    • Tabs
    • ToolBar: Overscrollによる挙動も簡単に指定できる
  • Backward Compatibility
    • 過去OSへの互換性カバー率も増加.
  • Good design apps : g.co/materialshowcase

Google Cloud Messaging 3.0

  • https://developers.google.com/cloud-messaging
  • 2012から今までに600+Kのアプリがプラットフォームに接続. 1.1 MQPS. ユーザベースでは1.5Bの端末で利用されてきた.
  • リクエストの数にすると25Trillion分にもなる
  • 端末がonlineであればメッセージの到達には約50msのパフォーマンス
  • GCMはWebsite(Chrome?)とiOSに対応.
  • Free
  • GCM のクラスプラットフォーム
    • Chrome
      • Windows, Mac, Linux, AndroidのChromeブラウザ上に対応.
      • Web Push API. JavascriptでRegistration Tokenを取得. あとはAndroidと同じ.
      • SubscribeにはNotificationが表示されユーザがこれを許可する必要がある.
    • iOS
      • APNS Serverに接続. APNS Tokenを取得
      • APNS TokenのGCMスコープ付きパラメータ(?)から得たInstanceIDからGCM Registration Tokenを得る.
      • Your serverにRegistration IDや端末識別情報を登録する.
      • GCMの送信について. Applicationがbackgroundの場合GCMはAPNS Serverを経由してメッセージを送信する.
      • ApplicationがForegroundになれば直接GCMと通信する.
    • Multipul Message
      • Registration IDの数だけYour serverはGCMへメッセージを投げる必要があった
      • Device Group と Notification Keyの概念を作った
      • Device は GCMとYour ServerにRegistする
      • your serverはDeviceグループをcreate/updateするためGCMに通知
      • GCMはDeviceグループへのNotification Keyを返却する. (req/resはjson形式)
      • req: operation/notificationKeyName/registrationIdArray, res, notification_key
      • Groupへのメッセージ送信はnotification_keyを指定するのみ.
    • Topic message
      • Topickを作成しユーザがこれをsubscribe. topickに対してメッセージを投げるとsubscribeしているユーザにのみ通知される
      • your serverは”to”: “/topicks/[yourTopic]” のメッセージをGCMに送ればOK.
      • unsbscribeも同様にone line.
      • topick message は本日 2015.05.29から使用可能
      • 1アプリ1MのSubscriptionまで可能.
GcmPubSub.getInstance(context).subscribe(regToken, "/topic/[youTopic]", null);
  • API
    • google developer consoleからGCMを簡易送信できるようになった
    • GCMがreceiveしたMessage IDや送信時刻, 端末の状態を一覧できる
2015/05/24

RecyclerViewをModel-View-Presenterで書く

Writing RecyclerView by Model-View-Presenter

RecyclerViewの実装をMVPアーキテクチャベースで実装する.
MVPの実装を助けるライブラリとしてはmortardaggerを使用する.

MainApp.java

アプリケーションスコープのMortarScopeを提供するためgetSystemServiceをオーバライドする.
このMortarScopeはDaggerのObjectGraphをObjectGraphServiceとしてアプリケーションスコープの粒度でアプリ内に提供する.

public class MainApp extends Application {
  private MortarScope rootScope;

  @Override
  public Object getSystemService(String name) {
    if (rootScope == null) {
      rootScope = MortarScope.buildRootScope()
          .withService(ObjectGraphService.SERVICE_NAME, ObjectGraph.create(new RootModule()))
          .build("Root");
    }

    return rootScope.hasService(name) ? rootScope.getService(name) : super.getSystemService(name);
  }
}

IDEの設定によってはgetSystemServiceの引数に渡せる定数を縛っていため警告が表示されるので無効化するか警告のレベルを下げておく.

RootModule.java

今回はDIライブラリとしてDaggerを採用している. Daggerのためにルートモジュールを作成しておく.

@Module(
    injects = MainRecyclerView.class
)
public class RootModule {
  @Provides
  @Singleton
  public MainPresenter provideMainPresenter() {
    return new MainPresenter();
  }
}

MainActivity.java

ActivityはMVPでいうところのViewに位置する. このサンプルではActivityは単なるアクティビティスコープを提供するコンポーネントに過ぎない.
PresenterのためにアクティビティスコープはBundleServiceRunnerを提供する.

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    // Return the identifier of the task this activity is in. 
    // This identifier will remain the same for the lifetime of the activity.
    // Return Task identifier, an opaque integer.
    String scopeName = getLocalClassName() + "-task-" + getTaskId();
    MortarScope parentScope = MortarScope.getScope(getApplication());
    activityScope = parentScope.findChild(scopeName);
    if (activityScope == null) {
      activityScope = parentScope.buildChild()
          .withService(BundleServiceRunner.SERVICE_NAME, new BundleServiceRunner())
          .build(scopeName);
    }
    BundleServiceRunner.getBundleServiceRunner(this).onCreate(savedInstanceState);

    setContentView(R.layout.activity_main);
  }

  @Override
  public Object getSystemService(String name) {
    return activityScope != null && activityScope.hasService(name) ? activityScope.getService(name)
        : super.getSystemService(name);
  }

MainRecyclerView.java

RecyclerViewを拡張し, Presenterと関連できるMainRecyclerViewを定義する.

public class MainRecyclerView extends RecyclerView {
    @Inject
    MainPresenter presenter;

MainRecyclerViewはMVPでいうViewに位置するため, ViewHolderとそれを更新するメソッドもこのクラスに含めておく.

    static class MainViewHolder extends RecyclerView.ViewHolder {
        private TextView titleTextView;
        private TextView summaryTextView;

        public MainViewHolder(View itemView) {
            super(itemView);
            titleTextView = (TextView) itemView.findViewById(android.R.id.text1);
            summaryTextView = (TextView) itemView.findViewById(android.R.id.text2);
        }

        // called from Presenter.
        public void setText(String title, String summary) {
            titleTextView.setText(title.toUpperCase());
            summaryTextView.setText("-" + summary);
        }
    }

RecyclerView自体のレイアウト定義もこのクラスの責務になる.
setLayoutManagerでのレイアウト指定はコンストラクタで済ませておく.
ただし, AdapterについてはModelとの関連やビジネスロジックを含むためPresenter側に定義する.

    public MainRecyclerView(Context context, AttributeSet attrs) {
        super(context, attrs);

        // レイアウトの決定はViewの責務.
        this.setLayoutManager(new LinearLayoutManager(context));

        // Modelとの関連づけはPresenterの責務
        // this.setHasFixedSize(false);
        // this.setAdapter(recyclerViewAdapter);

        ObjectGraphService.inject(context, this);
    }

RecyclerViewのリストアイテムを選択した場合のイベントはPresenterに伝える.

    private final OnClickListener itemClickListener = new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            int position = MainRecyclerView.this.getChildPosition(v);
            // Delegate to Presenter
            presenter.onItemSelected(position);
        }
    };

    public MainViewHolder createViewHolder(ViewGroup parent) {
        View v = LayoutInflater.from(parent.getContext())
                .inflate(android.R.layout.simple_list_item_2, parent, false);
        v.setOnClickListener(itemClickListener);
        return new MainViewHolder(v);
    }

あとはMortarでお決まりのコードを書いておく.

    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();
        presenter.takeView(this);
    }

    @Override
    protected void onDetachedFromWindow() {
        presenter.dropView(this);
        super.onDetachedFromWindow();
    }

MainPresenter.java

最後にPresenter. こちらはRecyclerViewのAdapterに相当する責務を書く.
RecyclerViewをセットアップし,

    @Override
    protected void onLoad(Bundle savedInstanceState) {
        MainRecyclerView recyclerView = getView();
        recyclerViewAdapter = new RecyclerViewAdapter(getView());

        // Modelとの関連づけはPresenterの責務
        recyclerView.setHasFixedSize(false);
        recyclerView.setAdapter(recyclerViewAdapter);

        // レイアウトの決定はViewの責務
        // recyclerView.setLayoutManager(new LinearLayoutManager(context));

リストアイテムが選択されたときの処理を記述し,

    public void onItemSelected(int position) {
        Log.i("yuki", "Item Selected! " + datasource.get(position));
    }

Adapterの処理を追加して仕上げる.

    private class RecyclerViewAdapter
            extends RecyclerView.Adapter<MainRecyclerView.MainViewHolder> {
        private MainRecyclerView recyclerView;

        RecyclerViewAdapter(MainRecyclerView recyclerView) {
            this.recyclerView = recyclerView;
        }

        @Override
        public MainRecyclerView.MainViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
            return recyclerView.createViewHolder(parent);
        }

        @Override
        public void onBindViewHolder(MainRecyclerView.MainViewHolder view, int position) {
            view.setText(datasource.get(position), datasource.get(position));
        }

        @Override
        public int getItemCount() {
            return datasource.size();
        }
    }

当然ModelとViewへの参照も持つ.

public class MainPresenter extends ViewPresenter<MainRecyclerView> {
    private RecyclerViewAdapter recyclerViewAdapter;
    private List<String> datasource
            = Arrays.asList("data1", "data2", "data3", "data4", "data5", "data6", "data7", "data8", "data9");

以上.

2015/05/21

Android : espresso.contribでRecyclerViewをテスト

RecyclerViewのテストにespresso-contribライブラリを導入する.

espresso-contrib

  • RecyclerViewActions: handles interactions with RecyclerViews
  • PickerActions: handles interactions with Date and Time pickers

今回の環境は下記.

  • Android Studio 1.2.1.1
  • Espresso 2.1
  • appcompat-v7:22.1.1
  • recyclerview-v7:21.0.3
  • espresso-contrib 2.1

build.gradle

JUnit4サポートのためTestRunnerを定義しておく.

  android {
    defaultConfig {
      ...
      testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
  }

今回はAppCompatとRecyclerViewを導入.

  dependencies {
    compile 'com.android.support:appcompat-v7:22.1.1'
    compile 'com.android.support:recyclerview-v7:21.0.3'

espresso-contribはsub-dependenciesとして次のライブラリへの依存関係を含んでいる.

  • com.android.support:support-annotations
  • com.android.support:support-v4
  • com.android.support:recyclerview-v7
  • com.android.support.test.espresso:espresso-core

espresso-contribの導入にはGradleの依存関係ツリーをコントロールする必要がある.

support-anotationsはAppCompatにも含まれおり, コンフリクト回避のため除外しておく.
compileコンフィギュレーションでsupport-anotationsを取込み済みなのでandroidTestCompileコンフィギュレーションでは不要.

  android {
     configurations {
         // Resolved 'com.android.support:support-annotations' versions for app (xx.x.x) and test app (xx.x.x) differ.
         // Add this statement if 'com.android.support:support-annotations:x.x.x' dependencies was already defined in compile configuration.
         androidTestCompile.exclude group: 'com.android.support', module: 'support-annotations'
     }
   }

espresso-contribはcom.android.support.test.espresso:espresso-coreを含んでいるためこれを定義する必要はない.

  dependencies {
    // You can compile without 'espresso-core'. Because 'espresso-contrib' has.
    // androidTestCompile 'com.android.support.test.espresso:espresso-core:2.1'

また, espresso-contribはcom.android.support:support-v4com.android.support:recyclerview-v7も含んでいるため,
コンフリクト回避のためこれを除外しておく.

  dependencies {
    androidTestCompile ('com.android.support.test.espresso:espresso-contrib:2.1') {
        exclude group: 'com.android.support', module: 'support-v4'
        exclude group: 'com.android.support', module: 'recyclerview-v7'
    }
    androidTestCompile 'com.android.support.test:rules:0.2'
  }

お決まりのLICENSE.txtのコンフリクトも解消しておく.

  android {
    packagingOptions {
        exclude 'LICENSE.txt'
    }
  }

Gradleの全容は下記を参照.
build.gradle

テストケースを書く

既にEspressoを実行できる準備はできているのでテストケースを書けば完了.

@RunWith(AndroidJUnit4.class)
public class ApplicationTest  {
    @Rule
    public ActivityTestRule<MainActivity> mActivityRule = new ActivityTestRule<>(MainActivity.class);

    @Test
    public void RecyclerViewItemのクリック() {
        onView(withId(R.id.list)).perform(
                RecyclerViewActions.actionOnItemAtPosition(0, click()));
    }
}

ApplicationTest.java

Point

// build.gradle
android {
    packagingOptions {
        exclude 'LICENSE.txt'
    }
    configurations {
        // Resolved 'com.android.support:support-annotations' versions for app (22.1.1) and test app (22.0.0) differ.
        // Add this statement if 'com.android.support:support-annotations:x.x.x' dependencies was already defined in compile configuration.
        androidTestCompile.exclude group: 'com.android.support', module: 'support-annotations'
    }
}

dependencies {
    compile fileTree(include: ['*.jar'], dir: 'libs')

    // 'appcompat-v7' has 'support-annotations'.
    compile 'com.android.support:appcompat-v7:22.1.1'
    compile 'com.android.support:recyclerview-v7:21.0.3'

    testCompile 'junit:junit:4.12'
    testCompile "org.mockito:mockito-core:1.9.5"

    // If you are on a Mac, you will probably need to configure the default JUnit test runner configuration
    // in order to work around a bug where Android Studio does not set the working directory to the module being tested.
    // This can be accomplished by editing the run configurations, Defaults -> JUnit and changing the working directory value to $MODULE_DIR$.
    //  - Resolved java.io.FileNotFoundException: build/intermediates/bundles/debug/AndroidManifest.xml (No such file or directory)
    //  - Resolved java.lang.UnsupportedOperationException: Robolectric does not support API level 1.
    //  - see. https://github.com/robolectric/robolectric/issues/1648
    //  - see. http://robolectric.org/getting-started/#Note for Mac Users
    testCompile 'org.robolectric:robolectric:3.0-rc2'

    // You can compile without 'espresso-core'. Because 'espresso-contrib' has.
    // androidTestCompile 'com.android.support.test.espresso:espresso-core:2.1'

    // When you use "espresso-contrib:2.0" with "rules:0.2".
    // might "com.android.dex.DexException: Multiple dex files define Landroid/support/test/BuildConfig;" occur.
    androidTestCompile ('com.android.support.test.espresso:espresso-contrib:2.1') {
        // Resolved java.lang.NoClassDefFoundError: your.package.name.EspressoTargetActivity
        // 'espresso-contrib' has already 'support-v4' package.
        exclude group: 'com.android.support', module: 'support-v4'

        // Resolved java.lang.IllegalAccessError: Class ref in pre-verified class resolved to unexpected implementation
        // 'espresso-contrib' has already 'recyclerview-v7' package.
        exclude group: 'com.android.support', module: 'recyclerview-v7'
    }

    // You can compile without 'runner'. Because 'rules' has.
    // androidTestCompile 'com.android.support.test:runner:0.2'
    androidTestCompile 'com.android.support.test:rules:0.2'
}

Copyright 2015 yuki312 All Right Reserved.

Licensed under the Apache License, Version 2.0 (the “License”);
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an “AS IS” BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

以上.

Android : Robolectric 3.0

Setup

Android Studio 1.2 で Robolectric3.0を準備する.

build.gradle

モジュールのbuild.gradleに下記ライブラリを追加.

    // UnitTest用.
    testCompile 'junit:junit:4.12'
    testCompile "org.mockito:mockito-core:1.9.5"

    // Robolectric用
    testCompile 'org.robolectric:robolectric:3.0-rc2'

Android gradle 1.1以降, AndroidStudioからのUnitTest実行がサポートされた.
http://tools.android.com/tech-docs/unit-testing-support

build.gradleの全容は下記.
app/build.gradle

Build VariantsのTest ArtifactをUnit Testsに設定し, test/javaでユニットテスト用のフォルダを作成しておく.

テストケースを書く

テストケースのRunnerにはRobolectricGradleTestRunner.classを指定.
ConfigアノテーションでGradleの出力パスをRobolectricに伝えるためBuildConfig.classを指定する.

@RunWith(RobolectricGradleTestRunner.class)
@Config(constants = BuildConfig.class, emulateSdk = 21)
public class MyTest {

テストケースの全容は下記.
MainActivityTest.java

テスト実行

現状だとテストを実行するには一手間必要である. そのままJUnitを実行すると次のエラーが発生する.

  java.io.FileNotFoundException: build/intermediates/bundles/debug/AndroidManifest.xml (No such file or directory)
  java.lang.UnsupportedOperationException: Robolectric does not support API level 1.

これを解決するためにRun configurationsからRobolectricを実行するプロファイルのworking directory$MODULE_DIR$を指定する.

参考:
https://github.com/robolectric/robolectric/issues/1648
http://robolectric.org/getting-started/ #Note for Mac Users

Point

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.android.support:appcompat-v7:22.1.1'

    // Unit test support on AndroidStudio. Android gradle version 1.1 or later Required.
    //  - see. http://tools.android.com/tech-docs/unit-testing-support
    testCompile 'junit:junit:4.12'
    testCompile "org.mockito:mockito-core:1.9.5"

    // If you are on a Mac, you will probably need to configure the default JUnit test runner configuration
    // in order to work around a bug where Android Studio does not set the working directory to the module being tested.
    // This can be accomplished by editing the run configurations, Defaults -> JUnit and changing the working directory value to $MODULE_DIR$.
    //  - Resolved java.io.FileNotFoundException: build/intermediates/bundles/debug/AndroidManifest.xml (No such file or directory)
    //  - Resolved java.lang.UnsupportedOperationException: Robolectric does not support API level 1.
    //  - see. https://github.com/robolectric/robolectric/issues/1648
    //  - see. http://robolectric.org/getting-started/#Note for Mac Users
    testCompile 'org.robolectric:robolectric:3.0-rc2'
}
@RunWith(RobolectricGradleTestRunner.class)
@Config(constants = BuildConfig.class, emulateSdk = 21)
public class MainActivityTest {
    @Before
    public void setUp() throws Exception {
        ShadowLog.stream = System.out;
    }

    @Test
    public void HelloRobolectric() throws Exception {
        final MainActivity activity = Robolectric.setupActivity(MainActivity.class);
        activity.onCreate(null);
        Log.d("yuki", "Hello Robolectric");
    }
}

以上.

2015/05/12

Android Realm BrowserでRealmDBをのぞく

Android Realm BrowserでRealmの中身をのぞいてみるサンプル.
サンプルプロジェクトはこちら

はじめに

Realmはモバイル向けに作られた軽量なNoSQL Database.
下記はAndroid向けに作られたrealm-javaについての記載である.

  • パフォーマンスはSQLiteやOrmLite, greenDAO等のORMよりもInsert/Count/Queryそれぞれ数倍高速に動作する.
  • 暗号化機能も備えており, パフォーマンスは通常時と比べてもほぼ変わらない.
  • iOS向けにも提供されている(クロスプラットフォーム).
  • ContentProviderの実装と比べて恐ろしく簡単に実装でき, 使い方も容易.

動作要件

realm-javaの動作要件は下記.

  • only Android.
  • AndroidStudio 0.8.6 or later.
  • latest Android SDK.
  • JDK 7 or later.
  • Android API Level 9 or higher.

準備

build.gradleに下記を追加.

compile 'io.realm:realm-android:0.80.1'

ProGuardに下記を追加.
Realmはコンパイル時にRealmObjectのProxyを生成する. ProGuardがこれを壊さないために下記を追加する.

-keepnames public class * extends io.realm.RealmObject
-keep class io.realm.** { *; }
-dontwarn javax.**
-dontwarn io.realm.**

Browse

Realmのデータベースを覗くソフトがMacOSX向けにリリースされている.
Realm Browser

アプリケーションフォルダ/files下に出力されたrealmファイルをこれにかませると中身をみられる.

Androidデバイス上でデータベースを閲覧するライブラリもある.
Android Realm Browser

build.gradleに下記を追加.

compile 'com.github.dmytrodanylyk.realm-browser:library:0.0.2'
// セットアップして...
RealmBrowser.getInstance().addRealmModel(Model.class);

// 閲覧用のActivityを呼び出せばok
RealmFilesActivity.start(this);

Model

Realmでのモデル定義. クラス自体はシンプルだが制約がある.

  • 主キーには@PrimaryKeyアノテーションを付与.
  • getter/setterはRealmのproxyオブジェクト生成のためだけに必要. getter/setterは実行されない
  • getter/setter以外のメソッドは定義できない

Realm向けのModelクラスは完全にRealm専用とした方が良い.
Realmがサポートするデータ型は boolean, short, ìnt, long, float, double, String, Date, byte[].
Realmではshort, int, longがすべてlong型として扱われる.
また, 1対多, 多対多なリレーションのためにRealmObjectのサブクラスとRealmList

public class Model extends RealmObject {
    @PrimaryKey
    private String pk;
    private int value;

    public String getPk() {
        return pk;
    }

    public void setPk(String pk) {
        this.pk = pk;
    }

    public int getValue() {
        return value;
    }

    public void setValue(int value) {
        this.value = value;
    }
}

Realm Databaseへ書き込み.

Realm realm = Realm.getInstance(this, "test.realm");
realm.executeTransaction(new Realm.Transaction() {
    @Override
    public void execute(Realm realm) {
        Model model = realm.createObject(Model.class);
        model.setPk(UUID.randomUUID().toString());
        model.setValue(new Date().getTime());
    }
});
realm.close();

Android Realm Browserで結果を見る. MainActivity

// セットアップして...
RealmBrowser.getInstance().addRealmModel(Model.class);

// 閲覧用のActivityを呼び出せばok
RealmFilesActivity.start(this);

以上.

2015/05/11

Android : Mortar - Say Goodbye to unwanted lifecycle

Mortar library

この章はmotar - GitHubの内容を参考に筆者の解釈のもと多くの加筆・修正が加えられています.
Mortarについての正確な情報を確認するには上記のオリジナルページをご確認ください.

一言で言うと, MortarはAndroidの厄介なライフサイクルをいい具合に隠蔽してくれるライブラリだ.
Mortarは(Fragmentが目指した)モジュールユニットなView層を提供するアーキテクチャの基礎となり, DaggerによるObjectGraphと柔軟なScopeの概念によりActivityのライフサイクルからPresenterを分離しMVPを促進する.

MortarはDaggerのObjectGraphあるいはDagger2のComponentsをSystemServiceかのような形式で提供することもできる. もちろん不要であれば気にする必要はない. Daggerは完全に分離されている.

Mortarでは全ての管理をSingletonなMortarScopeが行う.
MortarScopeはMortarの肝になる概念で, オブジェクトにその名の通り”スコープ”を与える. 通常はApplicationやActivityといった範囲でスコープを割り当てる. 重要なのは”Activity”がスコープなのであって”Activityのライフサイクル”がスコープではないということだ.
Activityを実装するケースでは, Activityのライフサイクル単位をスコープとするのではなく, ConfigurationChangeやRecreateといったAndroid Framework都合のライフサイクルを超えた”Activity”をスコープとすることが多い.
つまり, より抽象的には”1画面”をスコープとしたいのだ. MotarScopeはこういったスコープの制御を提供してくれる.

MortarScopeは通常Applicationのコンテキストで保持する. あるいは独自の短命なスコープを持った一過性のオブジェクトとして定義することも可能だ. スコープはサブスコープとしてネストさせることもできる. ネストされたスコープはより上位のスコープを隠蔽し透過的に扱える.
例えば短命なスコープを持つウィザード画面へのオブジェクトグラフを形成するには次のように記述できる.

// ObjectGraphServiceは内部でgetSystemServiceを呼んでいるに過ぎない. 
ObjectGraphService.inject(getContext(), this)

Mortarが提供する強力なプリインサービスの1つにBundleServiceがある.
このサービスはView(あるいはActivity Contextにアクセスするオブジェクト)がActivityの持つBundleへ安全にアクセスできる機能を提供する.
MVPアーキテクチャのアプローチではBundleService上でPresenterを構築し持続性を持たせる.
前述の通りPresenterはViewからは完全に分離され, Activityのライフサイクル(ConfigurationChangeやRecreate)とは無縁の関係にあるべきだ. BundleServiceがViewへのシンプルで安全なアクセスを提供してくれる.

The Big Picture

アプリケーションはsingletonなMortarScopeを持つことが通常である.
MortarScopegetSystemServiceメソッドをデリゲートし次のように機能させる.

public class MyApplication extends Application {
  private MortarScope rootScope;

  @Override public Object getSystemService(String name) {
    if (rootScope == null) rootScope = 
        MortarScope.buildRootScope().build();

    return rootScope.hasService(name) ? 
        rootScope.getService(name) : super.getSystemService(name);
  }
}

これでsingletonなコアサービスとしてのスコープを定義できる.
スコープはサブスコープを持つことができる. サブスコープはスコープの登録/解除を対で呼ぶ責任を負う.

  • Scoped.onExnterScope(MortarScope)
  • Scoped.onExitScope(MortarScope)

スコープの中でDaggerが持つObjectGraphのように他のサービスを提供するには次のように記述する.

@Override public Object getSystemService(String name) {
    if (rootScope == null) {
        rootScope = MortarScope.buildRootScope()
          .with(ObjectGraphService.SERVICE_NAME, 
              ObjectGraph.create(new RootModule()))
          .build();
    }

    return rootScope.hasService(name) ? 
        rootScope.getService(name) : super.getSystemService(name);
}

ContextをこのObjectGraphに登録するにはMortarScopeにプリインされているサービスObjectGraphServiceを使えば容易にできる.

public class MyView extends LinearLayout {
  @Inject SomeService service;

  public MyView(Context context, AttributeSet attrs) {
    super(context, attrs);
    ObjectGraphService.inject(context, this);
  }
}

MortarはActivityのライフサイクルに合うサブスコープを形成するにはActivityのgetSystemServiceメソッドでこれを構築する. ActivityでMortarScopeを構築する際にはBundleServiceRunnerを設定しておく. ActivityのConfigurationChangeやRecreateによる厄介な問題をBundleServiceRunnerは手助けする.
BundleServiceRunnerのBundleに必要な情報を詰め込み, Activityが然るべきタイミングでBundleServiceRunnerのonCreateonSaveInstanceStateを呼び出すことで情報のsave/restoreを可能にする.

public class MyActivity extends Activity {
  private MortarScope activityScope;

  @Override public Object getSystemService(String name) {
    MortarScope activityScope = 
        MortarScope.findChild(getApplicationContext(), getScopeName());

    if (activityScope == null) {
      activityScope = MortarScope.buildChild(getApplicationContext(), getScopeName())
          .withService(BundleServiceRunner.SERVICE_NAME, new BundleServiceRunner())
          .withService(HelloPresenter.class.getName(), new HelloPresenter())
          .build();
    }

    return activityScope.hasService(name) ?
        activityScope.getService(name) : super.getSystemService(name);
  }

  @Override protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    BundleServiceRunner.getBundleServiceRunner(this).onCreate(savedInstanceState);
    setContentView(R.layout.main_view);
  }

  @Override protected void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);
    BundleServiceRunner.getBundleServiceRunner(this).onSaveInstanceState(outState);
  }

  @Override protected void onDestroy() {
    if (isFinishing()) {
      MortarScope activityScope = findChild(getApplicationContext(), getScopeName());
      if (activityScope != null) activityScope.destroy();
    }

      super.onDestroy();
  }
}

View.onSaveInstanceStateで求められるようなParcellable化の手間はもはや必要なくなる.
MortarScopeを得た今, 多くのオブジェクトがBundleServiceにアクセスできる状態である.
つまり, save/restoreはBundleServiceRunnerに任せることができる状態である.

Reference.

2015/05/10

Android : Model-View-PresenterとAndroid

この記事は
- よりよい実装方法がある
- 説明に不足がある
といった理由から取り下げます.

アーキテクチャについての記事は追って書き直します.

Model - View - Presenter と Android

  • Viewは, データの出力を行い, ユーザ操作を受け付けPresenterにこれを伝搬するUI層. AndroidではActivity, Fragment, Viewに相当する.
  • Modelは, SQLite3やWebAPI, SharedPreferenceなどのレポジトリ.
  • Presenterは, ModelとViewのブリッジ役を担う. Modelの内容を整形しViewにそれを伝搬する. またViewへの入力を適切なModelに伝搬する.

Androidアプリケーションのプログラミングで厄介な問題の1つにActivity/Fragment/Viewといった固有のライフサイクルをもつオブジェクトでバックグラウンドタスクを管理することが挙げられる.
Model-View-ControllerアーキテクチャやModel-Viewアーキテクチャは, ModelとView/Controllerが関係を持つ. AndroidではController/ViewにあたるActivity, Fragment, Viewがライフサイクルを持っている.
Modelとの通信においてはController/Viewがそのライフサイクルを終え, 不意に破棄, 再生成されることを念頭に設計しなければならない.

MVCは古き良きアーキテクチャであったが, Androidでは開発者が気にしなければならない課題が少なくない.
昨今のアプリケーションのFEPはキーボードやマウス、ジョイスティックコントローラといった類いのものではなくなっている.
マテリアルデザインはリッチなユーザインタフェースを要求し, より複雑・高度化された”display”と”input”はViewとControllerの境界を曖昧にした.
MVCでは無理がある. 無理矢理MVCを適用して, その結果ViewだかControllerだかハッキリしないActivityやFragmentがGod object化してしまうのも無理はない.

Presenterはバックグラウンドタスクを管理する. またPresenterはActivity, Fragment, Viewのライフサイクルから分離させる. これにより前述の厄介な問題を排除し, KISSの原則を促進する.
また, AndroidアプリケーションではしばしばActivityがGod object化する傾向にあるが, Presenterへの適切な責務分担がこれを解消する.

PresenterはFragmentではない. Presenterは前述の通り厄介なライフサイクルからは分離されたオブジェクトである. AndroidにはConfigurationChangeの概念とActivityのRecreationの概念があることを忘れてはいけない. 面倒で推奨もされていないギミックでこれらを避けようとすることはできるが問題を見え辛くしているにすぎない.
FragmentやLoaderの類いはConfigurationChangeの課題を多少解消してくれる(retain-instance)が根本的な解決ではないし, ActivityのRecreationには対応できていない.
またFragmentやLoaderがTestabilityの面で優れていないのも重要なポイントである.

- ConfigChange ActivityRecreate ProcessRestart
Activity, Fragment, View save/restore save/restore save/restore
Fragment.setRetainInstance(true) no change save/restore save/restore
Static variables and threads no change no change reset

PresenterはGod object化しないように適切に責務分割される必要がある(でないと従来のActivityと同じ轍を踏むことになる).
PresenterはFragmentよりも容易に責務分割できるはずだ. なぜならPresenterは厄介なライフサイクルの呪縛から解放されているのだから.

Presenter Example

View

ViewはPresenterへの参照を持ち, Presenterを生成し自身と関連づける.
PresenterのライフサイクルをActivityのそれから切り離すためにPresenterはstaticフィールドとして宣言される.
注意するべきポイントはPresenterはtakeViewメソッドによりViewへの参照を持つことになるという点である.
View, つまりはActivity,Fragment,Viewへの参照がstaticフィールドに保持されるため, 上手く参照を破棄しないとメモリリークを引き起こす.
ライフサイクルが終わりを迎えるタイミングでPresenterに自身の破棄を要求し, かつstaticフィールドの参照をnullに設定する.

public class MainActivity extends ActionBarActivity {

    private static MainPresenter presenter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        if (presenter == null) {
            presenter = new MainPresenter();
        }
        presenter.takeView(this);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        presenter.takeView(null);
        if (isFinishing()) {
            presenter = null;
        }
    }

    public void onUpdateView(final CuteModel model) {
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                TextView v = ((TextView) findViewById(R.id.text));
                v.setText(v.getText() + ", " + model.getValue());
            }
        });
    }
}

Presenter

PresenterはModelとViewへの参照を持つ. Viewへの参照はView自身からtakeView()により関連づけされる. ModelはPresenterが生成し, 適切なタイミングでViewに変更内容を伝搬(updateView)する.
PresenterはViewのライフサイクルに左右されない. PresenterとViewの関係はModelの変更を伝搬する先がいる(view != null)かいない(view == null)かに留まる.
厄介なライフサイクルから解放されたPresenterは非同期ローディングや突発的な外部イベントにも柔軟に対応できる可能性を得る.
この例ではCuteModelが非同期にvalueをロードし結果をコールバックしてくるが, “呼び出されたにも関わらずコールバック先が破棄された!”あるいは”コールバックを受け取れる状態ではない”といった不可思議な状態には陥らないし, 無用なキャンセラレーションの実装も必要最小限で済む. もはやAndroidアプリケーションで非同期ローディングは怖くなくなった.

public class MainPresenter {
    private CuteModel model;
    private MainActivity view;

    public MainPresenter() {
        model = new CuteModel();
        model.query(new Listener() {
            @Override
            public void callback() {
                updateView();
            }
        });
    }

    public void takeView(MainActivity view) {
        this.view = view;
        updateView();
    }

    private void updateView() {
        if (view != null) {
            view.onUpdateView(model);
        }
    }
}

Model

Modelはビジネスロジックに集中できる. Listenerは存在する限り健全な状態であることが保証されている.

public class CuteModel {
    public interface Listener {
        void callback();
    }

    private int value = 0;

    public void query(final Listener listener) {
        Executors.newSingleThreadScheduledExecutor().schedule(new Runnable() {
            @Override
            public void run() {
                value = 100;
                listener.callback();
            }
        }, 5000, TimeUnit.MILLISECONDS);
    }

    public int getValue() {
        return value;
    }
}

Next step.

必要なアーキテクチャは揃ったが, PresenterとViewの間ではお決まりのやり取りがその数だけ存在する. コピーコードを避けるためにもここでKISSを推進するツールの導入を考えてみる.

それにはMortarとDaggerを使う選択肢がある. MvpWithRecyclerView

Reference.