Dev-Mind

Kotlin langage de référence Android

07/06/2019
Android 

En 2017 une grande annonce a été faite à Google IO. Le langage Kotlin devenait le deuxième langage de référence pour développer des applications Android. Kotlin a été créé par la société JetBrains, éditeur de Android Studio (IDE de référence pour le développement Android). JetBrains collabore depuis plusieurs année avec Google pour toujours améliorer ce studio de développement. Ce studio a été initialement été écrit en Java puis en Kotlin.

Pourquoi Google a t’il fait cette annonce ? Etait ce lié au procès avec la société Oracle sur l’utilisation de Java ? Etait ce lié aux possibilités offertes par ce langage ? Etait ce lié aux possibilités offertes par cette collabiration étroite entre les deux sociétés ?

Personnellement je pense que Kotlin a été adopté pour toutes ces raisons. Kotlin a essayé de mêler le meilleur de différents langages et je trouve qu’il a véritablement redonné un coup de boost aux développeurs Android (moi le premier). Deux ans après, 50% des développeurs Android utilisent Kotlin.

En mai à Google I/O 2019, Google a annoncé une nouvelle étape dans l’adoption de Kotlin. Les développements Android deviennent Kotlin-first. Google conseille aux développeurs d’utiliser Kotlin pour les nouveaux développements. En interne, les librairies commencent aussi à être écrite directement en Kotlin. Comme Kotlin est 100% interopérable avec Java ce virage ne va pas pénaliser les projets existants.

Dans cet article, nous allons revenir sur les intérêts du langage pour comprendre pourquoi Kotlin est devenu le langage de référence pour les développements Android.

Kotlin et Android

Kotlin simplifie la syntaxe

Quand nous utilisons le langage Java et tout particulièrement quand nous devons écrire une application Android, nous devons écrire beaucoup de code fastidieux. Kotlin met en avant le pragmatisme et la simplicité.

La philosophie de Kotlin est :

Tout ce qui peut être déduit par le compilateur, n’a pas besoin d’être écrit par le développeur.

Prenons l’exemple d’une classe Java permettant d’être exécutée et d’afficher un message Hello World.

public class HelloWorld {
    public static void main(String[] args) {
        String name = "Dev-Mind";
        System.out.println("Hello world " + name);
    }
}

En Kotlin vous pouvez faire la même chose en écrivant

fun main(){
    val name = "Dev-Mind"
    println("Hello world $name")
}
  • La visibilité public est celle par défaut et donc plus besoin de la définir à chaque fois

  • Vous pouvez écrire des fonctions non attachées à une classe (le compilateur le fera pour vous)

  • Les points virgules ne sont plus nécéssaires

  • Kotlin fait beaucoup d’inférence de type et vous n’avez pas besoin de définir le type si le compilateur peut le déduire (exemple du name)

  • Vous pouvez utiliser des templates de String et directement accéder au contenu d’une variable avec $

  • …​

Null safety

L’erreur la plus courante pour un développeur Java, est de se retrouver avec un programme qui plante suite à une exception Null Pointer Exception. En Java, un objet non alloué à une référence nulle. En Kotlin le null est interdit. Vous aurez une erreur de compilation si vous déclarer un objet et que ce dernier n’est pas initilialisé. Si vraiment vous voulez gérer une valeur nulle, tout le système de type a été doublé et vous pouvez ajouter ? à un type pour dire qu’une valeur peut être nulle

Par exemple

var name:String         // Erreur de compilation
var name:String = null  // Erreur de compilation
var name = "Dev-Mind"   // Valide et pas besoin de définir un type car le compilateur peut le deviner
var name:String? = null // Valide car on utilise le type String? qui veut dire String nullable

Au premier abord, cette fonctionnalité peut paraître contraignante mais c’est un réel plaisir à l’utiliser et ceci évite bon nombre de bugs d’inadvertance.

Immutabilité

Une autre force de Kotlin est de préconiser l’immutabilité. Quand vous définissez une valeur avec le mot clé val elle est non mutable. Si vous voulez changer une référence plus tard vous devrez utiliser le mot clé var.

val name = "Dev-Mind"
name = "Guillaume"      // Erreur de compilation car immutable

var name2 = "Dev-Mind"
name2 = "Guillaume"     // OK car mutable

Kotlin implémente les API Java pour les listes mais distingue les listes mutables et non mutables. Par défaut tout est immutable. Si vous voulez une liste mutable vous devez le préciser

val names = listOf("Dev-Mind", "Guillaume")
names.add("NewName")    // Erreur de compilation car add n'existe pas sur une liste immutable


val names = mutableListOf("Dev-Mind", "Guillaume")
names.add("NewName")

Même si Kotlin distingue les listes mutables et non mutables, Kotlin n’a pas réinventé de nouvelles classes pour gérer les listes. Kotlin s’appuie sur les types existants Java.

