Activity、Fragment、Viewにコンストラクタインジェクションする

Daggerライブラリを使い、Androidフレームワークが提供するActivityなどのクラスにコンストラクタインジェクションしたい、 そんな夢をみたAndroidエンジニアは数多くいると思います。

この記事ではそんな夢を叶える方法を紹介します。

サンプルコードはここにあります。

FragmentFactory

Fragmentに依存関係を注入する時、普通にやると以下のコードになると思います。

class MainFragment : Fragment() {
  @Inject lateinit var userHandler: UserHandler
  ...
}

これをコンストラクタインジェクションにしたい。

class MainFragment @Inject constructor(
  private val userHandler: UserHandler
) : Fragment() {
  ...
}

androidx.fragment:fragment:1.1.0-alpha01から、FragmentFactoryが追加されました!! これを使うことでコンストラクタインジェクションが可能になります。

MainFragmentインスタンスを生成するFragmentFactoryを作成します。

class MainFragmentFactory @Inject constructor(
  private val fragment: Provider<MainFragment>
) : FragmentFactory() {
  override fun instantiate(
    classLoader: ClassLoader,
    className: String,
    args: Bundle?
  ): Fragment {
    if (className == MainFragment::class.java.name) {
      return fragment.get()
    }
    return super.instantiate(classLoader, className, args)
  }
}

FragmentFactory.instantiateをoverrideし、そこでMainFragmentのインスタンスを生成します。

最後に、作成したMainFragmentFactoryをActivityのFragmentManagerに登録します。

class MainActivity : AppCompatActivity() {
  @Inject lateinit var fragmentFactory: MainFragmentFactory

  override fun onCreate(savedInstanceState: Bundle?) {
    DaggerAppComponent.create().inject(this)
    supportFragmentManager.fragmentFactory = fragmentFactory

    super.onCreate(savedInstanceState)
    ...

SupportFragmentManager.fragmentFactoryに登録します。 これで、Fragmentが生成されるときMainFragmentFactoryがフックされます。

SupportFragmentManager.fragmentFactoryにFactoryを登録するタイミングはsuper.onCreate(savedInstanceState)の前が良いと思います。 それはsuper.onCreateのタイミングで以前のFragmentが復元されるためです。 復元されるタイミングで適切なFactoryがないとクラッシュするので、復元する前で登録する必要があります。

LayoutInflater.Factory

次にViewです。LayoutInflater.Factoryが定義されています。 これを使うことでカスタムのコンストラクタを持ったViewを定義することが出来ます。

class MainTextView(
  context: Context,
  private val userHandler: UserHandler
) : TextView(context) {
  class Factory @Inject constructor(private val userHandler: UserHandler) {
    fun create(context: Context): MainTextView {
      return MainTextView(context, userHandler)
    }
  }
}

カスタムのコンストラクタを持ったViewは通常の方法ではインスタンスを生成できませんが、 LayoutInflater.Factoryを使うことで、インスタンスを生成できるようになります。

class MainLayoutInflaterFactory @Inject constructor(
  private val factory: MainTextView.Factory
) : LayoutInflater.Factory {
  override fun onCreateView(name: String, context: Context, attrs: AttributeSet?): View? {
    if (name == MainTextView::class.java.name) {
      return factory.create(context)
    }
    return null
  }
}

LayoutInflater.Factory.onCreateViewをoverrideし、MainTextViewインスタンスを生成します。

最後に、作成したMainLayoutInflaterFactoryをActivityのlayoutInflater.factoryに登録します。

class MainActivity : AppCompatActivity() {
  private lateinit var layoutInflaterFactory: MainLayoutInflaterFactory

  override fun onCreate(savedInstanceState: Bundle?) {
    DaggerAppComponent.create().inject(this)
    layoutInflater.factory = layoutInflaterFactory

    super.onCreate(savedInstanceState)
    ...

ActivityのLayoutInflater.factoryに登録します。登録するタイミングはsetContentViewの前が良いと思います。

AppComponentFactory

次にActivityです。 Activityに依存関係を注入する時、普通にやると以下のコードになると思います。

class MainActivity : Activity() {
  @Inject lateinit var presenter: UserPresenter
  @Inject lateinit var analytics: Analytics
  ...
}

これをコンストラクタインジェクションにしたい。

class MainActivity @Inject constructor(
  private val presenter: UserPresenter,
  private val analytics: Analytics
): Activity() {
  ...
}

しかし、この書き方はうまくいきません。なぜならActivityインスタンスはシステム側で自動的に生成されるためです。 カスタム定義のコンストラクタだと、システム側でインスタンスを生成することが出来ません。

これを解決するべく、API28からAppComponentFactoryというクラスが追加されました!!

MainActivityインスタンスを生成するAppComponentFactoryを作成します。

@Suppress("unused")
class MainAppComponentFactory : AppComponentFactory() {
  private lateinit var application: App

  override fun instantiateActivityCompat(
    cl: ClassLoader,
    className: String,
    intent: Intent?
  ): Activity {
    if (className == MainActivity::class.java.name) {
      return application.appComponent.mainActivity
    }
    return super.instantiateActivityCompat(cl, className, intent)
  }

  override fun instantiateApplicationCompat(cl: ClassLoader, className: String): Application {
    application = super.instantiateApplicationCompat(cl, className) as App
    return application
  }
}

次にAndroidマニフェストにMainAppComponentFactoryを登録します。

...
  <application
    android:allowBackup="true"
    android:name=".App"
    android:appComponentFactory="com.github.satoshun.example.sample.MainAppComponentFactory"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:roundIcon="@mipmap/ic_launcher_round"
    android:supportsRtl="true"
    android:theme="@style/AppTheme"
    tools:replace="android:appComponentFactory">
    ...

AppComponentFactory.instantiateActivityCompatをoverrideし、MainActivityインスタンスを生成します。

これで、カスタムのコンストラクタを持ったActivityインスタンスを生成することが出来ます!!

まとめ

何か疑問点があれば、twitterやサンプルコードのISSUEなどで聞いてください😃

Written by
あんどろいどでぃべろっぱぁー🍎