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

ほげほげ(仮)

仮死状態

やさしいDagger2

Androidその2 Advent Calendar 2016 - Qiita 2日目の記事です。

Dagger2は最初のハードルが高くてなかなか導入できなかったり、メリットがよく分からなかったりします。そういう方が雰囲気だけでも掴めれるように、簡単なサンプルを実装しながら確認できるようなのを書きました。

だいぶ長くなってしまった感じですが、あまり複雑なことはやっていないつもりです。

セットアップ

build.gradle に依存関係を追加します

compile 'com.google.dagger:dagger:2.7'
annotationProcessor 'com.google.dagger:dagger-compiler:2.7'

Hello Dagger2

すごくシンプルなサンプルです。

依存解決されるクラス

まずは依存解決されるクラスを作ります。Dogクラスとします。

public class Dog {
    public String getName() {
        return "ぽち";
    }
}

インスタンスを提供するクラス

依存を解決させるためにインスタンスを提供する必要があります。その設定をしていきます。

クラスには@Moduleをつけて、インスタンスを提供するメソッドには@Providesをつけます。

@Providesの戻り値は注入したい依存の型になります。今回はDogクラスを注入したいのでDogが戻り値になっています。

@Module
public class SampleModule {
    @Provides
    Dog provideDog() {
        return new Dog();
    }
}

依存解決のためのinterface

依存解決をしたいところと、どのモジュールを使うかを定義します。

@Componentの引数には使用するModuleクラスを設定します。

injectメソッドを定義して、引数には依存解決を実行するクラスを設定します。今回はMainActivityで依存解決を行います。

ビルドするとDaggerSampleComponentクラスが自動生成されます。これを次に使ってきます。

@Component(modules = SampleModule.class)
public interface SampleComponent {
    void inject(MainActivity activity);
}

実際に依存解決してみる

MainActivityを次のようなコードにします。よくあるサンプルではApplicationクラスでやってますが、とりあえずシンプルにAcitivtyで直接行います。

やってることはコードのほうにコメントしていますが、基本的にはインスタンスを注入してほしいものに@Injectをつけて、Componentを作って注入を実行する感じです。

今回はSampleModuleで設定したDogクラスを戻り値に設定したものが注入されます。

public class MainActivity extends AppCompatActivity {

    // インスタンスが注入されるフィールド
    @Inject
    Dog dog;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // SampleComponentからDaggerSampleComponentが自動生成されるので、それを使ってSampleComponentを作ります。
        SampleComponent component = DaggerSampleComponent.builder()
                // 使用するModuleのインスタンスを指定します。
                // (ここでdeprecatedになることがありますが、一旦すべてコードを書いてビルドすると消えると思います)
                .sampleModule(new SampleModule())
                .build();

        // 依存の注入を実行します
        component.inject(this);

        String name = dog.getName();

        Log.d("MainActivity", name);

    }
}

確認

ここまで実装して、実行するとログが出力されと思います。

これだけでは何のためにあるのかよくわからないと思いますが、とりあえずここでは、依存を解決するために、ModuleとComponentが必要だということを抑えておけば良いです。

Field InjectionとConstructor Injection

さきほどは少し違う依存解決方法を簡単にやります。

前回やったのは、クラスのフィールドに@Injectをつけてそこに注入したので、Field Injectionになります。

もう一つが、Constructor Injectionになります。コンストラクタのパラメータとして依存を注入します。

コンストラクタに注入されるクラス

新しくOwnerというクラスを作ります。

コンストラクタに先程のDogクラスをパラメータとして受け取るようになっています。さらに@Injectをつけています。

public class Owner {

    private Dog dog;

    @Inject
    public Owner(Dog dog) {
        this.dog = dog;
    }

    public String getPetName() {
        return dog.getName();
    }
}

MainActivityを修正

次のように修正します。

変更点はフィールドがDogからOwnerに変わりました。

OwnerについてはModuleに設定したりしてませんが、Dagger2が勝手にコンストラクタに注入してOwnerインスタンスを作ってくれています。

public class MainActivity extends AppCompatActivity {

    @Inject
    Owner owner;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        SampleComponent component = DaggerSampleComponent.builder()
                .sampleModule(new SampleModule())
                .build();

        component.inject(this);

        Log.d("MainActivity", owner.getPetName());

    }
}

確認

ここまで出来たら、実行して確認します。結果は先程変わっていませんが、Ownerの処理からDogの処理を呼ぶようになりました。

Constructor Injectionを使うとDagger2がModuleに設定されたものを使って、インスタンスを作ってくれます。

ぼくはこれがかなり便利だなぁって思っています。ContextとかをModuleに設定しておいて使いたいクラスのコンストラクタでパラメータにすることをよくやります。

interfaceを使う

これまでの使ったのは実装クラスでしたが、次はinterfaceを使うように変更します。

新しいinterfaceをつくる

新しくPetというinterfaceを作ります。

public interface Pet {

    String getName();

}