Kotlin vous pousse à appliquer des principes de la programmation fonctionnelle (dont l’immutabilité) pour le plus grand bien de votre code.

Valeurs par défaut

Kotlin vous permet de préciser des valeurs par défaut à vos différents paramètres de vos méthodes

Par exemple avec le code suivant,

fun formatDate(string: Date, format: String = "yyyy-MM-dd", addDay: Int =0) : String

vous pouvez avoir différentes manières d’appeler cette méthode

formatDate(Date())              // On ne précise pas les valeurs si celles par défaut sont suffisantes
formatDate(Date(), "yyyy")      // Dans mon cas je ne change que la deuxième valeur
formatDate(Date(), addDay = 2)  // Si je veux préciser une valeur particulière je peux u tiliser les paramètres nommés

Les paramètres nommés (comme sur la dernière ligne de notre exemple) sont très pratiques quand vous voulez apporter plus de lisibilité à votre code. Par exemple si vous avez la méthode suivante

fun findSpeaker(firstname: String, lastname: String): Speaker

Quand vous appelez votre méthode sans nommer les paramètres vous ne savez jamais si c’est le nom ou prénom qui est en premier. Il suffit que votre collègue change la signature et inverse l’ordre des paramètres et vous avez un bug totalement transparent.

val speaker1 = findSpeaker("Chet", "Haase")
val speaker1 = findSpeaker(firstname = "Chet", lastname = "Haase")  // les paramètres nommés amènent plus de lisibilité

Classes

Les classes sont bien évidemment disponible en Kotlin. Prenons un exemple pour regarder les différences avec les classes Java.

public class Parent{ }
public class Child extends Parent{}

En Java ces deux classes publiques doivent être définies dans 2 fichiers .java différent. En Kotlin vous pouvez écrire le tout dans un seul fichier

open class Parent

class Child : Parent()

Notez que la classe mère doit être précédée du mot clé open. Par défaut les classes Kotlin sont définies comme public final. Si vous voulez ouvrir une classe à la surcharge, vous devrez le préciser.

Classes POJO

Un POJO (Plain Old Java Object) est une simple classe qui va contenir des données. Généralement sur ce type d’objet

  • nous définissons des propriétés private

  • nous générons des constructeurs avec les valeurs obligatoires

  • nous générons des méthodes pour lire et modifier ces propriétés: getter, setter

  • nous générons des méthodes hashcode, equals, copy

  • et parfois nous écrivons aussi des builders pour créer rapidement et partiellement une instance de notre objet

Si j’essaie de créer une classe Speaker avec 4 propriétés id, firstname, lastname et age je vais me retrouver avec une classe d’environ 100 lignes.

Kotlin propose les data class pour lesquelles le compilateur va faire tout ce travail de génération pour vous. Le Pojo speaker se résume au code suivant

data class Speaker(val firstname: String,
                   val lastname: String,
                   val age: Int? = null,
                   val id: String = UUID.randomUUID().toString())

Quand votre classe a un seul constructeur vous pouvez le préciser dans la signature de la classe (comme dans notre classe Speaker). La suppression de tout le code inutile améliore la libilité.

Revenons à notre exemple, vous pouvez ainsi écrire

val s1 = Speaker("Chet", "Haase")
val s2 = Speaker(firstname = "Chet", lastname = "Haase")
val s3 = Speaker(firstname = "Chet", lastname = "Haase", id = "123")

val s4 = s1.copy(age = 999)
val s5 = s1.copy()

Le langage propose aussi la surcharge des opérateurs. L’opérateur == est surchargé et fait appel à la méthode equals.

s1 == s5   // => renvoie true car Kotlin fait appel à la méthode equals
s1 === s5  // => renverra faux car === permet de comparer des références

Classes internes

Quand vous programmez une application Android en Java, vous utilisez très souvent des classes internes.

public class HelloWorld {

    public String name(){
        return "Dev-Mind";
    }

    class A {
        public void hello(){
            System.out.println("Hello world" + name()); // NE COMPILE PAS car la méthode name() n'est pas visible
        }
    }
}

Les classes internes en Java (inner class) sont non statiques par défaut et vous pouvez donc utiliser les méthodes ou attributs globaux de la classe englobante dans la classe interne. Par exemple dans la classe A je peux utiliser la méthode name() de ma classe englobante HelloWorld.

Une classe interne non statique a une référence vers sa classe englobante. Si cette dernière n’est plus utilisée, le garbage collector ne peut pas faire son travail et la supprimer. En effet elle considérée active (utilisée par la classe interne). Dans un serveur d’application, quand nous utilisons des singletons ce concept ne pose pas de problème. Dans le monde Android, sur un device avec des ressources limitées, c’est plus problématique. Surtout si nous utilisons des classes internes dans des objets qui sont très souvent détruits et reconstruits (les activités sont supprimées et recréées après chaque changement de configuration). De nombreux développeurs se font avoir et introduisent des fuites mémoires de cette manière dans leurs applications

