Explication structure « Android Boilerplate »
Ce document n'est pas un TP, mais une explication de la structure « Android Boilerplate » disponible à l'adresse suivante :
Ce document vous donnera les clés afin de comprendre le fonctionnement et de vous l'approprier.
XML ou Compose ?
Nous sommes en 2021, le monde d'Android évolue… Pendant des années l'écriture des « layouts » (interface) n'était possible que via du XML. Il est maintenant possible d'écrire les layouts de manières bien plus modernes avec JetPack Compose. Le TP que vous suivez est toujours valide, mais repose sur l'utilisation de XML.
But du code
Le but du code fourni sur Github est de simplifier la mise en place d'une base applicative Android « moderne ». Il ne contient aucun code (presque). Il est donc clonnable / téléchargeable et utilisable tel quel, l'idée étant vraiment d'avoir presque un template d'application réutilisable à volonté.
Ceci étant annoncé, passons au détail du fonctionnement.
Récupérer le code
Pour récupérer le code source, vous avez deux possibilités :
- Le fichier zip en provenance de Github : À télécharger ici
- En clonant le repository :
git clone git@github.com:c4software/Android-Boilerplate-Koin-CoRoutines-OkHTTP.git
⚠️ Attention, si vous avez choisi de cloner le repository. Pensez bien à supprimer le dossier .git
à la racine des sources afin de ne pas garder l'historique de mon projet. ⚠️
Lancer le projet une première fois
Avant d'effectuer des modifications dans le projet, nous allons le lancer une première fois. Pour ça, il suffit d'ouvrir le projet avec Android Studio.
Une fois l'indexation terminée, vous devez pouvoir lancer le projet sur un émulateur ou sur votre téléphone. Ce qui devrait donner quelque chose comme :
La structure des dossiers
Afin de simplifier l'entrée dans le code, j'ai volontairement limité l'organisation des dossiers au strict minimum. Attention, ça ne veut pas dire que vous ne pouvez pas en créer d'autres pour organiser votre code au mieux.
data
: Contiens la définition (interface) de vos sources de données (exemple la définition des appels réseau).di
: La définition des éléments qui sont « injectés ».domain
: Votre code métier, celui qui fait le traitement (soit local, ou alors les appels aux APIs HTTP par exemple)utils
: L'ensemble de vos « helpers » / fonctions que vous vous servez à plusieurs endroits dans votre code.view
: Vos « vues », c'est-à-dire vos différents écrans de votre application.
MVVM ? Kézako !?
L’acronyme MVVM signifie Modèle Vue Vue-Modèle (Model–view–viewmodel). L'architecture MVVM est « plutôt récente » elle date de 2004, elle est inventée à la base par Microsoft afin de simplifier les problématiques de gestion de l'interface (en utilisant des mécaniques d'évènement)
Elle a récemment été popularisée par certains frameworks JavaScript, car elle permet d'implémenter « simplement » des interfaces avec une réactivité importante.
Cette méthode permet, tel le modèle MVC (modèle-vue-contrôleur), de séparer la vue de la logique et de l'accès aux données en accentuant les principes de liaison et d’évènement.
Il faut donc distinguer 3 parties :
- Le modèle : Les données au sens pures (de la data sous forme d'objet), elles peuvent provenir d'API, de base de données, de sources locales.
- La vue : L'affichage utilisé utilisateur, la gestion des clicks… Et uniquement ça, la logique associée à la donnée est effectuée dans le
Vue-Modèle
(via « le bus des évènements ») - Le Vue-Modèle : Intéragie avec la couche
modèle
et envoi les nouveaux états résultat à la vue (via le « bus des évènements »).
Nous allons, donc devoir définir « des » bus de communication entre le Vue-Modèle et la Vue afin de permettre l'actualisation des données. Cette organisation vous nous permettre une fois en place de ne manipuler essentiellement plus que de la donnée. La vue sera donc « automatiquement » mise à jour, et ce en fonction de l'état de la donnée (exemple les loaders / les mises à jour de liste, etc.)
📖Pour ceux ayant déjà fait du VueJS (ou autre framework JavaScript récent), le découpage est très proche, vous ne serez pas perdu.
DI ? Injection de dépendances, Koin quelques explications
En introduction j'ai indiqué que mon « Boilerplate » était le strict minimum viable pour un projet… Et bien je vous ai menti ! Mais garder confiance c'est pour votre bien…
Alors, l'injection des dépendances petite définition Wikipedia :
Il consiste à créer dynamiquement (injecter) les dépendances entre les différents objets en s'appuyant sur une description (fichier de configuration ou métadonnées) ou de manière programmatique. Ainsi les dépendances entre composants logiciels ne sont plus exprimées dans le code de manière statique, mais déterminées dynamiquement à l'exécution.
Pour faire simple, le but est de ne plus avoir à créer des objets dans votre code. Tout est géré « plus haut » afin de centraliser la configuration, la manière dont l'objet est créé, etc.
Quelques avantages à utiliser de l'injection :
- Réduction du code (les créations d'objets sont effectuées qu'une seule fois et injectées automatiquement grâce au typage).
- Réduction de la mémoire, logique moins d'instance d'objet identique créer à plusieurs endroits dans votre code.
- Isolation entre la logique de l'objet et votre code, vous n'êtes qu'un consommateur de fonctionnalités la logique peut-être carrément écrite par quelqu'un d'autre, voir dans certains cas externalisés dans des librairies externes (Kotlin Native par exemple).
- Etc.
Koin
Dans notre nous allons utiliser la librairie Koin, elle est complètement écrite en Kotlin, elle a comme avantage d'être simple à utiliser avec très peut de code à écrire (et donc à comprendre).
Concrètement ça ressemble à quoi
val appModule = module {
// Inject dependencies for the MainViewModel (the only UI in this boilerplate)
viewModel { MainViewModel(get(), get()) }
// Sample Remote Data Repository
single<SampleRemoteRepository>(createdAtStart = true) { SampleRemoteRemoteRepositoryImpl(get()) }
// Sample Local Data Repository
single<SampleLocalRepository>(createdAtStart = true) { SampleLocalRepositoryImpl() }
}
val remoteDataSourceModule = module {
// provided web components
single { createOkHttpClient() }
// Fill property
single { createWebService<SampleRemoteDataSource>(get(), BuildConfig.REMOTE_URI) }
}
val moduleApp = listOf(appModule, remoteDataSourceModule)
L'ensemble est, je pense, relativement parlant, mais regardons en détail le get()
, comme vous pouvez le voir celui-ci est présent un peu partout dans la déclaration de nos éléments à injecter. Ce mot-clé est magique il permet à Koin de détecter le type de paramètre attendu et d'injecter automatiquement le bon objet.
Par exemple nous indiquons que createWebService(client: OkHttpClient, url: String)
, automatiquement Koin va chercher dans les objets qu'il connait ceux correspondant à la signature (dans notre cas single { createOkHttpClient() }
) et BuildConfig.REMOTE_URI
étant la String attendu.
Dans le cas d'un objet de notre vue, nous avons dans le même principe :
viewModel { MainViewModel(get(), get()) }
qui représente le View-Modele de notre Activity.
Celui-ci attend deux paramètres :
MainViewModel(sampleRemoteRepository: SampleRemoteRepository, sampleLocalRepository: SampleLocalRepository)
.
Compliqué ? Pas tellement, avec la pratique ça vous semblera automatique. 😊
Modifier le package « sample »
Comme vous le savez, sur Android les applications doivent être uniques « de manière cryptographique » une partie du test est basé sur leur package. Nous allons donc faire en sorte de personnaliser le package afin de le rendre unique pour vous et votre téléphone.
Changer le nom de l'application
Si vous regardez dans votre liste d'application vous allez trouver une application nommée Boilerplate - Koin - Retrofit
. Pour le changer, c'est simple, il suffit d'éditer le fichier strings.xml
.
⚠️ En parlant de ce fichier, celui-ci doit contenir l'ensemble de vos textes (et évidemment pas uniquement le nom de votre application).
Changer la configuration de l'API
Centraliser la configuration dans une application est essentiel au-delà de l'organisation du code, c'est essentiel pour que vous puissiez travailler en équipe, mais également pour reprendre votre code sereinement dans quelques années (eh oui…). Dans notre application la configuration sera centralisée dans le fichier build.gradle
.
Si vous regardez le fichier en question, vous allez trouver buildConfigField
cette instruction nous permettra de définir de la configuration propre à l'environnement (Prod, Dev, Staging, etc.). Bref c'est génial !
J'ai donc initialisé dans mon petit Boilerplate REMOTE_URI
qui sera dans votre code Kotlin l'URL de votre serveur distant.
Repository ? Kézako !?
Contiens la logique autour de vos données. Elle expose au reste de l'application une API (Interne) permettant de gérer la mise à jour des données.
Cette « brique de code » va permettre d'agréger les différentes sources de données afin d'être utilisable simplement dans vos VueModel (ViewModel).
🛑 N'hésitez pas à découper autant qu'il le faut votre logique dans différents repository 🙏
LocalRepository ?
Dans le code fourni en exemple, le Local Repository
« simule » un repository qui accèderait à des données « local » c'est-à-dire dans votre téléphone (mémoire interne par exemple).
RemoteRepository ?
Dans le code fourni en exemple, le Remote Repository
« simule » une interaction avec « l'extérieur » de votre téléphone c'est-à-dire dans notre cas Internet
via un appel d'API via le protocole HTTP.
Ajouter une nouvelle route d'API distance
Ajouter une nouvelle route d'API à notre projet va se résumer à la modification de quelques fichiers. À premier vu ça peut sembler fastidieux, mais vous allez rapidement voir que ce découpage va nous permettre d'organiser le code au mieux afin de le rendre maintenant dans la durée. Et finalement n'est-ce pas le plus important ?
Je vais prendre un exemple simple, le souhaite ajouter une nouvelle route disponible sur https://rest.ensembl.org/
dans mon projet. Au hasard la route /info/rest?content-type=application/json
.
🤔Je rappelle au passage que la finalité est de « Récupérer l'information » du serveur, le faire transiter dans votre code, pour au final l'afficher quelque part dans votre application.
Déclarer l'appel HTTP dans SampleRemoteDataSource
Déclarer une méthode dans le fichier sampleRemoteDataSource.kt
, ce fichier est une Interface, qui va « déclarer » l'ensemble des méthodes HTTP appelable dans le code. La déclaration de celles-ci est effectuée via des annotations (symbolisé avec @
). Dans notre cas le fichier contient actuellement :
@GET("info/ping?content-type=application/json")
@Headers("Content-type: application/json")
suspend fun ping(): PingResult
Nous déclarons donc une méthode de type GET
qui consommera un retour en JSON.
Nous allons ajouter la seconde méthode de la même façon
@GET("info/rest?content-type=application/json")
@Headers("Content-type: application/json")
suspend fun restInfo(): RestResult
Vous allez devoir créer une Data Class RestResult
qui servira à déserialser le retour de l'API. Elle va ressembler à :
data class RestResult(val release: String) {}
👀Attention 👀 ranger le fichier dans le bon dossier/package ! À savoir data/models/RestResult
.
Comment ça fonctionne en deux mots ?
Déclarer une méthode dans une Interface pour permettre d'appeler un WebService !? C'est magique ? En réalité tout ça est possible grace à OkHTTP2, Retrofit, et l'injection de dépendance. Pour les curieux, toute la logique est ici src/main/java/com/boilerplate/app/di/remote_datasource_model.kt
Déclarer la méthode dans SampleRemoteRepository
La première étape était la déclaration dans l'interface, c'est chose faite. Maintenant nous allons déclarer notre méthode dans le Repository
, donc dans la brique qui va appeler la source de données.
Nous allons donc tout simplement :
- Ajouter la déclaration de la méthode dans l'interface
SampleRemoteRepository
nommée infoRest. - Implémenter la méthode
infoRest
dansSampleRemoteRemoteRepositoryImpl
afin de pouvoir appeler l'API.
L'appeler depuis le code
Pour tester (et uniquement pour tester), nous allons appeler la nouvelle méthode depuis la vue principale. La procédure va être relativement simple :
- Ajout d'une méthode dans
MainViewModel.kt
- La méthode doit implémenter les states. (Chargement, et retour de la « string reçu »)
- Appeler la méthode déclarée dans le MainViewModel depuis l'activity. (ex
myViewModel.getRestInfomations()
).
Dans l'implémentation actuelle, je vous propose d'afficher un un Toast
lors de la réception de la donnée.
Ajouter une nouvelle Activity
Maintenant que nous avons validé que notre code fonctionne, nous allons pouvoir ajouter une nouvelle vue. Nous avons une nouvelle route infoRest
qui pour l'instant est inutilisée, nous allons créer une vue et le code associé afin d'afficher l'information reçue du serveur.
Layout
La première étape va être la création de la vue. Pour ça créer un Layout XML comme nous avons déjà pu le voir ensemble.
Code
Le minimum de code pour que votre activity fonctionne est le suivant :
class YourActivity : AppCompatActivity() {
companion object {
fun getStartIntent(ctx: Context): Intent {
return Intent(ctx, YourActivity::class.java)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_demo)
}
}
👀 Comme toujours l'organisation du code est une chose très importante, ne placez pas votre classe n'importe où. Mais dans un package dans view
:
Si vous souhaitez plus de détail, inspirer du code présent dans le MainActivity.kt
ou dans les exemples que nous avons évoqués pendant le cours.
getStartIntent ?
Cette méthode a pour but de simplifier la lecture (et la navigation) entre les vues. Cette méthode est statique, elle sera appelée que vous souhaiterez appeler votre activity
depuis une autre vue
/ activity
. Elle retourne une Intent
qui nous servira à démarrer l'activity souhaitée.
Exemple :
startActivity(MainActivity.getStartIntent(this))
Le but également de créer des getStartIntent
est de simplifier la gestion du passage des paramètres. En effet, sur Android passer des paramètres à une activité se résume à les attacher à l'Intent. Centraliser la déclaration, permet également de centraliser cette logique.
Exemple :
companion object {
const val AGE_DU_CAPITPAINE = "AGE_DU_CAPITPAINE"
fun getStartIntent(ctx: Context, ageDuCapitaine: Int): Intent {
return Intent(ctx, MainActivity::class.java).apply {
putExtra(FROM_HOME, ageDuCapitaine)
}
}
}
// Pour récupérer cette valeur.
private fun ageDuCapitaine(): Int = intent.getBooleanExtra(AGE_DU_CAPITPAINE, 33)
Rendre accessible cette vue / activity
Maintenant que cette activity est créée, nous allons devoir la rendre « visible » par Android. Cette étape est relativement simple. Il suffit de laisser faire votre IDE pour lui faire autodéclarer le bon XML dans le fichier AndroidManifest.xml
.
Si vous souhaitez réaliser cette action à la main. Il suffit d'ajouter « dans / sous » l'élément application :
<activity android:name="com.boilerplate.app.view.main.MainActivity">
⚠️ Mais sérieusement, ne l'ajoutez pas à la main. Faite plutôt alt entrée sur le nom de votre class dans l'IDE l'action vous sera proposée.
Créer une home
En suivant le même principe que précédemment, créez une Home avec deux boutons permettant d'accéder à la MainActivity
et à InfoRestActivity
.
Petit rappel, pour « attacher » une action de clique sur un bouton :
btnMain.setOnClickListener {
startActivity(MainActivity.getStartIntent(this))
}
btnInfosRest.setOnClickListener {
startActivity(InfoRestActivity.getStartIntent(this))
}
Déclarer cette home comme activity principale de votre application
Un certain nombre de paramètres autour des intent est modifiable directement dans AndroidManifest.xml
, la déclaration de l'intent
à lancer au démarrage de l'application est faite via :
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
Déplacer l'intent filter
dans bloc correspondant à votre activity.
Connecter le tout
Votre application contient maintenant 3 activités :
- Une home.
- L'activité permettant de connaitre la version du serveur.
infoRest
- Une activité permettant de « réaliser des pings ».
Appeler les différends getStartIntent()
depuis les bonnes vues.
Exemple :
fun startMainActivity(){
startActivity(MainActivity.getStartIntent(this))
}
Utiliser un Repository depuis une nouvelle activity
Comme indiqué précédemment, nous n'allons pas directement appeler notre Repository
directement depuis notre Activity
.
Petit rappel
Nous allons découper notre logique en différentes parties :
- La logique de la vue va rester dans l'Activity.
- La logique des données de la vue va être mise dans la partie
ViewModel
. - La logique « de récupération » des données va être mise dans un
Repository
.
Créer un ViewModel pour une Activity
va se résumer à trois opérations :
- Créer une Class
YourActivityViewModel
et qui extend deBaseViewModel()
- Déclarer votre
YourActivityViewModel
dans l'activity en spécifiant que celui-ci sera automatiquement injecté. - Le déclarer dans l'injecteur de dépendances.
Création de votre YourActivityViewModel
Cette étape est la première, nous allons créer une Class qui contiendra la « logique » des données de la vue, le minimum que doit contenir cette classe est :
class YourActivityViewModel() : BaseViewModel() {
val states = MutableLiveData<ViewModelState>()
// Vous déclarerez ici vos méthodes et variables nécessaires
// au bon fonctionnement de votre application.
}
Vous voulez un exemple « plus grand » ?
Vous avez dans le projet un exemple de ViewModel
un peu plus complet, c'est le fichier MainViewModel.kt
il est également accessible ici
Déclarer votre ViewModel dans l'activity
Pour ça rien de bien compliqué, il suffit d'ajouter le code suivant :
private val myViewModel: YourActivityViewModel by viewModel()
Attention
Ne pas mettre le code n'importe où. Nous avons ici un attribut de class.
Déclaration dans l'injecteur de dépendance
Si vous souhaitez que ça fonctionne, vous devez dire à votre code comment le by viewModel()
va être résolu. Pour ça nous devons indiquer à notre injecteur de dépendance comment créer cette dépendance, cette déclaration est à faire dans le fichier app_module.kt
(il se trouve dans le package .di
).
Vous devez donc ajouter dans le appModule
le code suivant :
viewModel { YourActivityViewModel() }
🤓 Bien évidemment, vous ajoutez le code à la suite du viewModel
déjà présent 🤓