読者です 読者をやめる 読者になる 読者になる

techium

このブログは何かに追われないと頑張れない人たちが週一更新をノルマに技術情報を発信するブログです。もし何か調査して欲しい内容がありましたら、@kobashinG or @muchiki0226 までいただけますと気が向いたら調査するかもしれません。

AndroidでDagger2を使ってDIする。

Android Library Dagger2

今回はDagger2の使い方について解説していきます。
Dagger2は、Dependency Injection(以降DI)を利用する上で便利なライブラリです。
ここではData Bindingの解説で作成したアプリを改造し、武器を装備して雪山に出かけた上に攻撃をしてみます。

f:id:uentseit:20160422220849p:plain:w200 f:id:uentseit:20160422220852p:plain:w200 f:id:uentseit:20160422220856p:plain:w200 f:id:uentseit:20160422220859p:plain:w200

少し説明が長くなってしまうのですが、最後までお付合いください。

DIとは

DIとはDependency Injectionの略で、いわゆるデザインパターンの一種です。DIYとは何の関係性もありません。
Dependency Injection、日本語に訳すと依存性の注入となるらしいのですが全く意味がわからないのでjava言語で確認してみます。
まずはDagger2を使わずに、DIを実装してみましょう。

サンプルとして、まず今回のキモとなる「武器」を作成する必要があります。
そこで、以下のようなWeaponクラスを抽象クラスとして作成します。
[Weapon.java]

public class Weapon  {
    public final ObservableField<String> name = new ObservableField<>();  // Data Binding説明時の遺物。今回は無視。

    private IWeapon mWeapon;

    public Weapon(IWeapon weapon, String s) {
        this.name.set(s);   // Data Binding説明時の遺物。今回は無視。
        this.mWeapon = weapon;
    }

    public void actionY(){
        this.mWeapon.actionY();
    }

    public void equipment(){
        this.mWeapon.equipment();
    }

今回のサンプルでは、武器は装備(equipment)と攻撃(Yボタン)ができる必要があるので、それぞれequipment()メソッドとactionY()メソッドを用意しておきます。
さらに、実際に攻撃を行った時の処理を記述する武器の具象クラス、チャージアックスも作成します。(便宜上、トースト表示だけにしておきます)。
[ChargeAxe.java]

public class ChargeAxe implements IWeapon {

    private Context mContext;
    private final String name = "チャージアックス";

    public ChargeAxe(Context context){
        this.mContext = context;
    }

    @Override
    public void actionY() {
        Toast.makeText(this.mContext,"斧モードで振り下ろし!",Toast.LENGTH_SHORT).show();
    }

    @Override
    public void equipment() {
        Toast.makeText(this.mContext, this.name + "を装備しました。",Toast.LENGTH_SHORT).show();
    }
}

[IWeapon.java]

interface IWeapon {
    public void actionY();
    public void equipment();
}

武器の具象クラスとなるクラスはIWeaponインタフェースを実装しておき、ポリモーフィズムが使えるようにしておきます。
これで、あとはChargeAxeクラスのインスタンスをWeaponクラスに渡すことで、チャージアックスに「攻撃をする」機能と「装備をする」機能を与えることができました。

あとはMainActivityから呼んであげます。
[MainActivity.java]

public class MainActivity extends AppCompatActivity {

    private Hanter hanter;
    private Weapon mWeapon;

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

        final ActivityMainBinding binder = DataBindingUtil.setContentView(this, R.layout.activity_main);
        hanter = new Hanter("ハンタくん", "MAN");

        (・・・略・・・)

        mWeapon = new Weapon(new ChargeAxe(this), ""); // チャージアックスを生成

        // チャージアックスを装備
        binder.btnEquipment.setOnClickListener( new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                mWeapon.equipment();
            }
        });

        // 雪山Activityに遷移
        binder.btnToField.setOnClickListener( new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent intent = new Intent(getApplicationContext(), SnowMountain.class);
                startActivity(intent);
            }
        });
    }
}

これで、下図のように「武器を装備する!」ボタンを押下するとチャージアックスを装備するようになります。

f:id:uentseit:20160422220852p:plain:w200
また、雪山に移動するためのボタンも用意しておきましたので、雪山に移動し、武器で攻撃してみます。
[SnowMountain.java]

import techium.hatenablog.com.monhaan1.databinding.ActivitySnowMountainBinding;

public class SnowMountain extends AppCompatActivity {

    private Weapon mWeapon;

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

        mWeapon = new Weapon(new ChargeAxe(this), ""); // 11行目 チャージアックスを生成

