AndroidでDagger2を使ってDIする。
今回はDagger2の使い方について解説していきます。
Dagger2は、Dependency Injection(以降DI)を利用する上で便利なライブラリです。
ここではData Bindingの解説で作成したアプリを改造し、武器を装備して雪山に出かけた上に攻撃をしてみます。
少し説明が長くなってしまうのですが、最後までお付合いください。
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); } }); } }
これで、下図のように「武器を装備する!」ボタンを押下するとチャージアックスを装備するようになります。
また、雪山に移動するためのボタンも用意しておきましたので、雪山に移動し、武器で攻撃してみます。
[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ボタン」を押すと攻撃してくれます。
さて、上記のサンプルでは、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, ""); } }
長くなりましたが、以上です!