Introduction au langage de programmation Kotlin

Introduction au langage de programmation Kotlin

Dans le cadre du développement Android via Android Studio, je me suis rabattu sur deux choix pour le langage : Java car je le connais déjà, et Kotlin car c’est actuellement le langage le plus populaire pour cet environnement1. J’ai choisi ce dernier, car j’étais intrigué par son originalité et sa réputation de langage propre et bien conçu.

Java et Android

Par défaut, Android Studio propose une version de Java basée sur la dernière version non-commerciale : Java 8. Il n’est pas possible d’utiliser les versions les plus récentes de Java Standard Edition développées par Oracle (actuellement en version 15). Nous n’avons donc pas accès à beaucoup de fonctionnalités standard dans les langages modernes, comme l’inférence des types via le mot clé « var », les « switch » à retour direct, les blocs de texte (équivalents de « String Template » en javascript), et bien d’autres.

Kotlin

La nature commerciale de Java, et son contrôle par Oracle avec lequel Google a eu beaucoup de conflits légaux (spécifiquement à propos de Java2) a poussé ce dernier à choisir un nouveau langage moderne. Ce choix s’est porté sur Kotlin, développé par JetBrains (qui développait déjà IntelliJ IDEA sur lequel était basé l’IDE Android Studio3). Kotlin utilise la machine virtuelle Java mais peut aussi être transpilé en JavaScript.

Inférence de types sur les variables

L’inférence des types des variables est gérée par Kotlin. Supposons cette déclaration d’une liste mutable ingrédients, puis une autre immutable dans Java 8 :

ArrayList<Ingredient> mutableIngredients = ingredientDao.findAll();
final ArrayList<Ingredient> immutableIngredients = ingredientDao.findAll();

Voiçi l’équivalent dans Kotlin :

var mutableIngredients = ingredientDao.findAll();
val immutableIngredients = ingredientDao.findAll();

Gestion stricte des pointeurs « null » pour éviter les erreurs

Obligation de spécifier le statut « nullable » des variables

Les « NullPointerException » sont une des erreurs les plus courantes en programmation. Kotlin a été conçu dans le but de les éviter4.

Par défaut, les variables ne sont pas « nullables ». Je ne peux donc pas faire ça :

var myIngredient: Ingredient = null;

Je dois spécifier que la variable peut être null avec « ? » :

var myIngredient: Ingredient? = null;

Obligation d’initialiser les propriétés

Toutes les propriétés non-abstraites doivent être initialisées à leur déclaration, même si elles sont nullables. Elles ne sont pas « null » par défaut. Impossible donc de faire cela :

private val myIngredient:Ingredient;

…ou même cela :

private val myIngredient:Ingredient?;

… sans obtenir le message d’erreur suivant :

Erreur indiquant qu’une propriété doit être initialisée

Obligation de couvrir l’éventualité d’un « null » dans la logique

La possibilité d’avoir une valeur nulle avec un objet « nullable » doit être gérée à chaque endroit du code. Prenons cette déclaration d’une variable pouvant être nulle (donc de type « Ingredient? ») en l’absence d’Ingredient ayant un ID égal à 17 :

var myNullableIngredient = ingredientDao.findById(17);

Si je l’utilise dans le « if » suivant :

