Memigrasikan aplikasi Dagger ke Hilt

Dalam codelab ini, Anda akan mempelajari cara memigrasikan Dagger ke Hilt untuk injeksi dependensi (DI) di aplikasi Android. Codelab ini memigrasikan Menggunakan Dagger di codelab aplikasi Android Anda ke Hilt. Codelab ini bertujuan untuk menunjukkan cara merencanakan migrasi serta menjaga Dagger dan Hilt agar berfungsi berdampingan selama migrasi dengan menjaga aplikasi tetap berfungsi saat Anda memigrasikan setiap komponen Dagger ke Hilt.

Injeksi dependensi membantu penggunaan kembali kode, kemudahan pemfaktoran ulang, dan kemudahan pengujian. Hilt ditambahkan pada library DI yang populer, Dagger, untuk mendapatkan manfaat dari ketepatan waktu kompilasi, performa runtime, skalabilitas, dan dukungan Android Studio yang disediakan oleh Dagger.

Karena banyak class framework Android yang dibuatkan instance oleh OS itu sendiri, ada boilerplate terkait saat menggunakan Dagger di aplikasi Android. Hilt menghapus sebagian besar boilerplate ini dengan otomatis menghasilkan dan menyediakan:

  • Komponen untuk mengintegrasikan class framework Android dengan Dagger yang seharusnya Anda buat sendiri.
  • Anotasi cakupan untuk komponen yang dihasilkan Hilt secara otomatis.
  • Binding dan penentu standar.

Yang terpenting, karena Dagger dan Hilt dapat bekerja berdampingan, aplikasi dapat dimigrasikan sesuai kebutuhan.

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

  • Berpengalaman dengan sintaksis Kotlin.
  • Berpengalaman dengan Dagger.

Yang akan Anda pelajari

  • Cara menambahkan Hilt ke aplikasi Android.
  • Cara merencanakan strategi migrasi Anda.
  • Cara memigrasikan komponen ke Hilt dan menjaga agar kode Dagger yang ada tetap berfungsi.
  • Cara memigrasikan komponen tercakup.
  • Cara menguji aplikasi 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-dagger-to-hilt

Atau, Anda dapat mendownload repositori sebagai file Zip:

Download Zip

Membuka Android Studio

Jika perlu mendownload Android Studio, Anda dapat melakukannya di sini.

Penyiapan project

Project ini dibuat di beberapa cabang GitHub:

  • master adalah cabang yang Anda buka atau download. Titik awal codelab.
  • interop adalah cabang interop Dagger dan Hilt.
  • solution berisi solusi untuk codelab ini, termasuk pengujian dan ViewModels.

Sebaiknya Anda mengikuti codelab ini langkah demi langkah sesuai kemampuan Anda sendiri dimulai dengan cabang master.

Selama codelab, Anda akan melihat cuplikan kode yang harus ditambahkan ke project. Di beberapa tempat, Anda juga harus menghapus kode yang akan disebutkan secara eksplisit dalam komentar pada cuplikan kode.

Sebagai checkpoint, Anda memiliki cabang perantara yang tersedia jika memerlukan bantuan terkait langkah tertentu.

Untuk mendapatkan cabang solution menggunakan git, gunakan perintah ini:

$ git clone -b solution https://github.com/googlecodelabs/android-dagger-to-hilt

Atau download kode solusi dari sini:

Download kode akhir

Pertanyaan umum (FAQ)

Menjalankan aplikasi contoh

Pertama-tama, mari kita lihat bagaimana tampilan aplikasi contoh. 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 execute.png, lalu pilih emulator atau hubungkan perangkat Android Anda. Layar Pendaftaran akan muncul.

54d4e2a9bf8177c1.gif

Aplikasi terdiri dari 4 alur berbeda yang berfungsi dengan Dagger (diterapkan sebagai Aktivitas):

  • Pendaftaran: Pengguna dapat mendaftar dengan memasukkan nama pengguna, sandi, dan menyetujui persyaratan dan ketentuan kami.
  • Login: Pengguna dapat login menggunakan kredensial yang ditambahkan selama alur pendaftaran serta dapat membatalkan pendaftaran dari aplikasi.
  • Beranda: Pengguna disambut dan dapat melihat jumlah notifikasi miliknya yang belum dibaca.
  • Setelan: Pengguna dapat logout dan memuat ulang jumlah notifikasi yang belum dibaca (yang menghasilkan jumlah notifikasi acak).

Project ini mengikuti pola MVVM umum, dengan semua kompleksitas View dialihkan ke ViewModel. Luangkan waktu untuk membiasakan diri dengan struktur project.

8ecf1f9088eb2bb6.png

Panah mewakili dependensi antar-objek. Inilah yang disebut dengan grafik aplikasi: semua class aplikasi dan dependensi di antara semua class.

Kode di cabang master menggunakan Dagger untuk memasukkan dependensi. Daripada membuat Komponen secara manual, kami akan memfaktorkan ulang aplikasi agar menggunakan Hilt untuk menghasilkan Komponen dan kode terkait Dagger lainnya.

Dagger disiapkan di aplikasi seperti yang ditunjukkan pada diagram berikut. Titik pada jenis tertentu berarti jenis tersebut tercakup ke Komponen yang menyediakannya:

a1b8656d7fc17b7d.png

Agar mudah, dependensi Hilt telah ditambahkan ke project ini di cabang master yang telah Anda download di awal. Anda tidak perlu menambahkan kode berikut ke project karena tindakan tersebut 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 (level project) dan temukan dependensi Hilt berikut dalam classpath:

buildscript {
    ...
    ext.hilt_version = '2.28-alpha'
    dependencies {
        ...
        classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version"
    }
}

Buka app/build.gradle dan periksa deklarasi plugin gradle Hilt di bagian atas tepat di bawah plugin kotlin-kapt.

...
apply plugin: 'kotlin-kapt'
apply plugin: 'dagger.hilt.android.plugin'

android {
    ...
}

Terakhir, dependensi Hilt dan pemroses anotasi disertakan dalam project 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 mulai menggunakan Hilt.

