Arsitektur aplikasi yang direkomendasikan Android mendorong pembagian kode ke dalam class untuk mendapatkan manfaat dari pemisahan fokus, prinsip yang mana setiap class hierarki memiliki satu tanggung jawab yang sudah ditentukan. Hal ini mengarah ke lebih banyak class yang lebih kecil yang perlu dihubungkan bersama untuk memenuhi dependensi masing-masing.
Dependensi di antara class dapat direpresentasikan sebagai grafik, dengan setiap
class terhubung ke class tempatnya bergantung. Grafik aplikasi
merupakan hasil representasi dari semua class dan dependensinya.
Pada gambar 1, Anda dapat melihat abstraksi grafik aplikasi.
Ketika class A (ViewModel
) bergantung pada kelas B (Repository
), ada
garis yang menunjuk dari A ke B yang merepresentasikan dependensi tersebut.
Injeksi dependensi tersebut membantu membuat hubungan ini dan memungkinkan Anda menukar
implementasi untuk pengujian. Misalnya, saat menguji ViewModel
yang bergantung pada repositori, Anda dapat meneruskan implementasi
Repository
yang berbeda menggunakan implementasi tiruan untuk menguji berbagai kasus.
Dasar-dasar injeksi dependensi manual
Bagian ini membahas cara menerapkan injeksi dependensi manual dalam skenario aplikasi Android yang sebenarnya. Bagian ini juga membahas pendekatan berulang tentang cara mulai menggunakan injeksi dependensi di aplikasi Anda. Pendekatan ini meningkat hingga mencapai titik yang sangat mirip dengan yang akan dihasilkan oleh Dagger secara otomatis. Untuk mengetahui informasi selengkapnya tentang Dagger, baca Dasar-dasar Dagger.
Pertimbangkan alur sebagai sekumpulan layar di aplikasi Anda yang berkaitan dengan fitur. Login, pendaftaran, dan pembayaran merupakan contoh alur.
Saat membahas alur login untuk aplikasi Android standar, LoginActivity
bergantung pada LoginViewModel
, yang kemudian bergantung pada UserRepository
.
Lalu UserRepository
bergantung pada UserLocalDataSource
dan
UserRemoteDataSource
, yang kemudian bergantung pada Retrofit
layanan.
LoginActivity
adalah titik entri ke alur login dan pengguna
yang berinteraksi dengan aktivitas. Dengan demikian, LoginActivity
perlu membuat
LoginViewModel
dengan semua dependensinya.
Class Repository
dan DataSource
dari alur akan terlihat seperti ini:
Kotlin
class UserRepository( private val localDataSource: UserLocalDataSource, private val remoteDataSource: UserRemoteDataSource ) { ... } class UserLocalDataSource { ... } class UserRemoteDataSource( private val loginService: LoginRetrofitService ) { ... }
Java
class UserLocalDataSource { public UserLocalDataSource() { } ... } class UserRemoteDataSource { private final Retrofit retrofit; public UserRemoteDataSource(Retrofit retrofit) { this.retrofit = retrofit; } ... } class UserRepository { private final UserLocalDataSource userLocalDataSource; private final UserRemoteDataSource userRemoteDataSource; public UserRepository(UserLocalDataSource userLocalDataSource, UserRemoteDataSource userRemoteDataSource) { this.userLocalDataSource = userLocalDataSource; this.userRemoteDataSource = userRemoteDataSource; } ... }
Berikut tampilan LoginActivity
:
Kotlin
class LoginActivity: Activity() { private lateinit var loginViewModel: LoginViewModel override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // In order to satisfy the dependencies of LoginViewModel, you have to also // satisfy the dependencies of all of its dependencies recursively. // First, create retrofit which is the dependency of UserRemoteDataSource val retrofit = Retrofit.Builder() .baseUrl("https://example.com") .build() .create(LoginService::class.java) // Then, satisfy the dependencies of UserRepository val remoteDataSource = UserRemoteDataSource(retrofit) val localDataSource = UserLocalDataSource() // Now you can create an instance of UserRepository that LoginViewModel needs val userRepository = UserRepository(localDataSource, remoteDataSource) // Lastly, create an instance of LoginViewModel with userRepository loginViewModel = LoginViewModel(userRepository) } }
Java
public class MainActivity extends Activity { private LoginViewModel loginViewModel; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // In order to satisfy the dependencies of LoginViewModel, you have to also // satisfy the dependencies of all of its dependencies recursively. // First, create retrofit which is the dependency of UserRemoteDataSource Retrofit retrofit = new Retrofit.Builder() .baseUrl("https://example.com") .build() .create(LoginService.class); // Then, satisfy the dependencies of UserRepository UserRemoteDataSource remoteDataSource = new UserRemoteDataSource(retrofit); UserLocalDataSource localDataSource = new UserLocalDataSource(); // Now you can create an instance of UserRepository that LoginViewModel needs UserRepository userRepository = new UserRepository(localDataSource, remoteDataSource); // Lastly, create an instance of LoginViewModel with userRepository loginViewModel = new LoginViewModel(userRepository); } }
Ada masalah dengan pendekatan ini:
Ada banyak kode boilerplate. Jika ingin membuat instance
LoginViewModel
lain di bagian kode yang lain, Anda akan memiliki duplikasi kode.Dependensi harus dideklarasikan secara berurutan. Anda harus membuat instance
UserRepository
sebelumLoginViewModel
agar dapat membuatnya.Sulit untuk menggunakan objek kembali. Jika ingin menggunakan kembali
UserRepository
di beberapa fitur, Anda harus membuatnya mengikuti pola singleton. Pola singleton membuat pengujian lebih sulit karena semua pengujian memiliki instance singleton yang sama.
Mengelola dependensi dengan container
Untuk mengatasi masalah penggunaan kembali objek, Anda dapat membuat class
container dependensi sendiri yang digunakan untuk mendapatkan dependensi. Semua instance
yang disediakan oleh container ini dapat bersifat publik. Dalam contoh ini, karena Anda hanya memerlukan
instance UserRepository
, Anda dapat membuat dependensinya bersifat pribadi dengan
opsi untuk menjadikannya publik jika dependensi perlu disediakan:
Kotlin
// Container of objects shared across the whole app class AppContainer { // Since you want to expose userRepository out of the container, you need to satisfy // its dependencies as you did before private val retrofit = Retrofit.Builder() .baseUrl("https://example.com") .build() .create(LoginService::class.java) private val remoteDataSource = UserRemoteDataSource(retrofit) private val localDataSource = UserLocalDataSource() // userRepository is not private; it'll be exposed val userRepository = UserRepository(localDataSource, remoteDataSource) }
Java
// Container of objects shared across the whole app public class AppContainer { // Since you want to expose userRepository out of the container, you need to satisfy // its dependencies as you did before private Retrofit retrofit = new Retrofit.Builder() .baseUrl("https://example.com") .build() .create(LoginService.class); private UserRemoteDataSource remoteDataSource = new UserRemoteDataSource(retrofit); private UserLocalDataSource localDataSource = new UserLocalDataSource(); // userRepository is not private; it'll be exposed public UserRepository userRepository = new UserRepository(localDataSource, remoteDataSource); }
Karena dependensi ini digunakan di seluruh aplikasi, keduanya harus
ditempatkan di tempat umum sehingga dapat digunakan oleh semua aktivitas:
class Application
. Buat class
Application
kustom yang berisi instance AppContainer
.
Kotlin
// Custom Application class that needs to be specified // in the AndroidManifest.xml file class MyApplication : Application() { // Instance of AppContainer that will be used by all the Activities of the app val appContainer = AppContainer() }
Java
// Custom Application class that needs to be specified // in the AndroidManifest.xml file public class MyApplication extends Application { // Instance of AppContainer that will be used by all the Activities of the app public AppContainer appContainer = new AppContainer(); }
Sekarang Anda bisa mendapatkan instance AppContainer
dari aplikasi dan
mendapatkan instance UserRepository
yang dibagikan:
Kotlin
class LoginActivity: Activity() { private lateinit var loginViewModel: LoginViewModel override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // Gets userRepository from the instance of AppContainer in Application val appContainer = (application as MyApplication).appContainer loginViewModel = LoginViewModel(appContainer.userRepository) } }
Java
public class MainActivity extends Activity { private LoginViewModel loginViewModel; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // Gets userRepository from the instance of AppContainer in Application AppContainer appContainer = ((MyApplication) getApplication()).appContainer; loginViewModel = new LoginViewModel(appContainer.userRepository); } }
Dengan cara ini, Anda tidak memiliki UserRepository
singleton. Sebaliknya, Anda memiliki file
AppContainer
yang dibagikan di seluruh aktivitas yang berisi objek dari grafik
dan membuat instance objek tersebut sehingga dapat dikonsumsi class lain.
Jika LoginViewModel
diperlukan di lebih banyak tempat dalam aplikasi, Anda perlu memiliki
tempat terpusat untuk membuat instance
LoginViewModel
. Anda dapat memindahkan pembuatan LoginViewModel
ke container dan menyediakan
objek baru dari jenis tersebut dengan factory. Kode untuk LoginViewModelFactory
akan terlihat seperti ini:
Kotlin
// Definition of a Factory interface with a function to create objects of a type interface Factory<T> { fun create(): T } // Factory for LoginViewModel. // Since LoginViewModel depends on UserRepository, in order to create instances of // LoginViewModel, you need an instance of UserRepository that you pass as a parameter. class LoginViewModelFactory(private val userRepository: UserRepository) : Factory{ override fun create(): LoginViewModel { return LoginViewModel(userRepository) } }
Java
// Definition of a Factory interface with a function to create objects of a type public interface Factory<T> { T create(); } // Factory for LoginViewModel. // Since LoginViewModel depends on UserRepository, in order to create instances of // LoginViewModel, you need an instance of UserRepository that you pass as a parameter. class LoginViewModelFactory implements Factory{ private final UserRepository userRepository; public LoginViewModelFactory(UserRepository userRepository) { this.userRepository = userRepository; } @Override public LoginViewModel create() { return new LoginViewModel(userRepository); } }
Anda dapat menyertakan LoginViewModelFactory
di AppContainer
dan membuat
LoginActivity
menggunakannya:
Kotlin
// AppContainer can now provide instances of LoginViewModel with LoginViewModelFactory class AppContainer { ... val userRepository = UserRepository(localDataSource, remoteDataSource) val loginViewModelFactory = LoginViewModelFactory(userRepository) } class LoginActivity: Activity() { private lateinit var loginViewModel: LoginViewModel override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // Gets LoginViewModelFactory from the application instance of AppContainer // to create a new LoginViewModel instance val appContainer = (application as MyApplication).appContainer loginViewModel = appContainer.loginViewModelFactory.create() } }
Java
// AppContainer can now provide instances of LoginViewModel with LoginViewModelFactory public class AppContainer { ... public UserRepository userRepository = new UserRepository(localDataSource, remoteDataSource); public LoginViewModelFactory loginViewModelFactory = new LoginViewModelFactory(userRepository); } public class MainActivity extends Activity { private LoginViewModel loginViewModel; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // Gets LoginViewModelFactory from the application instance of AppContainer // to create a new LoginViewModel instance AppContainer appContainer = ((MyApplication) getApplication()).appContainer; loginViewModel = appContainer.loginViewModelFactory.create(); } }
Pendekatan ini lebih baik daripada yang sebelumnya, tetapi masih ada beberapa tantangan yang perlu dipertimbangkan:
Anda harus mengelola
AppContainer
sendiri, membuat instance untuk semua dependensi secara manual.Masih ada banyak kode boilerplate. Anda perlu membuat factory atau parameter secara manual bergantung pada apakah Anda ingin menggunakan kembali sebuah objek atau tidak.
Mengelola dependensi dalam alur aplikasi
AppContainer
menjadi rumit saat Anda ingin menyertakan lebih banyak fungsionalitas dalam
project. Saat aplikasi Anda menjadi lebih besar dan Anda mulai memperkenalkan alur fitur
yang berbeda, akan ada lebih banyak masalah yang muncul:
Jika memiliki alur yang berbeda, Anda mungkin ingin objek hanya ditampilkan dalam cakupan alur tersebut. Misalnya, saat membuat
LoginUserData
(yang mungkin terdiri dari nama pengguna dan sandi yang hanya digunakan dalam alur login), Anda tidak ingin mempertahankan data dari alur login lama dari pengguna lain. Anda ingin instance baru untuk setiap alur baru. Anda dapat mencapainya dengan membuat objekFlowContainer
di dalamAppContainer
seperti yang ditunjukkan dalam contoh kode berikutnya.Mengoptimalkan grafik aplikasi dan penampung alur juga bisa menjadi sulit. Jangan lupa menghapus instance yang tidak diperlukan, bergantung pada alur tempat Anda berada.
Bayangkan Anda memiliki alur login yang terdiri dari satu aktivitas (LoginActivity
)
dan beberapa fragmen (LoginUsernameFragment
dan LoginPasswordFragment
).
Tampilan ini ingin:
Mengakses instance
LoginUserData
yang sama yang perlu dibagikan hingga alur login selesai.Membuat instance
LoginUserData
baru saat alur dimulai lagi.
Anda dapat melakukannya dengan container alur login. Container ini perlu dibuat saat alur login dimulai dan dihapus dari memori saat alur berakhir.
Tambahkan LoginContainer
ke kode contoh. Anda ingin dapat membuat
beberapa instance LoginContainer
di aplikasi. Jadi, daripada membuatnya menjadi
singleton, buat instance tersebut menjadi class dengan dependensi yang diperlukan alur login dari
AppContainer
.
Kotlin
class LoginContainer(val userRepository: UserRepository) { val loginData = LoginUserData() val loginViewModelFactory = LoginViewModelFactory(userRepository) } // AppContainer contains LoginContainer now class AppContainer { ... val userRepository = UserRepository(localDataSource, remoteDataSource) // LoginContainer will be null when the user is NOT in the login flow var loginContainer: LoginContainer? = null }
Java
// Container with Login-specific dependencies class LoginContainer { private final UserRepository userRepository; public LoginContainer(UserRepository userRepository) { this.userRepository = userRepository; loginViewModelFactory = new LoginViewModelFactory(userRepository); } public LoginUserData loginData = new LoginUserData(); public LoginViewModelFactory loginViewModelFactory; } // AppContainer contains LoginContainer now public class AppContainer { ... public UserRepository userRepository = new UserRepository(localDataSource, remoteDataSource); // LoginContainer will be null when the user is NOT in the login flow public LoginContainer loginContainer; }
Setelah memiliki penampung khusus untuk alur, Anda harus memutuskan kapan harus membuat
dan menghapus instance penampung. Karena alur login Anda dimuat secara mandiri dalam satu aktivitas (LoginActivity
), aktivitas tersebutlah yang mengelola siklus proses
penampung itu. LoginActivity
dapat membuat instance di onCreate()
dan
menghapusnya di onDestroy()
.
Kotlin
class LoginActivity: Activity() { private lateinit var loginViewModel: LoginViewModel private lateinit var loginData: LoginUserData private lateinit var appContainer: AppContainer override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) appContainer = (application as MyApplication).appContainer // Login flow has started. Populate loginContainer in AppContainer appContainer.loginContainer = LoginContainer(appContainer.userRepository) loginViewModel = appContainer.loginContainer.loginViewModelFactory.create() loginData = appContainer.loginContainer.loginData } override fun onDestroy() { // Login flow is finishing // Removing the instance of loginContainer in the AppContainer appContainer.loginContainer = null super.onDestroy() } }
Java
public class LoginActivity extends Activity { private LoginViewModel loginViewModel; private LoginData loginData; private AppContainer appContainer; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); appContainer = ((MyApplication) getApplication()).appContainer; // Login flow has started. Populate loginContainer in AppContainer appContainer.loginContainer = new LoginContainer(appContainer.userRepository); loginViewModel = appContainer.loginContainer.loginViewModelFactory.create(); loginData = appContainer.loginContainer.loginData; } @Override protected void onDestroy() { // Login flow is finishing // Removing the instance of loginContainer in the AppContainer appContainer.loginContainer = null; super.onDestroy(); } }
Seperti LoginActivity
, fragmen login dapat mengakses LoginContainer
dari
AppContainer
dan menggunakan instance LoginUserData
bersama.
Karena dalam kasus ini Anda menangani logika siklus proses tampilan, Anda dapat menggunakan pengamatan siklus proses.
Kesimpulan
Injeksi dependensi merupakan teknik yang bagus untuk membuat aplikasi Android yang dapat diskalakan dan dapat diuji. Gunakan penampung sebagai cara untuk berbagi instance class di berbagai bagian aplikasi Anda dan sebagai pusat untuk membuat instance class menggunakan factory.
Saat aplikasi Anda semakin besar, Anda akan mulai melihat bahwa Anda menulis banyak kode boilerplate (seperti factory), yang dapat menyebabkannya rentan error. Anda juga harus mengelola sendiri cakupan dan siklus proses container, mengoptimalkan dan menghapus container yang tidak diperlukan lagi untuk mengosongkan memori. Melakukan ini dengan cara yang salah dapat menyebabkan bug halus dan kebocoran memori di aplikasi Anda.
Di bagian Dagger, Anda akan mempelajari cara menggunakan Dagger untuk mengotomatiskan proses ini dan membuat kode yang sama yang akan Anda tulis secara manual.