Dalam codelab ini, Anda akan mempelajari pentingnya injeksi dependensi (DI) untuk membuat aplikasi yang solid dan dapat diperluas yang diskalakan ke project besar. Kita akan menggunakan Hilt sebagai alat DI untuk mengelola dependensi.
Injeksi dependensi adalah teknik yang banyak digunakan dalam pemrograman dan sesuai dengan pengembangan Android. Dengan mengikuti prinsip-prinsip DI, Anda mengatur dasar arsitektur aplikasi yang baik.
Implementasi injeksi dependensi memberikan beberapa keuntungan berikut:
- Penggunaan kembali kode
- Kemudahan dalam pemfaktoran ulang
- Kemudahan dalam pengujian
Hilt adalah library injeksi dependensi dogmatis untuk Android yang mengurangi boilerplate ketika melakukan DI manual dalam project Anda. Untuk melakukan injeksi dependensi manual, Anda perlu membuat setiap class dan dependensinya secara manual, serta menggunakan container untuk memakai ulang dan mengelola dependensi.
Hilt menyediakan cara standar untuk melakukan injeksi DI pada aplikasi Anda dengan menyediakan container ke setiap komponen Android dalam project dan mengelola siklus proses container secara otomatis untuk Anda. Hal ini dilakukan dengan memanfaatkan library DI populer: Dagger.
Jika Anda mengalami masalah (bug kode, kesalahan gramatikal, susunan kata yang tidak jelas, dll.) saat mengerjakan codelab ini, laporkan masalah tersebut melalui link Laporkan kesalahan di pojok kiri bawah codelab.
Prasyarat
- Anda memiliki pengalaman dengan sintaksis Kotlin.
- Anda memahami mengapa injeksi dependensi penting dalam aplikasi Anda.
Yang akan Anda pelajari
- Cara menggunakan Hilt di aplikasi Android.
- Konsep Hilt yang relevan untuk membuat aplikasi berkelanjutan.
- Cara menambahkan beberapa binding ke jenis yang sama dengan penentu.
- Cara menggunakan
@EntryPoint
untuk mengakses container dari class yang tidak didukung Hilt. - Cara menggunakan unit dan uji instrumentasi untuk menguji aplikasi yang menggunakan Hilt.
Yang Anda butuhkan
- Android Studio 4.0 atau yang lebih baru.
Mendapatkan kode
Dapatkan kode codelab dari GitHub:
$ git clone https://github.com/googlecodelabs/android-hilt
Atau, Anda dapat mendownload repositori sebagai file Zip:
Membuka Android Studio
Codelab ini memerlukan Android Studio 4.0 atau versi yang lebih baru. Jika perlu mendownload Android Studio, Anda dapat melakukannya di sini.
Menjalankan aplikasi contoh
Dalam codelab ini, Anda akan menambahkan Hilt ke aplikasi yang mencatat interaksi pengguna dan menggunakan Room untuk menyimpan data ke database lokal.
Ikuti petunjuk ini untuk membuka aplikasi contoh di Android Studio:
- Jika Anda mendownload arsip zip, ekstrak file tersebut secara lokal.
- Buka project di Android Studio.
- Klik tombol Run , lalu pilih emulator atau hubungkan perangkat Android Anda.
Seperti yang dapat Anda lihat, log dibuat dan disimpan setiap kali Anda berinteraksi dengan salah satu tombol bernomor. Di layar Lihat Semua Log, Anda akan melihat daftar semua interaksi sebelumnya. Untuk menghapus log, ketuk tombol Hapus Log.
Penyiapan project
Project ini di-build di beberapa cabang GitHub:
master
adalah cabang yang Anda buka atau download. Ini adalah titik awal codelab.solution
berisi solusi untuk codelab ini.
Sebaiknya Anda memulai dengan kode di cabang master
dan mengikuti codelab langkah demi langkah sesuai kemampuan Anda.
Selama codelab, Anda akan melihat cuplikan kode yang harus ditambahkan ke project. Di beberapa tempat, Anda juga harus menghapus kode yang disebutkan secara eksplisit dalam komentar pada cuplikan kode.
Untuk mendapatkan cabang solution
menggunakan git, gunakan perintah ini:
$ git clone -b solution https://github.com/googlecodelabs/android-hilt
Atau download kode solusi dari sini:
Pertanyaan umum (FAQ)
Mengapa Hilt?
Jika melihat kode awal, Anda dapat melihat instance class ServiceLocator
yang disimpan di class LogApplication
. ServiceLocator
membuat dan menyimpan dependensi yang diperoleh sesuai permintaan oleh class yang membutuhkannya. Anda dapat menganggapnya sebagai container dependensi yang disertakan ke siklus proses aplikasi karena akan dimusnahkan saat aplikasi melakukannya.
Seperti yang dijelaskan dalam Panduan DI Android, Pencari Lokasi Layanan dimulai dengan kode boilerplate yang relatif sedikit, tetapi juga dengan skala yang cukup kecil. Untuk mengembangkan aplikasi Android dalam skala besar, Anda harus menggunakan Hilt.
Hilt menghapus boilerplate tidak diperlukan yang Anda butuhkan untuk menggunakan pola DI atau Pencari Lokasi Layanan manual di aplikasi Android dengan membuat kode yang semestinya Anda buat secara manual (mis. kode di class ServiceLocator
).
Pada langkah berikutnya, Anda akan menggunakan Hilt untuk mengganti class ServiceLocator
. Setelah itu, kami akan menambahkan fitur baru ke project untuk menjelajahi fungsi Hilt lainnya.
Hilt dalam project Anda
Hilt telah dikonfigurasi di cabang master
(kode yang Anda download). Anda tidak perlu menyertakan kode berikut dalam project karena sudah dilakukan untuk Anda. Meskipun demikian, mari kita lihat apa yang diperlukan untuk menggunakan Hilt di aplikasi Android.
Selain dependensi library, Hilt menggunakan plugin Gradle yang dikonfigurasi dalam project. Buka file build.gradle
root dan lihat dependensi Hilt berikut di classpath:
buildscript {
...
ext.hilt_version = '2.28-alpha'
dependencies {
...
classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version"
}
}
Kemudian, untuk menggunakan plugin gradle dalam modul app
, kami menetapkannya dalam file app/build.gradle
dengan menambahkan plugin ke bagian atas file, di bawah plugin kotlin-kapt
:
...
apply plugin: 'kotlin-kapt'
apply plugin: 'dagger.hilt.android.plugin'
android {
...
}
Terakhir, dependensi Hilt disertakan dalam project kami di file app/build.gradle
yang sama:
...
dependencies {
...
implementation "com.google.dagger:hilt-android:$hilt_version"
kapt "com.google.dagger:hilt-android-compiler:$hilt_version"
}
Semua library, termasuk Hilt, didownload saat Anda mem-build dan menyinkronkan project. Mari kita mulai menggunakan Hilt!
Demikian pula dengan cara penggunaan dan inisialisasi instance ServiceLocator
di class LogApplication
, untuk menambahkan container yang disertakan ke siklus proses aplikasi, kita perlu menganotasi class Application
dengan @HiltAndroidApp
. Buka LogApplication.kt
dan tambahkan anotasi ke class:
@HiltAndroidApp
class LogApplication : Application() {
...
}
@HiltAndroidApp
memicu pembuatan kode Hilt termasuk class dasar untuk aplikasi Anda yang dapat menggunakan injeksi dependensi. Container aplikasi adalah container induk aplikasi, yang berarti container lain dapat mengakses dependensi yang disediakannya.
Kini, aplikasi siap menggunakan Hilt!
Daripada mengambil dependensi sesuai permintaan dari ServiceLocator
di class, kita akan menggunakan Hilt untuk menyediakan dependensi tersebut. Mari mulai mengganti panggilan ke ServiceLocator
dari class.
Buka file ui/LogsFragment.kt
. LogsFragment
mengisi kolomnya di onAttach
. Alih-alih mengisi instance LoggerLocalDataSource
dan DateFormatter
secara manual menggunakan ServiceLocator
, kita dapat menggunakan Hilt untuk membuat dan mengelola instance jenis tersebut.
Agar LogsFragment
menggunakan Hilt, kita harus menganotasinya dengan @AndroidEntryPoint
:
@AndroidEntryPoint
class LogsFragment : Fragment() {
...
}
Menganotasi class Android dengan @AndroidEntryPoint
akan membuat container dependensi yang mengikuti siklus proses class Android.
Dengan @AndroidEntryPoint
, Hilt akan membuat container dependensi yang disertakan ke siklus proses LogsFragment
dan akan dapat memasukkan instance ke LogsFragment
. Bagaimana cara mendapatkan kolom yang diinjeksi oleh Hilt?
Kita dapat membuat Hilt menginjeksi instance dari jenis yang berbeda dengan anotasi @Inject
pada kolom yang ingin diinjeksi (mis. logger
dan dateFormatter
):
@AndroidEntryPoint
class LogsFragment : Fragment() {
@Inject lateinit var logger: LoggerLocalDataSource
@Inject lateinit var dateFormatter: DateFormatter
...
}
Inilah yang disebut dengan injeksi kolom.
Karena Hilt akan bertanggung jawab mengisi kolom tersebut, kita tidak memerlukan metode populateFields
lagi. Mari kita hapus metode tersebut dari class:
@AndroidEntryPoint
class LogsFragment : Fragment() {
// Remove following code from LogsFragment
override fun onAttach(context: Context) {
super.onAttach(context)
populateFields(context)
}
private fun populateFields(context: Context) {
logger = (context.applicationContext as LogApplication).serviceLocator.loggerLocalDataSource
dateFormatter =
(context.applicationContext as LogApplication).serviceLocator.provideDateFormatter()
}
...
}
Di balik layar, Hilt akan mengisi kolom tersebut dalam metode siklus proses onAttach()
dengan instance yang dibuat dalam container dependensi LogsFragment
yang dihasilkan secara otomatis.
Untuk melakukan injeksi kolom, Hilt perlu mengetahui cara menyediakan instance dependensi tersebut. Dalam hal ini, Hilt perlu mengetahui cara menyediakan instance LoggerLocalDataSource
dan DateFormatter
. Namun, Hilt belum mengetahui cara menyediakan instance tersebut.
Memberi tahu Hilt cara menyediakan dependensi dengan @Inject
Buka file ServiceLocator.kt
untuk melihat cara penerapan ServiceLocator
. Anda dapat melihat bagaimana panggilan provideDateFormatter()
selalu menampilkan instance DateFormatter
yang berbeda.
Ini sama persis dengan tindakan yang ingin kita peroleh dengan Hilt. Untungnya, DateFormatter
tidak bergantung pada class lain sehingga untuk saat ini kita tidak perlu khawatir dengan dependensi transitif.
Untuk memberi tahu Hilt cara memberikan instance suatu jenis, tambahkan anotasi @Inject ke konstruksi class yang Anda inginkan agar diinjeksi.
Buka file util/DateFormatter.kt
dan beri anotasi pada konstruktor DateFormatter
dengan @Inject
. Perlu diingat bahwa untuk menganotasi konstruktor di Kotlin, Anda juga memerlukan kata kunci constructor
:
class DateFormatter @Inject constructor() { ... }
Dengan ini, Hilt tahu cara menyediakan instance DateFormatter
. Hal yang sama harus dilakukan dengan LoggerLocalDataSource
. Buka file data/LoggerLocalDataSource.kt
dan beri anotasi pada konstruktornya dengan @Inject
:
class LoggerLocalDataSource @Inject constructor(private val logDao: LogDao) {
...
}
Jika membuka class ServiceLocator
lagi, Anda dapat melihat bahwa kita memiliki kolom LoggerLocalDataSource
publik. Itu berarti bahwa ServiceLocator
akan selalu menampilkan instance LoggerLocalDataSource
yang sama setiap kali dipanggil. Ini yang disebut dengan "mencakup instance ke container". Bagaimana cara melakukannya di Hilt?
Kita dapat menggunakan anotasi untuk memberi cakupan instance ke container. Karena Hilt dapat menghasilkan berbagai container yang memiliki siklus proses yang berbeda, terdapat anotasi berbeda yang mencakup container tersebut.
Anotasi yang mencakup instance ke container aplikasi adalah @Singleton
. Anotasi ini akan membuat container aplikasi selalu menyediakan instance yang sama, terlepas dari apakah jenis metode tersebut digunakan sebagai dependensi jenis lain atau apakah perlu diinjeksi ke kolom.
Logika yang sama dapat diterapkan ke semua container yang disertakan ke class Android. Anda dapat menemukan daftar semua anotasi pencakupan dalam dokumentasi. Misalnya, jika Anda ingin agar container aktivitas selalu memberikan instance jenis yang sama, Anda dapat menganotasi jenis tersebut dengan @ActivityScoped
.
Seperti yang disebutkan di atas, karena kita ingin agar container aplikasi selalu memberikan instance LoggerLocalDataSource
yang sama, kita memberi anotasi pada class-nya dengan @Singleton
:
@Singleton
class LoggerLocalDataSource @Inject constructor(private val logDao: LogDao) {
...
}
Sekarang, Hilt mengetahui cara menyediakan instance LoggerLocalDataSource
. Namun, kali ini, jenis tersebut memiliki dependensi transitif. Untuk memberikan instance LoggerLocalDataSource
, Hilt juga perlu mengetahui cara menyediakan instance LogDao
.
Namun, karena LogDao
adalah antarmuka, kita tidak dapat memberi anotasi pada konstruktornya dengan @Inject
karena antarmuka tidak memilikinya. Bagaimana cara memberi tahu Hilt cara menyediakan instance jenis ini?
Modul digunakan untuk menambahkan binding ke Hilt, atau dengan kata lain, untuk memberi tahu Hilt cara menyediakan instance dari jenis yang berbeda. Dalam modul Hilt, Anda menyertakan binding untuk jenis yang tidak dapat diinjeksi konstruktor seperti antarmuka atau class yang tidak terdapat dalam project Anda. Contohnya adalah OkHttpClient
- Anda perlu menggunakan builder tersebut untuk membuat instance.
Modul Hilt adalah class yang dianotasikan dengan @Module
dan @InstallIn
. @Module
memberi tahu Hilt bahwa ini adalah modul dan @InstallIn
memberi tahu Hilt di container mana binding tersebut tersedia dengan menentukan Komponen Hilt. Anda dapat menganggap Komponen Hilt sebagai container, dan daftar lengkap Komponen dapat ditemukan di sini.
Untuk setiap class Android yang dapat diinjeksi oleh Hilt, tersedia Komponen Hilt terkait. Misalnya, container Application
dikaitkan dengan ApplicationComponent
, dan container Fragment
dikaitkan dengan FragmentComponent
.
Membuat Modul
Mari kita buat modul Hilt tempat kita bisa menambahkan binding. Buat paket baru bernama di
di bawah paket hilt
dan buat file baru dengan nama DatabaseModule.kt
di dalam paket.
Karena LoggerLocalDataSource
tercakup dengan container aplikasi, binding LogDao
harus tersedia di container aplikasi. Kita menetapkan persyaratan tersebut menggunakan anotasi @InstallIn
dengan meneruskan class Komponen Hilt yang terkait dengannya (yaitu ApplicationComponent:class
):
package com.example.android.hilt.di
@InstallIn(ApplicationComponent::class)
@Module
object DatabaseModule {
}
Dalam implementasi class ServiceLocator
, instance LogDao
diperoleh dengan memanggil logsDatabase.logDao()
. Oleh karena itu, untuk menyediakan instance LogDao, kita memiliki dependensi transitif pada class AppDatabase
.
Menyediakan instance dengan @Provides
Kita dapat memberi anotasi pada fungsi dengan @Provides
dalam modul Hilt untuk memberi tahu Hilt cara menyediakan jenis yang tidak dapat diinjeksi melalui konstruktor.
Isi fungsi dari fungsi yang dianotasi @Provides
akan dijalankan setiap kali Hilt harus menyediakan instance dari jenis tersebut. Jenis nilai yang ditampilkan untuk fungsi yang dianotasi @Provides
memberi tahu Hilt jenis binding atau cara menyediakan instance dari jenis tersebut. Parameter fungsi adalah dependensi jenis.
Dalam kasus ini, kita akan menyertakan fungsi ini di class DatabaseModule
:
@Module
object DatabaseModule {
@Provides
fun provideLogDao(database: AppDatabase): LogDao {
return database.logDao()
}
}
Kode di atas memberi tahu Hilt bahwa database.logDao()
perlu dijalankan saat menyediakan instance LogDao
. Karena kita memiliki AppDatabase
sebagai dependensi transitif, kita juga perlu memberi tahu Hilt cara menyediakan instance jenis tersebut.
Karena AppDatabase
adalah class lain yang tidak dimiliki project ini karena dibuat oleh Room, kita juga dapat menyediakannya menggunakan fungsi @Provides
yang mirip dengan cara kita membuat instance database di class ServiceLocator
:
@Module
object DatabaseModule {
@Provides
@Singleton
fun provideDatabase(@ApplicationContext appContext: Context): AppDatabase {
return Room.databaseBuilder(
appContext,
AppDatabase::class.java,
"logging.db"
).build()
}
@Provides
fun provideLogDao(database: AppDatabase): LogDao {
return database.logDao()
}
}
Karena kita selalu ingin Hilt menyediakan instance database yang sama, kita menganotasi metode @Provides provideDatabase
dengan @Singleton
.
Setiap container Hilt dilengkapi dengan serangkaian binding default yang dapat diinjeksikan sebagai dependensi ke dalam binding kustom Anda. Hal ini berlaku untuk applicationContext
: untuk mengaksesnya, Anda perlu menganotasi kolom dengan @ApplicationContext
.
Menjalankan aplikasi
Sekarang, Hilt memiliki semua informasi yang diperlukan untuk menginjeksi instance di LogsFragment
. Namun, sebelum menjalankan aplikasi, Hilt harus mengetahui Activity
yang menghosting Fragment
agar dapat berfungsi. Kita perlu menganotasi MainActivity
dengan @AndroidEntryPoint
.
Buka file ui/MainActivity.kt
dan beri anotasi MainActivity
dengan @AndroidEntryPoint
:
@AndroidEntryPoint
class MainActivity : AppCompatActivity() { ... }
Sekarang, Anda dapat menjalankan aplikasi dan memeriksa apakah semuanya berfungsi dengan baik seperti sebelumnya.
Mari kita lanjutkan pemfaktoran ulang aplikasi untuk menghapus panggilan ServiceLocator
dari MainActivity
.
MainActivity
mendapatkan instance AppNavigator
dari ServiceLocator
yang memanggil fungsi provideNavigator(activity: FragmentActivity)
.
Karena AppNavigator
adalah antarmuka, kita tidak dapat menggunakan injeksi konstruktor. Untuk memberi tahu Hilt implementasi apa yang akan digunakan untuk antarmuka, Anda dapat menggunakan anotasi @Binds
pada fungsi di dalam modul Hilt.
@Binds
harus menganotasi fungsi abstrak (karena abstrak, fungsi tidak berisi kode apa pun dan class harus abstrak). Jenis nilai yang ditampilkan dari fungsi abstrak adalah antarmuka yang ingin kita sediakan implementasinya (yaitu AppNavigator
). Implementasi ditentukan dengan menambahkan parameter unik dengan jenis implementasi antarmuka (yaitu AppNavigatorImpl
).
Dapatkah kita menambahkan informasi ke class DatabaseModule
yang kita buat sebelumnya atau apakah kita memerlukan modul baru? Ada beberapa alasan mengapa kita harus membuat modul baru:
- Untuk pengaturan yang lebih baik, nama modul harus mencerminkan jenis informasi yang diberikan. Misalnya, tidak masuk akal untuk menyertakan binding navigasi dalam modul bernama
DatabaseModule
. - Modul
DatabaseModule
diinstal diApplicationComponent
, sehingga binding tersedia di container aplikasi. Informasi navigasi baru (yaituAppNavigator
) memerlukan informasi khusus dari Aktivitas (karenaAppNavigatorImpl
memilikiActivity
sebagai dependensi). Oleh karena itu, modul harus diinstal di containerActivity
sebagai ganti containerApplication
karena di sanalah tempat informasi tentangActivity
tersedia. - Modul Hilt tidak boleh berisi metode binding non-statis dan abstrak, sehingga Anda tidak dapat menempatkan anotasi
@Binds
dan@Provides
di class yang sama.
Buat file baru bernama NavigationModule.kt
di folder di
. Mari kita buat class abstrak baru yang dengan nama NavigationModule
yang dianotasikan dengan @Module
dan @InstallIn(ActivityComponent::class)
seperti yang dijelaskan di atas:
@InstallIn(ActivityComponent::class)
@Module
abstract class NavigationModule {
@Binds
abstract fun bindNavigator(impl: AppNavigatorImpl): AppNavigator
}
Di dalam modul, kita dapat menambahkan binding untuk AppNavigator
. Ini adalah fungsi abstrak yang menampilkan antarmuka yang kita informasikan kepada Hilt (yaitu AppNavigator
) dan parameternya adalah implementasi antarmuka tersebut (yaitu AppNavigatorImpl
).
Sekarang, kita harus memberi tahu Hilt cara menyediakan instance AppNavigatorImpl
. Karena class ini dapat kita injeksi melalui konstruktor, kita hanya menganotasi konstruktornya dengan @Inject
.
Buka file navigator/AppNavigatorImpl.kt
dan lakukan hal tersebut:
class AppNavigatorImpl @Inject constructor(
private val activity: FragmentActivity
) : AppNavigator {
...
}
AppNavigatorImpl
bergantung pada FragmentActivity
. Karena instance AppNavigator
disediakan dalam container Activity
(instance ini juga tersedia dalam container Fragment
dan container View
karena NavigationModule
diinstal di ActivityComponent
), FragmentActivity
sudah tersedia karena tersedia sebagai binding standar.
Menggunakan Hilt dalam Aktivitas
Sekarang, Hilt memiliki semua informasi untuk dapat menginjeksi instance AppNavigator
. Buka file MainActivity.kt
, lalu lakukan hal berikut ini:
- Anotasi kolom
navigator
dengan@Inject
untuk mendapatkan Hilt, - Hapus pengubah visibilitas
private
, dan - Hapus kode inisialisasi
navigator
di fungsionCreate
.
Kode baru akan terlihat seperti ini:
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject lateinit var navigator: AppNavigator
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
if (savedInstanceState == null) {
navigator.navigateTo(Screens.BUTTONS)
}
}
...
}
Menjalankan aplikasi
Anda dapat menjalankan aplikasi dan melihat cara kerjanya.
Menyelesaikan pemfaktoran ulang
Satu-satunya class yang masih menggunakan ServiceLocator
untuk mengambil dependensi adalah ButtonsFragment
. Karena Hilt sudah mengetahui cara menyediakan semua jenis yang dibutuhkan ButtonsFragment
, kita cukup melakukan injeksi kolom di class.
Seperti yang telah kita pelajari sebelumnya, agar class ini diinjeksi kolomnya oleh Hilt, kita harus:
- Menganotasi
ButtonsFragment
dengan@AndroidEntryPoint
, - Menghapus pengubah pribadi dari kolom
logger
dannavigator
dan menganotasinya dengan@Inject
, - Menghapus kode inisialisasi kolom (yaitu metode
onAttach
danpopulateFields
).
Kode untuk ButtonsFragment
:
@AndroidEntryPoint
class ButtonsFragment : Fragment() {
@Inject lateinit var logger: LoggerLocalDataSource
@Inject lateinit var navigator: AppNavigator
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_buttons, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
...
}
}
Perhatikan bahwa instance LoggerLocalDataSource
akan sama dengan instance yang kita gunakan di LogsFragment
karena jenisnya tercakup ke container aplikasi. Namun, instance AppNavigator
akan berbeda dari instance di MainActivity
karena kita belum mencakupkannya ke container Activity
masing-masing.
Pada tahap ini, class ServiceLocator
tidak lagi menyediakan dependensi sehingga kita dapat menghapusnya sepenuhnya dari project. Satu-satunya penggunaannya masih ada di class LogApplication
tempat kita menyimpan instance. Mari hapus class tersebut karena tidak diperlukan lagi.
Buka class LogApplication
dan hapus penggunaan ServiceLocator
. Kode baru untuk class Application
adalah:
@HiltAndroidApp
class LogApplication : Application()
Sekarang, jangan ragu untuk menghapus class ServiceLocator
dari project sekaligus. Karena ServiceLocator
masih digunakan dalam pengujian, hapus juga penggunaannya dari class AppTest
.
Konten dasar yang dibahas
Apa yang baru saja Anda pelajari seharusnya sudah cukup untuk menggunakan Hilt sebagai alat Injeksi Dependensi di aplikasi Android.
Mulai sekarang, kita akan menambahkan fungsi baru ke aplikasi untuk mempelajari cara menggunakan fitur Hilt lanjutan di berbagai situasi.
Setelah menghapus class ServiceLocator
dari project dan mempelajari dasar-dasar Hilt, mari tambahkan fungsionalitas baru ke aplikasi untuk mempelajari fitur Hilt lainnya.
Di bagian ini, Anda akan mempelajari:
- Cara membuat cakupan ke container Aktivitas.
- Apa yang dimaksud dengan penentu, masalah apa yang dipecahkan, dan cara menggunakannya.
Untuk menampilkan ini, kita memerlukan perilaku yang berbeda di aplikasi. Kita akan menukar penyimpanan log dari database ke daftar dalam memori dengan tujuan hanya merekam log selama sesi aplikasi.
Antarmuka LoggerDataSource
Mari kita mulai mengabstraksi sumber data menjadi antarmuka. Buat file baru dengan nama LoggerDataSource.kt
pada folder data
dengan konten berikut:
package com.example.android.hilt.data
// Common interface for Logger data sources.
interface LoggerDataSource {
fun addLog(msg: String)
fun getAllLogs(callback: (List<Log>) -> Unit)
fun removeLogs()
}
LoggerLocalDataSource
digunakan di kedua Fragmen: ButtonsFragment
dan LogsFragment
. Kita harus memfaktorkannya ulang untuk menggunakannya agar memakai instance LoggerDataSource
sebagai gantinya.
Buka LogsFragment
dan buat variabel logger jenis LoggerDataSource
:
@AndroidEntryPoint
class LogsFragment : Fragment() {
@Inject lateinit var logger: LoggerDataSource
...
}
Lakukan hal yang sama di ButtonsFragment
:
@AndroidEntryPoint
class ButtonsFragment : Fragment() {
@Inject lateinit var logger: LoggerDataSource
...
}
Selanjutnya, mari kita buat LoggerLocalDataSource
agar menerapkan antarmuka ini. Buka file data/LoggerLocalDataSource.kt
dan:
- Buat agar menerapkan antarmuka
LoggerDataSource
, dan - Tandai metodenya dengan
override
@Singleton
class LoggerLocalDataSource @Inject constructor(
private val logDao: LogDao
) : LoggerDataSource {
...
override fun addLog(msg: String) { ... }
override fun getAllLogs(callback: (List<Log>) -> Unit) { ... }
override fun removeLogs() { ... }
}
Sekarang, mari kita buat implementasi lain dari LoggerDataSource
bernama LoggerInMemoryDataSource
yang menyimpan log dalam memori. Buat file baru bernama LoggerInMemoryDataSource.kt
pada folder data
dengan konten berikut:
package com.example.android.hilt.data
import java.util.LinkedList
class LoggerInMemoryDataSource : LoggerDataSource {
private val logs = LinkedList<Log>()
override fun addLog(msg: String) {
logs.addFirst(Log(msg, System.currentTimeMillis()))
}
override fun getAllLogs(callback: (List<Log>) -> Unit) {
callback(logs)
}
override fun removeLogs() {
logs.clear()
}
}
Membuat cakupan ke container Aktivitas
Agar dapat menggunakan LoggerInMemoryDataSource
sebagai detail implementasi, kita perlu memberi tahu Hilt cara menyediakan instance jenis ini. Seperti sebelumnya, kita memberi anotasi konstruktor class dengan @Inject
:
class LoggerInMemoryDataSource @Inject constructor(
) : LoggerDataSource { ... }
Karena aplikasi kita hanya terdiri dari satu Aktivitas (juga disebut aplikasi Aktivitas tunggal), kita harus memiliki instance LoggerInMemoryDataSource
di container Activity
dan menggunakan kembali instance tersebut di Fragment
.
Kita dapat memperoleh perilaku logging dalam memori dengan membuat cakupan LoggerInMemoryDataSource
ke container Activity
: setiap Activity
yang dibuat akan memiliki container sendiri, yaitu instance yang berbeda. Pada setiap container, instance LoggerInMemoryDataSource
yang sama akan diberikan saat logger diperlukan sebagai dependensi atau untuk injeksi kolom. Selain itu, instance yang sama akan disediakan dalam container di bawah Hierarki komponen.
Setelah membuat cakupan ke dokumentasi Komponen, agar suatu jenis tercakup ke container Activity
, kita perlu menganotasi jenis tersebut dengan @ActivityScoped
:
@ActivityScoped
class LoggerInMemoryDataSource @Inject constructor(
) : LoggerDataSource { ... }
Saat ini, Hilt mengetahui cara menyediakan instance LoggerInMemoryDataSource
dan LoggerLocalDataSource
, tetapi bagaimana dengan LoggerDataSource
? Hilt tidak mengetahui implementasi mana yang akan digunakan saat LoggerDataSource
diminta.
Seperti yang kita ketahui dari bagian sebelumnya, kita dapat menggunakan anotasi @Binds
dalam modul untuk memberi tahu Hilt implementasi mana yang akan digunakan. Namun, bagaimana jika kita perlu menyediakan kedua implementasi dalam project yang sama? Misalnya, menggunakan LoggerInMemoryDataSource
saat aplikasi sedang berjalan dan LoggerLocalDataSource
dalam Service
.
Dua implementasi untuk antarmuka yang sama
Mari membuat file baru di folder di
bernama LoggingModule.kt
. Karena implementasi LoggerDataSource
yang berbeda tercakup ke container yang berbeda, kita tidak dapat menggunakan modul yang sama: LoggerInMemoryDataSource
dicakupkan ke container Activity
dan LoggerLocalDataSource
ke container Application
.
Untungnya, kita dapat menetapkan binding untuk kedua modul dalam file yang sama yang baru saja kita buat:
package com.example.android.hilt.di
@InstallIn(ApplicationComponent::class)
@Module
abstract class LoggingDatabaseModule {
@Singleton
@Binds
abstract fun bindDatabaseLogger(impl: LoggerLocalDataSource): LoggerDataSource
}
@InstallIn(ActivityComponent::class)
@Module
abstract class LoggingInMemoryModule {
@ActivityScoped
@Binds
abstract fun bindInMemoryLogger(impl: LoggerInMemoryDataSource): LoggerDataSource
}
Metode @Binds
harus memiliki anotasi pencakupan jika jenisnya adalah tercakup, jadi itu sebabnya fungsi di atas dianotasi dengan @Singleton
dan @ActivityScoped
. Jika @Binds
atau @Provides
digunakan sebagai binding untuk suatu jenis, anotasi pencakupan dalam jenis tersebut tidak digunakan lagi, sehingga Anda dapat melanjutkan dan menghapusnya dari class implementasi yang berbeda.
Jika mencoba mem-build project sekarang, Anda akan melihat error DuplicateBindings
.
error: [Dagger/DuplicateBindings] com.example.android.hilt.data.LoggerDataSource is bound multiple times
Hal ini karena jenis LoggerDataSource
sedang diinjeksi di Fragment
, tetapi Hilt tidak mengetahui implementasi mana yang akan digunakan karena ada dua binding dari jenis yang sama Bagaimana Hilt tahu yang mana yang harus digunakan?
Menggunakan penentu
Untuk memberi tahu Hilt cara menyediakan implementasi yang berbeda (beberapa binding) dari jenis yang sama, Anda dapat menggunakan penentu.
Kami perlu menetapkan penentu per implementasi karena setiap penentu akan digunakan untuk mengidentifikasi binding. Saat menginjeksi jenis di class Android atau memiliki jenis tersebut sebagai dependensi class lain, anotasi penentu harus digunakan untuk menghindari ambiguitas.
Karena penentu hanyalah anotasi, kita dapat menentukannya dalam file LoggingModule.kt
tempat kita menambahkan modul:
package com.example.android.hilt.di
@Qualifier
annotation class InMemoryLogger
@Qualifier
annotation class DatabaseLogger
Sekarang, penentu tersebut harus memberi anotasi pada fungsi @Binds
(atau @Provides
jika kita membutuhkannya) yang menyediakan setiap implementasi. Lihat kode lengkap dan perhatikan penggunaan penentu dalam metode @Binds
:
package com.example.android.hilt.di
@Qualifier
annotation class InMemoryLogger
@Qualifier
annotation class DatabaseLogger
@InstallIn(ApplicationComponent::class)
@Module
abstract class LoggingDatabaseModule {
@DatabaseLogger
@Singleton
@Binds
abstract fun bindDatabaseLogger(impl: LoggerLocalDataSource): LoggerDataSource
}
@InstallIn(ActivityComponent::class)
@Module
abstract class LoggingInMemoryModule {
@InMemoryLogger
@ActivityScoped
@Binds
abstract fun bindInMemoryLogger(impl: LoggerInMemoryDataSource): LoggerDataSource
}
Selain itu, penentu tersebut harus digunakan pada titik injeksi dengan implementasi yang ingin diinjeksikan. Dalam hal ini, kita akan menggunakan implementasi LoggerInMemoryDataSource
di Fragment
.
Buka LogsFragment
dan gunakan penentu @InMemoryLogger
pada kolom logger untuk memberi tahu Hilt agar menginjeksikan instance LoggerInMemoryDataSource
:
@AndroidEntryPoint
class LogsFragment : Fragment() {
@InMemoryLogger
@Inject lateinit var logger: LoggerDataSource
...
}
Lakukan hal yang sama untuk ButtonsFragment
:
@AndroidEntryPoint
class ButtonsFragment : Fragment() {
@InMemoryLogger
@Inject lateinit var logger: LoggerDataSource
...
}
Jika ingin mengubah implementasi database yang ingin digunakan, Anda hanya perlu memberi anotasi pada kolom yang diinjeksi dengan @DatabaseLogger
, bukan @InMemoryLogger
.
Menjalankan aplikasi
Kita bisa menjalankan aplikasi dan mengonfirmasi yang telah kita lakukan dengan berinteraksi dengan tombol-tombol tersebut dan mengamati log yang sesuai muncul pada layar "Lihat semua log".
Perhatikan bahwa log tidak disimpan ke database lagi. Log tersebut tidak akan muncul di antara sesi, setiap kali Anda menutup dan membuka aplikasi lagi, layar log akan kosong.
Setelah aplikasi dimigrasikan sepenuhnya ke Hilt, kita juga dapat memigrasikan uji instrumentasi yang dimiliki dalam project. Pengujian yang memeriksa fungsi aplikasi ada di file AppTest.kt
di folder app/androidTest
. Buka file tersebut.
Anda akan melihat bahwa aplikasi tersebut tidak dikompilasi karena kita telah menghapus class ServiceLocator
dari project. Hapus referensi ke ServiceLocator
yang tidak digunakan lagi dengan menghapus metode @After tearDown
dari class.
Pengujian androitTest
berjalan pada emulator. Pengujian happyPath
mengonfirmasi bahwa ketukan pada "Tombol 1" telah dicatat ke database. Karena aplikasi menggunakan database dalam memori, setelah pengujian selesai, semua log akan hilang.
Pengujian UI dengan Hilt
Hilt akan menginjeksi dependensi dalam pengujian UI seperti yang akan terjadi pada kode produksi.
Pengujian dengan Hilt tidak memerlukan pemeliharaan karena Hilt otomatis menghasilkan serangkaian komponen baru untuk setiap pengujian.
Menambahkan dependensi pengujian
Hilt menggunakan library tambahan dengan anotasi khusus pengujian yang mempermudah pengujian kode Anda dengan nama hilt-android-testing
yang harus ditambahkan ke project. Selain itu, karena Hilt perlu membuat kode untuk class di folder androidTest
, pemroses anotasinya juga harus dapat dijalankan di sana. Untuk mengaktifkannya, Anda harus menyertakan dua dependensi dalam file app/build.gradle
.
Untuk menambahkan dependensi ini, buka app/build.gradle
dan tambahkan konfigurasi ini ke bagian bawah dependencies
:
...
dependencies {
// Hilt testing dependency
androidTestImplementation "com.google.dagger:hilt-android-testing:$hilt_version"
// Make Hilt generate code in the androidTest folder
kaptAndroidTest "com.google.dagger:hilt-android-compiler:$hilt_version"
}
TestRunner Kustom
Pengujian yang diinstrumentasi menggunakan Hilt harus dijalankan di Application
yang mendukung Hilt. Library sudah dilengkapi dengan HiltTestApplication
yang dapat digunakan untuk menjalankan pengujian UI. Menentukan Application
yang akan digunakan dalam pengujian sudah dilakukan dengan membuat runner pengujian baru dalam project.
Di tingkat yang sama dengan tempat file AppTest.kt
berada pada folder androidTest
, buat file baru bernama CustomTestRunner
. CustomTestRunner
diperluas dari AndroidJUnitRunner dan diimplementasikan sebagai berikut:
class CustomTestRunner : AndroidJUnitRunner() {
override fun newApplication(cl: ClassLoader?, name: String?, context: Context?): Application {
return super.newApplication(cl, HiltTestApplication::class.java.name, context)
}
}
Berikutnya, kita harus memberi tahu project agar menggunakan runner pengujian ini untuk uji instrumentasi. Hal tersebut ditetapkan dalam atribut testInstrumentationRunner
dari file app/build.gradle
. Buka file, dan ganti konten testInstrumentationRunner
default dengan ini:
...
android {
...
defaultConfig {
...
testInstrumentationRunner "com.example.android.hilt.CustomTestRunner"
}
...
}
...
Sekarang kita siap menggunakan Hilt dalam pengujian UI.
Menjalankan pengujian yang menggunakan Hilt
Selanjutnya, agar class pengujian emulator dapat menggunakan Hilt, maka harus:
- Dianotasikan dengan
@HiltAndroidTest
yang bertanggung jawab membuat komponen Hilt untuk setiap pengujian - Menggunakan
HiltAndroidRule
yang mengelola status komponen dan digunakan untuk melakukan injeksi pada pengujian Anda.
Mari kita sertakan dalam AppTest
:
@RunWith(AndroidJUnit4::class)
@HiltAndroidTest
class AppTest {
@get:Rule
var hiltRule = HiltAndroidRule(this)
...
}
Sekarang, jika Anda menjalankan pengujian menggunakan tombol putar di samping definisi class atau definisi metode pengujian, emulator akan dimulai jika Anda telah mengonfigurasinya dan pengujian akan berhasil.
Untuk mempelajari lebih lanjut pengujian dan fitur seperti injeksi kolom atau mengganti binding dalam pengujian, lihat dokumentasi.
Di bagian codelab ini, kita akan mempelajari cara menggunakan anotasi @EntryPoint
yang digunakan untuk menginjeksi dependensi pada class yang tidak didukung oleh Hilt.
Seperti yang kita lihat sebelumnya, Hilt dilengkapi dengan dukungan untuk komponen Android yang paling umum. Namun, Anda mungkin perlu melakukan injeksi kolom di class yang tidak didukung langsung oleh Hilt atau tidak dapat menggunakan Hilt.
Dalam kasus tersebut, Anda dapat menggunakan @EntryPoint
. Titik entri adalah tempat batas Anda bisa mendapatkan objek yang disediakan Hilt dari kode yang tidak dapat menggunakan Hilt untuk menginjeksikan dependensinya. Ini adalah titik ketika kode pertama kali dimasukkan ke dalam container yang dikelola oleh Hilt.
Kasus penggunaan
Kita ingin mengekspor log di luar proses aplikasi. Untuk itu, kita perlu menggunakan ContentProvider
. Kita hanya mengizinkan konsumen untuk meminta satu log tertentu (misalnya id
) atau semua log dari aplikasi menggunakan ContentProvider
. Kita akan menggunakan database Room untuk mengambil data. Oleh karena itu, class LogDao
harus memperlihatkan metode yang menampilkan informasi yang diperlukan menggunakan database Cursor
. Buka file LogDao.kt
dan tambahkan metode berikut ke antarmuka.
@Dao
interface LogDao {
...
@Query("SELECT * FROM logs ORDER BY id DESC")
fun selectAllLogsCursor(): Cursor
@Query("SELECT * FROM logs WHERE id = :id")
fun selectLogById(id: Long): Cursor?
}
Selanjutnya, kita harus membuat class ContentProvider
baru dan mengganti metode query
untuk menampilkan Cursor
dengan log. Buat file baru bernama LogsContentProvider.kt
pada direktori contentprovider
baru dengan konten berikut:
package com.example.android.hilt.contentprovider
import android.content.ContentProvider
import android.content.ContentUris
import android.content.ContentValues
import android.content.Context
import android.content.UriMatcher
import android.database.Cursor
import android.net.Uri
import com.example.android.hilt.data.LogDao
import dagger.hilt.EntryPoint
import dagger.hilt.EntryPoints
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ApplicationComponent
import java.lang.UnsupportedOperationException
/** The authority of this content provider. */
private const val LOGS_TABLE = "logs"
/** The authority of this content provider. */
private const val AUTHORITY = "com.example.android.hilt.provider"
/** The match code for some items in the Logs table. */
private const val CODE_LOGS_DIR = 1
/** The match code for an item in the Logs table. */
private const val CODE_LOGS_ITEM = 2
/**
* A ContentProvider that exposes the logs outside the application process.
*/
class LogsContentProvider: ContentProvider() {
private val matcher: UriMatcher = UriMatcher(UriMatcher.NO_MATCH).apply {
addURI(AUTHORITY, LOGS_TABLE, CODE_LOGS_DIR)
addURI(AUTHORITY, "$LOGS_TABLE/*", CODE_LOGS_ITEM)
}
override fun onCreate(): Boolean {
return true
}
/**
* Queries all the logs or an individual log from the logs database.
*
* For the sake of this codelab, the logic has been simplified.
*/
override fun query(
uri: Uri,
projection: Array<out String>?,
selection: String?,
selectionArgs: Array<out String>?,
sortOrder: String?
): Cursor? {
val code: Int = matcher.match(uri)
return if (code == CODE_LOGS_DIR || code == CODE_LOGS_ITEM) {
val appContext = context?.applicationContext ?: throw IllegalStateException()
val logDao: LogDao = getLogDao(appContext)
val cursor: Cursor? = if (code == CODE_LOGS_DIR) {
logDao.selectAllLogsCursor()
} else {
logDao.selectLogById(ContentUris.parseId(uri))
}
cursor?.setNotificationUri(appContext.contentResolver, uri)
cursor
} else {
throw IllegalArgumentException("Unknown URI: $uri")
}
}
override fun insert(uri: Uri, values: ContentValues?): Uri? {
throw UnsupportedOperationException("Only reading operations are allowed")
}
override fun update(
uri: Uri,
values: ContentValues?,
selection: String?,
selectionArgs: Array<out String>?
): Int {
throw UnsupportedOperationException("Only reading operations are allowed")
}
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int {
throw UnsupportedOperationException("Only reading operations are allowed")
}
override fun getType(uri: Uri): String? {
throw UnsupportedOperationException("Only reading operations are allowed")
}
}
Anda akan melihat bahwa panggilan getLogDao(appContext)
tidak dapat dikompilasi. Kita perlu mengimplementasikannya dengan mengambil dependensi LogDao
dari container aplikasi Hilt. Namun, Hilt tidak mendukung injeksi ke ContentProvider
secara langsung seperti yang dilakukan dengan Aktivitas, misalnya, dengan @AndroidEntryPoint
.
Kita perlu membuat antarmuka baru yang dianotasi dengan @EntryPoint
untuk mengaksesnya.
@EntryPoint sedang dijalankan
Titik entri adalah antarmuka dengan metode pengakses untuk setiap jenis binding yang kita inginkan (termasuk penentunya). Selain itu, antarmuka harus dianotasi dengan @InstallIn
untuk menentukan komponen yang akan menginstal titik entri.
Praktik terbaik adalah menambahkan antarmuka titik entri baru di dalam class yang menggunakannya. Oleh karena itu, sertakan antarmuka dalam file LogsContentProvider.kt
:
class LogsContentProvider: ContentProvider() {
@InstallIn(ApplicationComponent::class)
@EntryPoint
interface LogsContentProviderEntryPoint {
fun logDao(): LogDao
}
...
}
Perhatikan bahwa antarmuka dianotasi dengan @EntryPoint
dan diinstal di ApplicationComponent
karena kita menginginkan dependensi dari instance container Application
. Di dalam antarmuka, kita menampilkan metode untuk binding yang ingin diakses, dalam hal ini, LogDao
.
Untuk mengakses titik entri, gunakan metode statis yang sesuai dari EntryPointAccessors
. Parameter harus berupa instance komponen atau objek @AndroidEntryPoint
yang berfungsi sebagai pemegang komponen. Pastikan bahwa komponen yang Anda teruskan sebagai parameter dan metode statis EntryPointAccessors
cocok dengan class Android dalam anotasi @InstallIn
pada antarmuka @EntryPoint
:
Sekarang, kita dapat menerapkan metode getLogDao
yang tidak ada pada kode di atas. Mari kita gunakan antarmuka titik entri yang kita tentukan di atas dalam class LogsContentProviderEntryPoint
:
class LogsContentProvider: ContentProvider() {
...
private fun getLogDao(appContext: Context): LogDao {
val hiltEntryPoint = EntryPointAccessors.fromApplication(
appContext,
LogsContentProviderEntryPoint::class.java
)
return hiltEntryPoint.logDao()
}
}
Perhatikan cara meneruskan applicationContext
ke metode EntryPoints.get
statis dan class antarmuka yang dianotasi dengan @EntryPoint
.
Sekarang Anda sudah terbiasa dengan Hilt dan seharusnya dapat menambahkannya ke aplikasi Android. Dalam codelab ini, Anda mempelajari:
- Cara menyiapkan Hilt di class Aplikasi menggunakan
@HiltAndroidApp
. - Cara menambahkan container dependensi ke komponen siklus proses Android yang berbeda menggunakan
@AndroidEntryPoint
. - Cara menggunakan modul untuk memberi tahu Hilt cara menyediakan jenis tertentu.
- Cara menggunakan penentu untuk menyediakan beberapa binding untuk jenis tertentu.
- Cara menguji aplikasi menggunakan Hilt.
- Kapan
@EntryPoint
berguna dan cara menggunakannya.