Anda mungkin tergoda untuk memigrasikan semuanya ke Hilt sekaligus, tetapi dalam project nyata, sebaiknya migrasi ke Hilt dilakukan secara bertahap agar aplikasi berhasil di-build dan berjalan tanpa error.

Saat bermigrasi ke Hilt, Anda perlu membagi prosesnya menjadi beberapa tahap. Pendekatan yang disarankan adalah memulai dengan memigrasikan Aplikasi atau komponen @Singleton, dan kemudian memigrasikan aktivitas dan fragmen.

Dalam codelab ini, Anda akan memigrasikan AppComponent terlebih dahulu dan kemudian setiap alur aplikasi yang dimulai dengan Pendaftaran, lalu Login, dan yang terakhir Utama dan Setelan.

Selama migrasi, Anda akan menghapus semua antarmuka @Component dan @Subcomponent serta menganotasi semua modul dengan @InstallIn.

Setelah migrasi, semua class Application/Activity/Fragment/View/Service/BroadcastReceiver harus dianotasi dengan @AndroidEntryPoint dan semua kode yang membuat instance atau menyebarkan komponen juga harus dihapus.

Untuk merencanakan migrasi, mari kita mulai dengan AppComponent.kt untuk memahami hierarki komponen.

@Singleton
// Definition of a Dagger component that adds info from the different modules to the graph
@Component(modules = [StorageModule::class, AppSubcomponents::class])
interface AppComponent {

    // Factory to create instances of the AppComponent
    @Component.Factory
    interface Factory {
        // With @BindsInstance, the Context passed in will be available in the graph
        fun create(@BindsInstance context: Context): AppComponent
    }

    // Types that can be retrieved from the graph
    fun registrationComponent(): RegistrationComponent.Factory
    fun loginComponent(): LoginComponent.Factory
    fun userManager(): UserManager
}

AppComponent dianotasi dengan @Component dan mencakup dua modul, yaitu StorageModule dan AppSubcomponents.

AppSubcomponents memiliki tiga komponen, yaitu RegistrationComponent, LoginComponent, dan UserComponent.

  • LoginComponent dimasukkan ke dalam LoginActivity
  • RegistrationComponent dimasukkan ke dalam RegistrationActivity, EnterDetailsFragment, dan TermsAndConditionsFragment. Komponen ini juga tercakup dalam RegistrationActivity.

UserComponent dimasukkan ke dalam MainActivity dan SettingsActivity.

Referensi ke ApplicationComponent dapat diganti dengan Komponen yang dihasilkan Hilt (tertaut ke semua komponen yang dihasilkan) yang memetakan ke Komponen yang Anda migrasikan di aplikasi.

Di bagian ini, Anda akan memigrasikan AppComponent. Anda perlu melakukan beberapa pekerjaan dasar agar kode Dagger yang ada tetap berfungsi saat memigrasikan setiap komponen ke Hilt pada langkah-langkah berikutnya.

Untuk melakukan inisialisasi Hilt dan memulai pembuatan kode, Anda perlu menganotasi class Application dengan anotasi Hilt.

Buka MyApplication.kt dan tambahkan anotasi @HiltAndroidApp ke class. Anotasi ini memberi tahu Hilt untuk memicu pembuatan kode yang akan diambil dan digunakan oleh Dagger dalam pemroses anotasinya.

MyApplication.kt

package com.example.android.dagger

import android.app.Application
import dagger.hilt.android.HiltAndroidApp

@HiltAndroidApp
open class MyApplication : Application() {

    // Instance of the AppComponent that will be used by all the Activities in the project
    val appComponent: AppComponent by lazy {
        initializeComponent()
    }

    open fun initializeComponent(): AppComponent {
        // Creates an instance of AppComponent using its Factory constructor
        // We pass the applicationContext that will be used as Context in the graph
        return DaggerAppComponent.factory().create(applicationContext)
    }
}

1. Memigrasikan modul Komponen

Untuk memulai, buka AppComponent.kt. AppComponent memiliki dua modul (StorageModule dan AppSubcomponents) yang ditambahkan ke anotasi @Component. Hal pertama yang perlu Anda lakukan adalah memigrasikan 2 modul ini, agar Hilt menambahkannya ke dalam ApplicationComponent yang dihasilkan.

Untuk melakukannya, buka AppSubcomponents.kt dan anotasikan class dengan anotasi @InstallIn. Anotasi @InstallIn memerlukan parameter untuk menambahkan modul ke komponen yang tepat. Dalam hal ini, saat memigrasikan komponen tingkat aplikasi, sebaiknya binding dibuat di ApplicationComponent.

AppSubcomponents.kt

// This module tells a Component which are its subcomponents
// Install this module in Hilt-generated ApplicationComponent
@InstallIn(ApplicationComponent::class)
@Module(
    subcomponents = [
        RegistrationComponent::class,
        LoginComponent::class,
        UserComponent::class
    ]
)
class AppSubcomponents

Anda harus melakukan perubahan yang sama di StorageModule. Buka StorageModule.kt dan tambahkan anotasi @InstallIn seperti yang dilakukan di langkah sebelumnya.

StorageModule.kt

// Tells Dagger this is a Dagger module
// Install this module in Hilt-generated ApplicationComponent
@InstallIn(ApplicationComponent::class)
@Module
abstract class StorageModule {

    // Makes Dagger provide SharedPreferencesStorage when a Storage type is requested
    @Binds
    abstract fun provideStorage(storage: SharedPreferencesStorage): Storage
}

Dengan anotasi @InstallIn, Anda sekali lagi memberi tahu Hilt untuk menambahkan modul ke ApplicationComponent yang dihasilkan Hilt.

Sekarang, mari kita kembali dan memeriksa AppComponent.kt. AppComponent memberikan dependensi untuk RegistrationComponent, LoginComponent, dan UserManager. Pada langkah berikutnya, Anda akan menyiapkan komponen ini untuk migrasi.

2. Memigrasikan jenis terekspos

