Google berkomitmen untuk mendorong terwujudnya keadilan ras bagi komunitas Kulit Hitam. Lihat caranya.

Injeksi dependensi manual

Arsitektur aplikasi yang direkomendasikan Android mendorong pembagian kode ke dalam class untuk mendapatkan manfaat dari pemisahan fokus, prinsip di 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.

Aplikasi Android biasanya terdiri dari banyak class, dan beberapa di antaranya bergantung pada satu sama lain.
Gambar 1. Model grafik aplikasi milik aplikasi Android

Dependensi di antara class dapat direpresentasikan sebagai grafik, di mana 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 untuk 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 bagaimana Anda dapat 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 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 mencakup 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 layanan Retrofit.

LoginActivity adalah titik masuk 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:

  1. Ada banyak kode boilerplate. Jika Anda ingin membuat instance LoginViewModel lain di bagian kode yang lain, Anda akan memiliki duplikasi kode.

  2. Dependensi harus dideklarasikan secara berurutan. Anda harus membuat instance UserRepository sebelum LoginViewModel agar dapat membuatnya.

  3. Sulit untuk menggunakan objek kembali. Jika Anda ingin menggunakan kembali UserRepository di beberapa fitur, Anda harus membuatnya mengikuti pola singleton. Pola tunggal 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 aplikasi. Buat class aplikasi 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 anak 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 pabrik. Kode untuk LoginViewModelFactory akan terlihat seperti ini:

Kotlin

    // Definition of a Factory interface with a function to create objects of a type
    interface Factory {
        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 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:

  1. Anda harus mengelola AppContainer sendiri, membuat instance untuk semua dependensi secara manual.

  2. Masih ada banyak kode boilerplate. Anda perlu membuat pabrik 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:

  1. Jika Anda memiliki alur yang berbeda, Anda mungkin ingin objek hanya ditampilkan dalam cakupan aliran 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 objek FlowContainer di dalam AppContainer seperti yang ditunjukkan dalam contoh kode berikutnya.

  2. Mengoptimalkan grafik aplikasi dan container 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:

  1. Mengakses instance LoginUserData yang sama yang perlu dibagikan hingga alur login selesai.

  2. 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 container khusus untuk alur, Anda harus memutuskan kapan harus membuat dan menghapus instance container. Karena alur login Anda dimuat secara mandiri dalam suatu aktivitas (LoginActivity), aktivitas tersebutlah yang mengelola siklus proses container 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 container sebagai cara untuk berbagi instance class di berbagai bagian aplikasi Anda dan sebagai pusat untuk membuat instance class menggunakan pabrik.

Saat aplikasi Anda semakin besar, Anda akan mulai melihat bahwa Anda menulis banyak kode boilerplate (seperti pabrik), yang dapat menyebabkan 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.

Pada halaman berikutnya, Anda akan mempelajari bagaimana Anda dapat menggunakan Dagger untuk mengotomatiskan proses ini dan membuat kode yang sama yang akan Anda tuliskan secara manual.