Ręczne wstrzykiwanie zależności

Zalecana architektura aplikacji na Androida sprzyja podziałowi kod należy podzielić na klasy, co umożliwia unikanie problemów. Jest to zasada, w których każda klasa ma jedną zdefiniowaną odpowiedzialność. Dzięki temu zwiększa się liczba mniejszych klas, które trzeba połączyć ze sobą. aby wypełniać swoje zależności.

Aplikacje na Androida składają się zwykle z wielu klas, a niektóre z nich
    zależy od siebie.
Rysunek 1. Model aplikacji na Androida wykres
.

Zależności między klasami można przedstawić w formie wykresu, na którym każda z nich klasa jest połączona z klasami, od których zależy. Reprezentacja wszystkich klas i ich zależności składają się na graf aplikacji. Na ilustracji 1 widać abstrakcyjny wykres aplikacji. Gdy klasa A (ViewModel) jest zależna od klasy B (Repository), istnieje linia łącząca punkty A do B, która reprezentuje tę zależność.

Wstrzykiwanie zależności pomaga nawiązywać te połączenia i umożliwia ich zamianę do testowania. Na przykład podczas testowania funkcji ViewModel w zależności od repozytorium, możesz przekazywać różne implementacje Repository z podróbkami, aby przetestować różne przypadki.

Podstawy ręcznego wstrzykiwania zależności

Z tej sekcji dowiesz się, jak zastosować ręczne wstrzykiwanie zależności w prawdziwym Androidzie w przypadku aplikacji. Omawiamy w nim powtarzalne podejście do tego, jak zacząć wstrzykiwanie zależności w aplikacji. Metoda ta będzie się doskonalić aż do osiągnięcia który jest bardzo podobny do tego, co Dagger automatycznie wygeneruje do Ciebie. Więcej informacji o krzyżyku znajdziesz w artykule Dagger – podstawy.

Potraktuj przepływ jako grupę ekranów w aplikacji, które odpowiadają funkcji. Przykładami procesów są logowanie, rejestracja i płatności.

W procesie logowania w typowej aplikacji na Androida LoginActivity zależy od kolumny LoginViewModel, która z kolei zależy od UserRepository. Następnie UserRepository zależy od wartości UserLocalDataSource i UserRemoteDataSource, która z kolei zależy od Retrofit posprzedażna.

LoginActivity to punkt wejścia do procesu logowania się, wchodzi w interakcję z aktywnością. Dlatego LoginActivity musi utworzyć LoginViewModel ze wszystkimi zależnościami.

Klasy Repository i DataSource procesu wyglądają tak:

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;
    }

    ...
}

Tak wygląda 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);
    }
}

W przypadku tego podejścia mogą wystąpić problemy:

  1. Do tego dochodzi dużo sztywnego kodu. Jeśli chcesz utworzyć kolejną instancję LoginViewModel w innej części kodu, spowoduje to jego zduplikowanie.

  2. Zależności muszą zostać zadeklarowane w odpowiedniej kolejności. Musisz utworzyć instancję UserRepository przed LoginViewModel, aby go utworzyć.

  3. Trudno jest je ponownie wykorzystać. Jeśli chcesz ponownie wykorzystać UserRepository w wielu funkcjach, musisz dostosować singleton. Wzorzec singletona utrudnia testowanie, ponieważ wszystkie testy z jedną instancją jednostkową.

Zarządzanie zależnościami za pomocą kontenera

Aby rozwiązać problem ponownego wykorzystywania obiektów, możesz utworzyć własne klasa kontenera zależności, której używasz do pobierania zależności. Wszystkie instancje udostępniane przez ten kontener mogą być publiczne. W tym przykładzie potrzebujesz tylko instancji UserRepository, możesz ustawić jego zależności jako prywatne za pomocą opcję ich publicznego udostępniania, jeśli zajdzie potrzeba ich udostępnienia:

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);
}

Te zależności są używane w całej aplikacji, dlatego muszą umieścić w wspólnym miejscu we wszystkich działaniach, z których mogą korzystać: Application. Utwórz niestandardową Klasa Application zawierająca instancję 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();
}

Teraz możesz pobrać instancję AppContainer z aplikacji uzyskaj udostępniony plik UserRepository instancji:

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);
    }
}

W ten sposób nie utworzysz ani jednego elementu UserRepository. Zamiast tego masz AppContainer wspólne dla wszystkich aktywności zawierających obiekty z grafu i tworzy instancje tych obiektów, które mogą być wykorzystywane przez inne klasy.