Saat memigrasikan aplikasi ke Hilt sepenuhnya, Hilt memungkinkan Anda meminta dependensi dari Dagger melalui titik entri secara manual. Dengan menggunakan titik entri, Anda dapat menjaga aplikasi tetap berfungsi saat memigrasikan setiap komponen Dagger. Pada langkah ini, Anda akan mengganti setiap komponen Dagger dengan pencarian dependensi manual di ApplicationComponent yang dihasilkan Hilt.

Untuk mendapatkan RegistrationComponent.Factory untuk RegistrationActivity.kt dari ApplicationComponent yang dihasilkan Hilt, Anda perlu membuat antarmuka EntryPoint baru yang dianotasi dengan @InstallIn. Anotasi InstallIn memberi tahu Hilt tempat pengambilan binding. Untuk mengakses titik entri, gunakan metode statis yang sesuai dari EntryPointAccessors. Parameter harus berupa instance komponen atau objek @AndroidEntryPoint yang berfungsi sebagai pemegang komponen.

RegistrationActivity.kt

class RegistrationActivity : AppCompatActivity() {

    @InstallIn(ApplicationComponent::class)
    @EntryPoint
    interface RegistrationEntryPoint {
        fun registrationComponent(): RegistrationComponent.Factory
    }

    ...
}

Sekarang Anda perlu mengganti kode terkait Dagger dengan RegistrationEntryPoint. Ubah inisialisasi registrationComponent agar menggunakan RegistrationEntryPoint. Dengan perubahan ini, RegistrationActivity dapat mengakses dependensinya melalui kode yang dihasilkan Hilt hingga dimigrasikan untuk menggunakan Hilt.

RegistrationActivity.kt

        // Creates an instance of Registration component by grabbing the factory from the app graph
        val entryPoint = EntryPointAccessors.fromApplication(applicationContext, RegistrationEntryPoint::class.java)
        registrationComponent = entryPoint.registrationComponent().create()

Berikutnya, Anda perlu melakukan pekerjaan dasar yang sama untuk semua jenis Komponen lain yang diekspos. Mari lanjutkan dengan LoginComponent.Factory. Buka LoginActivity dan buat antarmuka LoginEntryPoint yang dianotasi dengan @InstallIn dan @EntryPoint seperti yang Anda lakukan sebelumnya, tetapi kali ini memperlihatkan apa yang dibutuhkan LoginActivity dari komponen Hilt.

LoginActivity.kt

    @InstallIn(ApplicationComponent::class)
    @EntryPoint
    interface LoginEntryPoint {
        fun loginComponent(): LoginComponent.Factory
    }

Setelah Hilt mengetahui cara menyediakan LoginComponent, ganti panggilan inject() lama dengan loginComponent() EntryPoint.

LoginActivity.kt

        val entryPoint = EntryPointAccessors.fromApplication(applicationContext, LoginEntryPoint::class.java)
        entryPoint.loginComponent().create().inject(this)

Dua dari tiga jenis AppComponent yang diekspos diganti agar berfungsi dengan EntryPoint Hilt. Berikutnya, Anda perlu melakukan perubahan yang sama untuk UserManager. Tidak seperti RegistrationComponent dan LoginComponent, UserManager digunakan di MainActivity dan SettingsActivity. Anda hanya perlu membuat antarmuka EntryPoint satu kali. Antarmuka EntryPoint yang dianotasi dapat digunakan di kedua Aktivitas. Agar mudah, deklarasikan Antarmuka di MainActivity.

Untuk membuat antarmuka UserManagerEntryPoint, buka MainActivity.kt dan anotasi dengan @InstallIn dan @EntryPoint.

MainActivity.kt

    @InstallIn(ApplicationComponent::class)
    @EntryPoint
    interface UserManagerEntryPoint {
        fun userManager(): UserManager
    }

Sekarang ubah UserManager agar menggunakan UserManagerEntryPoint.

MainActivity.kt

        val entryPoint = EntryPointAccessors.fromApplication(applicationContext, UserManagerEntryPoint::class.java)
        val userManager = entryPoint.userManager()

Anda harus melakukan perubahan yang sama di SettingsActivity. Buka SettingsActivity.kt dan ganti cara UserManager dimasukkan.

SettingsActivity.kt

    val entryPoint = EntryPointAccessors.fromApplication(applicationContext, MainActivity.UserManagerEntryPoint::class.java)
    val userManager = entryPoint.userManager()

3. Menghapus Factory Komponen

Meneruskan Context ke komponen Dagger menggunakan @BindsInstance adalah pola yang umum. Hal ini tidak diperlukan di Hilt karena Context sudah tersedia sebagai binding standar.

Context biasanya diperlukan untuk mengakses resource, database, preferensi bersama, dan sebagainya. Hilt menyederhanakan proses injeksi ke konteks dengan menggunakan Penentu @ApplicationContext dan @ActivityContext.

Saat memigrasikan aplikasi, periksa jenis yang memerlukan Context sebagai dependensi dan ganti dengan jenis yang disediakan Hilt.

Dalam kasus ini, SharedPreferencesStorage memiliki Context sebagai dependensi. Untuk memberi tahu Hilt agar memasukkan konteks, buka SharedPreferencesStorage.kt. SharedPreferences memerlukan Context aplikasi, jadi tambahkan anotasi @ApplicationContext ke parameter konteks.

SharedPreferencesStorage.kt

