Masa depan injeksi ketergantungan di Android

Saya membawa perhatian Anda pada terjemahan dari artikel asli oleh Jamie Sanson
gambar


Membuat Aktivitas sebelum Android 9 Pie


Dependency injection (DI) adalah model umum yang digunakan dalam semua bentuk pengembangan karena sejumlah alasan. Berkat proyek Dagger, ini diambil sebagai templat yang digunakan dalam pengembangan untuk Android. Perubahan terbaru dalam Android 9 Pie telah membuat kami sekarang memiliki lebih banyak opsi ketika datang ke DI, terutama dengan kelas AppComponentFactory baru.




DI sangat penting dalam pengembangan Android modern. Ini memungkinkan Anda untuk mengurangi jumlah total kode ketika mendapatkan tautan ke layanan yang digunakan antar kelas, dan umumnya membagi aplikasi menjadi komponen. Pada artikel ini, kita akan fokus pada Dagger 2, perpustakaan DI paling umum yang digunakan dalam pengembangan Android. Diasumsikan bahwa Anda sudah memiliki pengetahuan dasar tentang cara kerjanya, tetapi tidak perlu memahami semua seluk-beluknya. Perlu dicatat bahwa artikel ini sedikit petualangan. Ini menarik dan semuanya, tetapi pada saat penulisan, Android 9 Pie bahkan tidak muncul di panel versi platform , jadi topik ini mungkin tidak akan relevan dengan pengembangan sehari-hari setidaknya selama beberapa tahun.


Ketergantungan injeksi di Android hari ini


Sederhananya, kami menggunakan DI untuk memberikan contoh kelas "ketergantungan" untuk kelas dependen kami, yaitu mereka yang melakukan pekerjaan. Katakanlah kita menggunakan pola Repositori untuk memproses logika terkait data kami, dan ingin menggunakan repositori kami di Aktivitas untuk menampilkan beberapa data kepada pengguna. Kami mungkin ingin menggunakan repositori yang sama di beberapa tempat, jadi kami menggunakan injeksi dependensi untuk membuatnya lebih mudah untuk berbagi contoh yang sama antara sekelompok kelas yang berbeda.


Pertama, kami akan menyediakan repositori. Kami akan mendefinisikan fungsi Provides dalam modul, memungkinkan Dagger tahu bahwa ini adalah contoh yang ingin kami terapkan. Harap dicatat bahwa repositori kami memerlukan instance konteks untuk bekerja dengan file dan jaringan. Kami akan menyediakannya dengan konteks aplikasi.


 @Module class AppModule(val appContext: Context) { @Provides @ApplicationScope fun provideApplicationContext(): Context = appContext @Provides @ApplicationScope fun provideRepository(context: Context): Repository = Repository(context) } 

Sekarang kita perlu mendefinisikan Component untuk menangani implementasi kelas-kelas di mana kita ingin menggunakan Repository kita.


 @ApplicationScope @Component(modules = [AppModule::class]) interface ApplicationComponent { fun inject(activity: MainActivity) } 

Akhirnya, kita dapat mengonfigurasi Activity kami untuk menggunakan repositori kami. Misalkan kita membuat instance ApplicationComponent kita di tempat lain.


 class MainActivity: AppCompatActivity() { @Inject lateinit var repository: Repository override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) //    application.applicationComponent.inject(this) //       } } 

Itu saja! Kami baru saja menyiapkan injeksi ketergantungan dalam aplikasi menggunakan Dagger. Ada beberapa cara untuk melakukan ini, tetapi ini sepertinya pendekatan yang paling mudah.


Apa yang salah dengan pendekatan saat ini?


Dalam contoh di atas, kami melihat dua jenis suntikan, satu lebih jelas daripada yang lain.


