初めまして。
この度、LINE LIVEクライアント開発チームにて技術就業型インターンシップに参加させていただきました、京都大学大学院1年の清水太朗です。
普段は”バイオロギング”という手法を用いて様々な生物(ドチザメやガゼルなど)の生態を明らかにする研究に取り組んでいます。
専門は情報学寄りではありませんが、メンターの方々の手厚いサポートのおかげで有意義な日々を送ることができました。
以下では、私が本インターンシップで取り組んだ、「Hiltによる効率的な依存性注入の実装」について紹介いたします。
背景・目的
本インターンではLINE LIVEのAndroidアプリの開発業務、特に「dagger.androidからHiltへの移行作業」に携わらせていただきました。
そもそも私は大規模なシステム開発の経験が無いため、本インターンは開発工程やシステム構造など小規模開発ではみられない手法を学ぶ非常に貴重な機会でした。
中でも「依存性注入」は大学の授業でも学ぶことの無かった考え方であり、自身の知見をさらに広げる、とても新鮮なものでした。
ここからは、その「依存性注入」に焦点を当てていきます。
依存性注入とは?
LINE LIVEのように規模が大きなアプリでは、各部品ごとにテストしたい!という場面が多くみられます。
いわゆる「単体テスト」が行えると何かと便利になるのですが、実際はあるクラス内に別のクラスの変数やインスタンスが入っている(以下、依存している)ことが多く、外部から動的にプログラムの動作を変更できないのでテストしづらい、という問題点があります。
その問題を解決するためには、「アプリケーションをコンポーネント化すること(汎用性・独立性を高めること)」が解決策として挙げられます。
つまり、アプリケーションをコンポーネントで構築することによってコンポーネント間で依存が無くなるため、単体テストが可能となるのです。
このように、システム開発において「アプリケーションのコンポーネント化」が推進されつつあるのですが、メリットはただ単に「単体テストが容易になること」だけではありません。
例えば、開発を行う際に「コンポーネント単位での開発がしやすくなる」というメリットがあります。
通常、アプリケーションの機能ごとに分担が割り当てられチーム開発が進められますが、オブジェクト間の依存性を考慮する必要があることから、柔軟なチーム開発とは言い切れません。
さらに、ある特定のオブジェクトだけ修正したい場合においても、そのオブジェクトに依存するオブジェクトも修正する必要が出てきてしまいます。
しかし、アプリケーションをコンポーネント化しオブジェクト間の依存性を取り除くと、他のオブジェクトへの影響を気にせずに開発を行うことができます。
また、アプリケーションのコンポーネント化には「仕様変更やバグの修正で影響範囲を限定できる」というメリットもあります。
通常はある部分に問題が生じた場合、その部分に依存していた場所も修正する必要があります。
しかし、アプリケーションがコンポーネント化されていると、それぞれのコンポーネント独立性の高さゆえに変更やバグの影響範囲も狭くなり、修正が容易となるのです。
このように「アプリケーションのコンポーネント化」には様々なメリットがあるのですが、一体どのようにして実現できるのでしょうか?
実際、コンポーネントの実現にはいろいろな方法がありますが、ここでは「依存性注入」に焦点を当てて話を進めます。
前述したように、あるクラス内に別のクラスの変数やインスタンスなどが入っている状態を「依存性がある」と言います。
依存性注入とは、「引数として外部からクラスや変数を受け取れるようにする」ことによって、そのような依存性を解消することを指します。
以下に依存性がある場合とない場合の図を示します。
右図のような設計をすることで各オブジェクトが独立できるため、アプリケーションがコンポーネント化できます。
このようにオブジェクト間に抽象ファイルを差し込み、依存性を解消することを、一般に「依存性注入」と言います。
それでは、LINE LIVEではどのようにして依存性を注入しているのでしょうか?
依存性注入ライブラリについて
実際、LINE LIVEでは「dagger.android」というライブラリを用いて依存性注入を実装しています。
そもそもDagger(https://dagger.dev/)とはオブジェクト間の依存関係を管理するコードを自動生成してくれるライブラリであり、簡単に依存性注入を行えることから、多くのAndroidアプリ開発で採用されています。
dagger.androidはそのDaggerのAndroid用の拡張版であり、こちらも多くのAndroidアプリ開発で採用されています。
しかし、2020年に「Hilt」というDagger上に構築された、dagger.androidとは異なるアプローチで依存性注入を実現するライブラリが発表されました。
Hiltはオブジェクトの作成方法と挿入位置を定義するだけで依存性注入を実装できることから、daggerやdagger.androidよりさらにコードが簡素化されるというメリットをもたらします。
そのため、現在はAndroidアプリ開発において依存性注入の実装にHiltが推奨されており、着々とHiltへの移行が進められています。
実際にLINE LIVEでも移行が進められ、そのお手伝いを私のインターン活動として行わせていただきました。
過程
dagger.androidからHiltへの移行は以下を参考にして進めていきました。
- Migrating to hilt : https://dagger.dev/hilt/migration-guide.html
-
DroidKaigi 2021 - 原理から完全理解するDagger Hilt Migration / Keita Kagurazaka : https://www.youtube.com/watch?v=EfN8wHhc8Nw
また、LINE LIVEでは以前よりHilt移行が進められていたことから、私は主にActivityとFragmentのHilt移行をお手伝いさせていただきました。
ここからはHilt移行の手順について紹介いたします。
Activityの移行
まず初めにActivityのHilt移行を行います。
ここでは例として各コンポーネント間の関係を下図に示します。
上部が今回Hiltで生成されるコンポーネントの関係、下部が従来dagger.androidで生成されていたコンポーネントの関係となります。
Hilt移行前、SampleActivityに注入するコンポーネントはdagger.androidによってビルド時に自動で生成されるSampleActivitySubComponentであるため、まずは注入するコンポーネントをHiltが用意しているActivityComponentに乗り換えさせます。
下にサンプルコードを示します。
@Module
class SampleActivityModule {
@Module
abstract class BindingModule {
@FragmentScoped
@ContributesAndroidInjector
abstract fun contributeSampleFragment(): SampleFragment
}
}
class SampleActivity : AppCompatActivity(), HasAndroidInjector {
@Inject
lateinit var androidInjector: DispatchingAndroidInjector<Any>
override fun androidInjector: AndroidInjector<Any> = androidInjector
override fun onCreate(saveInstanceState: Bundle?) {
AndroidInjection.inject(this)
super.onCreate(savedInstanceState)
}
}
dagger.androidでコンポーネントにアクセスしているのはonCreateメソッド内のAndroidInjection.inject(this)です。
そのため、まずはこのコードを削除します。
また、前述したようにHiltはオブジェクトの作成方法と挿入位置を定義するだけで依存性注入を実装できます。
今回のようなActivityの場合は、@AndroidEntryPointをつけることで、Hiltが生成する上部のActivityComponentに注入するコンポーネントを乗り換えさせることができます。
変更後のコードは以下のようになります。
@AndroidEntryPoint
class SampleActivity : AppCompatActivity(), HasAndroidInjector {
@Inject
lateinit var androidInjector: DispatchingAndroidInjector<Any>
override fun androidInjector: AndroidInjector<Any> = androidInjector
override fun onCreate(saveInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
}
3~5行目はdagger.androidのためのコードですが、ActivityからFragmentに依存関係の一覧を渡すための処理なので、関係のあるFragmentが全てHilt移行した後に削除します。
これでSampleActivitySubComponentは不要となったため、削除できます。
従って、以下のサンプルコードのように、dagger.androidが@ContributesAndroidInjectorを使ってSampleActivitySubComponentを自動生成する部分を削除します。
〜削除前〜
@Module
abstract class ActivityBindingModule {
@ActivityScoped
@ContributesAndroidInjector(
modules = [
SampleActivityModule::class,
SampleActivityModule.BindingModule::class
]
)
abstract fun contributeSampleActivity(): SampleActivity?
...
}
〜削除後〜
@Module
abstract class ActivityBindingModule {
...
}
削除後の各コンポーネント間の関係を下図に示します。
ここではSampleActivitySubComponentを自動生成する部分を削除する際にSampleActivityModule.BindingModule::class も削除しているので、この図のように現段階ではSampleFragmentSubComponentがどことも紐づいていません。
そのため、SampleActivitySubComponentに紐付いていたSampleFragmentSubComponentをHiltのActivityComponentに紐付ける必要があります。
SampleFragmentSubComponentを生成しているのはSampleFragmentModuleであるため、下記コードのように@InstallInでActivityComponentにSampleFragmentModuleをインストールすれば紐づけることができます。
@InstallIn(ActivityComponent::class)
@Module(
includes = [
SampleActivityModule.BindingModule::class,
]
)
interface DaggerAndroidActivityModule
これでActivityのHilt移行は完了です。
各コンポーネント間の関係を下図に示します。
Fragmentの移行
次にFragmentのHilt移行を行います。
Fragmentの移行もActivityの時と同様、注入を担当しているコンポーネントの変更から行います。
Hilt移行前、SampleFragmentに注入するコンポーネントはdagger.androidによってビルド時に自動で生成されるSampleFragmentSubComponentであるため、まずは注入するコンポーネントをHiltが用意しているFragmentComponentに乗り換えさせます。
下にサンプルコードを示します。
class SampleFragment : Fragment() {
override fun onAttach(context: Context) {
AndroidSupportInjection.inject(this)
super.onAttach(context)
...
}
}
ここでもActivityの移行の時と同様、dagger.androidによる注入部分を消去して@AndroidEntryPointをつけます。
このようにしてHiltが生成する上部のFragmentComponentに注入するコンポーネントを乗り換えさせることができます。
変更後のコードは以下のようになります。
@AndroidEntryPoint
class SampleFragment : Fragment() {
override fun onAttach(context: Context) {
super.onAttach(context)
...
}
}
また、SampleFragmentSubComponentは不要となったため、削除できます。
従って、以下のサンプルコードのように、dagger.androidが@ContributesAndroidInjectorを使ってSampleFragmentSubComponentを自動生成するコードが削除できます。
@Module
class SampleActivityModule {
@Module
abstract class BindingModule {
}
}
その後、空になったモジュールクラスも削除できるため、BindingModuleやSampleActivityModuleは削除することができます。
また、先ほどSampleFragmentSubComponentをHiltのActivityComponentに紐付けていた部分は必要無くなったので、削除します。
@InstallIn(ActivityComponent::class)
@Module(
includes = [
]
)
interface DaggerAndroidActivityModule
includes内は空になりましたが、他のActivityやFragmentの移行の際に使えるので、ここでは残しておきます。
これでFragmentのHilt移行は完了です。
各コンポーネント間の関係を下図に示します。
Activityの掃除
SampleActivityに関係する全てのFragmentがdagger.androidからHiltへ移行できた場合、ActivityからFragmentに依存関係の一覧を渡すために残しておいたdagger.androidのコードを削除することができます。
具体的には、以下のDispatchingAndroidInjector周りを全て取り除けます。
@AndroidEntryPoint
class SampleActivity : AppCompatActivity(), HasAndroidInjector {
@Inject
lateinit var androidInjector: DispatchingAndroidInjector<Any>
override fun androidInjector: AndroidInjector<Any> = androidInjector
override fun onCreate(saveInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
}
削除後のコードを以下に示します。
@AndroidEntryPoint
class SampleActivity : AppCompatActivity() {
override fun onCreate(saveInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
}
これでActivityとFragmentのHilt移行は終了となります。
感想
このようにHiltはdagger.androidなどと異なり、アノテーションを用いて簡単に依存性注入を実装・管理することができます。
インターンシップ期間内では全てのActivityやFragmentを移行することはできませんでしたが、Hiltへの完全移行が終了すると、従来よりテストの所要時間が大幅に削減されるとのことです。
また、移行前と比較すると、いくつかのファイルを減らすことができました。
そのため、ファイル管理面においてもHiltの導入は有益なものであると実感しました。
今後の個人開発ではLINE LIVEのような大規模なアプリを開発するかどうかは分かりませんが、小・中規模なアプリにおいてもある程度の恩恵は受けられそうなため、Hiltを導入してみようかと思っております。
今回のインターンシップは6週間を通して全ての日程がリモートで行われ、かつ個人的な話ですが、しばらくAndroid開発でKotlinを書いていなかったこともあり、最初は順調にインターンシップを送れるのか不安がありました。
しかし、メンターの高島さんや諏訪さんなど、開発チームの方々の手厚いサポートのおかげで、この6週間をとても有意義に過ごすことができました。
さらに技術的な面では、普段知ることがない大規模開発の工法や工程を実際に手を動かして体験でき、チーム開発を柔軟に遂行する上での考え方を飛躍的に成長させる、大変貴重な機会でした。
エンジニア的にも、人間的にも、インターンシップ参加前とは比べ物にならないくらい成長できた、とても濃密な6週間だったと思います。
普段の大学生活では決して味わうことができない貴重な経験のため、LINEのインターンシップに興味のある方はぜひエントリーしてみてください。
最後になりますが、本インターンシップではメンターの高島さんや諏訪さんをはじめ、大変多くの方々に様々な場面でお世話になりました。
まだまだ技術不足で拙い私を快く受け入れてくださったこと、心から感謝申し上げます。