Apprendre le langage de programmation Kotlin

Kotlin est un langage de programmation largement utilisé par les développeurs Android du monde entier. Cet article est un cours d'initiation à Kotlin qui vous aidera à démarrer rapidement.

Déclaration de variable

Kotlin utilise deux mots clés différents pour déclarer des variables : val et var.

  • Utilisez val pour une variable dont la valeur ne change jamais. Vous ne pouvez pas réattribuer une valeur à une variable déclarée à l'aide de val.
  • Utilisez var pour une variable dont la valeur peut changer.

Dans l'exemple ci-dessous, count est une variable de type Int à laquelle est attribuée une valeur initiale de 10 :

var count: Int = 10

Int est un type qui représente un nombre entier, l'un des nombreux types numériques pouvant être représentés dans Kotlin. Comme pour d'autres langages, en fonction de vos données numériques, vous pouvez également utiliser Byte, Short, Long, Float et Double.

Le mot clé var signifie que vous pouvez réattribuer des valeurs à count si nécessaire. Par exemple, vous pouvez modifier la valeur de count de 10 à 15 :

var count: Int = 10
count = 15

Cependant, certaines valeurs ne sont pas destinées à être modifiées. Prenons l'exemple d'un String appelé languageName. Si vous souhaitez vous assurer que languageName contient toujours la valeur "Kotlin", vous pouvez déclarer languageName à l'aide du mot clé val :

val languageName: String = "Kotlin"

Ces deux mots clés vous permettent d'indiquer clairement ce qui peut être modifié. Utilisez-les à votre avantage. Si une référence à une variable doit être réattribuable, déclarez-la en tant que var. Sinon, utilisez val.

Inférence de type

Pour reprendre l'exemple précédent, lorsque vous attribuez une valeur initiale à languageName, le compilateur Kotlin peut déduire le type à partir du type de la valeur attribuée.

Comme la valeur de "Kotlin" est de type String, le compilateur déduit que languageName est également une String. Notez que Kotlin est un langage à typage statique. Cela signifie que le type est résolu au moment de la compilation et ne change jamais.

Dans l'exemple suivant, languageName est déduit en tant que String. Vous ne pouvez donc pas appeler des fonctions qui ne font pas partie de la classe String :

val languageName = "Kotlin"
val upperCaseName = languageName.toUpperCase()

// Fails to compile
languageName.inc()

toUpperCase() est une fonction qui ne peut être appelée que sur des variables de type String. Comme le compilateur Kotlin a inféré que languageName est une String, vous pouvez appeler toUpperCase() de façon sécurisée. Cependant, comme inc() est une fonction d'opérateur Int, il ne peut pas être appelé dans un String. La méthode d'inférence de type de Kotlin permet la concision et la sûreté de typage.

Sécurité nulle

Dans certaines langues, une variable de type de référence peut être déclarée sans valeur explicite initiale. Dans ces cas, les variables contiennent généralement une valeur nulle. Par défaut, les variables Kotlin ne peuvent pas contenir de valeurs nulles. Cela signifie que l'extrait suivant n'est pas valide :

// Fails to compile
val languageName: String = null

Pour qu'une variable puisse contenir une valeur nulle, elle doit être de type nullable. Vous pouvez spécifier qu'une variable nullable en ajoutant le suffixe ? à son type, comme illustré dans l'exemple suivant :

val languageName: String? = null

Avec un type String?, vous pouvez attribuer une valeur String ou null à languageName.

Vous devez gérer les variables nullables avec précaution, au risque de générer une erreur NullPointerException. En Java, par exemple, si vous tentez d'appeler une méthode sur une valeur nulle, votre programme plante.

Kotlin propose plusieurs méthodes pour travailler de manière sécurisée avec des variables nullables. Pour en savoir plus, consultez la section Modèles Kotlin courants avec Android : possibilité de valeur nulle.

Expressions conditionnelles