Jeśli LoginViewModel jest potrzebny w większej liczbie miejsc w aplikacji, w jednym miejscu, w którym tworzysz instancje LoginViewModel, z całego świata. Możesz przenieść tworzenie obiektu LoginViewModel do kontenera i podać tego typu obiekty z fabryką. Kod dla aplikacji LoginViewModelFactory wygląda tak:

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);
    }
}

Możesz uwzględnić LoginViewModelFactory w AppContainer i ustawić w parametrze LoginActivity spożywa:

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();
    }
}

Ta metoda jest lepsza niż poprzednia, ale nadal będziemy stosować wyzwania, które należy wziąć pod uwagę:

  1. Musisz samodzielnie zarządzać domeną AppContainer, tworząc instancje dla wszystkich i innych zależności.

  2. To nadal bardzo dużo kodu. Musisz zbudować fabryki ręcznie w zależności od tego, czy chcesz ponownie użyć obiektu.

Zarządzanie zależnościami w przepływach aplikacji

Usługa AppContainer staje się skomplikowana, gdy chcesz dodać więcej funkcji do nad projektem. Gdy Twoja aplikacja stanie się większa i zaczniesz wprowadzać różne pojawia się więcej problemów.

  1. Jeśli stosujesz różne przepływy pracy, możesz chcieć, by obiekty znajdowały się zakresu tego procesu. Na przykład podczas tworzenia obiektu LoginUserData (który może być składa się z nazwy użytkownika i hasła używanych tylko podczas logowania), nie chcesz, zachowywania danych ze starego procesu logowania się innego użytkownika. Chcesz mieć nowy dla każdego nowego przepływu. Możesz to osiągnąć, tworząc FlowContainer w obiekcie AppContainer, jak pokazano w następnym przykładzie kodu.

  2. Optymalizacja wykresu aplikacji i kontenerów przepływu może też być trudna. Musisz pamiętać o usunięciu niepotrzebnych instancji w zależności od w którym żyjesz.

Załóżmy, że masz przepływ logowania, który składa się z jednej aktywności (LoginActivity) i kilka fragmentów (LoginUsernameFragment i LoginPasswordFragment). Te widoki chcą:

  1. Uzyskaj dostęp do tej samej instancji LoginUserData, która musi być udostępniona do momentu gdy proces logowania się zakończy.

  2. Utwórz nową instancję LoginUserData, gdy przepływ zacznie się od nowa.

Możesz to osiągnąć za pomocą kontenera procesu logowania. Ten kontener musi być utworzona podczas rozpoczynania procesu logowania i usuwana z pamięci po jego zakończeniu.

Dodajmy do przykładowego kodu LoginContainer. Chcesz mieć możliwość tworzenia wiele wystąpień ciągu LoginContainer w aplikacji, więc zamiast używania go w singleton, ustaw ją jako klasę z zależnościami, których potrzebuje przepływ logowania 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;
}

Gdy masz już kontener dostosowany do przepływu, musisz zdecydować, kiedy utworzyć i usuń instancję kontenera. Proces logowania się jest autonomiczny w działanie (LoginActivity) to działanie, które zarządza cyklem życia tego kontenera. LoginActivity może utworzyć instancję w: onCreate() oraz Usuń go w aplikacji 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();
    }
}

Podobnie jak LoginActivity, fragmenty logowania mogą uzyskać dostęp do LoginContainer z AppContainer i użyj współdzielonej instancji LoginUserData.

Bo w tym przypadku mamy do czynienia z logiką wyświetlania obserwacja cyklu życia ma sens.

Podsumowanie

Wstrzykiwanie zależności to dobra technika tworzenia skalowalnych i możliwych do testowania Aplikacje na Androida. Używanie kontenerów do udostępniania instancji klas w różnych poszczególnych części aplikacji oraz jako centralne miejsce do tworzenia instancji przy użyciu fabryk.

Gdy Twoja aplikacja się powiększy, zaczniesz pisać dużo (np. fabryki), który jest podatny na błędy. Musisz także samodzielnie zarządzać zakresem i cyklem życia kontenerów, optymalizując odrzucanie kontenerów, które nie są już potrzebne, aby zwolnić pamięć. Jeśli zrobisz to nieprawidłowo, mogą wystąpić drobne błędy i wycieki pamięci w Twojej aplikacji.

W sekcji Dagger dowiedz się, jak za pomocą Daggera zautomatyzować ten proces i wygenerować ten sam kod w innej sytuacji.