複数モジュールプロジェクトにおけるData Bindingの自動生成を眺める

この記事は RECRUIT MARKETING PARTNERS Advent Calendar 2017 の投稿記事です。

こんにちは。 スタディサプリ ENGLISHのAndroidを開発している @kawapaso です。当エントリではData Bindingによる自動生成ファイルについて調べてみました。

どうして調べたの?

上記プロダクト開発中に気になる挙動があったからです。

複数モジュールで構成されるプロジェクトの場合、 あるモジュールでData Bindingを有効にするためにはその親モジュールでもData Bindingを有効にしなくてはならないという特徴があります。

上記Androidプロダクトは複数モジュールで構成しており1)DDDの境界づけられたコンテキストに基づいてモジュール管理をしています。参考ページ、 これに直面しました。別に問題でも何でもないのですが、 直感に反する仕様であったため、 理由を探ろうと思った次第です。

結論としては、 ResourceIdの関係で子モジュールから自動生成されるファイルも親モジュールが管理する必要があるからでした。以下、 調べたことをつらつらと述べていきます。

調べていいことある?

理解が深まります。

Data Bindingにまつわるエラーはあまり親切でないことが多々あります。 特にKotlinやDaggerなどで他のAnnotationProcessorと併用していると、 エラーメッセージがそちらに引っ張られて参考にならなかったりします。そんな場合でも内部の挙動を知っていればトラブルシューティングしやすくなる、 といったメリットがあります。

1. 単一モジュールで自動生成

それでは早速見ていきます。まずはシンプルに単一モジュールのプロジェクトでData Bindingを使用します。

ソースコード

Android Studioで 新規プロジェクトを立ち上げ、 以下のようにファイルを作成・編集します。

public class ParentModuleActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_parent_module);
        ViewDataBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_parent_module);
    }
}
<layout>
    <data>
        <variable name="viewModel" type="com.example.test.ParentModuleViewModel"/>
    </data>
    ...
</layout>
android {
    ...
    dataBinding {
        enabled = true
    }
}

生成されたコード

上記コードから自動生成されたクラスを見てみます。

XXXBindingクラス

DataBindingUtil.setContentView()DataBindingUtil.infrate()などで取得しているおなじみのやつです。 Data Bindingのキモとなるクラスですね。特に指定しなければ、 XXXBinding(XXXはlayoutファイルの名前をCamelCaseにしたもの)と命名されます。

public class ActivityParentModuleBinding extends android.databinding.ViewDataBinding {
    ...
    public final android.widget.Button aButton;
    ...
    public void setViewModel(com.example.test.ParentModuleViewModel ViewModel) {
        updateRegistration(0, ViewModel);
        this.mViewModel = ViewModel;
        synchronized(this) {
            mDirtyFlags |= 0x1L;
        }
        notifyPropertyChanged(BR.viewModel);
        super.requestRebind();
    }
    ...
}

3行目のようにViewコンポネントを保持しているのがわかります。

また5行目に生成されているsetViewModel()は、 activity_parent_module.xml の

<variable name="viewModel" type="com.example.test.ParentModuleViewModel"/>

で指定したname="viewModel"から命名されます。

ここら辺は予想通りといったところでしょうか。

2. 複数モジュールで自動生成

次に本題の複数モジュールのプロジェクトで検証します。今回はParentModule・ChildModuleの2モジュール構成とし、 双方でData Bindingを使用します。

ソースコード

Android Studioで新規モジュールを作成し、 以下のようにファイルを作成・編集します。

public class ChildModuleActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_child_module);
        ViewDataBinding binding  = DataBindingUtil.setContentView(this, R.layout.activity_child_module);
    }
}
<layout>
    <data>
        <variable name="viewModel" type="com.example.test.childmodule.ChildModuleViewModel"/>
    </data>
    ...
</layout>
android {
    ...
    dataBinding {
        enabled = true
    }
}
android {
    ...
    dependencies {
        ...
        compile project(path: ":childmodule")
    }
}

生成されたコード

まず子モジュールのbuild/generated/以下に生成されるファイルを見ます。

子モジュールには抽象クラスのXXXBindingが生成される

