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を使うことができます.

以上です.

2017/02/18

Dagger2. MultibindingでComponentを綺麗に仕上げる

はじめに

2017年に入ってDagger2もバージョン2.9を迎えました.
Androidでも使われることが多いDIフレームワークも, バージョンを重ねるごとに便利なAPIが増えています.

本稿はAndroidアプリを例に, Activityに依存するComponentからインジェクションする方法について, 過去のAPIを使用した方法と, 新しいAPIを使用した方法とで比較を行い, より綺麗なインジェクションを実現していきます.

今回紹介する内容+αとソースコードはGitHubにもアップしていますのでそちらもあわせてご覧ください :)

goto GitHub pages

Subcomponent. 親と子の密結合問題

Androidでは, コンポーネントをアクティビティの単位で分割することがよくあります(e.g. MainActivityComponent, SettingActivityComponent, etc.)
そのようなコンポーネントが依存性の解決にアプリケーションスコープのオブジェクトを必要とする場合や, 他のコンポーネントからも参照される共有オブジェクトを持つ場合にSubcomponentとして定義されることがあります.
サブコンポーネントの仕組みは, それぞれのいくつかのコンポーネントが持つ共通部分をまとめて定義したり, スコープの観点からみた”親-子”を定義したりするのに大変便利です.
ただ, 古いバージョンのDaggerではサブコンポーネントから依存される親コンポーネントは, サブコンポーネントのクラスを知っている必要があり, 親と子の結合度が高くなってしまう問題がありました.

@Component(...)
public interface AppComponent {
  // 親であるAppComponentは子にあたるサブコンポーネントを全て知っておく必要がある :(
  MainActivityComponent plus(MainActivityModule module);
  SettingActivityComponent plus(SettingActivityModule module);

この問題はDagger2.7で追加された@Modulesubcomponent属性を使うことで解決できます.

Module.subcomponents

@Modulesubcomponent属性はサブコンポーネントの親を指定するための新しい手段を提供します.

subcomponent属性を持つモジュールは親コンポーネントと子コンポーネントの関係を築く橋渡し役になります.
このモジュールは親コンポーネントの@Component(modules=...)に定義されることで, 親コンポーネントに属することになります.

// subcomponent属性には対象となるサブコンポーネントクラスを定義する  
@Module(subcomponents = {MainComponent.class, SubComponent.class})
public abstract class ActivityBindingModule {
}

// 親コンポーネントのmodulesに上記モジュールを追加する
@Component(modules = {AppModule.class, ActivityBindingModule.class})
public interface AppComponent { … }

この方法を使う場合は@Module(subcomponents={...})で指定したサブコンポーネントをどのように構築するのかを定義するコンポーネントビルダーが必要になります.
サブコンポーネントの内部インタフェースとしてビルダーを新たに宣言し, これに@Component.Builderアノテーションをつけます.
また, (サブ)コンポーネントビルダーにはいくつかの実装ルールがあるのでそれに従います.

@Subcomponent(modules = MainModule.class)
public interface MainComponent {
  @Subcomponent.Builder interface Builder {
    Builder activityModule(MainModule module);
    MainComponent build();
  }

サブコンポーネントのビルダーはDaggerによってプライベートなインナークラスとして自動生成されます.
必要なビルダーはActivityBindingModuleをインストールした親コンポーネントから提供されます.

@Component(modules = { AppModule.class, ActivityBindingModule.class })
public interface AppComponent {
  MainComponent.Builder mainComponentBuilder();
  SettingComponent.Builder settingComponentBuilder();

ここまでの変更によって, 親コンポーネントが抱えていたサブコンポーネントとの密結合関係がコンポーネントビルダーとの密結合関係に変わりました.
まだ, 親コンポーネントはサブコンポーネントのことを知っている状態で, サブコンポーネントが増えると親コンポーネントもあわせて修正しないといけません.

次はマルチバインディングの機能を使ってこの問題に対処します.

Multibinding. コンポーネントマップの自動生成

Dagger2.4から, 生成したオブジェクトをコレクションにバインドするマルチバインディング機能が追加されました.
これにより, 値がプリセットされたSetMapをインジェクションできるようになりました.
次はこのマルチバインディングを使ってサブコンポーネントビルダーのマップコレクションを作り, 親コンポーネントとコンポーネントビルダーの関係を疎にして, サブコンポーネントを柔軟に追加できる仕組みを作っていきます.

まずはじめに, マルチバインディングでマップに格納するKeyValueを決めておきます.
Androidではコンポーネットをアクティビティ単位で分割することが多いのでKeyにはアクティビティのクラスオブジェクトを格納し, Valueにはコンポーネントビルダーを格納することにします.

アプリケーションクラスが各アクティビティの詳細を知らなくてもいいように, アクティビティに直接関わるコンポーネントを抽象化したActivityComponentインタフェースとモジュールを抽象化したActivityModuleインタフェースを定義します. これらはまだマーカーインタフェース扱いですが後々メソッドを定義していきます.

public interface ActivityComponent {
}

public interface ActivityModule {
}

// Activityと関連するコンポーネントとモジュールはこれを実装する
@Subcomponent(modules = MainComponent.MainModule.class)
public interface MainComponent extends ActivityComponent<MainActivity> {
  ...
  @Module
  class MainModule extends ActivityModule {
    ...
  }
}

コンポーネントビルダーも抽象化します.
上記で定義したインタフェースActivityModuleをパラメータとして受け取り, ActivityComponentを構築して返すビルダーパターンのインタフェースです.

public interface ActivityComponentBuilder<M extends ActivityModule, C extends ActivityComponent> {
  ActivityComponentBuilder<M, C> activityModule(M activityModule);

