Pour créer une application de haute qualité, il faut porter une grande attention à son architecture, car son rôle est décisif. En effet, les décisions les plus fondamentales qui déterminent la structure et l’interaction des composants reposent sur l’architecture. Par conséquent, choisir l’architecture appropriée est l’un des points clés dans la création d’un produit décent. Pour que l’application soit fiable, évolutive et facile à tester, il est nécessaire dès le début du développement de poser les principes de son fonctionnement. Cela permet d’éliminer l’interconnexion rigide des éléments, de faciliter la lecture et la modification du code, et d’assurer un équilibre entre la qualité et la vitesse de développement.
Dans l’article soumis à l’attention des lecteurs, nous nous efforçons d’examiner une option d’implémentation d’une architecture de haute qualité en utilisant la pile technologique suivante : Kotlin (langage de développement), MVVM (logique d’interaction des composants de l’application), Koin (injection de dépendances) et coroutines (multithreading). Pourquoi cette pile ? Kotlin est le langage officiellement recommandé par Google pour le développement Android. Comparé à Java, il élimine un grand nombre de codes répétitifs, offre une approche bénéfique de la sécurité des null, des classes de données, etc. MVVM (Model-View-ViewModel) est un modèle architectural qui permet de séparer l’interface utilisateur, la logique métier et les sources de données. Koin est une bibliothèque légère avec un ensemble de fonctions pratiques pour implémenter le concept d’injection de dépendances. Elle facilite la création de singletons, la création d’étendues personnalisées et l’injection de dépendances. Enfin, les coroutines sont une fonctionnalité intéressante de Kotlin pour le multithreading. Elles peuvent être considérées comme des flux légers dont la création ne nécessite pas beaucoup de ressources.
Notre application de test s’appellera « Dog Explorer » et affichera une liste de différentes races de chiens. Nous récupérerons les données de la ressource https://api.thedogapi.com/
Tout d’abord, créez un nouveau projet dans l’IDE Android Studio. Parmi les modèles proposés, sélectionnez « Empty Activity ». Le langage de développement est Kotlin ; le niveau API minimum est 21 (Android 5) ; l’utilisation des artefacts androidx est définie sur true.
Connectez les dépendances nécessaires au projet. Tout d’abord, ajoutez la bibliothèque Koin. Dans le fichier build.gradle (module app), insérez les lignes suivantes :
implementation "org.koin:koin-core:$koin_version"
implementation "org.koin:koin-core-ext:$koin_version"
implementation "org.koin:koin-androidx-ext:$koin_version"
La variable $ koin_version est enregistrée dans le fichier build.gradle du projet entier. Après ext.kotlin_version, ajoutez la ligne ext.koin_version = ‘2.0.1’. Veuillez noter que le projet utilise la deuxième version de Koin.
Pour les requêtes serveur, nous utiliserons les bibliothèques OkHttp et Retrofit 2. Selon le même principe, ajoutez au fichier build.gradle (module app) :
implementation "com.squareup.retrofit2:retrofit:$retrofit_version"
implementation "com.squareup.retrofit2:converter-gson:$retrofit_version"
implementation "com.squareup.okhttp3:logging-interceptor:$logging_interceptor_version"
implementation
"com.jakewharton.retrofit:retrofit2-kotlin-coroutines-adapter:$coroutine_adapter_version"
et dans le fichier build.gradle du projet entier :
ext.retrofit_version = '2.6.0'
ext.logging_interceptor_version = '3.12.1'
ext.coroutine_adapter_version = '0.9.2'
Nous synchronisons les fichiers Gradle.
Nous pouvons maintenant préparer les premières injections. Dans Koin, le fournisseur d’objets est la classe `Module`, créée à l’aide d’une fonction au nom inattendu `module()`. Pour la créer, nous utilisons la merveilleuse capacité de Kotlin : les fonctions et variables au niveau du package. Contrairement à Java, Kotlin vous permet de stocker des fonctions et des variables non seulement dans des classes, mais aussi dans des fichiers. En termes de signification, cela est similaire aux classes utilitaires statiques de Java, mais beaucoup plus pratique. Dans le package `dogexplorer`, créez un nouveau package `di`, et à l’intérieur, le fichier `appModule`. Dans ce fichier, créez un objet de la classe `Module` :
val appModule = module {}
Les noms du fichier et de la variable n’ont pas à correspondre. Dans notre projet, cela est fait pour des raisons de commodité et de compréhension.
Pour travailler avec le réseau, nous avons besoin de 3 classes : `OkHttpClient` (pour les requêtes réelles), `Retrofit` (pour la conversion pratique de nos objets en JSON et vice-versa) et l’interface `NetworkApi` (dans laquelle nous définissons les requêtes serveur).
Créez 3 fonctions. La première renverra une instance d’`OkHttpClient` avec l’intercepteur de journalisation connecté – un élément avantageux pour contrôler les données envoyées et reçues.
fun createOkHttpClient(): OkHttpClient {
val httpLoggingInterceptor = HttpLoggingInterceptor()
httpLoggingInterceptor.level = HttpLoggingInterceptor.Level.BODY
return OkHttpClient.Builder()
.addInterceptor(httpLoggingInterceptor).build()
}
La seconde crée une instance de `Retrofit`. Nous spécifions Gson comme convertisseur de nos objets en JSON et vice-versa, et `CoroutineCallAdapter` pour encapsuler les réponses serveur dans des coroutines pour l’asynchronisme. L’URL de base peut être placée dans des constantes pour plus de commodité. Pour ce faire, nous créons le package `utils` à l’intérieur du package `dogexplorer`, puis à l’intérieur de celui-ci, nous créons le fichier `constants.kt`, dans lequel nous ajoutons la ligne `val baseUrl = “https://api.thedogapi.com/v1/“`.
fun createRetrofit(okHttpClient: OkHttpClient): Retrofit {
return Retrofit.Builder()
.addConverterFactory(GsonConverterFaGsonctory.create())
.addCallAdapterFactory(CoroutineCallAdapterFactory())
.baseUrl(baseUrl)
.client(okHttpClient)
.build()
}
La troisième fonction devrait ressembler à ceci :
fun createNetworkApi(retrofit: Retrofit): NetworkApi {
return retrofit.create(NetworkApi::class.java)
}
Étant donné que nous n’avons pas encore créé l’interface, nous l’ajouterons au projet. Nous créons le package `model` et à l’intérieur de celui-ci `NetworkApi`. Veuillez noter que vous ne devez pas ajouter un fichier ou une classe Kotlin, mais une interface.
interface NetworkApi {}
Comme nous n’avons pas besoin de créer une nouvelle instance de `NetworkApi` à chaque fois que nous l’ajoutons au `ViewModel`, nous utiliserons la fonction `single` pour créer des singletons. Modifiez la fonction `appModule()` pour qu’elle ressemble à ceci :
val appModule = module {
single { createRetrofit(createOkHttpClient()) }
single { createNetworkApi(get()) }
}
Pour que `NetworkApi` devienne utile, nous y définirons des méthodes pour obtenir une liste de races de chiens et obtenir des images. Avant d’écrire les méthodes, il est conseillé de se familiariser avec la documentation de notre backend. Elle est disponible à l’adresse https://docs.thedogapi.com/. Comme vous pouvez le constater, le serveur exigera une clé API pour chaque requête. Par conséquent, tout d’abord, nous devons l’obtenir en remplissant un simple formulaire à partir de l’e-mail et de la description de notre application sur la page https://thedogapi.com/signup. L’e-mail avec la clé générée de type chaîne « 580e40f4-4144-8d75-a8fb-89a822a3126f » vous sera envoyé à l’adresse e-mail que vous avez spécifiée (Attention ! Ceci est une clé invalide, juste un exemple). Enregistrez votre clé dans le fichier `constants.kt` sous forme de variable :
val xApiKey = "580e40f4-4144-8d75-a8fb-89a822a3126f"
La méthode `getBreeds()` nous retournera une liste de races de chiens. Tout d’abord, vous devez créer une classe wrapper dans laquelle la bibliothèque Gson convertira la réponse du serveur. Vous pouvez la créer manuellement en créant une classe et en définissant les attributs appropriés. Une liste des champs requis est indiquée à l’adresse https://docs.thedogapi.com/api-reference/models/breed.
Cependant, nous recommandons fortement la seconde méthode, qui est beaucoup plus productive. Ajoutez le plugin « JSON To Kotlin Class » à Android Studio. Pour ce faire, allez dans le menu de recherche des plugins ouverts : Fichier / Paramètres / Plugins. Dans le champ de recherche, entrez le nom souhaité, installez le plugin et redémarrez l’IDE.
Ensuite, dans le package `models`, ajoutez le nouveau package `entities`. Nous appelons le menu contextuel avec un clic droit de la souris et parmi les options de nouvel élément, sélectionnons « Kotlin Data Class File from JSON ». Ce plugin permet de générer des classes de données Kotlin à partir d’un exemple JSON. Vous pouvez obtenir une réponse d’exemple à l’adresse https://docs.thedogapi.com/api-reference/breeds/breeds-list en envoyant une requête de test. Nous copions la réponse reçue dans le champ de saisie du plugin, indiquons le nom de la classe `Breed` et cliquons sur « Generate ». Le plugin créera 3 classes de données : `Breed`, `Height` et `Weight`. Les classes de données en Kotlin fournissent une fonctionnalité pratique pour créer les classes dites POJO (Plain Old Java Object) destinées uniquement au stockage de données. Elles généreront automatiquement les fonctions `equals()`, `hashCode()` et `copy()`, ainsi que les setters et getters pour chaque attribut. Le code est très concis, mais efficace.
Ayant obtenu une classe wrapper, nous pouvons terminer l’écriture de la fonction pour obtenir une liste de races. Il y a 2 options pour son implémentation. La première consiste à ajouter un modificateur `suspend` qui permet de mettre la fonction en pause. La seconde option consiste à encapsuler l’appel dans l’interface `Deferred`, qui est une future non bloquante et annulable. Dans ce cas, l’appel est plus concis. Attardons-nous sur celle-ci.
@GET("breeds")
fun getBreeds(@Header("x-api-key") xApiKey: String): Deferred<List>
Une annotation au-dessus de la fonction signifie que notre application effectuera une requête GET vers le point de terminaison `URL_DE_BASE` + breeds. Pour simplifier, nous passons l’en-tête comme paramètre de requête. Dans des applications plus complexes, il est conseillé de l’ajouter à un intercepteur d’authentification séparé.
La fonctionnalité de travail avec le réseau est pratiquement terminée. Nous passons maintenant à l’implémentation de l’approche MVVM. Si vous n’êtes pas familier avec elle, nous vous recommandons de lire l’article de Hazem Saleh.
Tout d’abord, organisons notre projet. Dans le package `dogexplorer`, créez le package `UI`, qui à son tour contient le package `main`. Nous déplaçons `MainActivity` dans ce package. Ici, nous créons une nouvelle classe `MainViewModel`, qui hérite de `ViewModel`. Pour fonctionner, elle a besoin de `networkApi` (pour demander des données au serveur), de `coroutineScope` (pour gérer les coroutines) et de `LiveData` pour transférer les données à `Activity`. `Dispatchers.IO` dans le constructeur de `CoroutineScope` signifie que nous utilisons le pool de threads recommandé pour les opérations I/O avec une limite de 64 ou plus (si le périphérique dispose d’un processeur avec plus de 64 cœurs).
class MainViewModel(
val networkApi: NetworkApi,
val coroutineScope: CoroutineScope = CoroutineScope(Dispatchers.IO)
) : ViewModel() {
val breeds = MutableLiveData<List>()
}
Ajoutez une fonction pour obtenir une liste de races. Dans notre `coroutineScope`, nous créons une nouvelle coroutine avec le constructeur de coroutine `launch`. Ici, nous obtenons une liste de races depuis le serveur et la publions dans `LiveData`.
fun getBreeds() {
coroutineScope.launch {
val breeds = networkApi.getBreeds(xApiKey).await()
breedsLiveData.postValue(breeds)
}
}
Nous ajoutons également l’annulation de la coroutine dans la fonction `onCleared()` de notre `MainViewModel`. Lorsque le `MainViewModel` est détruit, les coroutines en cours d’exécution dans son `coroutineScope` seront arrêtées.
override fun onCleared() {
super.onCleared()
coroutineScope.coroutineContext.cancel()
}
Nous devons maintenant ajouter `MainViewModel` à `MainActivity`. Dans le package `di`, créez un fichier séparé `viewModelModule.kt`. Nous y définissons la variable `viewModelModule`. Nous aurions pu également l’ajouter au fichier `appModule.kt`, mais avec l’exemple ci-dessous, ce sera plus clair. Cette fois, nous injectons la dépendance en utilisant une méthode spéciale `viewModel()`, qui permet d’injecter des descendants de `ViewModel` dans une `Activity` ou un `Fragment`.
val viewModelModule = module {
viewModel { MainViewModel(get()) }
}
Pour obtenir une référence à `MainViewModel`, il suffit d’ajouter une ligne à `MainActivity`.
val mainViewModel: MainViewModel by viewModel()
Pour que nos injections fonctionnent, nous devons démarrer la bibliothèque Koin. Cela se produit au démarrage de l’application. Pour ce faire, créez une classe `App` qui hérite de `Application`, et ajoutez du code à sa méthode `onCreate()`. N’oubliez pas d’enregistrer la nouvelle classe dans le manifeste de l’application.
class App : Application() {
override fun onCreate() {
super.onCreate()
startKoin {
androidContext(this@App)
androidLogger()
modules(listOf(appModule, viewModelModule))
}
}
}
La touche finale : nous affichons les résultats de la requête au serveur sur l’écran de notre application. Tout d’abord, ajoutez une nouvelle dépendance dans `build.gradle` (module app). Synchronisez Gradle.
implementation 'androidx.recyclerview:recyclerview:1.1.0-beta04'
Modifiez la mise en page de l’écran principal en ajoutant un `RecyclerView`.
<!--?xml version="1.0" encoding="utf-8"?-->
Dans le package `res/layout`, ajoutez la mise en page de l’élément de liste de notre `RecyclerView` appelée `dog_item.xml`. Elle est aussi simple que possible et affichera le nom de la race.
<!--?xml version="1.0" encoding="utf-8"?-->
Créez un adaptateur pour `RecyclerView`.
class DogAdapter(val breeds: List) : RecyclerView.Adapter() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.dog_item, parent, false)
return ViewHolder(view)
}
override fun getItemCount(): Int {
return breeds.size
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.breedName.setText(breeds[position].name)
}
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
internal var breedName: TextView
init {
breedName = itemView.findViewById(R.id.dog_name)
}
}
}
Enfin, nous pouvons interroger les données et les afficher dans `DogAdapter`. Pour ce faire, ajoutez le code dans la méthode `onCreate` de `MainActivity` :
if (savedInstanceState == null) mainViewModel.getBreeds()
mainViewModel.breedsLiveData.observe(this, Observer {
recycler.apply {
adapter = DogAdapter(it)
layoutManager = LinearLayoutManager(this@MainActivity)
}
})
Maintenant, nous pouvons lancer notre projet et rayonner de fierté car nous venons de maîtriser une nouvelle pile de développement Android.
Découvrez comment nous avons développé une solution pour une logistique de fitness claire en utilisant Kotlin comme technologie principale