class SharedPreferencesStorage @Inject constructor(
    @ApplicationContext context: Context
) : Storage {

//...

4. Memigrasikan metode injeksi

Selanjutnya, Anda harus memeriksa kode komponen untuk metode inject() dan menganotasi class terkait dengan @AndroidEntryPoint. Dalam kasus kami, AppComponent tidak memiliki metode inject() sehingga Anda tidak perlu melakukan apa pun.

5. Menghapus class AppComponent

Karena sudah menambahkan EntryPoints untuk semua komponen yang tercantum di AppComponent.kt, Anda dapat menghapus AppComponent.kt.

6. Menghapus kode yang menggunakan Komponen untuk bermigrasi

Anda tidak memerlukan kode untuk melakukan inisialisasi AppComponent kustom di class aplikasi. Sebagai gantinya, class Aplikasi menggunakan ApplicationComponent yang dihasilkan Hilt. Hapus semua kode di dalam isi class. Kode akhir akan terlihat seperti kode yang tercantum di bawah.

MyApplication.kt

package com.example.android.dagger

import android.app.Application
import dagger.hilt.android.HiltAndroidApp

@HiltAndroidApp
open class MyApplication : Application()

Dengan ini, Anda telah berhasil menambahkan Hilt ke Aplikasi, menghapus AppComponent, dan mengubah kode Dagger untuk memasukkan dependensi melalui AppComponent yang dihasilkan Hilt. Saat Anda mem-build dan mencoba aplikasi di perangkat atau emulator, aplikasi seharusnya berfungsi seperti biasanya. Di bagian berikut, kita akan memigrasikan setiap Aktivitas dan Fragmen untuk menggunakan Hilt.

Setelah memigrasikan komponen Aplikasi dan menyiapkan dasar-dasarnya, Anda dapat memigrasikan setiap Komponen ke Hilt satu per satu.

Mari mulai memigrasikan alur login. Anda dapat meminta Hilt untuk membuat LoginComponent dan menggunakannya di LoginActivity, daripada membuatnya sendiri secara manual.

Anda dapat mengikuti langkah yang sama dengan yang digunakan di bagian sebelumnya, tetapi kali ini menggunakan ActivityComponent yang dihasilkan Hilt, karena kita akan memigrasikan Komponen yang dikelola oleh Aktivitas.

Untuk memulai, buka LoginComponent.kt. LoginComponent tidak memiliki modul sehingga Anda tidak perlu melakukan apa pun. Agar Hilt menghasilkan komponen untuk LoginActivity dan memasukkannya, Anda perlu menganotasi aktivitas dengan @AndroidEntryPoint.

LoginActivity.kt

@AndroidEntryPoint
class LoginActivity : AppCompatActivity() {

    //...
}

Ini adalah kode yang perlu Anda tambahkan untuk memigrasikan LoginActivity ke Hilt. Karena Hilt akan menghasilkan kode terkait Dagger, Anda hanya perlu melakukan beberapa pembersihan. Hapus antarmuka LoginEntryPoint.

LoginActivity.kt

    //Remove
    //@InstallIn(ApplicationComponent::class)
    //@EntryPoint
    //interface LoginEntryPoint {
    //    fun loginComponent(): LoginComponent.Factory
    //}

Berikutnya, hapus kode EntryPoint di onCreate().

LoginActivity.kt

override fun onCreate(savedInstanceState: Bundle?) {
   //Remove
   //val entryPoint = EntryPoints.get(applicationContext, LoginActivity.LoginEntryPoint::class.java)
   //entryPoint.loginComponent().create().inject(this)

    super.onCreate(savedInstanceState)

    ...
}

Karena Hilt akan menghasilkan komponen ini, temukan dan hapus LoginComponent.kt.

LoginComponent saat ini tercantum sebagai subkomponen di AppSubcomponents.kt. Anda dapat menghapus LoginComponent dari daftar subkomponen dengan aman karena Hilt akan membuat binding untuk Anda.

AppSubcomponents.kt

// This module tells a Component which are its subcomponents
@InstallIn(ApplicationComponent::class)
@Module(
    subcomponents = [
        RegistrationComponent::class,
        UserComponent::class
    ]
)
class AppSubcomponents

Anda hanya memerlukan ini untuk memigrasikan LoginActivity agar menggunakan Hilt. Di bagian ini, Anda telah menghapus lebih banyak kode daripada yang ditambahkan. Anda tidak hanya mengetikkan lebih sedikit kode saat menggunakan Hilt, tetapi ini juga berarti lebih sedikit kode untuk memelihara dan memunculkan bug.

Di bagian ini, Anda akan memigrasikan alur pendaftaran. Untuk merencanakan migrasi, mari kita lihat RegistrationComponent. Buka RegistrationComponent.kt dan scroll ke fungsi inject(). RegistrationComponent bertanggung jawab untuk memasukkan dependensi ke RegistrationActivity, EnterDetailsFragment, dan TermsAndConditionsFragment.

Mari mulai memigrasikan RegistrationActivity. Buka RegistrationActivity.kt dan anotasi class dengan @AndroidEntryPoint.

RegistrationActivity.kt

@AndroidEntryPoint
class RegistrationActivity : AppCompatActivity() {
    //...
}

Setelah RegistrationActivity terdaftar ke Hilt, Anda dapat menghapus Antarmuka RegistrationEntryPoint dan kode terkait EntryPoint dari fungsi onCreate().

RegistrationActivity.kt

//Remove
//@InstallIn(ApplicationComponent::class)
//@EntryPoint
//interface RegistrationEntryPoint {
//    fun registrationComponent(): RegistrationComponent.Factory
//}

override fun onCreate(savedInstanceState: Bundle?) {
    //Remove
    //val entryPoint = EntryPoints.get(applicationContext, RegistrationEntryPoint::class.java)
    //registrationComponent = entryPoint.registrationComponent().create()

    registrationComponent.inject(this)
    super.onCreate(savedInstanceState)
    //..
}

Hilt bertanggung jawab untuk menghasilkan komponen dan memasukkan dependensi sehingga Anda dapat menghapus variabel registrationComponent dan panggilan injeksi pada komponen Dagger yang dihapus.

RegistrationActivity.kt

// Remove
// lateinit var registrationComponent: RegistrationComponent

override fun onCreate(savedInstanceState: Bundle?) {
    //Remove
    //registrationComponent.inject(this)
    super.onCreate(savedInstanceState)

    //..
}

Berikutnya, buka EnterDetailsFragment.kt. Anotasi EnterDetailsFragment dengan @AndroidEntryPoint, sama seperti yang Anda lakukan di RegistrationActivity.

EnterDetailsFragment.kt

@AndroidEntryPoint
class EnterDetailsFragment : Fragment() {

    //...
}

Karena Hilt menyediakan dependensi, panggilan inject() pada komponen Dagger yang dihapus tidak diperlukan. Hapus fungsi onAttach().

Langkah berikutnya adalah memigrasikan TermsAndConditionsFragment. Buka TermsAndConditionsFragment.kt, anotasikan class, dan hapus fungsi onAttach() seperti yang Anda lakukan di langkah sebelumnya. Kode akhir akan terlihat seperti ini.

TermsAndConditionsFragment.kt

@AndroidEntryPoint
class TermsAndConditionsFragment : Fragment() {

    @Inject
    lateinit var registrationViewModel: RegistrationViewModel

    //override fun onAttach(context: Context) {
    //    super.onAttach(context)
    //
    //    // Grabs the registrationComponent from the Activity and injects this Fragment
    //    (activity as RegistrationActivity).registrationComponent.inject(this)
    //}

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        val view = inflater.inflate(R.layout.fragment_terms_and_conditions, container, false)

        view.findViewById<Button>(R.id.next).setOnClickListener {
            registrationViewModel.acceptTCs()
            (activity as RegistrationActivity).onTermsAndConditionsAccepted()
        }

        return view
    }
}