Yang pertama yang Anda lewatkan dikenal sebagai menanamkan dalam konstruktor . Ini adalah metode menyediakan dependensi melalui konstruktor suatu kelas, yang berarti bahwa kelas yang menggunakan dependensi tidak memiliki gagasan tentang asal-usul instance. Ini dianggap sebagai bentuk injeksi ketergantungan yang paling murni, karena ini merangkum logika injeksi kami ke dalam kelas Module kami dengan sempurna. Dalam contoh kami, kami menggunakan pendekatan ini untuk menyediakan repositori:


 fun provideRepository(context: Context): Repository = Repository(context) 

Untuk ini kami membutuhkan Context , yang kami sediakan di fungsi provideApplicationContext() .


Hal kedua, yang lebih jelas, yang kami lihat adalah implementasi kelas di lapangan . Metode ini digunakan di MainActivity kami untuk menyediakan toko kami. Di sini kita mendefinisikan bidang sebagai penerima injeksi menggunakan anotasi Inject . Kemudian, dalam fungsi onCreate kami onCreate kami memberi tahu ApplicationComponent bahwa dependensi harus disuntikkan ke bidang kami. Itu tidak terlihat sebersih menanamkan dalam konstruktor, karena kami memiliki referensi eksplisit untuk komponen kami, yang berarti bahwa konsep embedding merembes ke dalam kelas dependen kami. Kelemahan lain dalam kelas Kerangka Android, karena kita perlu memastikan bahwa hal pertama yang kita lakukan adalah menyediakan dependensi. Jika ini terjadi pada titik yang salah dalam siklus hidup, kita mungkin secara tidak sengaja mencoba menggunakan objek yang belum diinisialisasi.


Idealnya, Anda harus benar-benar menyingkirkan implementasi di bidang kelas. Pendekatan ini melewatkan informasi implementasi untuk kelas yang tidak perlu mengetahuinya, dan berpotensi menyebabkan masalah siklus hidup. Kami melihat upaya untuk melakukannya dengan lebih baik, dan Belati di Android adalah cara yang cukup andal, tetapi pada akhirnya akan lebih baik jika kita bisa menggunakan penyematan pada konstruktor. Saat ini, kami tidak dapat menggunakan pendekatan ini untuk sejumlah kelas kerangka kerja, seperti "Aktivitas", "Layanan", "Aplikasi", dll., Karena dibuat untuk kita oleh sistem. Tampaknya saat ini kami terjebak dalam memperkenalkan kelas ke bidang. Namun demikian, Android 9 Pie sedang mempersiapkan sesuatu yang menarik, yang, mungkin, secara fundamental akan mengubah segalanya.


Ketergantungan injeksi di Android 9 Pie


Seperti yang disebutkan di awal artikel, Android 9 Pie memiliki kelas AppComponentFactory. Dokumentasi untuk itu agak langka, dan hanya diposting di situs web pengembang seperti:


Antarmuka yang digunakan untuk mengontrol penciptaan elemen manifes.

Ini menarik. โ€œElemen manifesโ€ di sini merujuk pada kelas yang kami daftarkan dalam file AndroidManifest kami - seperti Activity, Layanan, dan kelas Aplikasi kami. Ini memungkinkan kita untuk "mengontrol penciptaan" elemen-elemen ini ... jadi, hei, dapatkah kita sekarang menetapkan aturan untuk membuat Kegiatan kita? Sungguh menyenangkan!


Mari menggali lebih dalam. Kami akan mulai dengan memperluas AppComponentFactory dan mengganti metode instantiateActivity .


 class InjectionComponentFactory: AppComponentFactory() { private val repository = NonContextRepository() override fun instantiateActivity(cl: ClassLoader, className: String, intent: Intent?): Activity { return when { className == MainActivity::class.java.name -> MainActivity(repository) else -> super.instantiateActivity(cl, className, intent) } } } 

Sekarang kita perlu mendeklarasikan pabrik komponen kita di manifes di dalam tag aplikasi .


 <application android:allowBackup="true" android:label="@string/app_name" android:icon="@mipmap/ic_launcher" android:name=".InjectionApp" android:appComponentFactory="com.mypackage.injectiontest.component.InjectionComponentFactory" android:theme="@style/AppTheme" tools:replace="android:appComponentFactory"> 