  C build();
}

// Activityと関連するサブコンポーネントビルダはこれを実装する
@Subcomponent.Builder
interface Builder extends ActivityComponentBuilder<MainModule, MainComponent> {
}

アクティビティがアクティビティコンポーネントを取得したい場合の手順は次の通りです.

  1. ActivityComponentBuilderの実装クラスにあたるビルダーインスタンスを取得
  2. 必要なActivityModuleをビルダーに指定する
  3. buildメソッドでActivityComponentを構築してコンポーネントインスタンスを取得

今時点ではまだコンポーネントビルダーを取得するために親コンポーネントは具体的なクラス名を指定する必要がある状態です.

@Component(modules = { AppModule.class, ActivityBindingModule.class })
public interface AppComponent {
  MainComponent.Builder mainComponentBuilder();
  SettingComponent.Builder settingComponentBuilder();

アクティビティがコンポーネントを取得する時にはAppComponent経由で必要なコンポーネントビルダーを取得することになります.
現状でもまだ親コンポーネントからサブコンポーネントビルダーへの依存を分離できていません.
次はいよいよ, 用意したActivityComponent, ActivityModule, ActivityComponentBuilderとマルチバインディング機能を使って依存関係を取り除いていきます.

マルチバインディングでマップに格納するオブジェクトを提供するにはキーにする型をはっきりさせる必要があります.
今回は, アクティビティのクラス情報をキーとするので専用のアノテーションを作成します.
キーとしてClassを受け取り, その型をActivityのサブクラスに制限するジェネリクスを指定しておくのがポイントです.
これで, マルチバインディングが行われるマップのキーにはActivityのサブクラスだけが許可されるようになります.

@MapKey
public @interface ActivityMapKey {
  Class<? extends Activity> value();
}

それでは, マルチバインディングの対象となるマップを定義しましょう.
マップを提供する方法は他のオブジェクトと同じくモジュールのメソッドとして定義します.
今回はサブコンポーネントを柔軟に追加できるように改良することが目的なので, これを定義するモジュールは@Module(subcomponents=...)を宣言しているActivityBindingModuleが妥当でしょう.

ActivityBindingModuleにマルチバインディングされるマップの定義を追加したものが下記です.

@Module(subcomponents = {MainComponent.class, SubComponent.class})
public abstract class ActivityBindingModule {
  @Provides @IntoMap @ActivityMapKey(MainActivity.class)
  public ActivityComponentBuilder mainComponentBuilder(
      MainComponent.Builder builder) {
    return builder;
  }

  @Provides @IntoMap @ActivityMapKey(SettingActivity.class)
  public ActivityComponentBuilder settingComponentBuilder(
      SettingComponent.Builder builder) {
    return builder;
  }
}

ここでさらに, Dagger2.4から導入された@Bindsを使えば, こういったボイラープレートなプロバイダーメソッドの定義を簡略化できます.
Dagger2.5からはマルチバインディングに対しても使えるようになったので, これを使ってシンプルに定義したものが下記です.

@Module(subcomponents = {MainComponent.class, SettingComponent.class})
public abstract class ActivityBindingModule {
  @Binds @IntoMap @ActivityMapKey(MainActivity.class)
  public abstract ActivityComponentBuilder mainComponentBuilder(
      MainComponent.Builder builder);