Dengan perubahan ini, Anda telah memigrasikan semua aktivitas dan fragmen yang tercantum di RegistrationComponent sehingga Anda dapat menghapus RegistrationComponent.kt.

Setelah menghapus RegistrationComponent, Anda harus menghapus referensinya dari daftar subkomponen di AppSubcomponents.

AppSubcomponents.kt

@InstallIn(ApplicationComponent::class)
// This module tells a Component which are its subcomponents
@Module(
    subcomponents = [
        UserComponent::class
    ]
)
class AppSubcomponents

Ada satu hal lagi yang perlu dilakukan untuk menyelesaikan migrasi alur Pendaftaran. Alur Pendaftaran mendeklarasikan dan menggunakan cakupannya sendiri, yaitu ActivityScope. Cakupan mengontrol siklus proses dependensi. Dalam hal ini, ActivityScope memberi tahu Dagger untuk memasukkan instance RegistrationViewModel yang sama dalam alur yang dimulai dengan RegistrationActivity. Hilt menyediakan cakupan siklus proses bawaan untuk mendukung hal ini.

Buka RegistrationViewModel dan ubah anotasi @ActivityScope dengan @ActivityScoped yang disediakan Hilt.

RegistrationViewModel.kt

@ActivityScoped
class RegistrationViewModel @Inject constructor(val userManager: UserManager) {

    //...
}

Karena ActivityScope tidak digunakan di tempat lain, Anda dapat menghapus ActivityScope.kt dengan aman.

Sekarang jalankan aplikasi dan coba alur Pendaftaran. Anda dapat menggunakan nama pengguna dan sandi saat ini untuk login atau membatalkan pendaftaran dan mendaftar ulang dengan akun baru untuk memastikan bahwa alur tersebut berfungsi seperti biasanya.

Saat ini, Dagger dan Hilt berfungsi bersama dalam aplikasi. Hilt memasukkan semua dependensi kecuali untuk UserManager. Di bagian berikutnya, Anda akan bermigrasi sepenuhnya ke Hilt dari Dagger dengan memigrasikan UserManager.

Sejauh codelab ini, Anda telah berhasil memigrasikan sebagian besar aplikasi contoh ke Hilt kecuali satu komponen, yaitu UserComponent. UserComponent dianotasi dengan cakupan khusus, @LoggedUserScope. Artinya, UserComponent akan memasukkan instance UserManager yang sama ke class yang dianotasi dengan @LoggedUserScope.

UserComponent tidak memetakan ke komponen Hilt yang tersedia karena siklus prosesnya tidak dikelola oleh class Android. Karena penambahan komponen kustom di bagian tengah hierarki yang dihasilkan Hilt tidak didukung, Anda memiliki dua opsi:

  1. Biarkan Hilt dan Dagger bekerja secara berdampingan dalam status project saat ini.
  2. Migrasikan komponen tercakup ke komponen Hilt yang tersedia dan terdekat (dalam hal ini ApplicationComponent) dan gunakan nullability saat diperlukan.

Anda telah mencapai proses #1 di langkah sebelumnya. Pada langkah ini, Anda akan mengikuti proses #2 agar aplikasi sepenuhnya dimigrasikan ke Hilt. Namun, di aplikasi yang sebenarnya, Anda bebas memilih mana yang lebih sesuai untuk kasus penggunaan tertentu.

Pada langkah ini, UserComponent akan dimigrasikan untuk menjadi bagian dari ApplicationComponent Hilt. Jika ada modul dalam komponen tersebut, modul tersebut juga harus diinstal di ApplicationComponent.

Satu-satunya jenis tercakup di UserComponent adalah UserDataRepository - yang dianotasi dengan @LoggedUserScope. Karena UserComponent akan tergabung dengan ApplicationComponent Hilt, UserDataRepository akan dianotasi dengan @Singleton dan Anda perlu mengubah logika untuk menjadikannya null saat pengguna logout.

UserManager sudah dianotasi dengan @Singleton. Artinya, Anda dapat memberikan instance yang sama di seluruh aplikasi dan dengan beberapa perubahan, Anda dapat memperoleh fungsi yang sama dengan Hilt. Mari mulai dengan mengubah cara kerja UserManager dan UserDataRepository, karena Anda perlu melakukan dasar-dasarnya terlebih dahulu.

Buka UserManager.kt dan terapkan perubahan berikut.

  • Ganti parameter UserComponent.Factory dengan UserDataRepository di konstruktor karena Anda tidak perlu membuat instance UserComponent lagi. Parameter tersebut memiliki UserDataRepository sebagai dependensi
  • Karena Hilt akan menghasilkan kode komponen, hapus UserComponent dan penyetelnya.
  • Ubah fungsi isUserLoggedIn() untuk memeriksa nama pengguna dari userRepository, bukan dari userComponent.
  • Tambahkan nama pengguna sebagai parameter ke fungsi userJustLoggedIn().
  • Ubah isi fungsi userJustLoggedIn() untuk memanggil initData dengan userName pada userDataRepository. Sebagai ganti userComponent yang akan Anda hapus selama migrasi.
  • Tambahkan username ke panggilan userJustLoggedIn() di fungsi registerUser() dan loginUser().
  • Hapus userComponent dari fungsi logout() dan ganti dengan panggilan ke userDataRepository.cleanUp().