そして、Dogがそれを実装する形に修正します。

public class Dog implements Pet {

    @Override
    public String getName() {
        return "ぽち";
    }
}

interfaceを使うように修正していく

まずはModuleを修正します。

@Module
public class SampleModule {

    @Provides
    Pet providePet() {
        return new Dog();
    }
}

次にOwnerのパラメータを変更します。

public class Owner {

    private Pet pet;

    @Inject
    public Owner(Pet pet) {
        this.pet = pet;
    }

    public String getPetName() {
        return pet.getName();
    }
}

確認

実行して確認します。結果は変わっていません。

ここで確認するポイントはOwnerのクラスになります。

今までDogという実装クラスに依存していましたが、interfaceに変更することでそことの依存がなくなり、interfaceのみに依存する感じになりました。

Build Variantsで動作を変更する

Build Variantsで動作を変更するようにします。Build Variants自体についはドキュメントやググったりしてください。

今回はProductFlavorsで分けるようにします。

build.gradle

build.gradleを下記のようにして、dogcatのProductFlavorを作ります。

android {
    ...
    productFlavors {
        dog {
        }
        cat {
        }
    }
}

フォルダ作成

srcフォルダの下にdog/javaのフォルダを作り、さらにパッケージを作成しておきます。同様にcat/javaも作成しておきます。

たぶん、どちらかのjavaフォルダがソースフォルダとして認識されないと思います。メニューのView → Tool Windows → Build VariantsからBuild Variantを切り替えることで直ると思います。

実行するときもBuild VariantsのWindowから切り替えることになります

f:id:STAR_ZERO:20161105094229p:plain

Catクラス

新しくPetinterfaceを実装したクラスを追加します。これはmainフォルダのソースフォルダに追加します。

public class Cat implements Pet {

    @Override
    public String getName() {
        return "たま";
    }
}

main

mainフォルダ内にあるSampleModuleクラスを削除しておきます。

BuidTypeやProductFlavorでmainフォルダ内に同一クラス名を使うことはできないためです。代わりにdogフォルダとcatフォルダにSampleModuleを追加するようにします。

catのProductFlavor

catのProductFlavor実行時には先程のCatクラスを使うようにします。

catフォルダのソースフォルダに下記を追加します。今まで異なるのはDogクラスのインスタンスを返却するのではなく、Catクラスを返却するようにします。

@Module
public class SampleModule {

    @Provides
    Pet providePet() {
        return new Cat();
    }
}

f:id:STAR_ZERO:20161105094307p:plain

dogのProductFlavor

dogのProductFlavorの場合は今までのDogクラスを使います。

dogフォルダのソースフォルダに下記を追加します。

@Module
public class SampleModule {

    @Provides
    Pet providePet() {
        return new Dog();
    }
}

f:id:STAR_ZERO:20161105094319p:plain

確認

Build VariantsのWindowでcatDebugdogDebugを切り替えて実行してみてください。

それぞれ、違う結果になると思います。

Build Variantsで実装クラスを丸ごと差し替えて、それぞれで違う動作が可能になります。BuildTypeのreleaseとdebugで処理を分けたい場合や、ProductFlavorの有料版と無料版で処理を分けたい場合などに使えます。

テストを書いてみる

Ownerクラスのテストを書いてみます。このサンプルコードだとDagger2なくても問題なくかけると思いますが…

テスト

テストを次のように書いてみます。androidTestのほうではなくて、testのほうです。

Ownerに渡すパラメータは匿名クラスとしてその場で生成しています。

今回の場合はあまりメリットを感じないかもしれないですが、直接Dogインスタンスを渡してません。

例えばDogクラスのgetNameAPI通信している場合にDogクラスに直接依存している状態だとテストを書くのが結構ツライ感じになったと思います。さらに、もしDogクラスのコンストラクタでContextが必要だったりするとかなり厳しい感じだと思います。

今回のようにinterfaceを使ってテスト時は動作を変更することで、Ownerのテストが書きやすくなりました。

public class OwnerTest {

    @Test
    public void getPetName() throws Exception {
        Owner owner = new Owner(new Pet() {
            @Override
            public String getName() {
                return "ペットの名前";
            }
        });

        assertThat(owner.getPetName(), is("ペットの名前"));
    }
}

確認

テストを実行してみてパスすることを確認します。

Constructor Injectionを使ってたおかげで、テストがだいぶ書きやすくなりそうな感じです。

まとめ

今回のサンプルはあまり実践的ではないかと思いますが、雰囲気を分かってもらえれば嬉しいです。

Dagger2のテストのサンプルとかだと結構UIについてが多かったりしますが、UI以外のテストにもとても有効だと思いますし、導入がしやすいかなぁって思います。

実際に手を動かして確認しないと理解しにくのもあるので、Dagger2を試したことない人は一度やってみたほうが早いかなぁって思います。複雑な部分もありますが、単純な使い方だとそこまで苦労せず使えると思います。(ぼくは単純な使い方しかできないですが…)