Kotlin propose plusieurs méthodes permettant d'implémenter une logique conditionnelle. La plus courante est l'instruction if-else. Si une expression placée entre parenthèses à côté d'un mot clé if renvoie true, le code qu'elle contient (le code situé immédiatement après et entouré d'accolades) est exécuté. Sinon, le code de la branche else est exécuté.

if (count == 42) {
    println("I have the answer.")
} else {
    println("The answer eludes me.")
}

Vous pouvez représenter plusieurs conditions avec l'instruction else if. Vous pouvez ainsi représenter une logique plus précise et complexe dans une seule instruction conditionnelle, comme illustré dans l'exemple suivant :

if (count == 42) {
    println("I have the answer.")
} else if (count > 35) {
    println("The answer is close.")
} else {
    println("The answer eludes me.")
}

Les instructions conditionnelles sont utiles pour représenter une logique avec état, mais nécessitent souvent des répétitions. Dans l'exemple ci-dessus, vous imprimez simplement un String dans chaque branche. Pour éviter cette répétition, Kotlin permet d'utiliser des expressions conditionnelles. L'exemple peut être réécrit comme suit :

val answerString: String = if (count == 42) {
    "I have the answer."
} else if (count > 35) {
    "The answer is close."
} else {
    "The answer eludes me."
}

println(answerString)

Implicitement, chaque branche conditionnelle renvoie le résultat de l'expression sur sa dernière ligne. Vous n'avez donc pas besoin d'utiliser le mot clé return. Comme le résultat des trois branches est de type String, le résultat de l'expression if-else est également de type String. Dans cet exemple, une valeur initiale est attribuée à answerString à partir du résultat de l'expression if-else. L'inférence de type peut être utilisée pour omettre la déclaration de type explicite pour answerString, mais il est souvent judicieux de l'inclure pour plus de clarté.

Lorsque la complexité de votre instruction conditionnelle augmente, vous pouvez envisager de remplacer votre expression if-else par une expression when, comme illustré dans l'exemple suivant :

val answerString = when {
    count == 42 -> "I have the answer."
    count > 35 -> "The answer is close."
    else -> "The answer eludes me."
}

println(answerString)

Chaque branche d'une expression when est représentée par une condition, une flèche (->) et un résultat. Si la condition du côté gauche de la flèche prend la valeur "true", alors le résultat de l'expression du côté droit est renvoyé. Notez que l'exécution ne passe pas d'une branche à l'autre. Le code de l'exemple d'expression when est fonctionnellement équivalent à celui de l'exemple précédent, mais il est sans doute plus facile à lire.

Les conditionnels de Kotlin mettent en évidence l'une de ses fonctionnalités les plus puissantes : la diffusion intelligente. Plutôt que d'utiliser l'opérateur d'appel sécurisé ou l'opérateur d'assertion "non nul" pour travailler avec des valeurs nullables, vous pouvez vérifier si une variable contient une référence à une valeur nulle à l'aide d'une instruction conditionnelle, comme illustré dans l'exemple suivant :

val languageName: String? = null
if (languageName != null) {
    // No need to write languageName?.toUpperCase()
    println(languageName.toUpperCase())
}

Dans la branche conditionnelle, languageName peut être traité comme une valeur non nullable. Kotlin est suffisamment intelligent pour reconnaître que la condition pour exécuter la branche est que languageName ne contienne pas une valeur nulle. Ainsi, vous ne devez pas traiter languageName comme nullable dans cette branche. Cette diffusion intelligente fonctionne pour les vérifications de valeur nulle, les vérifications de type ou toute condition remplissant les conditions d'un contrat.

Fonctions

Vous pouvez regrouper une ou plusieurs expressions dans une fonction. Plutôt que de répéter la même série d'expressions chaque fois que vous avez besoin d'un résultat, vous pouvez encapsuler les expressions dans une fonction et appeler cette dernière.

Pour déclarer une fonction, utilisez le mot clé fun suivi du nom de la fonction. Ensuite, définissez les types d'entrées que votre fonction reçoit, le cas échéant, et déclarez le type de sortie qu'elle renvoie. Le corps d'une fonction est l'endroit où vous définissez les expressions appelées lorsque votre fonction est appelée.

Sur la base des exemples précédents, voici une fonction Kotlin complète :

fun generateAnswerString(): String {
    val answerString = if (count == 42) {
        "I have the answer."
    } else {
        "The answer eludes me"
    }

    return answerString
}

Dans l'exemple ci-dessus, la fonction porte le nom generateAnswerString. Elle ne reçoit aucune entrée et renvoie un résultat de type String. Pour appeler une fonction, utilisez son nom, suivi de l'opérateur d'appel (()). Dans l'exemple ci-dessous, la variable answerString est initialisée avec le résultat de generateAnswerString().

val answerString = generateAnswerString()

Les fonctions peuvent utiliser des arguments en entrée, comme illustré dans l'exemple suivant :

fun generateAnswerString(countThreshold: Int): String {
    val answerString = if (count > countThreshold) {
        "I have the answer."
    } else {
        "The answer eludes me."
    }

    return answerString
}

Lorsque vous déclarez une fonction, vous pouvez spécifier un nombre illimité d'arguments et leur type. Dans l'exemple ci-dessus, generateAnswerString() accepte un argument nommé countThreshold de type Int. Dans la fonction, vous pouvez faire référence à l'argument en utilisant son nom.

Lorsque vous appelez cette fonction, vous devez inclure un argument entre les parenthèses de l'appel de fonction :

val answerString = generateAnswerString(42)

Simplifier des déclarations de fonction

generateAnswerString() est une fonction assez simple. Elle déclare une variable, puis la renvoie immédiatement. Lorsque le résultat d'une seule expression est renvoyé par une fonction, vous pouvez éviter de déclarer une variable locale en renvoyant directement le résultat de l'expression if-else contenue dans la fonction, comme illustré dans l'exemple suivant :

fun generateAnswerString(countThreshold: Int): String {
    return if (count > countThreshold) {
        "I have the answer."
    } else {
        "The answer eludes me."
    }
}

Vous pouvez également remplacer le mot clé renvoyé par l'opérateur d'attribution :

fun generateAnswerString(countThreshold: Int): String = if (count > countThreshold) {
        "I have the answer"
    } else {
        "The answer eludes me"
    }

Fonctions anonymes

Toutes les fonctions n'ont pas besoin d'un nom. Certaines sont plus directement identifiées par leurs entrées et sorties. Ces fonctions sont appelées fonctions anonymes. Vous pouvez conserver une référence à une fonction anonyme et l'utiliser pour appeler la fonction anonyme ultérieurement. Vous pouvez également transmettre la référence dans votre application, comme pour les autres types de références.

val stringLengthFunc: (String) -> Int = { input ->
    input.length
}

Tout comme les fonctions nommées, les fonctions anonymes peuvent contenir un nombre illimité d'expressions. La valeur renvoyée par la fonction est le résultat de l'expression finale.

Dans l'exemple ci-dessus, stringLengthFunc contient une référence à une fonction anonyme qui prend une String en entrée et renvoie la longueur de la String d'entrée en tant que sortie de type Int. Pour cette raison, le type de la fonction est (String) -> Int. Cependant, ce code n'appelle pas la fonction. Pour récupérer le résultat de la fonction, vous devez l'appeler avec comme vous le feriez pour une fonction nommée. Vous devez fournir une String lorsque vous appelez stringLengthFunc, comme illustré dans l'exemple suivant :

val stringLengthFunc: (String) -> Int = { input ->
    input.length
}

val stringLength: Int = stringLengthFunc("Android")

Fonctions d'ordre supérieur

Une fonction peut utiliser une autre fonction comme argument. Les fonctions qui utilisent une autre fonction comme argument sont appelées fonctions d'ordre supérieur. Ce procédé est utile pour communiquer entre les composants de la même manière que vous utiliseriez une interface de rappel en Java.

Voici un exemple d'une fonction d'ordre supérieur :

fun stringMapper(str: String, mapper: (String) -> Int): Int {
    // Invoke function
    return mapper(str)
}

La fonction stringMapper() prend une String et une fonction qui renvoie une valeur Int à partir d'une String que vous lui transmettez.

Vous pouvez appeler stringMapper() en transmettant une String et une fonction qui répond à l'autre paramètre d'entrée, à savoir une fonction qui prend String en entrée et génère un Int, comme illustré dans l'exemple suivant :

stringMapper("Android", { input ->
    input.length
})

Si la fonction anonyme est le dernier paramètre défini sur une fonction, vous pouvez la transmettre en dehors des parenthèses utilisées pour l'appeler, comme illustré dans l'exemple suivant :

stringMapper("Android") { input ->
    input.length
}

Des fonctions anonymes sont disponibles dans la bibliothèque standard Kotlin. Pour en savoir plus, consultez la page Higher-Order Functions and Lambdas (Fonctions d'ordre supérieur et lambdas).

Classes

Tous les types mentionnés jusqu'à présent sont intégrés au langage de programmation Kotlin. Si vous souhaitez ajouter un type personnalisé, vous pouvez définir une classe à l'aide du mot clé class, comme illustré dans l'exemple suivant :

class Car

Propriétés

Les classes représentent l'état à l'aide de propriétés. Une propriété est une variable de niveau classe qui peut inclure un getter, un setter et un champ de support. Puisqu'une voiture a besoin de roues pour rouler, vous pouvez ajouter une liste d'objets Wheel comme propriété de Car, comme illustré dans l'exemple suivant :

class Car {
    val wheels = listOf<Wheel>()
}

Notez que wheels est une public val, ce qui signifie que wheels est accessible en dehors de la classe Car et ne peut pas être réattribué. Si vous souhaitez obtenir une instance de Car, vous devez d'abord appeler son constructeur. Vous pouvez ensuite accéder à n'importe laquelle de ses propriétés accessibles.

val car = Car() // construct a Car
val wheels = car.wheels // retrieve the wheels value from the Car

Si vous souhaitez personnaliser vos roues, vous pouvez définir un constructeur personnalisé qui spécifie la façon dont les propriétés de votre classe sont initialisées :

class Car(val wheels: List<Wheel>)

Dans l'exemple ci-dessus, le constructeur de classe prend un List<Wheel> en tant qu'argument de constructeur et l'utilise pour initialiser sa propriété wheels.

Fonctions de classe et encapsulation

Les classes utilisent des fonctions pour modéliser le comportement. Les fonctions peuvent modifier l'état, ce qui vous permet de n'exposer que les données que vous souhaitez exposer. Ce contrôle d'accès fait partie d'un concept orienté objet plus large appelé encapsulation.

Dans l'exemple suivant, la propriété doorLock est privée et ne peut être utilisée en dehors de la classe Car. Pour déverrouiller la voiture, vous devez appeler la fonction unlockDoor() qui transmet une clé valide, comme illustré dans l'exemple suivant :

class Car(val wheels: List<Wheel>) {

    private val doorLock: DoorLock = ...

    fun unlockDoor(key: Key): Boolean {
        // Return true if key is valid for door lock, false otherwise
    }
}

Si vous souhaitez personnaliser la façon dont une propriété est référencée, vous pouvez fournir un getter et un setter personnalisés. Par exemple, si vous souhaitez exposer le getter d'une propriété tout en restreignant l'accès à son setter, vous pouvez le définir comme private :

class Car(val wheels: List<Wheel>) {

    private val doorLock: DoorLock = ...

    var gallonsOfFuelInTank: Int = 15
        private set

    fun unlockDoor(key: Key): Boolean {
        // Return true if key is valid for door lock, false otherwise
    }
}

En combinant des propriétés et des fonctions, vous pouvez créer des classes modélisant tous les types d'objets.

Interopérabilité

L'une des caractéristiques les plus importantes de Kotlin réside dans son interopérabilité fluide avec Java. Étant donné que le code Kotlin compile le bytecode JVM, votre code Kotlin peut appeler directement du code Java et inversement. Cela signifie que vous pouvez utiliser des bibliothèques Java existantes directement depuis Kotlin. De plus, la majorité des API Android sont écrites en Java, et vous pouvez les appeler directement à partir de Kotlin.

Étapes suivantes

Kotlin est un langage pragmatique et polyvalent qui bénéficie d'une adoption et d'une prise en charge croissantes. Nous vous invitons à l'essayer si vous ne l'avez pas encore fait. Pour les prochaines étapes, consultez la documentation officielle de Kotlin ainsi que le guide sur l'application des modèles Kotlin courants dans vos applications Android.