dely engineering blog

レシピ動画サービス「kurashiru」を運営するdelyのテックブログ

マルチモジュール時代のDagger2によるDI

こんにちは。dely株式会社のAndroidアプリチームのうめもりです。今年もdelyはAdvent Calendarをやることになりました。開発部の面々が色々な記事を今年も書いてくれますので、是非ほかの記事も見て行ってください。

qiita.com

adventar.org

この記事はdely Advent Calendarの1日目の記事です。早速やっていきましょう。

Androidのマルチモジュール構成のアプリケーション上でDagger2を用いて依存性解決を行うやり方を、簡単なマルチモジュール構成のアプリケーションを例に紹介します。

想定するモジュール構成

今回想定するアプリケーションのモジュール構成は次のようになっています。

f:id:delyumemori:20191201153322p:plain

  • App Module - メインのAndroid App Module、依存性の解決はすべてここで行う
  • UI Module - Feature Moduleで定義したクラスを利用するActivityやFragmentをここに定義する
  • Feature Module - UI Moduleから呼ばれるクラスを定義する

一般的なマルチモジュール構成に近いと思われるシンプルな構成にしてみました。実際アプリケーションが大きくなった場合は、UI ModuleやFeature Moduleを分割したりする必要が出てくると思いますが、その場合でも今回の方法と同じように対応することができるはずです。

記事内ではApp ModuleからFetaure Moduleへの依存は出てきませんが、このようなモジュール構成にするとApp ModuleからFeature Moduleへ依存する形でServiceを作成するなど、実際にはApp ModuleからFeature Moduleへの依存関係が発生すると思われます。

Dagger2のスコープ

Dagger2のスコープは

  • @Singleton
  • @ViewScope

の2つの定義にしました。

@Singleton は最初からあるスコープですが、 @ViewScopeについては新規に定義したScopeです。 Activity/FragmentごとにScopeを切るようにしてみました。

Scopeの定義は次のようになっています。

@dagger.Scope
@Retention(AnnotationRetention.RUNTIME)
annotation class ViewScope {
}

Feature Module側では何も考えずに依存性を定義して@Injectをつける

Feature Module内では、通常のDagger2と同じようにクラス定義を行うだけです。

クラス定義の例

class RecipeFeatureImpl @javax.inject.Inject internal constructor(
    recipeApiProvider: RecipeApiProvider
) : RecipeFeature {

    private val recipeApi = recipeApiProvider.recipeApi

    override fun createLatestRecipeFeedContainer(): FeedContainer<RecipeEntity> = FeedContainer(
        LatestRecipeFeedApi(recipeApi),
        20
    )
}

Dagger2のModule定義が必要な場合は、Feature Module内に定義してもよいですし、App Module内に定義してそれを使ってしまうのもよいと思いますが、依存性解決の定義についてはApp Module内でほぼすべてを行うので、そちらに集約してしまうのがよいかもしれません。

UI Moduleではinject用のinterfaceを定義する

UI ModuleではFeature Module側で定義したクラスに依存する形でActivityやFragmentを定義します。

あまり規模の大きくないアプリケーションでは実際にはこのModuleでDagger2のComponent定義を行ってしまってもいいとは思いますが、今回はComponent定義をUI定義から分離することを考えます。

必要なのは、ActivityやFragmentに対して依存性を注入するためのinterfaceです。なのでまずはそれを素直に定義しましょう。

injector定義のサンプル

interface ViewInjectors {

    fun inject(mainActivity: MainActivity)

    fun inject(recipeDetailActivity: RecipeDetailActivity)

    fun inject(latestRecipeFeedFragment: RecipeFeedLatestFragment)
}

Dagger2のComponent定義はApp Module内で行うので、アノテーションなどをつける必要はありません。

さて、実際のActivityやFragment内では定義したinterfaceを使って依存性注入処理をする必要があるので、 どこからかinject用のinterfaceを取得して注入用のメソッドを呼ぶ必要があります。

今回は、カスタムApplicationクラスにinterfaceを実装することにして、Applicationクラスからinterfaceを取得するinterfaceを作成しましょう。実際のコードはこのようになります。

interface ViewInjectorsProvider {

    fun provide(activity: FragmentActivity): ViewInjectors

    fun provide(fragment: Fragment): ViewInjectors
}

そして、Activity/Fragment側では、定義したinterfaceにキャストしてinjectするコードを記述します。

class MainActivity : AppCompatActivity() {

    @javax.inject.Inject
    lateinit var viewModel: MainViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        (application as ViewInjectorsProvider).inject(this)
        
        // 省略
    }
}

Dagger2のComponentはApp Moduleに定義し、依存性解決を全て行う

最後に、App Module内でDagger2のComponent定義を行います。今回は、@Singletonと@ViewScopeの二つのScope定義があるので、Component定義も2つになります。

@javax.inject.Singleton
@dagger.Component(
    modules = // 依存Moduleを記述
)
interface SampleSingletonComponent {

    fun viewComponent(
        // SampleViewComponentの依存Moduleを記述
    ): SampleViewComponent
}

Fragment/Activity側で実際にinjectするScopeのComponentは、UI Moduleで定義したinterfaceを継承します。

@ViewScope
@dagger.Subcomponent(
    modules = // 依存Moduleを記述
)
interface SampleViewComponent : ViewInjectors

Component定義を行ったら、今度はApplicationに先ほどUI Moduleで定義したinterfaceを実装しましょう。一度ビルドを行えば、Daggerによって生成されたComponentを参照できるようになっているはずです。

class SampleApplication : Application(), ViewInjectorsProvider {

    private lateinit var appComponent: SampleAppComponent

    override fun onCreate() {
        super.onCreate()

        appComponent = DaggerSampleAppComponent.create()
    }

    override fun provide(activity: FragmentActivity) = appComponent.viewComponent(
        // インスタンス化に必要なModuleを渡す
    )

    override fun provide(fragment: Fragment) = appComponent.viewComponent(
        // インスタンス化に必要なModuleを渡す
    )
}

終わりに

以上、マルチモジュール構成のアプリケーションでDagger2による依存性解決を行うやり方を紹介しました。

ポイントとしては、依存性解決にかかわるコードは最終的にモジュールを集約するモジュールにまとめて記述するようにするという点です。そこさえ守っていればそう難しくはありません。

さて、明日(12/2)はサーバーサイドエンジニアの望月さんによる「NetflixのFast JSON APIを使ってみた」という記事です。お楽しみに。

qiita.com

adventar.org

ちなみに

delyではAndroidエンジニアを絶賛募集しております、もしご興味あればこちらのリンクからご気軽にエントリーください!

www.wantedly.com

delyの開発チームについて詳しく知りたい方はこちらもあわせてどうぞ!

CXOとVPoEへのインタビュー記事はこちら!

wevox.io