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.
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 $
…
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.
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.
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é
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.
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
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.
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)
}
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
.
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
Kotlin s’est servi des fonctions d’ordre supérieur (et des extension) pour simplifier l’utilisation des stream Java
public inline fun <T> Iterable<T>.find(predicate: (T) -> Boolean): T? {
return firstOrNull(predicate)
}
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.
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
}
}
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 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.
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.
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.
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/