Concepts et implémentation de Jetpack Compose
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 conduit à un plus grand nombre de classes plus petites qui doivent être connectées pour répondre aux dépendances les unes des autres.
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 ligne pointe de A vers B pour représenter 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 deRepository à 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 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 à leur tour dépendent d'un Retrofit service.
LoginActivity est le point d'entrée du flux de connexion, et l'utilisateur interagit avec l'activité. LoginActivity doit donc 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
LoginViewModeldans une autre partie du code, le code sera dupliqué.Les dépendances doivent être déclarées dans l'ordre. Vous devez instancier
UserRepositoryavantLoginViewModelpour le créer.Il est difficile de réutiliser des objets. Si vous souhaitez réutiliser
UserRepositorydans 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);
}
Étant donné que 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 obtenir l'instance partagée de 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 plusieurs endroits de l'application, il est judicieux de disposer d'un emplacement centralisé où vous créez 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<LoginViewModel> {
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<LoginViewModel> {
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'
AppContainervous-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 être constitué du nom d'utilisateur et du mot de passe utilisés uniquement dans le flux de connexion), vous ne souhaitez pas conserver les données d'un ancien flux de connexion d'un autre utilisateur. Vous souhaitez une nouvelle instance pour chaque nouveau flux. Pour ce faire, vous pouvez créer des objetsFlowContainerdans 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. Vous devez vous souvenir 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
LoginUserDataqui doit être partagée jusqu'à la fin du flux de connexion. - Créer une instance de
LoginUserDatalorsque 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 souhaitez pouvoir créer plusieurs instances de LoginContainer dans l'application. Au lieu d'en faire un Singleton, créez-en une classe avec les dépendances dont le flux de connexion a besoin à partir de l'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;
}
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 partagée LoginUserData.
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.