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.
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:
Do tego dochodzi dużo sztywnego kodu. Jeśli chcesz utworzyć kolejną instancję
LoginViewModel
w innej części kodu, spowoduje to jego zduplikowanie.Zależności muszą zostać zadeklarowane w odpowiedniej kolejności. Musisz utworzyć instancję
UserRepository
przedLoginViewModel
, aby go utworzyć.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ę:
Musisz samodzielnie zarządzać domeną
AppContainer
, tworząc instancje dla wszystkich i innych zależności.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.
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ącFlowContainer
w obiekcieAppContainer
, jak pokazano w następnym przykładzie kodu.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ą:
Uzyskaj dostęp do tej samej instancji
LoginUserData
, która musi być udostępniona do momentu gdy proces logowania się zakończy.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.