この記事は Manuel Vivo による Android Developers - Medium の記事 " Introduction to Hilt in the MAD Skills series " を元に翻訳・加筆したものです。詳しくは元記事をご覧ください。
MAD Skills 記事シリーズの Hilt についての記事です。この記事では、依存関係インジェクション(DI)が皆さんのアプリや Hilt にとって重要である理由について説明します。Hilt は、Android で DI を行うための Jetpack の推奨ソリューションです。
動画で視聴したい方は、こちらをご覧ください。
Android アプリで依存関係インジェクションの原理に従うことで、優れたアプリ アーキテクチャの土台を築くことができます。その結果、コードの再利用性が高まり、リファクタリングやテストが簡単になります。DI のメリットの詳細は、こちらをご覧ください。
プロジェクトでクラスのインスタンスを作成する場合、そのクラスが必要とする依存関係や推移的依存関係を満たしていくことで、依存関係グラフを手動で実現できます。
しかし、毎回これを手動で行うと、ボイラープレート コードが必要になり、エラーも起こりやすくなる可能性があります。たとえば、オープンソースの Google I/O アプリ iosched で利用している ViewModel をご覧ください。依存関係と推移的依存関係を含めると、FeedViewModel を作成するために必要なコードがどのくらいの量になるか想像できますか?
FeedViewModel
class FeedViewModel( private val loadCurrentMomentUseCase: LoadCurrentMomentUseCase, loadAnnouncementsUseCase: LoadAnnouncementsUseCase, private val loadStarredAndReservedSessionsUseCase: LoadStarredAndReservedSessionsUseCase, getTimeZoneUseCase: GetTimeZoneUseCase, getConferenceStateUseCase: GetConferenceStateUseCase, private val timeProvider: TimeProvider, private val analyticsHelper: AnalyticsHelper, private val signInViewModelDelegate: SignInViewModelDelegate, themedActivityDelegate: ThemedActivityDelegate, private val snackbarMessageManager: SnackbarMessageManager) : ViewModel(), FeedEventListener, ThemedActivityDelegate by themedActivityDelegate, SignInViewModelDelegate by signInViewModelDelegate { /* ... */}
class FeedViewModel(
private val loadCurrentMomentUseCase: LoadCurrentMomentUseCase,
loadAnnouncementsUseCase: LoadAnnouncementsUseCase,
private val loadStarredAndReservedSessionsUseCase: LoadStarredAndReservedSessionsUseCase,
getTimeZoneUseCase: GetTimeZoneUseCase,
getConferenceStateUseCase: GetConferenceStateUseCase,
private val timeProvider: TimeProvider,
private val analyticsHelper: AnalyticsHelper,
private val signInViewModelDelegate: SignInViewModelDelegate,
themedActivityDelegate: ThemedActivityDelegate,
private val snackbarMessageManager: SnackbarMessageManager
) : ViewModel(),
FeedEventListener,
ThemedActivityDelegate by themedActivityDelegate,
SignInViewModelDelegate by signInViewModelDelegate {
/* ... */
}
これは難解で繰り返しが多いので、容易に間違った依存関係を取得してしまうこともあると思います。依存関係インジェクション ライブラリを使えば、依存関係を手動で提供することなく、DI のメリットを活用できます。必要なコードはライブラリがすべて生成してくれます。その際に活躍するのが Hilt です。
Hilt は Google が開発した依存関係インジェクション ライブラリです。Hilt を使えば手動で書かなければならないボイラープレートをすべて生成してくれるので、アプリで DI のベスト プラクティスを最大限に活用できます。
Hilt はアノテーションを使ってコンパイル時にコードを生成するので、実行はとても高速です。その際に利用するのが、JVM の DI ライブラリである Dagger です。Hilt は、Dagger をベースに作られています。
Hilt は Android アプリの Jetpack 推奨 DI ソリューションであり、ツールや他の Jetpack ライブラリのサポートも含まれています。
Hilt を使うアプリには、@HiltAndroidApp アノテーションを付けた Application クラスを含める必要があります。このアノテーションにより、コンパイル時に Hilt のコード生成が実行されます。また、Hilt がアクティビティに依存関係を注入するには、そのアクティビティに @AndroidEntryPoint アノテーションを付けておく必要があります。
@HiltAndroidApp
@AndroidEntryPoint
@HiltAndroidAppclass MusicApp : Application() @AndroidEntryPointclass PlayActivity : AppCompatActivity() { /* ... */ }
class MusicApp : Application()
class PlayActivity : AppCompatActivity() { /* ... */ }
依存関係を注入するには、Hilt から注入したい変数に @Inject アノテーションを付けます。Hilt が注入したすべての変数は、super.onCreate が呼び出されたときに利用できるようになります。
@Inject
super.onCreate
@AndroidEntryPointclass PlayActivity : AppCompatActivity() { @Inject lateinit var player: MusicPlayer override fun onCreate(savedInstanceState: Bundle) { super.onCreate(bundle) player.play("YHLQMDLG") }}
class PlayActivity : AppCompatActivity() {
@Inject lateinit var player: MusicPlayer
override fun onCreate(savedInstanceState: Bundle) {
super.onCreate(bundle)
player.play("YHLQMDLG")
この例では、PlayActivity に MusicPlayer を注入しています。しかし、Hilt は MusicPlayer 型のインスタンスを提供する方法をどのようにして認識しているのでしょうか。実は、この段階ではまだ認識していません。 Hilt にその方法を伝えるためにアノテーションを使います。
PlayActivity
MusicPlayer
クラスのコンストラクタに @Inject アノテーションを付けることで、Hilt にそのクラスのインスタンスの作成方法を伝えることができます。
class MusicPlayer @Inject constructor() { fun play(id: String) { ... }}
class MusicPlayer @Inject constructor() {
fun play(id: String) { ... }
アクティビティへの依存関係の注入に必要なのは、これだけです。とても簡単でしたね。最初の例は簡単で、MusicPlayer は他の型に依存していません。しかし、他の依存関係がパラメータとして渡されると、Hilt はそれを管理し、MusicPlayer のインスタンスを提供する際にその依存関係を満たさなければなりません。
実は、ここで示した例はとても簡単で、単純すぎるものです。しかし、これを手動で行う場合、どうするかを考えてみてください。
手動で DI を行う場合、必要な型を提供し、提供するインスタンスのライフサイクルを管理する 依存関係コンテナ クラスを作ることが考えられます。つまり、まさに Hilt が内部で行っていることです。
アクティビティに @AndroidEntryPoint アノテーションを付けると、PlayActivity と関連付けられた依存関係コンテナが自動的に作成され、管理されます。手動で行うこの実装を PlayActivityContainer と呼ぶことにしましょう。MusicPlayer に @Inject アノテーションを付けることで、MusicPlayer 型のインスタンスの提供方法をコンテナに伝えます。
PlayActivityContainer
// PlayActivity annotated with @AndroidEntryPointclass PlayActivityContainer { // MusicPlayer annotated with @Inject fun provideMusicPlayer() = MusicPlayer() }
// PlayActivity annotated with @AndroidEntryPoint
class PlayActivityContainer {
// MusicPlayer annotated with @Inject
fun provideMusicPlayer() = MusicPlayer()
そしてアクティビティでは、コンテナのインスタンスを作成し、それを使ってアクティビティの依存関係を設定します。これも Hilt ではアクティビティに @AndroidEntryPoint アノテーションを付けることで処理してくれます。
class PlayActivity : AppCompatActivity() { private lateinit var player: MusicPlayer // Created by Hilt when annotating the activity with @AndroidEntryPoint private lateinit var container: PlayActivityContainer override fun onCreate(savedInstanceState: Bundle) { // @AndroidEntryPoint also creates and populates fields for you container = PlayActivityContainer() player = container.provideMusicPlayer() super.onCreate(bundle) player.play("YHLQMDLG") }}
private lateinit var player: MusicPlayer
// Created by Hilt when annotating the activity with @AndroidEntryPoint
private lateinit var container: PlayActivityContainer
// @AndroidEntryPoint also creates and populates fields for you
container = PlayActivityContainer()
player = container.provideMusicPlayer()
ここまで説明してきたのは、クラスのコンストラクタに @Inject アノテーションを付けると、そのクラスのインスタンスの提供方法を Hilt に伝えられることです。また、@AndroidEntryPoint が付いたクラスの変数にこのアノテーションを付けると、Hilt はその型のインスタンスをそのクラスに注入します。
@AndroidEntryPoint は、アクティビティだけでなく、ほとんどの Android フレームワーク クラスに追加できます。このアノテーションは、そのクラスの依存関係コンテナのインスタンスを作成し、@Inject アノテーションが付いたすべての変数を設定します。
Application クラスに付けられた @HiltAndroidApp アノテーションは、Hilt のコード生成をトリガーにするだけでなく、Application クラスに関連付けられた依存関係コンテナも作成します。
Application
Hilt の基本について理解できたので、もう少し複雑な例を見てみましょう。次の例では、MusicPlayer のコンストラクタが依存関係 MusicDatabase を受け取ります。
MusicDatabase
class MusicPlayer @Inject constructor( private val db: MusicDatabase) { fun play(id: String) { ... }}
private val db: MusicDatabase
) {
ここでは、MusicDatabase のインスタンスを提供する方法を Hilt に伝えなければなりません。型がインターフェースである場合や、例えばライブラリから提供されているために自分がクラスを所有していない場合は、コンストラクタに @Inject アノテーションを付けることはできません。
アプリで、永続化ライブラリとして Room を使っているとしましょう。再度、PlayActivityContainer を手動で実装する場合について考えてみます。MusicDatabase を提供するとき、Room を使うと MusicDatabase は抽象クラスになります。そのため、依存関係を提供するコードを実行します。次に、MusicPlayer のインスタンスを提供するときに、MusicDatabase の依存関係を提供する(または満たす)メソッドを呼び出す必要があります。
class PlayActivityContainer(val context: Context) { fun provideMusicDatabase(): MusicDatabase { return Room.databaseBuilder( context, MusicDatabase::class.java, "music.db" ).build() } fun provideMusicPlayer() = MusicPlayer( provideMusicDatabase() )}
class PlayActivityContainer(val context: Context) {
fun provideMusicDatabase(): MusicDatabase {
return Room.databaseBuilder(
context, MusicDatabase::class.java, "music.db"
).build()
fun provideMusicPlayer() = MusicPlayer(
provideMusicDatabase()
)
Hilt では、推移的依存関係について心配する必要はありません。Hlit はすべての推移的依存関係を自動的に取得しますが、MusicDatabase 型のインスタンスの提供方法を伝えておかなければなりません。これを行うために、Hilt のモジュールを使います。
Hilt のモジュールとは、@Module アノテーションが付いたクラスです。このクラスでは、ある型のインスタンスの提供方法を Hilt に伝える関数を作成できます。Hilt が認識するこの情報は、Hilt の専門用語で バインディング とも呼ばれます。
@Module
@Module@InstallIn(SingletonComponent::class)object DataModule { @Provides fun provideMusicDB(@ApplicationContext context: Context): MusicDatabase { return Room.databaseBuilder( context, MusicDatabase::class.java, "music.db" ).build() }}
@InstallIn(SingletonComponent::class)
object DataModule {
@Provides
fun provideMusicDB(@ApplicationContext context: Context): MusicDatabase {
@Provides アノテーションが付いた関数で、MusicDatabase 型のインスタンスの提供方法を Hilt に伝えています。関数本体には、Hilt が実行するコードのブロックが含まれており、先ほどの手動実装のコードとまったく同じものです。
戻り値の型が MusicDatabase となっているので、Hilt はこの関数が提供する型を認識できます。また、関数のパラメータから、対応する型の依存関係も認識できます。この例では、既に Hilt が利用できる ApplicationContext がパラメータになっています。このコードから、Hilt は MusicDatabase 型のインスタンスの提供方法を認識します。別の表現を使うなら、MusicDatabase の バインディング を取得したことになります。
ApplicationContext
Hilt のモジュールには、@InstallIn アノテーションも付いています。これは、この情報がどの依存関係コンテナやコンポーネントで利用できるかを示します。では、コンポーネントとは何でしょうか。この点について詳しく説明しましょう。
@InstallIn
Hilt が生成する コンポーネント クラスは、先ほど手動でプログラミングしたコンテナのように、型のインスタンスを提供する役割を担います。Hilt はコンパイル時にアプリケーションの依存関係グラフをたどり、すべての推移的依存関係の型を提供するコードを生成します。
Hilt が生成する コンポーネント クラスは、型のインスタンスを提供する役割を担う
Hilt は、ほとんどの Android フレームワーク クラスに対して、コンポーネント、つまり依存関係コンテナを生成します。各コンポーネントの情報(バインディング)は、コンポーネント階層を伝播します。
Hilt のコンポーネント階層
MusicDatabase バインディングが Application クラスに対応する SingletonComponent で利用できる場合、その他のコンポーネントでも利用できます。
SingletonComponent
これらのコンポーネントは、コンパイル時に Hilt によって自動生成されます。コンポーネントが作成、管理され、対応する Android フレームワーク クラスに関連付けられるのは、これらのクラスに @AndroidEntryPoint アノテーションを付けたときです。
モジュールの @InstallIn アノテーションは、そういったバインディングが利用できる場所や、利用できる他のバインディングを管理するうえで便利です。
再び、手動で作成した PlayActivityContainer コードについて考えてみます。気づいた方もいらっしゃるかもしれませんが、MusicDatabase の依存関係が必要になるたびに、別のインスタンスが作成されています。
アプリ全体で MusicDatabase の同じインスタンスを再利用したい場合もあるので、この動作は理想的ではありません。そこで、関数を使うのではなく、変数に格納すれば同じインスタンスを共有できます。
class PlayActivityContainer { val musicDatabase: MusicDatabase = Room.databaseBuilder( context, MusicDatabase::class.java, "music.db" ).build() fun provideMusicPlayer() = MusicPlayer(musicDatabase)}
val musicDatabase: MusicDatabase =
Room.databaseBuilder(
fun provideMusicPlayer() = MusicPlayer(musicDatabase)
つまり、MusicDatabase 型のスコープがこのコンテナに適用されるようにすることで、依存関係として常に同じインスタンスが提供されるようにしています。これを Hilt で行うには、どうすればよいでしょうか。もうおわかりと思いますが、ここでも別のアノテーションを使います。
@Provides メソッドに @Singleton アノテーションを付けると、そのコンポーネントでは常にこの型の同じインスタンスを共有するように Hilt に伝えることができます。
@Singleton
@Module@InstallIn(SingletonComponent::class)object DataModule { @Singleton @Provides fun provideMusicDB(@ApplicationContext context: Context): MusicDatabase { return Room.databaseBuilder( context, MusicDatabase::class.java, "music.db" ).build() }}
@Singleton はスコープ アノテーションです。それぞれの Hilt コンポーネントには、1 つのスコープ アノテーションが対応付けられています。
それぞれの Hilt コンポーネントのスコープ アノテーション
ある型のスコープを ActivityComponent にしたい場合、ActivityScoped アノテーションを使います。スコープ アノテーションはモジュールで利用できますが、コンストラクタに @Inject アノテーションが付いているクラスでも利用できます。
ActivityComponent
ActivityScoped
バインディングには次の 2 つのタイプがあります。
Hilt は、ViewModel、Navigation、Compose、WorkManager といった人気のある Jetpack ライブラリと統合されています。
ViewModel 以外と統合する場合は、別のライブラリをプロジェクトに追加する必要があります。詳細は、ドキュメントをご覧ください。このブログ投稿の冒頭で紹介した iosched の FeedViewModel コードを覚えているでしょうか。Hilt を使うと、どのようになるか見てみましょう。
@HiltViewModelclass FeedViewModel @Inject constructor( private val loadCurrentMomentUseCase: LoadCurrentMomentUseCase, loadAnnouncementsUseCase: LoadAnnouncementsUseCase, private val loadStarredAndReservedSessionsUseCase: LoadStarredAndReservedSessionsUseCase, getTimeZoneUseCase: GetTimeZoneUseCase, getConferenceStateUseCase: GetConferenceStateUseCase, private val timeProvider: TimeProvider, private val analyticsHelper: AnalyticsHelper, private val signInViewModelDelegate: SignInViewModelDelegate, themedActivityDelegate: ThemedActivityDelegate, private val snackbarMessageManager: SnackbarMessageManager) : ViewModel(), FeedEventListener, ThemedActivityDelegate by themedActivityDelegate, SignInViewModelDelegate by signInViewModelDelegate { /* ... */}
@HiltViewModel
class FeedViewModel @Inject constructor(
この ViewModel のインスタンスを提供する方法を Hilt に伝えるために、コンストラクタに @Inject アノテーションが付いています。その点を除けば、クラスに @HiltViewModel アノテーションを付けるだけです。
これでだけです!ViewModel のプロバイダを手動で作成する必要はありません。Hilt がそれを行ってくれます。
Hilt は、よく使われている別の依存関係インジェクション ライブラリ Dagger がベースになっています。今後のエピソードには、Dagger が頻繁に登場する予定です。現在 Dagger をご利用の場合、Dagger と Hilt は連携して動作できます。移行 API の詳細は、ガイド(英語)をご覧ください。
Hilt についてさらに詳しく知りたい方は、特によく使われるアノテーションやその効果、使用方法を説明したクイック リファレンス(英語)をご覧ください。Hilt のドキュメントのほかに、ハンズオン形式で学習できる Codelab も公開しています。
今回のエピソードは以上ですが、この話はこれで終わりではありません。MAD Skills シリーズはさらに続くので、Android Developers の Medium 記事 (英語) をフォローして、投稿されたときに確認できるようにしておきましょう。
Reviewed by Tamao Imura - Developer Marketing Manager, Google Play
この記事は Nick Rout による Android Developers Blog の記事 "MAD Skills Material Design Components: Wrap-Up" を元に翻訳・加筆したものです。詳しくは元記事をご覧ください。
Modern Android Development(最先端の Android 開発)について取り上げる連載シリーズ MAD Skills の動画と記事の 3 番目のトピックが完結しました。今回は、マテリアル デザイン コンポーネント(MDC)についてご説明しました。このライブラリは、マテリアル コンポーネントを Android ウィジェットとして提供します。これを使うと、マテリアル テーマ、ダークテーマ、モーションなど、material.io で使われているデザイン パターンを簡単に実装できます。
説明した内容については、下記にまとめた各エピソードからご確認ください。これらの動画では、MDC についての最新の記事や、既存のサンプルアプリ、Codelab を詳しく説明しています。また、MCD チームのエンジニアによる Q&Aセッションも含まれています 。
最初のエピソードは、Nick Butcher が、なぜ私たちが MDC の利用を推奨するのかなど、今回の MAD Skills シリーズ全体の概要を説明しています。その後、マテリアル テーマ、ダークテーマ、モーションについて詳しく掘り下げていきます。また、MDC を Jetpack Compose と合わせて使う方法、MDC やテーマ、スタイルのベスト プラクティスを含むようにアップデートされた Android Studio のテンプレートについてもお話ししています。
エピソード 2 では、Nick Rout がマテリアル テーマについて説明し、Android で MDC を使ってこれを実装するチュートリアルについて解説しています。主な内容は、Theme.MaterialComponents.* アプリのテーマを設定し、material.io のツールを使用して色や種類、形状の属性を選択し、最終的にそれらをテーマに追加して、ウィジェットがどのように自動的に反応して UI を適応させるかを確認します。また、テーマカラー属性を解決する、イメージに図形を適用するなど、MDC が特定のシナリオ向けに提供している便利なユーティリティ クラスについても説明します。
Theme.MaterialComponents.*
Chris Banes が、Android アプリで MDC を使ってダークテーマを実装する方法を紹介しています。説明する内容は、Force Dark を使って短時間で変換しビューを除外する方法、デザインを選んでダークテーマを手動で作成する方法、`.DayNight` MDC アプリテーマ、`.PrimarySurface` MDC ウィジェット スタイル、そしてシステム UI を扱う方法などです。
エピソード 4 では、Nick Rout がマテリアルのモーション システムについて解説しています。また、既存の「Android でマテリアル モーションを使って美しい画面遷移を構築する」Codelab の手順を詳しくフォローしています。この Codelab では、Reply サンプルアプリを使って、コンテナ変換、共有軸、フェードスルー、フェードという遷移パターンを活用してスムーズでわかりやすいユーザー エクスペリエンスを実現する方法を紹介しています。また、Fragment(Navigation コンポーネントを含む)、Activity、View を使うシナリオについて説明しています。
エピソード 5 は、Android コミュニティから Google Developer Expert (GDE) の Zarah Dominguez さんが、MDC カタログアプリをウィジェットの機能や API の例として参考にしながら紹介してくれました。そのほか、異なる画面やフローにまたがる一貫したデザイン言語を確保するために、彼女が取り組んでいるアプリに「テーマショーケース」ページを構築することが、どのように有益であるかを説明しています。
最後のまとめとして、Chet Haase が MDC エンジニアリング チームの Dan Nizri と Connie Shi と一緒に Q&A セッションを行い、Twitter や YouTube で寄せられた皆さんからの質問に回答しました。このセッションでは MDC の起源、AppCompat との関係、今までの改善点について解説したほか、テーマやリソースを整理するためのベストプラクティス、さまざまなフォントやタイポグラフィ スタイルの使用方法、シェイプ テーミングなどについてお話しました。また、私たちはお気に入りの Material コンポーネントをすべて公開し、最後に、将来的に MDCと Jetpack Compose という Androidの次世代 UI ツールキットでは、デフォルトで Material Design が組み込まれる新しいコンポーネントが登場することについて議論しました。
このシリーズでは、MDC のデモとして、次の 2 つのサンプルアプリを使いました。
これらは、もう 1 つの Material Studies のサンプルアプリである Owl とともに、GitHub リポジトリの MDC サンプルで確認できます。
Reviewed by Takeshi Hagikura - Developer Relations Team and Hidenori Fujii - Google Play Developer Marketing APAC
この記事は Chris Banes による Android Developers Blog の記事 "MAD Skills — Become an Android App Bundle expert" を元に翻訳・加筆したものです。詳しくは元記事をご覧ください。
Modern Android Development(最先端の Android 開発)の Android App Bundle ミニシリーズが最終回のリアルタイム Q&A セッションで完結しました。私は Chet Haase、Wojtek Kaliciński、Iurii Makhno とともに、Twitter の #AskAndroid ハッシュタグやライブ ストリームのチャットから寄せられたたくさんの質問にお答えしました。
ここで少し時間を巻き戻して、最初から振り返ってみることにしましょう。
最初のエピソードでは Wojtek が、なぜデベロッパーやアプリにとって App Bundle が重要なのかを説明し、このシリーズの方向性を示しました。
このエピソードでは、Wojtek が Play Console について詳しく解説しました。Play App Signing をオプトインする方法を学習でき、Play App Signing をオプトインする際に利用できるオプションについて理解できるはずです。
この動画と合わせて、 ブログ記事 Answers to common questions about Play App Signing やアプリ署名についての Android ドキュメント、Play Console のヘルプページの Google Play アプリ署名を使用する も参照することをおすすめします。
次に、初めての Android App Bundle をビルドしてアップロードする方法を学びました。このエピソードでは、Android Studio とコマンドライン インターフェースを使ってバンドルをビルドする手順について、私がご説明しています。
このエピソードはブログ記事(英語)で読むこともできます。合わせて、App Bundle のドキュメントもご覧ください。
このエピソードでは、配信オプションについて学ぶことができます。インストール時の配信に加え、条件付き配信やオンデマンド配信など、あらゆることを解説しています。また、 GitHub のサンプルについても説明しています。
このエピソードもブログ記事(英語)で読むことができます。さらに、重要な参考資料として Play Core ガイドも準備しています。
App Bundle のテスト方法について疑問に思ったことはないでしょうか。もうその必要はありません。Wojtek が ご説明する App Bundle をローカルと Play Console でテストする方法についての動画をご覧ください。
このエピソードのコンテンツは、ブログ記事(英語)やガイド Android App Bundle のテスト で読むこともできます。
さらに、Play Console のデベロッパー ツールにガイドを掲載しており、Play Console のヘルプページでは内部アプリ共有の説明も確認できます。
また、bundletool をダウンロードしたい方は、こちらをご覧ください。
Android GDE の Angélica Oliveira さんが、Android App Bundle への切り替えを行った手順と、そのときに経験した大幅なサイズの削減について解説しています。
Twitter で質問を募集したところ、皆さんから #AskAndroid ハッシュタグ付きの返信をいただき、リアルタイム Q&A セッションの間も質問は続きました。Chet と Wojtek、Iurii、そして私がカメラの前に立ち、皆さんの質問にお答えしました。
詳しくは 2021 年の新しい Android App Bundle とターゲット API レベル要件をご覧ください。
Reviewed by Yuichi Araki - Developer Relations Team and Hidenori Fujii - Google Play Developer Marketing APAC