Saat selesai, kode akhir untuk UserManager.kt akan terlihat seperti ini.

UserManager.kt

@Singleton
class UserManager @Inject constructor(
    private val storage: Storage,
    // Since UserManager will be in charge of managing the UserComponent lifecycle,
    // it needs to know how to create instances of it
    private val userDataRepository: UserDataRepository
) {

    val username: String
        get() = storage.getString(REGISTERED_USER)

    fun isUserLoggedIn() = userDataRepository.username != null

    fun isUserRegistered() = storage.getString(REGISTERED_USER).isNotEmpty()

    fun registerUser(username: String, password: String) {
        storage.setString(REGISTERED_USER, username)
        storage.setString("$username$PASSWORD_SUFFIX", password)
        userJustLoggedIn(username)
    }

    fun loginUser(username: String, password: String): Boolean {
        val registeredUser = this.username
        if (registeredUser != username) return false

        val registeredPassword = storage.getString("$username$PASSWORD_SUFFIX")
        if (registeredPassword != password) return false

        userJustLoggedIn(username)
        return true
    }

    fun logout() {
        userDataRepository.cleanUp()
    }

    fun unregister() {
        val username = storage.getString(REGISTERED_USER)
        storage.setString(REGISTERED_USER, "")
        storage.setString("$username$PASSWORD_SUFFIX", "")
        logout()
    }

    private fun userJustLoggedIn(username: String) {
        // When the user logs in, we create populate data in UserComponent
        userDataRepository.initData(username)
    }
}

Sekarang, setelah menyelesaikan UserManager, Anda perlu melakukan beberapa perubahan di UserDataRepository. Buka UserDataRepository.kt dan terapkan perubahan berikut.

  • Hapus @LoggedUserScope karena dependensi ini akan dikelola oleh Hilt.
  • UserDataRepository sudah dimasukkan ke dalam UserManager. Untuk menghindari dependensi siklus, hapus parameter UserManager dari konstruktor UserDataRepository.
  • Ubah unreadNotifications menjadi nullable dan ubah penyetel menjadi pribadi.
  • Tambahkan variabel nullable baru, username, dan ubah penyetel menjadi pribadi.
  • Tambahkan fungsi baru initData() yang menetapkan username dan unreadNotifications ke angka acak.
  • Tambahkan fungsi baru cleanUp() untuk mereset jumlah username dan unreadNotifications. Tetapkan username ke null dan unreadNotifications ke -1.
  • Terakhir, pindahkan fungsi randomInt() ke dalam isi class.

Saat selesai, kode akhir akan terlihat seperti ini.

UserDataRepository.kt

@Singleton
class UserDataRepository @Inject constructor() {

    var username: String? = null
        private set

    var unreadNotifications: Int? = null
        private set

    init {
        unreadNotifications = randomInt()
    }

    fun refreshUnreadNotifications() {
        unreadNotifications = randomInt()
    }
    fun initData(username: String) {
        this.username = username
        unreadNotifications = randomInt()
    }

    fun cleanUp() {
        username = null
        unreadNotifications = -1
    }

    private fun randomInt(): Int {
        return Random.nextInt(until = 100)
    }
}

Untuk menyelesaikan migrasi UserComponent, buka UserComponent.kt dan scroll ke bawah ke metode inject(). Dependensi ini digunakan di MainActivity dan SettingsActivity. Mari mulai dengan memigrasikan MainActivity. Buka MainActivity.kt dan anotasikan class tersebut dengan @AndroidEntryPoint.

MainActivity.kt

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

    //...
}

Hapus antarmuka UserManagerEntryPoint dan kode terkait titik entri dari onCreate().

MainActivity.kt

//@InstallIn(ApplicationComponent::class)
//@EntryPoint
//interface UserManagerEntryPoint {
//    fun userManager(): UserManager
//}

override fun onCreate(savedInstanceState: Bundle?) {
    //val entryPoint = EntryPoints.get(applicationContext, UserManagerEntryPoint::class.java)
    //val userManager = entryPoint.userManager()
    super.onCreate(savedInstanceState)

    //...
}

Deklarasikan lateinit var untuk UserManager dan anotasikan dengan anotasi @Inject agar Hilt dapat memasukkan dependensi.

MainActivity.kt

@Inject
lateinit var userManager: UserManager

Karena UserManager akan dimasukkan oleh Hilt, hapus panggilan inject() di UserComponent.

MainActivity.kt

        //Remove
        //userManager.userComponent!!.inject(this)
        setupViews()
    }
}

Ini yang Anda perlukan untuk MainActivity. Sekarang, Anda dapat melakukan perubahan yang sama untuk memigrasikan SettingsActivity. Buka SettingsActivity dan anotasikan dengan @AndroidEntryPoint.

SettingsActivity.kt

@AndroidEntryPoint
class SettingsActivity : AppCompatActivity() {
    //...
}

Buat lateinit var untuk UserManager dan anotasikan dengan @Inject.

SettingsActivity.kt

    @Inject
    lateinit var userManager: UserManager

Hapus kode titik masuk dan panggilan injeksi pada userComponent(). Saat selesai, fungsi onCreate() akan terlihat seperti ini.

SettingsActivity.kt

    override fun onCreate(savedInstanceState: Bundle?) {

        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_settings)

        setupViews()
    }

Sekarang Anda dapat membersihkan resource yang tidak digunakan untuk menyelesaikan migrasi. Hapus class LoggedUserScope.kt, UserComponent.kt, dan AppSubcomponent.kt.

Sekarang jalankan dan coba aplikasi lagi. Aplikasi seharusnya berfungsi sama seperti jika digunakan dengan Dagger.