        ActivitySnowMountainBinding binder = DataBindingUtil.setContentView(this, R.layout.activity_snow_mountain);

        // チャージアックスで攻撃
        binder.btnY.setOnClickListener( new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                mWeapon.actionY();
            }
        });
    }
}

これで「Yボタン」を押すと攻撃してくれます。
f:id:uentseit:20160422220859p:plain:w200

さて、上記のサンプルでは、Weaponクラスは「攻撃する」と「装備する」ことはできますが、その中身はIWeaponインタフェースを実装した具象クラスに「依存」しています。
また11行目、IWeaponインタフェースを実装したChargeAxeクラスを、Weaponクラスに「注入」することで、「攻撃」時と「装備」時に処理の中身(今回はトースト表示)を与えています。
二つ合わせて、「依存性の注入」ということになる、ということのようです。

このデザインパターンの特徴として、IWeaponインタフェースを実装したクラス複数作成することで、簡単に「攻撃」と「装備」を行える武器を作成することができます。
試しに、HeavyBogan(ヘビーボーガン)を作成すると以下のようになります。
[HeavyBogan.java]

public class HeavyBogan implements IWeapon {

    private Context mContext;
    private final String name = "ヘビーボーガン";

    public HeavyBogan(Context context){
        this.mContext = context;
    }

    @Override
    public void actionY() {
        Toast.makeText(this.mContext,"ぶっ放しました。",Toast.LENGTH_SHORT).show();
    }

    @Override
    public void equipment() {
        Toast.makeText(this.mContext, this.name + "を装備しました。",Toast.LENGTH_SHORT).show();
    }
}

ChargeAxeと同じく、攻撃(actionY)と装備(equipment)を行った時の処理が記述されています(トーストのメッセージを変えただけですが...)。

さて、ここでポイントとなるのが、ChargeAxeの代わりにHeavyBoganを実装しようとする時にあります。
以下のようにMainActivityとSnowMountainの両方を修正する必要があるわけです。
[MainActivity.java]

        (・・・略・・・)
        mWeapon = new Weapon(new HeavyBogan(this), ""); // ヘビーボーガンのインスタンスを生成
        (・・・略・・・)

[SnowMountain.java]

        (・・・略・・・)
        mWeapon = new Weapon(new HeavyBogan(this), ""); // ヘビーボーガンのインスタンスを生成
        (・・・略・・・)

まぁ、今回サンプルは二つのクラスを変更するだけで済みますが、大きなプログラムではさらに多くの箇所を変更する必要があるでしょう。
となると、修正漏れなどが発生する可能性も出るかもしれません。

ここで役に立つのがDagger2です。

Dagger2で実装する

上記のサンプルをDagger2で実装してみます。
まぁ結論から言いますと、チャージアックスからヘビーボーガンへの変更を行う場合に、修正箇所が一箇所で済みます。
まずはDagger2を使う準備として、build.gradle(app)を以下のように修正し、Syncしておきます。
[build.gradle(app)]

apply plugin: 'android-apt'

   (・・・略・・・)

dependencies {

   (・・・略・・・)

    compile 'com.google.dagger:dagger:2.0.2'
    apt 'com.google.dagger:dagger-compiler:2.0.2'
    provided 'javax.annotation:jsr250-api:1.0'
}

準備ができたら、まずは「Module」と「Component」を作成します。
「Module」では、依存関係にあるクラスのインスタンスを返却するメソッドをそれぞれ定義します。
今回の例では、武器の抽象クラスWeaponと、具象クラスChargeAxeが依存の関係にあるので、以下のようにModuleを定義します。
[WeaponModule.java]

@Module
public class WeaponModule {

    @Provides
    public IWeapon provideIWeapon(Context context){
        return new ChargeAxe(context);
    }

    @Provides
    public Weapon provideWeapon(IWeapon weapon) {
        return new Weapon(weapon, "");
    }
}

Moduleクラスには@Moduleアノテーションを付与します。 依存関係にあるクラスの内、注入をしたい方のクラスを返すメソッド(今回はprovideIWeapon)に、Providesアノテーションを付与します。 これで、IWeaponインタフェースを実装したクラスを必要としているメソッド(provideWeapon)に、自動でそのインスタンスが渡されることになります。 また、今回MainActivityとSnowMountainにWeaponクラスを提供したいため、provideWeaponメソッドにも@Providesを付与しておきます。

さて、これだけではprovideIWeaponメソッドにContextを渡すプロバイダーがいないので、以下のようなModuleも用意しておきます。

@Module
public class AppModule {
    private final Application application;

    public AppModule(Application application) {
        this.application = application;
    }