public abstract class ActivityChildModuleBinding extends ViewDataBinding {
    public final android.widget.TextView moduleATextView;
    // variables
    protected com.example.modulea.ChildModuleModel mViewModel;
    protected ActivityChildModuleBinding(android.databinding.DataBindingComponent bindingComponent, android.view.View root_, int localFieldCount
        , android.widget.TextView moduleATextView
    ) {
        super(bindingComponent, root_, localFieldCount);
        this.moduleATextView = moduleATextView;
    }
    //getters and abstract setters
    public abstract void setViewModel(com.example.modulea.ChildModuleModel ViewModel);
    public com.example.modulea.ChildModuleModel getViewModel() {
        return mViewModel;
    }
    public static ActivityChildModuleBinding inflate(android.view.LayoutInflater inflater, android.view.ViewGroup root, boolean attachToRoot) {
        return inflate(inflater, root, attachToRoot, android.databinding.DataBindingUtil.getDefaultComponent());
    }
    public static ActivityChildModuleBinding inflate(android.view.LayoutInflater inflater) {
        return inflate(inflater, android.databinding.DataBindingUtil.getDefaultComponent());
    }
    public static ActivityChildModuleBinding bind(android.view.View view) {
        return null;
    }
    public static ActivityChildModuleBinding inflate(android.view.LayoutInflater inflater, android.view.ViewGroup root, boolean attachToRoot, android.databinding.DataBindingComponent bindingComponent) {
        return null;
    }
    public static ActivityChildModuleBinding inflate(android.view.LayoutInflater inflater, android.databinding.DataBindingComponent bindingComponent) {
        return null;
    }
    public static ActivityChildModuleBinding bind(android.view.View view, android.databinding.DataBindingComponent bindingComponent) {
        return null;
    }
}

単一モジュールの時とは違い、 XXXBindingクラスが抽象クラスになっていることがわかります。 nullを返すだけのメソッドも多いですね。

親モジュールには実態のXXXBindingが生成される

実は親モジュールのbuild/generated/以下にも全く同じパッケージ・クラス名のファイルが生成されています。

public class ActivityChildModuleBinding extends android.databinding.ViewDataBinding  {
    ...
    public void setViewModel(com.example.modulea.ChildModuleModel ViewModel) {
        updateRegistration(0, ViewModel);
        this.mViewModel = ViewModel;
        synchronized(this) {
            mDirtyFlags |= 0x1L;
        }
        notifyPropertyChanged(BR.viewModel);
        super.requestRebind();
    }
    ...
    public static ActivityChildModuleBinding inflate(android.view.LayoutInflater inflater, android.view.ViewGroup root, boolean attachToRoot) {
        return inflate(inflater, root, attachToRoot, android.databinding.DataBindingUtil.getDefaultComponent());
    }
    public static ActivityChildModuleBinding inflate(android.view.LayoutInflater inflater, android.view.ViewGroup root, boolean attachToRoot, android.databinding.DataBindingComponent bindingComponent) {
        return android.databinding.DataBindingUtil.<activityChildModuleBinding>inflate(inflater, com.example.modulea.R.layout.activity_child_module, root, attachToRoot, bindingComponent);
    }
    public static ActivityChildModuleBinding inflate(android.view.LayoutInflater inflater) {
        return inflate(inflater, android.databinding.DataBindingUtil.getDefaultComponent());
    }
    public static ActivityChildModuleBinding inflate(android.view.LayoutInflater inflater, android.databinding.DataBindingComponent bindingComponent) {
        return bind(inflater.inflate(com.example.modulea.R.layout.activity_child_module, null, false), bindingComponent);
    }
    public static ActivityChildModuleBinding bind(android.view.View view) {
        return bind(view, android.databinding.DataBindingUtil.getDefaultComponent());
    }
    public static ActivityChildModuleBinding bind(android.view.View view, android.databinding.DataBindingComponent bindingComponent) {
        if (!"layout/activity_child_module_0".equals(view.getTag())) {
            throw new RuntimeException("view tag isn't correct on view:" + view.getTag());
        }
        return new ActivityChildModuleBinding(bindingComponent, view);
    }
}

こちらは抽象クラスではありません。 子モジュールのActivityChildModuleBindingでは抽象メソッドだった setViewModel()も実装されており、 その他各メソッドも正しく実装されているようです。

どっちのXXXBindingが使われているの?

それでは我々が実際に利用しているのはどちらのクラスなのでしょうか。