En Java pour éviter le problème vous devez utiliser des static inner class. En Kotlin quand vous créez une nested class vous n’avez pas accès aux variables et méthodes de la classe (équivalent d’une classe interne statique)

class HelloWorld {

    fun name() = "Dev-Mind"

    class A {
        fun hello() {
            println("Hello world" + name())
        }
    }
}

Vous pouvez tout de même créer l’équivalent d’une inner class en utilisant la syntaxe internal inner class. Une fois encore le langage a pris le parti de simplifier le cas d’utilisation le plus courant.

Classes anonymes

En Android nous écrivons souvent des classes anonymes. Par exemple à chaque fois que nous écrivons un listener d’événement. Nous avons le même problème de référence entre la classe englobante et la classe anonyme.

button.setOnClickListener{
      // votre code
}

Kotlin ne propose pas de solution dans ce cas, mais vous devez garder conscience que vous devrez toujours casser cette référence à la classe englobante quand l’objet sera arrêté ou recyclé.

override fun onStop() {
    super.onStop()
    button.setOnClickListener(null)
}

Extensions de fonction

Quand nous programmons nous utilisons de nombreuses librairies externes sur lesquelles nous n’avons pas la main. Prenons un cas d’utilisation. Nous somme l’INSEE et nous devons faire des statistiques par âge

Un citoyen est défini par la data class suivante

data class Citizen(val inseeNumber: String,
                   val firstname: String,
                   val lastname: String,
                   val sexe: Sexe,
                   val birthdate: LocalDate)

Pour déterminer l’âge vous pouvez écrire une classe utilitaire

fun getAge(date: LocalDate) = LocalDate.now().year - date.year

Avec Kotlin vous pouvez aussi étendre la classe LocalDate et créer une nouvelle méthode (extension de fonction) qui vous sera propre et que vous pourrez utiliser dans tout votre projet. Par exemple

fun LocalDate.getAge() = LocalDate.now().year - this.year

// Ce qui permet d'écrire
LocalDate.parse("1977-01-01").getAge()

Mieux au lieu d’exposer une fonction vous pouvez exposer une propriété

val LocalDate.age
    get() = LocalDate.now().year - this.year

// Ce qui permet d'écrire
LocalDate.parse("1977-01-01").age

Prenons un autre exemple lié à Android. Très souvent quand nous créons une application, nous surchargeons l’objet Application Android pour créer notre propre instance. Pour éviter les cast à répétition dans les activités vous pouvez écrire

class DevMindApplication : Application() {
   // code...
}
val AppCompatActivity.devmindApp
    get() = this.application as DevMindApplication

Ainsi dans vos activités vous pouvez directement faire appel à votre instance de l’application en utilisant devmindApp.

Fonctions d’ordre supérieur

Une fonction d’ordre supérieure est une fonction qui prend une fonction comme argument.

Dans ce cas vous n’avez pas besoin de passer une lambda lors de l’appel à la méthode mais vous pouvez ajouter un bloc d’exécution juste après l’appel de la méthode

Dit comme ça vous devez être perdu et c’est normal

Exemple dans le langage

Kotlin s’est servi des fonctions d’ordre supérieur (et des extension) pour simplifier l’utilisation des stream Java

Issu de kotlin.collections
public inline fun <t> Iterable<t>.find(predicate: (T) -> Boolean): T? {
    return firstOrNull(predicate)
}</t></t>

Si nous avons une collection de speakers nous pouvons sélectionner le premier qui a le prénom Guillaume via ce code

val guillaume = speakers.firstOrNull {
    it.firstname == "Guillaume"  // it correpond à l'item en cours
}

// Vous auriez pu aussi écrire
val guillaume = speakers.firstOrNull { speaker ->
    speaker.firstname == "Guillaume"
}

// Ici la syntaxe Java (où vous passez une lambda provoque une erreur de compilation)
val guillaume = speakers.firstOrNull(speaker -> speaker.firstname == "Guillaume") // ne compile pas

En Java, pour rappel vous auriez écrit

val guillaumeSpeakers = speakers.stream()
                                .filter(s -> s.getFirstname().equals("Guillaume"))
                                .findFirst()
                                .orElse(null);

L’API Stream Java est très agréable à utiliser, mais les collections et les fonctions d’extensions Kotlin le sont encore plus.

Ecrire un DSL

Kotlin est de plus en plus connu pour la souplesse offerte pour écrire un DSL avec un typage fort. Gradle est en train par exemple de remplacer Groovy par Kotlin pour avoir un DSL plus puissant

