Jérémy Le Piolet - Blog

Android Jetpack - Le développement Android 2.0

Android Jetpack est une surcouche au framework Android de base qui veut simplifier certaines interactions, remplacer certaines tâches répétitives et en proposer de nouvelles. En un mot : dépoussiérer le développement d’application Android !

La promesse est intéressante d’autant que Jetpack semble assez complet. Elle adresse aussi bien des problématiques d’architecture que d’interface ou de base de données : il y a forcément quelque chose d’intéressant à piocher !

Présenté il y a deux ans, ce n’est que dernièrement que j’ai vraiment eu l’occasion de tester une bonne partie de ces composants sur un nouveau projet. Voici un petit résumé de ce que j’en ai pensé.

Kotlin

Inévitablement, je ne peux parler de Jetpack sans parler de Kotlin. Même si les deux ne sont pas complètement liés, ce nouveau projet a été l’occasion de tester ce nouveau langage, d’autant que Jetpack apporte des subtilités propres à Android. Comme j’ai utilisé les deux en même temps, je vais avoir du mal à différencier ce qui est du Kotlin pur de ce qui est apporté par Jetpack mais je ne pense pas que ce manquement soit si important.

Kotlin, c’est une surcouche de Java, comparable à ce qu’est Typescript pour Javascript. On y trouve beaucoup de choses communes avec Java, des choses en plus, des choix tranchés et des fonctionnalités que l’on retrouvera probablement dans Java dans le futur. Kotlin est d’ailleurs converti dans le même Bytecode que Java avant exécution, ce qui rend complètement compatible l’utilisation des deux langages au sein d’une même application. Là encore, c’est intéressant sur le papier.