Ada satu langkah penting lagi yang perlu dilakukan sebelum menyelesaikan migrasi aplikasi ke Hilt. Sejauh ini Anda telah memigrasikan semua kode aplikasi, tetapi tidak dengan pengujiannya. Hilt memasukkan dependensi dalam pengujian seperti yang dilakukan di kode aplikasi. Pengujian dengan Hilt tidak memerlukan pemeliharaan karena Hilt otomatis menghasilkan serangkaian komponen baru untuk setiap pengujian.

Pengujian unit

Mari kita mulai dengan Pengujian unit. Anda tidak perlu menggunakan Hilt untuk pengujian unit karena Anda bisa langsung memanggil konstruktor class target yang meneruskan dependensi palsu atau tiruan seperti jika konstruktor tidak dianotasi.

Jika menjalankan pengujian unit, Anda akan melihat UserManagerTest gagal. Anda telah melakukan banyak proses dan perubahan di UserManager, termasuk parameter konstruktornya di bagian sebelumnya. Buka UserManagerTest.kt yang masih bergantung pada UserComponent dan UserComponentFactory. Karena Anda sudah mengubah parameter UserManager, ubah parameter UserComponent.Factory dengan instance UserDataRepository baru.

UserManagerTest.kt

    @Before
    fun setup() {
        storage = FakeStorage()
        userManager = UserManager(storage, UserDataRepository())
    }

Selesai. Jalankan pengujian lagi dan semua pengujian unit seharusnya berhasil.

Menambahkan Dependensi Pengujian

Sebelum melanjutkan, buka app/build.gradle dan pastikan dependensi Hilt berikut ada. Hilt menggunakan hilt-android-testing untuk anotasi khusus pengujian. Selain itu, karena Hilt perlu menghasilkan kode untuk class di folder androidTest, pemroses anotasinya juga harus dapat dijalankan di sana.

app/build.gradle

    // Hilt testing dependencies
    androidTestImplementation "com.google.dagger:hilt-android-testing:$hilt_version"
    kaptAndroidTest "com.google.dagger:hilt-android-compiler:$hilt_version"

Pengujian UI

Hilt otomatis menghasilkan komponen pengujian dan Aplikasi pengujian untuk setiap pengujian. Untuk memulai, buka TestAppComponent.kt untuk merencanakan migrasi. TestAppComponent memiliki 2 modul, yaitu TestStorageModule dan AppSubcomponents. Anda telah memigrasikan dan menghapus AppSubcomponents, jadi Anda dapat melanjutkan proses migrasi TestStorageModule.

Buka TestStorageModule.kt dan anotasikan class dengan anotasi @InstallIn.

TestStorageModule.kt

@InstallIn(ApplicationComponent::class)
@Module
abstract class TestStorageModule {
    //...

Karena Anda telah selesai memigrasikan semua modul, lanjutkan dan hapus TestAppComponent.

Selanjutnya, tambahkan Hilt ke ApplicationTest. Anda harus menganotasi setiap pengujian UI yang menggunakan Hilt dengan @HiltAndroidTest. Anotasi ini bertanggung jawab untuk menghasilkan komponen Hilt untuk setiap pengujian.

Buka ApplicationTest.kt dan tambahkan anotasi berikut:

  • @HiltAndroidTest untuk memberi tahu Hilt agar menghasilkan komponen untuk pengujian ini.
  • @UninstallModules(StorageModule::class) untuk memberi tahu Hilt agar meng-uninstal StorageModule yang dideklarasikan dalam kode aplikasi sehingga TestStorageModule akan dimasukkan selama pengujian.
  • Anda juga perlu menambahkan HiltAndroidRule ke ApplicationTest. Aturan pengujian ini mengelola status komponen dan digunakan untuk melakukan injeksi pada pengujian Anda. Kode akhir akan terlihat seperti ini.

ApplicationTest.kt

@UninstallModules(StorageModule::class)
@HiltAndroidTest
class ApplicationTest {

    @get:Rule
    var hiltRule = HiltAndroidRule(this)

    //...

Karena Hilt menghasilkan Application baru untuk setiap uji instrumentasi, kita perlu menentukan bahwa Application yang dihasilkan Hilt harus digunakan saat menjalankan pengujian UI. Untuk melakukannya, kita memerlukan runner pengujian kustom.

Aplikasi codelab sudah memiliki runner pengujian kustom. Buka MyCustomTestRunner.kt

Hilt sudah dilengkapi dengan Application yang dapat Anda gunakan untuk pengujian, yang bernama HiltTestApplication. Anda perlu mengubah MyTestApplication::class.java dengan HiltTestApplication::class.java di isi fungsi newApplication().

MyCustomTestRunner.kt

class MyCustomTestRunner : AndroidJUnitRunner() {