  @Binds @IntoMap @ActivityMapKey(SettingActivity.class)
  public abstract ActivityComponentBuilder settingComponentBuilder(
      SettingComponent.Builder builder);
}

さて, これでClass<? extends Activity>型をキーに持ち, 値にはActivityComponentBuilder型が格納されるMapを自動生成できるようになりました.
これを提供するプロバイダーメソッドは親コンポーネントにあたるAppComponentに定義します.

@Component(modules = { AppComponent.AppModule.class, ActivityBindingModule.class })
public interface AppComponent {
  Map<Class<? extends Activity>, ActivityComponentBuilder> activityComponentBuilders();

これによって, Daggerがマルチバインディングによってコンポーネントビルダーのマップを構築し, それをactivityComponentBuilders()メソッド経由で提供するようになりました.
マルチバインディング適用前と異なる点は, もはやAppComponentがサブコンポーネント(MainComponentBuilder etc.)について知らなくてよくなったという点です.
今やAppComponentは抽象化されたビルダーActivityComponentBuilderのことだけ知っていればよいのです.
これでついに親コンポーネントがサブコンポーネントから独立しました Yay!
アクティビティとサブコンポーネントが追加されてもアプリケーションクラスを変更する理由はもはやありません.
アクティビティに関係するサブコンポーネントに追加・変更がある場合の修正はActivityBindingModuleに閉じています.

plus one

これで一通りの実装は完了ですが, もう一歩進めましょう.

アクティビティのコンポーネントにはinjectメソッドを定義することがよくあります.
なのでActivityComponentにこれを定義します.

public interface ActivityComponent<T extends Activity> extends MembersInjector<T> {
}

もうひとつ, ActivityModuleにはアクティビティインスタンスを保持させることがよくあるので, その定義を追加しておきます.

@Module
public abstract class ActivityModule<T extends Activity> {
  protected final T activity;

  public ActivityModule(T activity) {
    this.activity = activity;
  }

  @Provides public T provideActivity() {
    return activity;
  }
}

アクティビティコンポーネントを取得するときは, 下記の手順です.

  1. ActivityComponentBuilderの実装クラスにあたるビルダーインスタンスを取得
  2. 必要なActivityModuleをビルダーに指定する
  3. buildメソッドでActivityComponentを構築してコンポーネントインスタンスを取得

ActivityComponentBuilderを取得するメソッドは以前と同様, 親コンポーネントにあたるAppComponentに定義されているので, 下記の要領で取得します.
あとはビルダーに必要なモジュールを指定してコンポーネントを構築します.

// コンポーネントビルダーのマップを取得
Map<Class<? extends Activity>, ActivityComponentBuilder> map =
    ((AppComponent) context.getApplicationContext().activityComponentBuilders();

// コンポーネントビルダーのインスタンスを取得
MainComponent.Builder builder = (MainComponent.Builder)map.get(MainActivity.class);

// コンポーネントビルダーでコンポーネントを構築
MainComponent component = builder.activityModule(new MainModule(activity)).build();

いい感じですね. コンポーネントビルダーが取得できればコンポーネントを構築すれば目的のコンポーネントが取得できます.
コンポーネントが取得できれば, injectMembers(activity)で依存性を注入できます.

それでは最後に主要なクラス達を整理しておきます.

// アクティビティのコンポーネントを表現するインタフェース
public interface ActivityComponent<T extends Activity> extends MembersInjector<T> {...}

// アクティビティのモジュールを表現する基底クラス
public abstract class ActivityModule<T extends Activity> {...}

// 具体的なアクティビティコンポーネントとそのビルダとモジュール
@Subcomponent(modules = MainComponent.MainModule.class)
public interface MainComponent extends ActivityComponent<MainActivity> {
  @Subcomponent.Builder
  interface Builder extends ActivityComponentBuilder<MainModule, MainComponent> {
  }

  @Module
  class MainModule extends ActivityModule<MainActivity> {
    ...
  }
}

// アクティビティモジュールのバインドを定義するクラス
@Module(subcomponents = { MainComponent.class })
public abstract class ActivityBindingModule {
  @Singleton @Binds @IntoMap @ActivityMapKey(MainActivity.class)
  public abstract ActivityComponentBuilder mainActivityComponentBuilder(
      MainComponent.Builder builder);
}

// 親コンポーネントにあたるアプリケーションコンポーネント
@Component(modules = { AppModule.class, ActivityBindingModule.class })
public interface AppComponent {
  Map<Class<? extends Activity>, ActivityComponentBuilder> activityComponentBuilders();
  @Module
  class AppModule { ... }
}

// アクティビティコンポーネントを取得するコード
Map<Class<? extends Activity>, ActivityComponentBuilder> map =
    ((AppComponent) context.getApplicationContext().activityComponentBuilders();
MainComponent.Builder builder = (MainComponent.Builder)map.get(MainActivity.class);
MainComponent component = builder.activityModule(new MainModule(activity)).build();

// 依存性を注入
component.injectMembers(activity);

Daggerの新しいAPIを使ってアクティビティコンポーネントを取得することができました :)
さらに, もう一歩進めてDaggerによってスコープ制御されているコンポーネントをMortarライブラリで更に使いやすくする方法がありますが, これは次の機会にします.
今回紹介した内容+αと動くソースコードをGitHubにもアップしています. そちらもあわせてご覧ください :)

goto GitHub pages

以上です.