Akhirnya, kita dapat meluncurkan aplikasi kita ... dan berfungsi! NonContextRepository kami disediakan melalui konstruktor MainActivity. Dengan anggun!


Harap dicatat bahwa ada beberapa pemesanan. Kami tidak dapat menggunakan Context sini, karena bahkan sebelum keberadaannya, panggilan ke fungsi kami terjadi - ini membingungkan! Kita dapat melangkah lebih jauh sehingga konstruktor mengimplementasikan kelas Aplikasi kita, tetapi mari kita lihat bagaimana Belati dapat membuat ini lebih mudah.


Bertemu - Belati Multi-Bind


Saya tidak akan membahas detail operasi penjilidan belati Dagger di bawah kap, karena ini berada di luar cakupan artikel ini. Yang perlu Anda ketahui adalah bahwa ia menyediakan cara yang baik untuk menyuntikkan ke konstruktor kelas tanpa harus memanggil konstruktor secara manual. Kita bisa menggunakan ini untuk dengan mudah mengimplementasikan kelas kerangka kerja dengan cara yang dapat diskalakan. Mari kita lihat bagaimana semuanya bertambah.


Mari kita atur Aktivitas kita terlebih dahulu untuk mencari tahu ke mana harus pergi berikutnya.


 class MainActivity @Inject constructor( private val repository: NonContextRepository ): Activity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) //       } } 

Ini segera menunjukkan bahwa hampir tidak ada penyebutan ketergantungan. Satu-satunya hal yang kita lihat adalah anotasi Inject sebelum konstruktor.


Sekarang Anda perlu mengubah komponen dan modul Belati:


 @Component(modules = [ApplicationModule::class]) interface ApplicationComponent { fun inject(factory: InjectionComponentFactory) } 

 @Module(includes = [ComponentModule::class]) class ApplicationModule { @Provides fun provideRepository(): NonContextRepository = NonContextRepository() } 

Tidak banyak yang berubah. Sekarang kita hanya perlu menerapkan pabrik komponen kita, tetapi bagaimana kita membuat elemen manifes kita? Di sini kita membutuhkan ComponentModule . Mari kita lihat:


 @Module abstract class ComponentModule { @Binds @IntoMap @ComponentKey(MainActivity::class) abstract fun bindMainActivity(activity: MainActivity): Any @Binds abstract fun bindComponentHelper(componentHelper: ComponentHelper): ComponentInstanceHelper } @Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER) @Retention(AnnotationRetention.RUNTIME) @MapKey internal annotation class ComponentKey(val clazz: KClass<out Any>) 

Ya, hanya beberapa penjelasan. Di sini kita menghubungkan Activity kita dengan peta, mengimplementasikan peta ini di kelas ComponentHelper kita dan menyediakan ComponentHelper ini - semuanya dalam dua instruksi Binds . Belati tahu cara membuat Instansiasi MainActivity kami berkat anotasi MainActivity Inject sehingga dapat "mengikat" penyedia ke kelas ini, secara otomatis menyediakan dependensi yang kami butuhkan untuk konstruktor. ComponentHelper kami ComponentHelper sebagai berikut.


 class ComponentHelper @Inject constructor( private val creators: Map<Class<out Any>, @JvmSuppressWildcards Provider<Any>> ): ComponentInstanceHelper { @Suppress("UNCHECKED_CAST") override fun <T> resolve(className: String): T? = creators .filter { it.key.name == className } .values .firstOrNull() ?.get() as? T } interface InstanceComponentHelper { fun <T> resolve(className: String): T? } 

Sederhananya, kami sekarang memiliki peta kelas untuk pemasok untuk kelas-kelas ini. Ketika kami mencoba menyelesaikan suatu kelas dengan nama, kami cukup menemukan penyedia untuk kelas ini (jika kami memilikinya), memanggilnya untuk mendapatkan instance baru dari kelas ini, dan mengembalikannya.