La syntaxe Kotlin est assez similaire à Java et aux langages dérivés (C# entre autres). L’assimilation de la lecture du code Kotlin est donc très rapide. La prise en main est beaucoup plus difficile sur l’écriture.

Kotlin est différent de Java, mais pas entièrement. Une grande partie du langage repose sur de petites différences qui rendent l’écriture les premières semaines assez laborieuses pour quelqu’un habitué à Java.

Par exemple, il m’a fallu presque deux semaines pour vraiment imprimer que le type de la variable allait après le nom de la variable au lieu de la précéder, comme en Java (et encore, je me surprenais à refaire l’erreur assez régulièrement 3 mois après). Ce n’est pas méchant, ce n’est pas compliqué mais en pratique, les habitudes ayant la vie dure, le linter de l’éditeur devient un allié extrêmement précieux. Il ne faut d’ailleurs pas hésiter à le configurer à un niveau plus agressif que la configuration standard, du moins au début.

Kotlin fait également de nombreux choix par défaut. Là où Java demande de toujours déclarer la visibilité d’une variable ou d’une fonction (public, package, _protected ou private), Kotlin considère que tout est par défaut public, sauf indicaton contraire. De même, une méthode ne retourne rien par défaut en Kotlin là où Java demande de préciser void. Là encore, c’est un coup à prendre et à surveiller les premiers temps : soit on veut rajouter des public non nécéssaires, soit on prend la tendance inverse et on oublie de préciser les private.

//Code Java
public void helloWorld(String name){
    if(name == null){
        name = "World"; //valeur par défaut si la paramètre est null
    }
    System.out.print("Hello, " + name + "!");
}
//Equivalent  Kotlin
fun hello(name: String = "World") { // valeur par défaut si le paramètre est null
    println("Hello, $name!")
}

Comparatif Java/Kotlin sur une fonction

Une grande force de Kotlin est sa gestion de la nullité par défaut. C’est simple : si vous essayez de mettre null dans une variable, le compilateur vous jette. Il faut explicitement indiquer que l’on a une variable null. Avec ce paradigme vient aussi plusieurs ajouts intéressants comme le Elvis Operateur (opérateur pointé qui n’exécute pas la fonction pointée si l’objet est null) ou le retard d’initialisation (lateinit qui permet d’indiquer que si c’est null, c’est pas grave, on initialisera plus tard).

C’est bien mais pas aussi intéressant que sur le papier. On est pour moi juste au-dessus du niveau d’utilité que les annotations @NonNull et @Nullable : ce n’est plus le linter mais le compilateur qui signale les erreurs, on ne peut pas le bypasser. Mais les comportements sont similaires.

De fait, cette gestion de la nullité introduit un faux sentiment de sécurité : après s’être fait taper sur les doigts une ou deux fois par le compilateur parce que l’on mettait des null dans des variables, on a l’impression que les NullPointerException n’arriveront plus. Et là, c’est le drame.

D’une part parce que tout l’écosystème android n’est pas en Kotlin et on n’est pas à l’abri qu’une librairie renvoie une valeur null que le compilateur ne verra pas passer. D’autre part, le cycle de vie si particulier d’Android fait que des variables que l’on initialise pas de suite grâce à lateinit peuvent parfois se retrouver appelée sans avoir été initialisée (et le compilateur ne l’a pas vu). Enfin, le Elvis Opérateur est très pratique mais nous fait souvent oublier de mettre un else au bout comme valeur par défaut pour continuer : on plante donc une ligne en dessous du code qui aurait posé soucis parce qu’on a mal initialisé la ligne recevant le Elvis Opérateur.

Bref, la gestion des valeurs null est extrêmement intéressante mais reste à bien encadrer, surtout avec des débutants que le faux sentiment de sécurité induit en erreur. Sur le projet qui a permis de se faire la main, on n’a pas échappé à quelques NullPointerException et ni le linter, ni le compilateur ne les avaient vues.

//Code Java
String lastName ;
if(bob != null && bob.getLastName() != null){
    lastName = bob.getLastName();
}else{
    lastName = "" ;
}
//Equivalent Kotlin
val lastName = bob?.getLastName() ?: "" //conversion en String implicite si getLastName retourne effectivement un String

Comparatif Java/Kotlin sur la gestion de la valeur null avec Elvis Operateur.

Parmi les petites choses intéressantes en vrac sur Kotlin, j’ai noté :

  • Les data classes qui permettent de faire des classes d’objet sans devoir écrire le constructeur ni les getter, setter, toString qui vont avec. Et ça, c’est vraiment bien.
  • Les with et let qui permettent de faire du mapping de valeurs sans devoir toujours les réécrire ou utiliser une variable supplémentaire.
  • L’utilisation directe des variables dans les chaînes de caractères sans devoir faire de concaténation
  • Les coroutines qui permettent de simplifier les traitements asynchrones. Vraiment un bon point car sur Android, ça permet de se passer des AsyncTasks et de grandement simplifier le déroulé des tâches.
  • Des petites choses issues de la programmation fonctionnelles, comme les lambda expressions (incluses dans Java mais pas Android)
//Code Java
public class Person {
    public String name;
    public String surName;
    
    public String toString(){
        return name + " - "+ surName; 
    }
}
//Equivalent  Kotlin
data class Person(var name: String, var surName : String)

Comparatif Java/Kotlin sur une classe de modèle. La DataClass évite d'écrire les getter/setter et méthodes annexes

Au final, l’expérience est plutôt positive. Comme toujours, les subtilités du langage demandent un peu de temps pour être maîtrisées : il y a des éléments vraiment pertinents alors que d’autres se résument à du sucre syntaxique pas toujours bien utilisé en pratique. Si l’apport de certains éléments dans le langage est indéniablement un vrai bénéfice (les data class, au bout de la 15ème, on est bien content de ne pas avoir dû refaire tous les constructeurs, getters et setters en plus), on introduit également de nouveaux points d’attention à ne pas négliger.

Le bénéfice de Kotlin sur Java reste quand même supérieur et je ne doute pas que le langage va encore s’améliorer dans les prochaines années : investir dessus me semble une bonne idée.

Les différents modules

Si l’on revient sur Android Jetpack à proprement parlé, on compte à ce jour une trentaine de modules dans le projet. Je ne les ait pas tous testés, certains n’étant d’ailleurs que des reprises d’anciens modules existants (par exemple, la librairie de compatibilité).

De façon générale, leur intégration dans un projet reste assez aisée : la plupart du temps, il n’y a qu’à ajouter la dépendance qui va bien dans le fichier Gradle et le module peut ensuite être utilisé dans le code. Certains nécessitent d’activer un flag particulier lors du build gradle mais là encore, c’est simple et documenté. Tant que tous les modules sont sur des versions compatibles, ça se passe très bien.

Dans le cadre d’une migration de code, la plupart des imports changent : si vous utilisez la librairie de compatibilité par exemple, on passe d’imports en com.android vers com.androidx. Mais pas de panique, ça a été anticipé : Android Studio propose une option de migration qui fait les changements dans tous les fichiers et ça fonctionne très bien. Cela a pris 10 minutes et je n’ai pas eu a repassé derrière.

Seule précaution à prendre si vous travaillez à plusieurs : gelez les développements et assurez-vous que tout le code est mergé avant de faire la migration vers androidx. Si une personne effectue un commit après coup avec du code non androidx, le gestionnaire de versions va merger des dépendances com.android et com.androidx et là, il faudra faire les rectifications manuellement dans chaque fichier : au lieu d’y passer 10 minutes, on peut y passer facilement 2 heures ou plus.

La problématique est d’ailleurs la même avec un pull sur du code qui a été touché entre-temps : le gestionnaire de version peut très bien merger deux versions de la même librairie. Pour l’avoir expérimenté, il vaut mieux s’assurer que tout soit fixé avant, ça se passe mieux.

Room

Je commence ce tour d’horizon par le module Room, la librairie de persistance des données pour les bases de données SQLite.

Honnêtement, je ne me suis jamais vraiment intéressé aux ORM sur Android. La plupart du temps je gérais une table ou deux pour une application et je n’ai jamais vraiment pris le temps de regarder ce qui se faisait de ce côté-là, si de telles librairies existaient d’ailleurs. Mes requêtes, je les faisais à la main, comme décrit dans la documentation android, avec notamment une gestion de content values que l’on remplissait/dé-remplissait au fur et à mesure des requêtes. Parfois rébarbatif mais sur une table ou deux, ça passe.

On change de dimension quand le nombre de tables augmente et je dois dire que j’ai été bien content de trouver Room à ce moment-là. Room ajoute un niveau d’abstraction sur SQLite et ce n’est pas superflu. Grâce à un jeu d’annotations, on déclare les objets que l’on veut manipuler, les requêtes que l’on souhaite effectuer et le module se charge de faire tout le boulot rébarbatif. Fini donc la gestion parfois laborieuse des ContentValues et les interactions bas-niveau, on retrouve quelque chose de plus fluide à utiliser ; le matching database - objet est transparent, plus besoin de mettre les mains dans le cambouis.

Et ça fonctionne globalement bien sur 80-90% des cas.

@Entity(tableName = "users")
data class User(@PrimaryKey
                @ColumnInfo(name = "userid")
                val id: String = UUID.randomUUID().toString(),
                @ColumnInfo(name = "username")
                val userName: String)

Exemple de classe de modèles avec Room. Les annotations décrivent l'équivalence avec la base de données - code issu de la documentation

//Avec Room
@Insert  //Annotation indiquant que la fonction réalise une insertion
fun insertUser(user: User)

val myUser = User("toto")
insertUser(myUser) //sauvegarde en base
 
 //-----------------   
//Sans Room
val myUser = User("toto")

val db = dbHelper.writableDatabase
val values = ContentValues().apply {
    put("userid", myUser.id)
    put("userName", myUser.userName)
}
db?.insert("users", null, values)

Comparatif pour l'insertion d'un élément en base avec et sans Room

Là où j’ai commencé à rencontrer des problèmes, c’est lorsque j’ai voulu faire des jointures. Là, il faut creuser plus loin dans la documentation car ce n’est pas toujours très simple et on tombe sur de mauvaises surprises. En particulier sur des relations one to one que j’ai parfois été obligé de traiter comme du one-to-many (le many n’ayant qu’un seul élément).

Il y a aussi des soucis de nommages qui ne sont pas explicitement décrit dans la documentation : si vous avez l’habitude d’appeler vos colonnes d’id id, quand vous allez faire vos jointures, la librairie sera incapable de différencier les ids de la table A de la table B et vous retournera donc des colonnes vides. Assez logique en un sens sauf qu’il ne semble pas y avoir de façon simple d’aiguiller correctement les valeurs sur ces quelques requêtes hormis renommer les champs id en id_a et id_b. Un peu dommage.

Néanmoins, dans la grosse majorité des cas, Room est une réelle avancée et une vraie simplification du travail avec base de données SQLite.

Autre gros morceau, le composant Navigation veut modifier la façon dont on enchaîne les écrans (et donc les fragments liés). Il est vrai que c’est un point rébarbatif à traiter, avec une gestion de la pile d’enchainement pas toujours très claire, des Bundles à créer à la main pour transmettre des valeurs, j’en passe et des meilleurs.

Screenshot d'android studio sur l'édietru de graphe. On y voit deux fragments reliés l'un à l'autre ainsi que le panneau d'option pour configurer la transition entre eux.

Exemple de graphe dans Android Studio (image issue de la documentation)

Ici, on propose de gérer les enchainements d’écrans plus graphiquement. Sous Android Studio, on ajoute les fragments et on dessine les liens entre eux. Les liens sont ensuite paramétrables et on peut indiquer s’ils doivent être inscrits dans la pile d’écran ou non, s’ils ont des paramètres, des animations, etc. Cela génère ensuite un graphe (au format XML) qu’il n’y a plus qu’à relier ce graphe au nouveau composant NavHostFragment que l’on inclut dans le layout de l’activité cible. Côté code, les enchainements deviennent plus simples : il suffit, dans le fragment cible, de récupérer un composant NavController qui contient tous les enchainements d’écrans décrit dans le graphe et d’indiquer quel lien on souhaite utiliser.

Dis comme ça, ça peut paraître un peu complexe et il faut très honnêtement essayer une fois sur un petit proto pour bien comprendre le fonctionnement, d’autant que ça pousse à une architecture mono-activité. Mais une fois maîtrisé, c’est royal.

Rien que le fait de visualiser graphiquement les différents fragments et les liens entre eux fait gagner un temps énorme : plus besoin de passer en revue 10 fichiers pour trouver la bonne interaction. Côté code, les enchainements sont simplifiés (plus besoin d’appeler le FragmentManager et de gérer la pile manuellement), le passage d’un paramètre se résume à un simple ajout du dit paramètre lors de l’appel à l’enchainement au lieu de créer un Bundle spécifique, le module se chargeant automatiquement d’encapsuler/décapsuler correctement la valeur (via SafeArgs).

//Avec le composant de navigation
override fun onClick(view: View) {
    val action =
        SpecifyAmountFragmentDirections
            .actionSpecifyAmountFragmentToConfirmationFragment("monParam")    
            //Récupération du "lien" entre les deux fragments, "monParam" est une valeur à transmettre entre les deux, déclarée sur le graphe de navigation
    view.findNavController().navigate(action)   // déclenchement du changement de fragment
}

 
 //-----------------   
//Sans le composant navigation

val confirmationFragment = ConfirmationFragment()

//Passage du paramètre
Bundle args = Bundle()
args.putString("param", "monParam")
confirmationFragment.arguments = args

//Déclenchement du changement de fragment
val transaction = supportFragmentManager.beginTransaction().apply {
  replace(R.id.fragment_container, confirmationFragment)
  addToBackStack(null)
}
transaction.commit();

Comparatif pour le changement de fragment avec et sans navigation component

Je note quand même quelques bémols mais qui seront surement corrigé par la suite :

  • Le mode graphique consomme beaucoup de ressources quand on commence à avoir beaucoup d’écrans affichés. C’est logique mais ça oblige à aller directement dans le XML, qui est moins pratique.
  • Le mode graphique n’affiche que le mode portrait. Dommage car sur les applications tablette, les formats portraits/paysages sont souvent très différents et il est bizarre de devoir penser des enchaînements d’écrans paysages en voyant la version portrait
  • Le composant donne une fausse impression de simplicité préjudiciable aux débutants : toute la partie sur la gestion de la pile des écrans est bien mais masque finalement la réalité de la plateforme derrière. Quand on maîtrise, aucun souci, on retrouve ses petits et c’est un vrai plus. Quand on est moins à l’aise, on passe à côté de certaines choses et on ne comprend plus certains enchainements d’écrans (surtout les retours arrière) alors que sur graphique, ils semblent corrects, juste parce que les options de gestion de la pile sont mal renseignées.
  • Le nommage des relations entre écrans a bien gérer. Par défaut, le composant la nomme fragmentA_to_fragmentB. C’est bien sauf que si l’on souhaite ajouter un fragment A2 entre A et B, soit on déplace le lien graphique de A vers A2 et dans ce cas le nom du lien reste fragmentA_to_fragmentB (ça devient vite compliqué de se rappeler que non, ce n’est pas vers B qu’il va mais vers A2), soit supprimer le lien et en recréer un nouveau avec un nouvel id (mais il faut mettre à jour le code). Donc il faut bien faire attention au nommage des relations : un fragmentA_next et fragmentA_back sera peut-être plus utile qu’un fragmentA_to_fragmentB qu’il faudra renommer régulièrement.

Néanmoins, ce module reste une grande avancée dans le développement et les quelques erreurs de jeunesses seront certainement corrigées.

Databinding, LiveData et ViewModel

Il s’agit ici de 3 modules différents mais j’en parle de façon groupée car leur utilisation commune s’impose assez vite :

  • DataBinding : permet de relier directement des objets/variables/méthodes et les layout XML. Cela se traduit aussi bien par le passage d’une variable (par exemple, un viewModel) que la création d’Adapter personnalisés pour le XML (en gros, ajouter de nouveaux attributs aux balises XML).
  • LiveData : encapsule des variables/objets dans une surcouche qui écoute et notifie des changements de la variable contenue. Par exemple, couplée avec le dataBinding, ça permet de mettre automatiquement à jour l’interface lorsqu’une valeur est mise à jour, sans devoir forcément aller cibler explicitement le composant ou relancer les traitements associés.
  • ViewModel : déporte la gestion des variables/objets propres à une interface dans un objet non sensible aux interruptions du cycle de vie. Classiquement, lorsqu’on tourne son téléphone, l’activité et le fragment sont détruits puis recrées, ce qui oblige à sauvegarder/recharger tout, en plus de sauvegarder/récupérer l’état temporaire de l’interface (par exemple, si l’utilisateur a commencé à saisir des données). Le ViewModel se charge de ça : il suffit de l’enregistrer auprès du fragment concerné et de déporter les données sensibles aux interruptions et le ViewModel se chargera de les gérer au fil des interruptions, sans devoir trop s’en soucier.

Honnêtement, ce trio révolutionne beaucoup la construction des écrans et des échanges interface/code. Très vite, le ViewModel s’impose comme une solution pratique pour la gestion des données modifiées par les interruptions. Dès lors, le Databinding de ce ViewModel directement auprès de l’interface trouve son sens et les LiveData achèvent le tout.

Schéma comparant le cycle de vie d'une activité avec celui dun view model. On remarque que le viewModel est insensible au changement d'état de l'activité, jusqu'à la destrcution de celle-ci

Cycle de vie du ViewModel et d'une activité (image issue de la documentation)

En gros, cela permet de s’affranchir de la plupart des traitements d’ajouts des listeners (on appelle directement la bonne méthode du ViewModel depuis l’interface) et de mise à jour de l’interface (setText, etc.) grâce aux LiveData qui propagent directement les nouvelles valeurs. On élague ainsi beaucoup de code et j’ai été surpris de voir que des écrans où l’on montait à une centaine de ligne de code se trouvait réduit à une vingtaine, la grande majorité étant juste les affectations d’écouteurs et la mise à jour des valeurs. Au final, les layouts XML prennent un peu plus de poids (mais ça reste limité) et le code de l’autre côté s’allège énormément, améliorant par là sa relecture et sa maintenabilité.

<!--Avec dataBinding d'un view model dans le xml-->
<TextView
    android:text="@{viewmodel.userName}" />
 
 //-----------------   
//Sans dataBinding, dans le code
findViewById<TextView>(R.id.sample_text).apply {
    text = viewModel.userName
}

Comparatif pour le remplissage d'une valeur dans un champ texte avec dataBindg et sans dataBinding

En revanche, comme pour le composant précédent, ce trio donne un faux sentiment de simplicité et masque pas mal d’éléments sous-jacents. Il y a toujours des cas un peu à la marge qui nécessiteront de faire de la gestion manuelle des traitements suite aux modifications du cycle de vie et qui demandent un minimum de connaissances sur le sujet. De même, une mauvaise déclaration du ViewModel et des LiveData peut conduire à l’introduction de fuites mémoires : il faut y prêter attention.

Néanmoins, là encore, le bénéfice l’emporte largement.

Autres Composants

Comme je n’ai pas utilisé tous les composants et que certains ne sont pas d’aussi grandes avancées que les autres (ou juste une fusion de plusieurs briques), je les détaille succinctement ici :

  • AppCompat : c’est la librairie de support. La seule différence ici est le changement de l’import qui passe de com.android à com.androidx, le reste est similaire à ce qui existait déjà.
  • Layout : c’est la compilation des layouts existants où se retrouve le fameux ConstraintLayout. Concernant ce dernier, ça reste le composant de positionnement ultime mais toujours incroyablement complexe à utiliser, d’autant que l’éditeur visuel ne fait pas forcément les choix les plus judicieux lors du positionnement des contraintes. Un composant vraiment très bien, puissant, mais extrêmement difficile d’accès
  • WorkManager : c’est un ensemble de méthode pour la gestion de certaines tâches de fond asynchrones. Par exemple, lancement d’une synchronisation avec un serveur lorsque la connexion wifi est disponible. Très clairement ma bête noire : je me suis arraché les cheveux dessus pour programmer certaines tâches à intervalles réguliers, il y a énormément de petites subtilités à prendre en compte et je pense que je me suis battu un peu trop avec Doze, le système draconien de gestion des tâches d’arrière-plan. Je pense qu’il me manque encore un peu de maîtrise du sujet mais ça semble quand même complexe à maîtriser.
  • Permisions : le module de gestion des permissions. Rien de nouveau sous le soleil.
  • Test : là encore, c’est principalement le couple JUnit/Espresso. Quelques petites nouveautés mais rien de révolutionnaire.

Les composants non cités sont ceux que je n’ai pas utilisés.

Conclusion

Parler de révolution pour Android Jetpack ne serait pas usurpé. Lorsque j’ai développé ma première application avec, j’ai vraiment eu l’impression de changer de technologie sans être complètement dans quelque chose de nouveau. En tout cas, ces nouveautés apportent de vraies choses et proposent de vraies solutions à des problèmes que présentait la plateforme : on a désormais une stack technique complète, pensée et qui devrait permettre d’aboutir à une homogénéisation des développements et règles de production entre les équipes.

Je suis personnellement conquis par ces nouveaux éléments et à l’heure actuelle, je pense que tout nouveau projet d’application Android devrait inclure les éléments de Jetpack. Pour les projets existants, il faut juger sur place : si l’application est conséquente, bien établie et fonctionne correctement, le coût de la migration ne sera pas forcément justifié. Les petites applications, la migration de certaines fonctions peut s’envisager.

Néanmoins, je pense qu’il y a quelques précautions à prendre avant de se lancer :

  • Prendre bien le temps de tout étudier avant. Certaines choses changent complètement et avoir fait des tests au préalables avant de se lancer sur une vraie production est plus que recommandé au risque d’avoir une mauvaise première expérience avec. En cela, le cours de vidéos mis en ligne par Google est une ressource essentielle et indispensable. À suivre de façon assidue, en prenant bien le temps de faire tous les exercices !
  • Bien maîtriser les bases d’Android comme les cycles de vies des activités/fragments. Jetpack encapsule beaucoup de choses et, surtout quand on débute, ça peut créer des manques. On finira probablement comme en web avec des personnes qui connaissent un framework mais assez peu la plateforme en dessous. Cela ne pose pas de problème dans la majorité des cas mais pour l’heure, Jetpack manque encore un peu de maturité pour se passer complètement de mettre les mains dans le cambouis.
  • Embrasser pleinement l’ensemble et pas juste des morceaux. Même si c’est techniquement possible de n’utiliser que certains modules (et que certains sont assez indépendants), ils sont quand même pensés pour s’interfacer les uns avec les autres en harmonie et n’en utiliser qu’une partie conduit à des constructions bizarres parfois. Par exemple, j’ai d’abord utilisé les ViewModel sans LiveData ni DataBinding et je me suis assez vite rendu compte que ça fonctionnait mieux ensemble parce que je me rajoutais un niveau de complexité à essayer de mettre à jour les interface depuis le ViewModel (en plus d’introduire des fuites mémoires et un couplage fort entre les objets).

Rien d’insurmontable. Jetpack, c’est bien

Quelques liens en compléments

  • La documentation officielle de Jetpack : https://developer.android.com/jetpack
  • La documentation officielle de Kotlin : https://kotlinlang.org/docs/reference/ Utile pour chercher certaines informations spécifiques mais pas toujours pratique.
  • Les tutos vidéos sur Udacity pour la prise en main des différents modules : https://www.udacity.com/course/new-android-fundamentals--ud851 . Vraiment très bien fait. Le cours de base est gratuit après inscription et suffisant pour quelqu’un ayant déjà des connaissances en Android. Il ne faut pas se fier aux titres des leçons et je recommande de tout faire : le parcours est logique et l’utilisation des composants Jetpack se fait tout du long, même si ce n’est pas précisé explicitement dans le descriptif. Cela permet aussi, si on ne le connait pas, d’aborder Kotlin en douceur sans devoir apprendre le langage avant.