if ( myNullableIngredient.name == "poivre" ){

…j’ai ce message d’erreur :

Erreur indiquant que la possibilité d’un « null » n’est pas prise en compte

…qui disparait si j’utilise l’opérateur « null safe » représenté par « ?. »

if ( myNullableIngredient?.name == "poivre" ){

…car si « myNullableIngredient » est null, cette expression équivaut à cela :

if ( null == "poivre" ){

Un autre opérateur utile pour gérer les nulls est l’opérateur « elvis » représenté par « ?: », qui retourne l’expression de droite uniquement si celle de gauche est égale à « null ». Prenons cette déclaration :

var myIngredient:Ingredient = myNullableIngredient

…elle me donne l’erreur suivante car j’initialise un Ingrédient « non nullable » depuis un ingrédient « nullable »:

Erreur indiquant que je ne peux pas mélanger les objets « nullables » et « non nullables »

Si je réécris la déclaration de la manière suivante, « myIngredient » prend pour valeur un Ingrédient vide dans le cas ou « myNullableIngredient » a pour valeur « null », et je n’ai plus d’erreur :

var myIngredient:Ingredient = myNullableIngredient ?: Ingredient()

Pour les amateurs de « NullPointerException »…

Une des seules manières d’obtenir une erreur « NullPointerException » dans Kotlin est d’utiliser l’opérateur « !! » , qui signale qu’on doit supposer qu’une variable n’a pas une valeur nulle. Le code suivant est donc valide :

if ( myNullableIngredient!!.name == "poivre" ){

Avoir un code « null safe » est un énorme avantage niveau stabilité et facilité de tests. On utilise cet opérateur à nos risques et périls, car l’exemple précédent lèvera une exception si « myNullableIngredient » est null.

Apprendre à se passer de variables et de méthodes statiques

Dans le même esprit, Kotlin a délibérément omis les variables et méthodes statiques. En effet, une variable statique n’a pas de contexte : c’est l’équivalent d’une globale. Abuser de variables globales porte à faire du code « spaghetti », dont le flux et les interactions sont imprévisibles.

Pour la configuration globale de mon application Android par exemple, il aurait été pratique d’avoir une variable « configuration » statique, pour ensuite par exemple récupérer mon jeton API depuis n’importe quel endroit. Impossible avec Kotlin. Je dois donc définir ma propre classe d’application dans le « manifest » :

<application
    android:name=".lib.NestiMobileApplication"

…qui hérite de « Application » et possède une propriété « configuration » :

class NestiMobileApplication : Application() {

    lateinit var configuration:ApplicationConfiguration

Ce contexte d’application étant disponible dans la plupart de mes classes Android, je peux maintenant accéder à ma propriété « configuration », par exemple depuis mes « Activity » depuis leur propriété « applicationContext ».

Dans une classe ou je n’ai pas accès à ce contexte mais qui a besoin d’accéder à la configuration, je dois faire passer mon objet de configuration dans le constructeur quand j’en initialise une instance :

val dataSource = NestiApiDataSource(applicationContext.configuration)

Le contenu de mon fichier de configuration sera maintenant accessible depuis ma classe de données API :

configuration.getNode("api/@clientToken").stringValue

Objets « compagnon »

Dans les cas où je ne peux pas utiliser la stratégie d’« injection  de dépendance » dans le constructeur comme avec l’exemple précédent,  il existe un concept d’objet « companion » pour accéder à une méthode de classe sans avoir à l’instancier .

Par exemple, dans mon application Android j’utilise un objet « StatusContainer » qui contient ces propriétés :

  • Une référence vers un objet « contenu », par exemple une liste d’entités « Ingredient » que je récupère d’un API
  • Un statut, par exemple « SUCCESS » (la récupération a réussi), « LOADING » (la récupération est en cours), « ERROR » (la récupération a raté).
  • Un message en cas d’erreur

Cela permet à ma vue de changer son affichage de l’objet « contenu » (liste d’ingrédients) en fonction de son statut simplement en « observant » mon objet « StatusContainer » :

  • Quand je demande une liste d’ingrédients à l’API, mon objet « StatusContainer » observé est initialisé à la valeur « StatusContainer.loading() » qui retourne un objet « StatusContainer » sans objet « contenu », et avec un statut égal à « LOADING ». Ma vue affiche alors un indicateur de chargement.
  • Quand je récupère ma liste, mon objet observé passe à « StatusContainer.success(ingredients) » qui retourne un objet « StatusContainer » ayant « ingredients » comme objet contenu, et un statut du conteneur de « SUCCESS ». Ma vue sait qu’elle peut afficher la liste à ce moment.
  • En cas d’erreur, je change la valeur de mon objet observé à « StatusContainer.error(« Mon message d’erreur ») » et ma vue en sera prévenue et pourra afficher mon message.

Ces appels sont identiques à des appels de méthodes statiques dans Java et sont un exemple de pattern « Factory ». Ils sont implémentés via un objet « companion » qui est en fait un « Singleton » créé à la première utilisation de « StatusContainer » :

Exemple de « companion object »

Chacune de ces méthodes retourne un StatusContainer (contenant ou pas des données). Elles peuvent être appelées comme une méthode statique.

Derniers mots sur Kotlin

Cette introduction au langage ne fait bien évidemment qu’affleurer ses avantages et ses possibilités. Je suis impressionné par sa nature concise et les astuces qu’il offre pour élaborer un code efficace et fiable. Je compte l’utiliser dans mes futurs projets mobiles.


[1] https://developer.android.com/kotlin

[2] https://en.wikipedia.org/wiki/Google_LLC_v._Oracle_America,_Inc.

[3] https://en.wikipedia.org/wiki/Android_Studio

[4] https://kotlinlang.org/docs/null-safety.html#nullable-types-and-non-null-types


Erik Shea

2
0

Laisser un commentaire