L'architecture de l'application recommandée d'Android vous encourage à diviser votre code en classes pour bénéficier de la séparation des tâches, un principe selon lequel chaque classe de la hiérarchie a une seule responsabilité définie. Cela génère davantage de petites classes qui doivent être connectées entre elles pour satisfaire leurs dépendances respectives.
Les dépendances entre les classes peuvent être représentées sous forme de graphique, dans lequel chaque classe est connectée aux classes dont elle dépend. Le graphique d'application représente toutes vos classes et leurs dépendances.
La figure 1 illustre l'abstraction du graphique de l'application.
Lorsque la classe A (ViewModel
) dépend de la classe B (Repository
), une flèche reliant A à B représente cette dépendance.
L'injection de dépendances permet d'établir ces connexions et de remplacer les implémentations à des fins de test. Par exemple, lorsque vous testez un ViewModel
qui dépend d'un dépôt, vous pouvez transmettre différentes implémentations de Repository
à l'aide de données fictives ou de simulations pour tester les différents cas.
Principes de base de l'injection de dépendances manuelle
Cette section explique comment appliquer l'injection de dépendances manuelle dans un scénario d'application Android réel. Elle décrit une approche itérative permettant de commencer à utiliser l'injection de dépendances dans votre application. L'approche s'améliore jusqu'à atteindre un point très semblable à ce que Dagger générerait automatiquement pour vous. Pour en savoir plus sur Dagger, consultez la section Principes de base de Dagger.
Un flux est un groupe d'écrans de l'application qui correspond à une fonctionnalité. La connexion, l'enregistrement et le règlement sont des exemples de flux.
Lorsqu'elle couvre un flux de connexion pour une application Android standard, la LoginActivity
dépend de LoginViewModel
, qui à son tour dépend de UserRepository
.
Ensuite, UserRepository
dépend d'un UserLocalDataSource
et d'un
UserRemoteDataSource
, qui à son tour dépend d'un Retrofit
Google Cloud.
LoginActivity
est le point d'entrée du flux de connexion. L'utilisateur interagit avec l'activité. Ainsi, LoginActivity
doit créer le LoginViewModel
avec toutes ses dépendances.
Les classes Repository
et DataSource
du flux se présentent comme suit :
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; } ... }
Voici à quoi ressemble 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); } }
Cette approche présente des problèmes :
Il y a beaucoup de code récurrent. Si vous souhaitez créer une autre instance de
LoginViewModel
dans une autre partie du code, le code sera dupliqué.Les dépendances doivent être déclarées dans l'ordre. Vous devez instancier
UserRepository
avantLoginViewModel
pour le créer.Il est difficile de réutiliser des objets. Si vous souhaitez réutiliser
UserRepository
dans plusieurs fonctionnalités, vous devez appliquer le modèle Singleton. Le modèle Singleton rend les tests plus difficiles, car tous les tests partagent la même instance Singleton.
Gérer les dépendances avec un conteneur
Pour résoudre le problème lié à la réutilisation des objets, vous pouvez créer votre propre classe de conteneur de dépendances utilisée pour obtenir des dépendances. Toutes les instances fournies par ce conteneur peuvent être publiques. Dans l'exemple, comme vous n'avez besoin que d'une instance de UserRepository
, vous pouvez rendre ses dépendances privées, avec la possibilité de les rendre publiques à l'avenir si elles doivent être fournies :
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); }
Comme ces dépendances sont utilisées dans l'ensemble de l'application, elles doivent être placées dans un emplacement commun que toutes les activités peuvent utiliser : la classe Application
. Créez une classe Application
personnalisée contenant une 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(); }
Vous pouvez maintenant obtenir l'instance de l'AppContainer
, à partir de l'application, et l'instance partagée du UserRepository
:
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); } }
Ainsi, vous ne disposez pas d'un UserRepository
Singleton. Vous disposez plutôt d'un AppContainer
partagé dans toutes les activités qui contient des objets du graphique et crée des instances de ces objets que d'autres classes peuvent utiliser.
Si LoginViewModel
est nécessaire dans un plus grand nombre d'emplacements de l'application, il est judicieux de disposer d'un emplacement centralisé où créer des instances de LoginViewModel
. Vous pouvez déplacer la création de LoginViewModel
vers le conteneur et fournir de nouveaux objets de ce type avec une fabrique. Le code d'une LoginViewModelFactory
se présente comme suit :
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); } }
Vous pouvez inclure la LoginViewModelFactory
dans l'AppContainer
et faire en sorte que LoginActivity
l'utilise :
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(); } }
Bien que cette approche soit préférable à la précédente, vous devez encore prendre en compte les difficultés suivantes :
Vous devez gérer l'
AppContainer
vous-même en créant manuellement des instances pour toutes les dépendances.Il y a encore beaucoup de code récurrent. Vous devez créer des fabriques ou des paramètres manuellement selon que vous souhaitez réutiliser un objet ou non.
Gérer les dépendances dans les flux d'application
AppContainer
se complexifie si vous souhaitez inclure davantage de fonctionnalités dans le projet. Les problèmes se multiplient lorsque votre application prend de l'ampleur et que vous commencez à introduire différents flux de fonctionnalités :
Lorsque vous disposez de différents flux, vous devrez peut-être limiter les objets au champ d'application de ce flux. Par exemple, lorsque vous créez
LoginUserData
(qui peut comprendre le nom d'utilisateur et le mot de passe utilisés uniquement dans le flux de connexion), vous ne devez pas conserver les données d'un ancien flux de connexion provenant d'un autre utilisateur. Vous avez besoin d'une nouvelle instance pour chaque nouveau flux. Pour ce faire, vous pouvez créer des objetsFlowContainer
dans l'AppContainer
, comme illustré dans l'exemple de code suivant.L'optimisation du graphique d'application et des conteneurs de flux peut également s'avérer difficile. N'oubliez pas de supprimer les instances dont vous n'avez pas besoin, en fonction du flux dans lequel vous vous trouvez.
Imaginez que vous disposez d'un flux de connexion composé d'une activité (LoginActivity
) et de plusieurs fragments (LoginUsernameFragment
et LoginPasswordFragment
). Ces vues cherchent à :
Accéder à la même instance
LoginUserData
qui doit être partagée jusqu'à la fin du flux de connexion.Créer une instance de
LoginUserData
lorsque le flux redémarre.
Pour ce faire, vous pouvez utiliser un conteneur de flux de connexion. Ce conteneur doit être créé au début du flux de connexion et supprimé de la mémoire à la fin du flux.
Ajoutons un LoginContainer
à l'exemple de code. Vous devez pouvoir créer plusieurs instances de LoginContainer
dans l'application. Par conséquent, au lieu de le transformer en Singleton, transformez-le en classe avec les dépendances de l'AppContainer
nécessaires au flux de connexion.
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; }
Une fois que vous disposez d'un conteneur spécifique à un flux, vous devez décider quand créer et supprimer l'instance de conteneur. Étant donné que votre flux de connexion est autonome dans une activité (LoginActivity
), c'est l'activité qui gère le cycle de vie de ce conteneur. LoginActivity
peut créer l'instance dans onCreate()
et la supprimer dans 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(); } }
Tout comme LoginActivity
, les fragments de connexion peuvent accéder au LoginContainer
à partir de l'AppContainer
et utiliser l'instance LoginUserData
partagée.
Dans ce cas, comme il s'agit d'une logique de cycle de vie de la vue, il est judicieux d'utiliser l'observation du cycle de vie.
Conclusion
L'injection de dépendances est une technique efficace pour créer des applications Android évolutives et testables. Utilisez les conteneurs pour partager des instances de classes dans différentes parties de votre application, mais également pour centraliser la création d'instances de classes à l'aide de fabriques.
Lorsque votre application prend de l'ampleur, vous commencez à constater que vous écrivez beaucoup de code récurrent (comme des fabriques), qui peut être source d'erreurs. Vous devez également gérer vous-même le champ d'application et le cycle de vie des conteneurs, en optimisant et en supprimant les conteneurs qui ne sont plus nécessaires afin de libérer de la mémoire. Si vous ne le faites pas correctement, vous risquez d'entraîner des bugs et des fuites de mémoire subtiles dans votre application.
Dans la section "Dagger", vous apprendrez à utiliser Dagger pour automatiser ce processus et générer le même code que celui que vous auriez écrit manuellement.