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

以上です.