Akhirnya, kita perlu membuat perubahan pada AppComponentFactory untuk menggunakan kelas pembantu baru kita.


 class InjectionComponentFactory: AppComponentFactory() { @Inject lateinit var componentHelper: ComponentInstanceHelper init { DaggerApplicationComponent.create().inject(this) } override fun instantiateActivity(cl: ClassLoader, className: String, intent: Intent?): Activity { return componentHelper .resolve<Activity>(className) ?.apply { setIntent(intent) } ?: super.instantiateActivity(cl, className, intent) } } 

Jalankan kode lagi. Semuanya bekerja! Menyenangkan sekali.


Masalah Implementasi Konstruktor


Judul seperti itu mungkin tidak terlihat sangat mengesankan. Meskipun kami dapat menanamkan sebagian besar contoh dalam mode normal dengan menyuntikkannya ke konstruktor, kami tidak memiliki cara yang jelas untuk menyediakan konteks untuk dependensi kami dengan cara standar. Tapi Context di Android adalah segalanya. Diperlukan untuk akses ke pengaturan, jaringan, konfigurasi aplikasi dan banyak lagi. Ketergantungan kami sering kali adalah hal-hal yang menggunakan layanan terkait data, seperti jaringan dan pengaturan. Kita dapat menyiasatinya dengan menulis ulang dependensi kita menjadi fungsi murni, atau dengan menginisialisasi segala sesuatu dengan instance konteks dalam kelas Application kita, tetapi dibutuhkan lebih banyak pekerjaan untuk menentukan cara terbaik untuk melakukan ini.


Kerugian lain dari pendekatan ini adalah definisi ruang lingkup. Dalam Dagger, salah satu konsep kunci untuk mengimplementasikan injeksi dependensi berkinerja tinggi dengan pemisahan hubungan kelas yang baik adalah modularitas objek grafik dan penggunaan ruang lingkup. Meskipun pendekatan ini tidak melarang penggunaan modul, itu membatasi penggunaan ruang lingkup. AppComponentFactory ada pada tingkat abstraksi yang sama sekali berbeda relatif terhadap kelas kerangka kerja standar kami - kami tidak dapat memperoleh tautan ke sana secara terprogram, jadi kami tidak memiliki cara untuk menginstruksinya untuk menyediakan dependensi untuk Activity dalam lingkup yang berbeda.


Ada banyak cara untuk menyelesaikan masalah kami dengan lingkup dalam praktik, salah satunya adalah dengan menggunakan FragmentFactory untuk menanamkan fragmen kami dalam konstruktor dengan cakupan. Saya tidak akan merinci, tetapi ternyata sekarang kita memiliki metode untuk mengontrol pembuatan fragmen, yang tidak hanya memberi kita kebebasan yang jauh lebih besar dalam hal ruang lingkup, tetapi juga memiliki kompatibilitas ke belakang.


Kesimpulan


Android 9 Pie memperkenalkan cara untuk menggunakan penyematan dalam konstruktor untuk menyediakan dependensi di kelas kerangka kerja kami, seperti "Aktivitas" dan "Aplikasi". Kami melihat bahwa dengan Dagger Multi-binding, kami dapat dengan mudah memberikan dependensi pada level aplikasi.


Konstruktor yang mengimplementasikan semua komponen kami sangat menarik, dan kami bahkan dapat melakukan sesuatu untuk membuatnya berfungsi dengan baik dengan instance konteks. Ini adalah masa depan yang menjanjikan, tetapi hanya tersedia mulai dengan API 28. Jika Anda ingin menjangkau kurang dari 0,5% pengguna, Anda dapat mencobanya. Jika tidak, Anda harus menunggu dan melihat apakah metode seperti itu tetap relevan dalam beberapa tahun.

Source: https://habr.com/ru/post/id444530/


All Articles