    @Provides
    Context provideApplicationContext() {
        return application.getApplicationContext();
    }
}

わざわざModuleを分けなくても、WeaponModuleクラスにprovideApplicationContextメソッドを定義してしまっても良いわけですが、例えばWeaponModule以外に、ProtectModule(防具のモジュール)を作成したとし、そこでもContextに依存しているクラスが存在する場合、それぞれのModuleで同じprovideApplicationContextメソッドを定義する必要が出てしまいます。そこで、複数のモジュールに提供したいクラスについては、モジュールを分けて定義しておいたほうが賢明です。

さて、これで必要なModuleは揃いましたので、次にComponentでそれぞれのModuleと、Moduleを利用したいActivityを関連付けます。
[EquipmentComponent.java]

@Singleton
@Component(modules = {AppModule.class, WeaponModule.class})
public interface EquipmentComponent {
    void inject(MainActivity mainActivity);
    void inject(SnowMountain snowMountain);
}

Componentはインタフェースとして定義し、Componentアノテーションを付与します。
2行目で、依存関係にあるモジュールを連ねて定義します。これで依存関係が解決されるようになります。
あとはActivityで呼び出します。
[MainActivity.java]

public class MainActivity extends AppCompatActivity {

    private Hanter hanter;
    private IWeapon mWeapon;

    @Inject    // 6行目
    Weapon mWeapon;

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

        final ActivityMainBinding binder = DataBindingUtil.setContentView(this, R.layout.activity_main);
        hanter = new Hanter("ハンタくん", "MAN");

        (・・・略・・・)

        mWeapon = new Weapon(new ChargeAxe(this), ""); // チャージアックスのインスタンスを生成

        DaggerEquipmentComponent.builder()                             // 20行目
                .appModule(new AppModule(getApplication()))       // 21行目
                .build().inject(this);                                                      // 22行目

        // チャージアックスを装備
        binder.btnEquipment.setOnClickListener( new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                mWeapon.equipment();
            }
        });

        // 雪山Activityに遷移
        binder.btnToField.setOnClickListener( new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent intent = new Intent(getApplicationContext(), SnowMountain.class);
                startActivity(intent);
            }
        });
    }
}

6行目でWeaponクラスのmWeaponを定義しています。 Injectアノテーションを付与することで、Providesアノテーションを付与したprovideWeaponメソッドが、自動で返り値の型を判断してWeaponクラスのインスタンスをmWeaponに注入してくれます。
Weaponクラスのインスタンスを生成する代わりに、20行目あたりからDaggerEquipmentComponentクラスを使用して、依存関係を解決した上でMainActivityで使えるようにしています。
DaggerEquipmentComponentクラスは、EquipmentComponentを定義した時点で自動生成されます。
21行目では、依存性を解決したいModuleをComponentに渡しています(appModuleメソッドも自動生成)。
今回はAppModuleとWeaponModuleの依存性を解決する必要があるので、以下のようにする必要があるように思いますが、モジュールのコンストラクタが引数を必要としない場合はあえて記述する必要はありません。

// .weaponModule(new WeaponModule())は特に不要。
.appModule(new AppModule(getApplication())).weaponModule(new WeaponModule())

SnowMountainの方でも同様にします。
[SnowMountain.java]

import techium.hatenablog.com.monhaan1.databinding.ActivitySnowMountainBinding;

public class SnowMountain extends AppCompatActivity {

    @Inject
    Weapon mWeapon;

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

        DaggerEquipmentComponent.builder()
                .appModule(new AppModule(getApplication()))
                .build().inject(this);

        // mWeapon = new Weapon(new ChargeAxe(this), ""); // チャージアックスを注入したインスタンスを生成

        // チャージアックスで攻撃
        ActivitySnowMountainBinding binder = DataBindingUtil.setContentView(this, R.layout.activity_snow_mountain);
        binder.btnY.setOnClickListener( new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                mWeapon.actionY();
            }
        });
    }
}

さて、これで攻撃もできるようになりました。
動かしてみると、冒頭の画像通りの動作となります。

ChargeAxeの代わりにHeavyBoganを使いたい場合には、以下のように変更するだけで済みます!
[WeaponModule.java]

@Module
public class WeaponModule {

    @Provides
    public IWeapon provideIWeapon(Context context){
        return new HeavyBogan(context);
    }

    @Provides
    public Weapon provideWeapon(IWeapon weapon) {
        return new Weapon(weapon, "");
    }
}

f:id:uentseit:20160422220740p:plain:w200 f:id:uentseit:20160422220801p:plain:w200

長くなりましたが、以上です!