Injeksi dependensi manual

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.

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, 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:

  1. Ada banyak kode boilerplate. Jika 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 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:

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

  2. 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:

  1. 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 objek FlowContainer di dalam AppContainer seperti yang ditunjukkan dalam contoh kode berikutnya.

  2. 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:

  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 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.