XXXBindingクラスのインスタンスを取得するDataBindingUtil.infrale()の中身をみてみます。

public class DataBindingUtil {
    private static DataBinderMapper sMapper = new DataBinderMapper();
    ...
    public static <t extends ViewDataBinding> T inflate(...){
        ...
        return bind(bindingComponent, view, layoutId);
    }
    ...
    static <t extends ViewDataBinding> T bind(DataBindingComponent bindingComponent, View root,
            int layoutId) {
        return (T) sMapper.getDataBinder(bindingComponent, root, layoutId);
    }
    ...
}

DataBindingUtilクラスが所持するDataBinderMapperが、 ViewDataBindingを継承するインスタンスを管理しているようです。

それではDataBindingMapper.javaを見てみましょう。

package android.databinding;
class DataBinderMapper  {
    ...
    public android.databinding.ViewDataBinding getDataBinder(android.databinding.DataBindingComponent bindingComponent, android.view.View view, int layoutId) {
        switch(layoutId) {
                case com.example.test.R.layout.activity_parent_module:
                    return com.example.test.databinding.ActivityParentModuleBinding.bind(view, bindingComponent);
                case com.example.test.childmodule.R.layout.activity_child_module:
                    return com.example.test.childmodule.databinding.ActivityChildModuleBinding.bind(view, bindingComponent);
        }
        return null;
    }
    ...
}

(DataBindingMapperはandroid.databindingパッケージのクラスですが、 Reflection を利用してビルド時にがっつり書き換えられています。)

子モジュールに生成されたActivityChildModuleBindingのbind()は返り値がnullでした。一方、DataBindingMapperの9行目で参照しているActivityChildModuleBindingのbind()はしっかり値を返しているようです。つまり親モジュールに生成されている方のクラスであることがわかります。

コーディング時に子モジュールから参照しているActivityChildModuleBindingは子モジュールに生成されたクラスのはずです(子モジュールから親モジュールは参照できません)。しかし、 DataBindingUtilから取得するインスタンスは親モジュールに生成されたクラスとなっている。

...なるほど?

どうしてこんな仕組みなの?

結論から言うと、 Application全体のユニークなResourceIdを解決しないと実態を作れないからです。

Android APKをビルドする際、 Applicationモジュールは自身と依存するLibraryモジュールに含まれる全てのResourceを結合し、 それぞれにユニークなIDを振ります。逆に言うと、 Applicationモジュールが結合する前の、 各Libraryモジュールの中で定義されるResourceIdは他のLibraryモジュールのResourceIdと衝突している可能性があります。

さて、 ActivityChildModuleBindingをもう一度見てみます。

public class ActivityChildModuleBinding extends android.databinding.ViewDataBinding  {
    ...
    public static ActivityChildModuleBinding inflate(android.view.LayoutInflater inflater, android.view.ViewGroup root, boolean attachToRoot, android.databinding.DataBindingComponent bindingComponent) {
        return android.databinding.DataBindingUtil.<activityChildModuleBinding>inflate(inflater, com.example.modulea.R.layout.activity_child_module, root, attachToRoot, bindingComponent);
    }
    ...
}

XXXBindingは自身のlayoutIdも保持しており、 これを元にinflateしています。このDataBindingUtil.inflate()の中では、 前節で追った通り

switch(layoutId) {
    case com.example.test.R.layout.activity_parent_module:
        return com.example.test.databinding.ActivityParentModuleBinding.bind(view, bindingComponent);
    case com.example.test.childmodule.R.layout.activity_child_module:
        return com.example.test.childmodule.databinding.ActivityChildModuleBinding.bind(view, bindingComponent);
}

layoutId によってどのクラスをbindするか振り分けています。

layoutId を元に適切に振り分けるためには、 これがApplication全体でユニークなIDであることが大前提となります。このため、 layoutIdを保持したいXXXBindingの実態は、 Application全体のResourceIdを解決できるApplicationモジュール(=親モジュール)に作られる、 ということと思われます。

さいごに

親モジュールでもData Bindingを有効にしないといけない理由がわかりスッキリしました。これからも黒魔術自動生成と仲良く付き合っていこうと思います。

脚注

脚注
1 DDDの境界づけられたコンテキストに基づいてモジュール管理をしています。参考ページ