    override fun newApplication(cl: ClassLoader?, name: String?, context: Context?): Application {

        return super.newApplication(cl, HiltTestApplication::class.java.name, context)
    }
}

Dengan perubahan ini, sekarang Anda dapat menghapus file MyTestApplication.kt dengan aman. Sekarang lanjutkan dan jalankan pengujian. Semua pengujian seharusnya berhasil.

Hilt menyertakan ekstensi untuk menyediakan class dari library Jetpack lainnya, seperti WorkManager dan ViewModel. ViewModels dalam project codelab adalah class biasa yang tidak memperluas ViewModel dari Komponen Arsitektur. Sebelum menambahkan dukungan Hilt untuk ViewModels, migrasikan ViewModels di aplikasi ke ViewModels Komponen Arsitektur.

Untuk mengintegrasikan dengan ViewModel, Anda perlu menambahkan dependensi tambahan berikut ke file gradle. Dependensi ini sudah ditambahkan untuk Anda. Perlu diingat bahwa selain library, Anda perlu menambahkan pemroses anotasi tambahan yang berfungsi mendukung pemroses anotasi Hilt:

// app/build.gradle file

...
dependencies {
  ...
  implementation "androidx.fragment:fragment-ktx:1.2.4"
  implementation 'androidx.hilt:hilt-lifecycle-viewmodel:$hilt_jetpack_version'
  kapt 'androidx.hilt:hilt-compiler:$hilt_jetpack_version'
  kaptAndroidTest 'androidx.hilt:hilt-compiler:$hilt_jetpack_version'
}

Untuk memigrasikan class biasa ke ViewModel, Anda perlu memperluas ViewModel().

Buka MainViewModel.kt dan tambahkan : ViewModel(). Proses ini cukup untuk memigrasikan ViewModels Komponen Arsitektur, tetapi Anda juga perlu memberi tahu Hilt cara memberikan instance ViewModel. Untuk melakukannya, tambahkan anotasi @ViewModelInject di konstruktor ViewModel. Ganti anotasi @Inject dengan @ViewModelInject.

MainViewModel.kt

class MainViewModel @ViewModelInject constructor(
    private val userDataRepository: UserDataRepository
): ViewModel() {
//...
}

Selanjutnya, buka LoginViewModel dan lakukan perubahan yang sama. Kode akhir akan terlihat seperti ini.

LoginViewModel.kt

class LoginViewModel @ViewModelInject constructor(
    private val userManager: UserManager
): ViewModel() {
//...
}

Demikian pula, buka RegistrationViewModel.kt lalu migrasikan ke ViewModel() dan tambahkan anotasi Hilt. Anda tidak memerlukan anotasi @ActivityScoped karena dengan metode ekstensi viewModels() dan activityViewModels(), Anda dapat mengontrol cakupan ViewModel ini.

RegistrationViewModel.kt

class RegistrationViewModel @ViewModelInject constructor(
    val userManager: UserManager
) : ViewModel() {

Lakukan perubahan yang sama untuk memigrasikan EnterDetailsViewModel dan SettingViewModel. Kode akhir untuk kedua class ini akan terlihat seperti ini.

EnterDetailsViewModel.kt

class EnterDetailsViewModel @ViewModelInject constructor() : ViewModel() {

SettingViewModel.kt

class SettingsViewModel @ViewModelInject constructor(
     private val userDataRepository: UserDataRepository,
     private val userManager: UserManager
) : ViewModel() {

Setelah semua ViewModels dimigrasikan ke Viewmodels Komponen Arsitektur dan dianotasi dengan anotasi Hilt, Anda dapat memigrasikan cara injeksi.

Selanjutnya, Anda perlu mengubah cara ViewModels diinisialisasi di lapisan View. ViewModels dibuat oleh OS dan cara mendapatkannya adalah dengan menggunakan fungsi delegasi by viewModels().

Buka MainActivity.kt, ganti anotasi @Inject dengan ekstensi Jetpack. Perlu diingat bahwa Anda juga harus menghapus lateinit, mengubah var menjadi val, dan menandai kolom private.

MainActivity.kt

//    @Inject
//    lateinit var mainViewModel: MainViewModel
    private val mainViewModel: MainViewModel by viewModels()

Demikian pula, buka LoginActivity.kt dan ubah cara mendapatkan ViewModel.

LoginActivity.kt

//    @Inject
//    lateinit var loginViewModel: LoginViewModel
    private val loginViewModel: LoginViewModel by viewModels()

Selanjutnya, buka RegistrationActivity.kt dan terapkan perubahan serupa untuk mendapatkan registrationViewModel.

RegistrationActivity.kt

//    @Inject
//    lateinit var registrationViewModel: RegistrationViewModel
    private val registrationViewModel: RegistrationViewModel by viewModels()

Buka EnterDetailsFragment.kt. Ganti cara mendapatkan EnterDetailsViewModel.

EnterDetailsFragment.kt

    private val enterDetailsViewModel: EnterDetailsViewModel by viewModels()

Demikian pula, ganti cara mendapatkan registrationViewModel, tetapi kali ini gunakan fungsi delegasi activityViewModels(), bukan viewModels().. Saat registrationViewModel dimasukkan, Hilt akan memasukkan ViewModel cakupan tingkat aktivitas.

EnterDetailsFragment.kt

    private val registrationViewModel: RegistrationViewModel by activityViewModels()

Buka TermsAndConditionsFragment.kt dan sekali lagi gunakan fungsi ekstensi activityViewModels(), bukan viewModels(), untuk mendapatkan registrationViewModel.

TermsAndConditionsFragment.kt

    private val registrationViewModel: RegistrationViewModel by activityViewModels()

Terakhir, buka SettingsActivity.kt dan migrasikan cara settingsViewModel diperoleh.

SettingsActivity.kt

    private val settingsViewModel: SettingsViewModel by viewModels()

Sekarang, jalankan aplikasi dan pastikan aplikasi berfungsi seperti yang diharapkan.

Selamat! Anda berhasil memigrasikan aplikasi untuk menggunakan Hilt. Tidak hanya menyelesaikan migrasi, Anda juga berhasil menjaga aplikasi tetap berfungsi saat memigrasikan komponen Dagger satu per satu.

Dalam codelab ini, Anda telah mempelajari cara memulai komponen Aplikasi dan membuat dasar yang diperlukan agar Hilt berfungsi dengan komponen Dagger yang ada. Dari situ, Anda telah memigrasikan setiap Komponen Dagger ke Hilt dengan menggunakan Anotasi Hilt pada Aktivitas dan Fragmen dan menghapus kode terkait Dagger. Setiap kali Anda menyelesaikan migrasi suatu komponen, aplikasi bekerja dan berfungsi seperti yang diharapkan. Anda juga memigrasikan dependensi Context dan ApplicationContext dengan anotasi @ActivityContext dan @ApplicationContext yang disediakan Hilt. Anda telah memigrasikan komponen Android lainnya. Terakhir, Anda berhasil memigrasikan pengujian dan menyelesaikan migrasi ke Hilt.

Bacaan lebih lanjut

Untuk mempelajari cara memigrasikan aplikasi ke Hilt lebih lanjut, lihat dokumentasi Migrasi ke Hilt. Selain informasi lebih lanjut tentang migrasi Dagger ke Hilt, Anda juga dapat melihat informasi tentang migrasi aplikasi dagger.android.