Un exemple

class Cell(val content: String)

class Row(val cells: MutableList<cell> = mutableListOf()) {
    fun cell(adder: () -> Cell): Row {
        cells.add(adder())
        return this
    }
}

class Table(val rows: MutableList<row> = mutableListOf()) {
    fun row(adder: () -> Row): Table {
        rows.add(adder())
        return this
    }
}</row></cell>

Dans ma classe Table j’ai ajouté une fonction row (avec une fonction en argument) qui permet d’ajouter une ligne. La même chose a été faite dans la classe Row pour une cellule. Du coup je peux écrire

val table = Table()
    .row { Row().cell { Cell("Test") }}
    .row { Row().cell { Cell("Test2") }}

Android

Android bénéficie beaucoup des fonctions d’ordre supérieur et des extensions. Ces fonctionnalités du langage ont permis de considérablement simplfier le langage. Prenons des exemples

Ecriture d’un listener d’événement

itemView.setOnClickListener {
     // Code du listener directement
}

Quand vous devez itérer et enchainer l’appel à plusieurs setters d’un objet

holder.speakerName.text = user.fullname
holder.speakerBio.text = user.descriptionFr
holder.speakerBirthday.text = user.birthday

// => devient
holder.apply {
    speakerName.text = user.fullname
    speakerBio.text = user.descriptionFr
    speakerBirthday.text = user.birthday
}

Et il y a des dizaines d’autres exemples.

Coroutines

Une coroutine est un bloc de traitement qui permet d’exécuter du code non bloquant en asynchrone. C’est un thread allégé. Vous pouvez lancer plein de couroutines sur un même thread. Vous pouvez aussi démarrer un traitement sur un thread et finir son exécution sur un autre.

Commençons par faire un rappel sur le développement Android. Quand une application est lancée, elle est lancée sur un thread principal. On parle de main thread ou UI thread. En effet le rendering, les événements, les appels systèmes sont gérés sur ce thread.

Android Main Thread

Si vous lancez un traitement métier plus ou moins long (calcul, récupération de données, accès à une base), vous ne devez pas encombrer ce thread principal pour ne pas bloquer l’utilisateur. Par exemple si vous lancez une requête base de données, tout est figé tant que la réponse n’est pas traitée. Android est d’ailleurs intolérable la dessus. Si votre application bloque le thread principal, le système killera votre application.

Sans Kotlin, vous devez lancer tous les traitements plus ou moins longs dans un autre thread. Et quand vous avez un résultat vous devez interagir avec la vue dans le thread principal pour que les données soient actualisées. Niveau code vous devez écrire un bon nombre de ligne pour écrire tout ça.

En Kotlin vous pouvez passer par les Coroutines. Dans l’exemple si dessous nous déclarons une activité qui va lancé un accès à la base dans une coroutine et quand le résultat est là nous nous raccrochons au thread principal pour mettre à jour la vue.

// Votre activité implemente l'interface CoroutineScope
open class MyActivity : AppCompatActivity(), CoroutineScope {

   // Si vous lancez votre coroutine vous devez indiquer dans quel thread elle sera lancé. Par défaut un nouveau
   override val coroutineContext: CoroutineContext
       get() = Dispatchers.Default

   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)

       // ...

       // Lancement de la coroutine
       launch {

          // Vous faites un traitement plus ou moins long (appel base de données)
		  val speaker = speakerDao.readOne(speakerUiid)

          // Quand vous avez un résultat vous vous rattachez au thread principal
          // pour mettre à jour la vue
		  withContext(Dispatchers.Main){
             speaker.apply {
                  speakerLastname.text = speaker.lastname
                  speakerCountry.text = speaker.country
             }
          }
 	   }
   }
}

Les couroutines simplifient tous les appels acynchrones, ou les appels synchrones pouvant être longs de votre application. Le code est plus restreint, plus lisible mais aussi plus performant car les couroutines sont beaucoup plus légères qu’un thread.

Conclusion

J’ai essayé de vous montrer dans cet article pourquoi Kotlin est bien plus qu’une alternative à Java pour l’écriture des applications Android.

Je vous conseille cette vidéo de Jean Baptiste Nizet qui montre l’intérêt de ce que je viens de dire en livecoding (sauf l’aspect coroutine).

Personnellement je pense que le langage Java va petit à petit disparaître sur Android. Si vous voulez utiliser Kotlin en dehors d’Android vous pouvez le faire sans problème. Kotlin fait aussi partie des langages supportés par le framework Spring.

Pour plus d’informations sur Kotlin & Android vous pouvez aller sur https://developer.android.com/kotlin/


Article précédent : Application pour saisir les scores
Article suivant : Comprendre la programmation Android Vous pouvez laisser un commentaire ou poser une question sur cet article par mail ou sur twitter.