Convertir du code en langage Kotlin

Dans cet atelier de programmation, vous apprendrez à convertir du code Java en Kotlin. Vous découvrirez également quelles sont les conventions du langage Kotlin et comment vous assurer que votre code les respecte.

Cet atelier de programmation s'adresse aux développeurs qui utilisent Java et qui envisagent de faire migrer leur projet vers Kotlin. Pour commencer, vous allez convertir quelques classes Java en langage Kotlin à l'aide de l'IDE. Nous examinerons ensuite le code converti, et nous verrons comment l'améliorer en le rendant plus idiomatique et en évitant les pièges les plus courants.

Points abordés

Vous apprendrez à convertir du code Java en Kotlin. Ce faisant, vous découvrirez les fonctionnalités et concepts suivants du langage Kotlin :

  • Gérer la possibilité de valeur nulle
  • Implémenter des singletons
  • Classes de données
  • Gérer des chaînes
  • Opérateur Elvis
  • Déstructuration
  • Propriétés et propriétés de support
  • Arguments par défaut et paramètres nommés
  • Utiliser des collections
  • Fonctions d'extension
  • Paramètres et fonctions de niveau supérieur
  • Mots clés let, apply, with et run

Hypothèses

Vous devez déjà bien connaître le langage Java.

Prérequis

Créer un projet

Si vous utilisez IntelliJ IDEA, créez un projet Java avec Kotlin/JVM.

Si vous utilisez Android Studio, créez un projet sans aucune activité. Vous pouvez utiliser n'importe quelle version minimale du SDK. Cela n'a aucune incidence sur le résultat.

Le code

Nous allons créer un objet de modèle User, ainsi qu'une classe Singleton Repository qui fonctionne avec les objets User et qui affiche des listes d'utilisateurs et de noms d'utilisateur mis en forme.

Créez un fichier nommé User.java sous app/java/<nom_du_package> et collez-y le code suivant :

public class User {

    @Nullable
    private String firstName;
    @Nullable
    private String lastName;

    public User(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }

    public String getFirstName() {
        return firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }

}

Vous remarquerez que votre IDE indique que @Nullable n'est pas défini. Vous allez donc importer androidx.annotation.Nullable si vous utilisez Android Studio ou org.jetbrains.annotations.Nullable si vous utilisez IntelliJ.

Créez un fichier nommé Repository.java et collez-y le code suivant :

import java.util.ArrayList;
import java.util.List;

public class Repository {

    private static Repository INSTANCE = null;

    private List<User> users = null;

    public static Repository getInstance() {
        if (INSTANCE == null) {
            synchronized (Repository.class) {
                if (INSTANCE == null) {
                    INSTANCE = new Repository();
                }
            }
        }
        return INSTANCE;
    }

    // keeping the constructor private to enforce the usage of getInstance
    private Repository() {

        User user1 = new User("Jane", "");
        User user2 = new User("John", null);
        User user3 = new User("Anne", "Doe");

        users = new ArrayList();
        users.add(user1);
        users.add(user2);
        users.add(user3);
    }

    public List<User> getUsers() {
        return users;
    }

    public List<String> getFormattedUserNames() {
        List<String> userNames = new ArrayList<>(users.size());
        for (User user : users) {
            String name;

            if (user.getLastName() != null) {
                if (user.getFirstName() != null) {
                    name = user.getFirstName() + " " + user.getLastName();
                } else {
                    name = user.getLastName();
                }
            } else if (user.getFirstName() != null) {
                name = user.getFirstName();
            } else {
                name = "Unknown";
            }
            userNames.add(name);
        }
        return userNames;
    }
}

Notre IDE se débrouille plutôt bien pour convertir automatiquement du code Java en code Kotlin, mais parfois, un peu d'aide est nécessaire. Laissons à l'IDE le soin d'effectuer un premier passage au niveau de la conversion. Nous examinerons ensuite le code obtenu pour comprendre comment et pourquoi il a été converti de cette manière.

Accédez au fichier User.java et convertissez-le en Kotlin : Barre de menu -> Code -> Convert Java File to Kotlin File (Convertir le fichier Java en fichier Kotlin).

Si l'IDE vous invite à effectuer une correction après la conversion, appuyez sur Yes (Oui).

25e57db5b8e76557.png

Le code Kotlin suivant doit s'afficher :

class User(var firstName: String?, var lastName: String?)

Notez que User.java a été renommé en User.kt. L'extension des fichiers Kotlin est .kt.

Notre classe Java User comportait deux propriétés : firstName et lastName. Chacune d'elles possédait une méthode "getter" et "setter". Sa valeur pouvait donc être modifiée. Le mot clé de Kotlin pour les variables modifiables est var. Le convertisseur utilise donc var pour chacune de ces propriétés. Si nos propriétés Java avaient comporté uniquement des "getters", elles auraient été immuables et elles auraient été déclarées comme variables val. val est semblable au mot clé final en langage Java.

L'une des principales différences entre Kotlin et Java réside dans le fait que Kotlin spécifie explicitement si une variable peut accepter une valeur nulle. Pour cela, un ? est ajouté à la déclaration du type.

Étant donné que nous avons marqué les propriétés firstName et lastName comme pouvant être nulles, le convertisseur automatique les a également marquées comme telles avec String?. Si vous annotez vos membres Java en tant que valeurs non nulles (en utilisant org.jetbrains.annotations.NotNull ou androidx.annotation.NonNull), le convertisseur s'en rend compte et rend également les champs non nuls en langage Kotlin.

La conversion de base a déjà été effectuée. Cependant, nous pouvons écrire cela de façon plus idiomatique. Voyons comment faire.

Classe de données

Notre classe User ne contient que des données. Kotlin utilise un mot clé pour les classes associées à ce rôle : data. Si vous marquez cette classe en tant que classe data, le compilateur crée automatiquement des "getters" et des "setters". Il déduit également les fonctions equals(), hashCode() et toString().

Ajoutons le mot clé data à notre classe User :

data class User(var firstName: String, var lastName: String)

Tout comme Java, le langage Kotlin peut contenir un constructeur principal et un ou plusieurs constructeurs secondaires. Celui de l'exemple ci-dessus est le constructeur principal de la classe User. Si vous convertissez une classe Java comportant plusieurs constructeurs, le convertisseur en crée également plusieurs automatiquement en langage Kotlin. Ils sont définis à l'aide du mot clé constructor.

Si vous souhaitez créer une instance de cette classe, vous pouvez procéder comme suit :

val user1 = User("Jane", "Doe")

Égalité

Kotlin présente deux types d'égalité :

  • L'égalité structurelle utilise l'opérateur == et appelle equals() pour déterminer si deux instances sont égales.
  • L'égalité référentielle utilise l'opérateur === et vérifie si deux références pointent vers le même objet.

Les propriétés définies dans le constructeur principal de la classe de données seront utilisées pour les vérifications d'égalité structurelle.

val user1 = User("Jane", "Doe")
val user2 = User("Jane", "Doe")
val structurallyEqual = user1 == user2 // true
val referentiallyEqual = user1 === user2 // false

En langage Kotlin, nous pouvons attribuer des valeurs par défaut aux arguments dans les appels de fonction. Ces valeurs par défaut sont utilisées lorsque les arguments sont omis. Dans ce langage, les constructeurs sont également des fonctions. Nous pouvons donc utiliser des arguments par défaut pour indiquer que la valeur par défaut de lastName est null. Pour ce faire, nous attribuons simplement null à lastName.

data class User(var firstName: String?, var lastName: String? = null)

// usage
val jane = User("Jane") // same as User("Jane", null)
val joe = User("Joe", "Doe")

Kotlin vous permet d'ajouter un libellé à vos arguments lorsque vos fonctions sont appelées :

val john = User(firstName = "John", lastName = "Doe")

Examinons un cas d'utilisation différent. Supposons que la propriété firstName ait comme valeur par défaut null, mais que cela ne soit pas le cas de lastName. Étant donné que le paramètre par défaut précède un paramètre dépourvu de valeur par défaut, vous devez appeler la fonction avec des arguments nommés :

data class User(var firstName: String? = null, var lastName: String?)

// usage
val jane = User(lastName = "Doe") // same as User(null, "Doe")
val john = User("John", "Doe")

Les valeurs par défaut constituent un concept important et fréquemment utilisé dans le code Kotlin. Dans notre atelier de programmation, nous souhaitons toujours spécifier le prénom et le nom dans une déclaration d'objet User. L'utilisation de valeurs par défaut s'avère donc inutile.

Avant de poursuivre cet atelier de programmation, assurez-vous que votre classe User est bien de type data. Nous allons maintenant convertir la classe Repository en langage Kotlin. Le résultat de la conversion automatique doit se présenter comme suit :

import java.util.*

class Repository private constructor() {
    private var users: MutableList<User?>? = null
    fun getUsers(): List<User?>? {
        return users
    }

    val formattedUserNames: List<String?>
        get() {
            val userNames: MutableList<String?> =
                ArrayList(users!!.size)
            for (user in users) {
                var name: String
                name = if (user!!.lastName != null) {
                    if (user!!.firstName != null) {
                        user!!.firstName + " " + user!!.lastName
                    } else {
                        user!!.lastName
                    }
                } else if (user!!.firstName != null) {
                    user!!.firstName
                } else {
                    "Unknown"
                }
                userNames.add(name)
            }
            return userNames
        }

    companion object {
        private var INSTANCE: Repository? = null
        val instance: Repository?
            get() {
                if (INSTANCE == null) {
                    synchronized(Repository::class.java) {
                        if (INSTANCE == null) {
                            INSTANCE =
                                Repository()
                        }
                    }
                }
                return INSTANCE
            }
    }

    // keeping the constructor private to enforce the usage of getInstance
    init {
        val user1 = User("Jane", "")
        val user2 = User("John", null)
        val user3 = User("Anne", "Doe")
        users = ArrayList<Any?>()
        users.add(user1)
        users.add(user2)
        users.add(user3)
    }
}

Examinons les opérations effectuées par le convertisseur automatique :

  • La liste des users peut avoir une valeur nulle, car l'objet n'a pas été instancié au moment de la déclaration.
  • En langage Kotlin, les fonctions telles que getUsers() sont déclarées avec le modificateur fun.
  • La méthode getFormattedUserNames() est maintenant une propriété appelée formattedUserNames.
  • L'itération sur la liste d'utilisateurs (qui faisait initialement partie de getFormattedUserNames() ) présente une syntaxe différente de celle en Java.
  • Le champ static fait désormais partie d'un bloc companion object.
  • Un bloc init a été ajouté.

Avant d'aller plus loin, nous allons faire un peu de ménage dans le code. En examinant le constructeur, on constate que le convertisseur a transformé notre liste users en une liste modifiable contenant des objets qui peuvent être nuls. Bien que la liste puisse effectivement être nulle, supposons qu'elle ne puisse pas contenir d'utilisateurs avec une valeur nulle. Nous allons donc procéder comme suit :

  • Nous allons supprimer le ? dans User? à l'intérieur de la déclaration de type users.
  • Nous allons supprimer le ? dans User? pour le type renvoyé getUsers(), afin qu'il renvoie List<User>?.

Bloc d'initialisation (init)

En langage Kotlin, le constructeur principal ne peut contenir aucun code. Le code d'initialisation est donc placé dans des blocs init. La fonctionnalité reste la même.

class Repository private constructor() {
    ...
    init {
        val user1 = User("Jane", "")
        val user2 = User("John", null)
        val user3 = User("Anne", "Doe")
        users = ArrayList<Any?>()
        users.add(user1)
        users.add(user2)
        users.add(user3)
    }
}

Une grande partie du code init gère les propriétés d'initialisation. Cela peut également être effectué dans la déclaration de la propriété. Par exemple, dans la version Kotlin de notre classe Repository, nous constatons que la propriété "users" a été initialisée dans la déclaration.

private var users: MutableList<User>? = null

Propriétés et méthodes static du langage Kotlin

En langage Java, le mot clé static est utilisé pour les champs ou les fonctions pour indiquer qu'ils appartiennent à une classe, mais pas à une instance de la classe. C'est pourquoi nous avons créé le champ statique INSTANCE dans notre classe Repository. L'équivalent Kotlin de cet élément est le bloc companion object. Dans ce cas, vous déclarez également les champs et les fonctions statiques. Le convertisseur a créé le bloc Objet associé et a déplacé le champ INSTANCE à cet emplacement.

Gérer les singletons

Puisque nous n'avons besoin que d'une seule instance de la classe Repository, nous avons utilisé le patron de conception (singleton) en langage Java. En langage Kotlin, vous pouvez appliquer ce patron au niveau du compilateur en remplaçant le mot clé class par object.

Supprimez le constructeur privé et remplacez la définition de classe par object Repository. Supprimez également l'objet associé.

object Repository {

    private var users: MutableList<User>? = null
    fun getUsers(): List<User>? {
       return users
    }

    val formattedUserNames: List<String>
        get() {
            val userNames: MutableList<String> =
                ArrayList(users!!.size)
        for (user in users) {
            var name: String
            name = if (user!!.lastName != null) {
                if (user!!.firstName != null) {
                    user!!.firstName + " " + user!!.lastName
                } else {
                    user!!.lastName
                }
            } else if (user!!.firstName != null) {
                user!!.firstName
            } else {
                "Unknown"
            }
            userNames.add(name)
       }
       return userNames
   }

    // keeping the constructor private to enforce the usage of getInstance
    init {
        val user1 = User("Jane", "")
        val user2 = User("John", null)
        val user3 = User("Anne", "Doe")
        users = ArrayList<Any?>()
        users.add(user1)
        users.add(user2)
        users.add(user3)
    }
}

Lors de l'utilisation de la classe object, nous appelons simplement les fonctions et les propriétés directement sur l'objet, comme ceci :

val formattedUserNames = Repository.formattedUserNames

Notez que si une propriété ne comporte pas de modificateur de visibilité, elle est publique par défaut, comme c'est le cas pour la propriété formattedUserNames dans l'objet Repository.

Lors de la conversion de la classe Repository en langage Kotlin, le convertisseur automatique a transformé la liste d'utilisateurs en une liste pouvant accepter les valeurs nulles, car elle n'avait pas été initialisée sur un objet lors de sa déclaration. Par conséquent, pour toutes les utilisations de l'objet users, l'opérateur d'assertion "non nul" !! doit être utilisé. (Vous verrez users!! et user!! dans tout le code converti.) L'opérateur !! convertit toute variable en un type "non nul", de sorte que vous puissiez accéder aux propriétés ou appeler les fonctions qui y sont associées. Toutefois, une exception est générée si la valeur de la variable est effectivement nulle. En utilisant !!, vous courez le risque que des exceptions soient générées au moment de l'exécution.

Il est préférable de gérer la possibilité de valeur nulle en utilisant l'une des méthodes suivantes :

  • Effectuer un contrôle de valeurs nulles ( if (users != null) {...} )
  • Utiliser l'opérateur Elvis ?: (décrit dans la suite de cet atelier de programmation)
  • Utiliser certaines des fonctions standards de Kotlin (décrites dans la suite de cet atelier de programmation)

Dans le cas présent, nous savons qu'il n'est pas nécessaire que la liste d'utilisateurs puisse être de type "null", car elle est initialisée immédiatement après la création de l'objet (dans le bloc init). Nous pouvons donc instancier directement l'objet users lorsque nous le déclarons.

Lors de la création d'instances de types Collection, Kotlin propose plusieurs fonctions d'assistance permettant de rendre votre code plus lisible et plus flexible. Dans cet exemple, MutableList est utilisé pour users :

private var users: MutableList<User>? = null

Pour plus de simplicité, nous pouvons utiliser la fonction mutableListOf() et fournir le type d'élément de liste. mutableListOf<User>() crée une liste vide pouvant contenir des objets User. Étant donné que le compilateur peut maintenant déduire le type de données de la variable, supprimez la déclaration de type explicite de la propriété users.

private val users = mutableListOf<User>()

var a également été remplacé par val, car les utilisateurs seront associés à une référence immuable à la liste d'utilisateurs. Notez que la référence est immuable, mais que la liste proprement dite ne l'est pas (vous pouvez donc y ajouter ou en supprimer des éléments).

Étant donné que la variable users est déjà initialisée, supprimez cette initialisation du bloc init :

users = ArrayList<Any?>()

Le bloc init devrait alors se présenter comme suit :

init {
    val user1 = User("Jane", "")
    val user2 = User("John", null)
    val user3 = User("Anne", "Doe")

    users.add(user1)
    users.add(user2)
    users.add(user3)
}

Compte tenu de ces modifications, notre propriété users est désormais non nulle. Nous pouvons donc supprimer toutes les occurrences d'opérateur !! inutiles. Notez que les erreurs de compilation continueront de s'afficher dans Android Studio. Pour les corriger, exécutez les étapes suivantes des ateliers de programmation.

val userNames: MutableList<String?> = ArrayList(users.size)
for (user in users) {
    var name: String
    name = if (user.lastName != null) {
        if (user.firstName != null) {
            user.firstName + " " + user.lastName
        } else {
            user.lastName
        }
    } else if (user.firstName != null) {
        user.firstName
    } else {
        "Unknown"
    }
    userNames.add(name)
}

Pour la valeur userNames, si vous spécifiez que le type ArrayList contient Strings, vous pouvez supprimer le type explicite dans la déclaration, car il sera déduit.

val userNames = ArrayList<String>(users.size)

Déstructuration

Kotlin permet de déstructurer un objet en plusieurs variables à l'aide d'une syntaxe appelée déclaration de déstructuration. Nous créons plusieurs variables qui peuvent être utilisées séparément.

Par exemple, les classes data acceptent la déstructuration afin que nous puissions déstructurer l'objet User dans la boucle for en (firstName, lastName). Cela nous permet de travailler directement avec les valeurs firstName et lastName. Mettez à jour la boucle for comme illustré ci-dessous. Remplacez toutes les instances de user.firstName par firstName et remplacez user.lastName par lastName.

for ((firstName, lastName) in users) {
    var name: String
    name = if (lastName != null) {
        if (firstName != null) {
            firstName + " " + lastName
        } else {
            lastName
        }
    } else if (firstName != null) {
        firstName
    } else {
        "Unknown"
    }
    userNames.add(name)
}

Expression if

Le format des noms dans la liste userNames n'est pas encore conforme à nos attentes. Étant donné que les valeurs lastName et firstName peuvent être définies sur null, nous devons gérer la possibilité de valeur nulle lors de la compilation de la liste des noms d'utilisateur mis en forme. Nous souhaitons afficher "Unknown" si l'un de ces noms est manquant. Une fois la variable name définie, elle ne sera plus modifiée. Nous pouvons donc utiliser val au lieu de var. Effectuez d'abord cette modification.

val name: String

Examinez le code qui définit la variable de nom. C'est peut-être la première fois que vous voyez une variable définie sur "égal" et un bloc de code if/else. Cela est autorisé, car if, when, for et while sont des expressions en langage Kotlin ; elles renvoient donc une valeur. La dernière ligne de l'instruction if sera attribuée à name. La seule fonction de ce bloc est d'initialiser la valeur name.

De manière générale, la logique présentée ici est la suivante : si la valeur de lastName est nulle, name est défini sur firstName ou sur "Unknown".

name = if (lastName != null) {
    if (firstName != null) {
        firstName + " " + lastName
    } else {
        lastName
    }
} else if (firstName != null) {
    firstName
} else {
    "Unknown"
}

Opérateur Elvis

Ce code peut être écrit de manière plus idiomatique à l'aide de l'opérateur Elvis ?:. L'opérateur Elvis renvoie l'expression de gauche si elle n'est pas nulle ou l'expression de droite si la valeur est nulle.

Ainsi, dans le code suivant, firstName est renvoyé s'il n'est pas nul. Si firstName est nul, l'expression renvoie la valeur de droite, "Unknown" :

name = if (lastName != null) {
    ...
} else {
    firstName ?: "Unknown"
}

Kotlin simplifie l'utilisation des Strings grâce aux modèles de chaîne. Les modèles de chaîne vous permettent de référencer des variables à l'intérieur de déclarations de chaîne en les faisant précéder du symbole $. Vous pouvez également insérer une expression dans une déclaration de chaîne en la plaçant entre { } et en la faisant précéder du symbole $. Exemple : ${user.firstName}.

Votre code utilise actuellement une concaténation de chaîne pour combiner firstName et lastName dans le nom d'utilisateur.

if (firstName != null) {
    firstName + " " + lastName
}

Remplacez plutôt la concaténation de chaîne par :

if (firstName != null) {
    "$firstName $lastName"
}

Les modèles de chaîne permettent de simplifier votre code.

Votre IDE affiche des avertissements si le code peut être rédigé de manière plus idiomatique. Notez que le code est souligné en ondulé, et lorsque vous passez la souris dessus, une suggestion vous indique comment le refactoriser.

Un avertissement doit normalement être affiché pour indiquer que la déclaration name peut être associée à l'affectation. C'est ce que nous allons faire. Étant donné que le type de la variable name peut être déduit, nous pouvons supprimer la déclaration de type String explicite. Voici à quoi ressemble maintenant notre propriété formattedUserNames :

val formattedUserNames: List<String?>
    get() {
        val userNames = ArrayList<String>(users.size)
        for ((firstName, lastName) in users) {
            val name = if (lastName != null) {
                if (firstName != null) {
                    "$firstName $lastName"
                } else {
                    lastName
                }
            } else {
                firstName ?: "Unknown"
            }
            userNames.add(name)
        }
        return userNames
    }

Nous pouvons effectuer un ajustement supplémentaire. Notre logique d'interface utilisateur affiche "Unknown" si le prénom et le nom sont manquants. Par conséquent, nous n'acceptons pas les objets nuls. Pour le type de données formattedUserNames, nous allons donc remplacer List<String?> par List<String>.

val formattedUserNames: List<String>

Examinons maintenant de plus près le "getter" formattedUserNames pour savoir comment le rendre plus idiomatique. Pour le moment, le code effectue les opérations suivantes :

  • Il crée une liste de chaînes.
  • Il parcourt la liste des utilisateurs.
  • Il construit le nom mis en forme de chaque utilisateur, sur la base de son prénom et de son nom.
  • Il renvoie la liste qui vient d'être créée.
    val formattedUserNames: List<String>
        get() {
            val userNames = ArrayList<String>(users.size)
            for ((firstName, lastName) in users) {
                val name = if (lastName != null) {
                    if (firstName != null) {
                        "$firstName $lastName"
                    } else {
                        lastName
                    }
                } else {
                    firstName ?: "Unknown"
                }
                userNames.add(name)
            }
            return userNames
        }

Kotlin fournit une longue liste de transformations de collections pour un développement plus rapide et plus sûr, en étendant les fonctionnalités de l'API Java Collections. L'une d'elles est la fonction map. Cette fonction renvoie une nouvelle liste contenant les résultats de l'application de la fonction de transformation donnée à chaque élément de la liste d'origine. Ainsi, au lieu de créer une autre liste et de parcourir manuellement la liste des utilisateurs, nous pouvons utiliser la fonction map et déplacer la logique de la boucle for dans le corps map. Par défaut, le nom de l'élément de liste actuel utilisé dans map est it. Cependant, pour des raisons de lisibilité, vous pouvez remplacer it par votre propre nom de variable. Dans cet exemple, nous allons l'appeler user :

val formattedUserNames: List<String>
        get() {
            return users.map { user ->
                val name = if (user.lastName != null) {
                    if (user.firstName != null) {
                        "${user.firstName} ${user.lastName}"
                    } else {
                        user.lastName ?: "Unknown"
                    }
                }  else {
                    user.firstName ?: "Unknown"
                }
                name
            }
        }

Notez que nous utilisons l'opérateur Elvis pour renvoyer "Unknown" si la valeur de user.lastName est nulle, étant donné que user.lastName est de type String? et que String est requis pour name.

...
else {
    user.lastName ?: "Unknown"
}
...

Pour encore plus de simplicité, la variable name peut être complètement supprimée :

val formattedUserNames: List<String>
        get() {
            return users.map { user ->
                if (user.lastName != null) {
                    if (user.firstName != null) {
                        "${user.firstName} ${user.lastName}"
                    } else {
                        user.lastName ?: "Unknown"
                    }
                }  else {
                    user.firstName ?: "Unknown"
                }
            }
        }

Nous avons constaté que le convertisseur automatique remplaçait la fonction getFormattedUserNames() par une propriété appelée formattedUserNames et pourvue d'un "getter" personnalisé. En arrière-plan, Kotlin génère toujours une méthode getFormattedUserNames() qui renvoie un objet List.

En langage Java, nos propriétés de classe seraient exposées par le biais de fonctions "getter" et "setter". Kotlin permet de mieux différencier les propriétés d'une classe, exprimées avec des champs, de ses fonctionnalités (c'est-à-dire des actions qu'elle peut réaliser), exprimées par des fonctions. Dans le cas présent, la classe Repository est très simple et n'effectue aucune action. Elle ne comporte donc que des champs.

La logique qui avait été déclenchée dans la fonction Java getFormattedUserNames() est désormais déclenchée lors de l'appel de la méthode "getter" de la propriété Kotlin formattedUserNames.

Bien qu'aucun champ ne corresponde explicitement à la propriété formattedUserNames, Kotlin fournit un champ de support automatique nommé field auquel nous pouvons accéder, au besoin, à partir de méthodes "getter" et "setter" personnalisées.

Dans certains cas, nous aimerions toutefois bénéficier de fonctionnalités supplémentaires qui ne sont pas proposées par le champ de support automatique.

Prenons l'exemple suivant.

Notre classe Repository contient une liste modifiable d'utilisateurs qui est exposée dans la fonction getUsers() générée à partir de notre code Java :

fun getUsers(): List<User>? {
    return users
}

Le problème, c'est qu'en renvoyant users, tout utilisateur de la classe Repository peut modifier la liste d'utilisateurs, ce qui n'est pas vraiment une bonne idée ! Pour y remédier, nous allons utiliser une propriété de support.

Commencez par renommer users en _users. Mettez en surbrillance le nom de la variable, puis cliquez avec le bouton droit sur Refactor > Rename (Refactoriser > Renommer). Ajoutez ensuite une propriété immuable publique qui renvoie une liste d'utilisateurs. Appelons-la users :

private val _users = mutableListOf<User>()
val users: List<User>
    get() = _users

À ce stade, vous pouvez supprimer la méthode getUsers().

Avec cette modification, la propriété privée _users devient la propriété de support pour la propriété publique users. En dehors de la classe Repository, la liste _users ne peut pas être modifiée, car les utilisateurs de la classe ne peuvent y accéder que via users.

Code complet :

object Repository {

    private val _users = mutableListOf<User>()
    val users: List<User>
        get() = _users

    val formattedUserNames: List<String>
        get() {
            return _users.map { user ->
                if (user.lastName != null) {
                    if (user.firstName != null) {
                        "${user.firstName} ${user.lastName}"
                    } else {
                        user.lastName ?: "Unknown"
                    }
                }  else {
                    user.firstName ?: "Unknown"
                }
            }
        }

    init {
        val user1 = User("Jane", "")
        val user2 = User("John", null)
        val user3 = User("Anne", "Doe")

        _users.add(user1)
        _users.add(user2)
        _users.add(user3)
    }
}

Maintenant, la classe Repository sait comment calculer le nom d'utilisateur mis en forme pour un objet User. Cependant, si vous souhaitez réutiliser la même logique de mise en forme dans d'autres classes, vous devez la copier et la coller, ou la déplacer vers la classe User.

Kotlin permet de déclarer des fonctions et des propriétés en dehors de tout objet, classe ou interface. Par exemple, la fonction mutableListOf() que nous avons utilisée pour créer une instance de List est déjà définie dans Collections.kt à partir de la bibliothèque standard Kotlin.

En langage Java, si vous avez besoin d'une fonctionnalité utilitaire, il est probable que vous créiez une classe Util et déclariez cette fonctionnalité en tant que fonction statique. En langage Kotlin, vous pouvez déclarer des fonctions de niveau supérieur sans avoir de classe. Cependant, Kotlin offre également la possibilité de créer des fonctions d'extension. Il s'agit de fonctions qui étendent un certain type, mais qui sont déclarées en dehors de celui-ci.

La visibilité des fonctions et des propriétés d'extension peut être limitée en utilisant des modificateurs de visibilité. Ces modificateurs limitent l'utilisation des extensions aux seules classes qui en ont besoin et ne "polluent" pas l'espace de noms.

Pour la classe User, nous pouvons soit ajouter une fonction d'extension qui calcule le nom mis en forme, soit conserver le nom mis en forme dans une propriété d'extension. Elle peut être ajoutée en dehors de la classe Repository, dans le même fichier :

// extension function
fun User.getFormattedName(): String {
    return if (lastName != null) {
        if (firstName != null) {
            "$firstName $lastName"
        } else {
            lastName ?: "Unknown"
        }
    } else {
        firstName ?: "Unknown"
    }
}

// extension property
val User.userFormattedName: String
    get() {
        return if (lastName != null) {
            if (firstName != null) {
                "$firstName $lastName"
            } else {
                lastName ?: "Unknown"
            }
        } else {
            firstName ?: "Unknown"
        }
    }

// usage:
val user = User(...)
val name = user.getFormattedName()
val formattedName = user.userFormattedName

Nous pouvons ensuite utiliser les fonctions et les propriétés d'extension comme si elles faisaient partie de la classe User.

Le nom mis en forme étant une propriété de la classe User, et non une fonctionnalité de la classe Repository, nous allons utiliser la propriété d'extension. Voici à quoi ressemble maintenant notre fichier Repository :

val User.formattedName: String
    get() {
        return if (lastName != null) {
            if (firstName != null) {
                "$firstName $lastName"
            } else {
                lastName ?: "Unknown"
            }
        } else {
            firstName ?: "Unknown"
        }
    }

object Repository {

    private val _users = mutableListOf<User>()
    val users: List<User>
      get() = _users

    val formattedUserNames: List<String>
        get() {
            return _users.map { user -> user.formattedName }
        }

    init {
        val user1 = User("Jane", "")
        val user2 = User("John", null)
        val user3 = User("Anne", "Doe")

        _users.add(user1)
        _users.add(user2)
        _users.add(user3)
    }
}

La bibliothèque standard Kotlin utilise des fonctions d'extension pour étendre les fonctionnalités de plusieurs API Java. Nombre des fonctionnalités de Iterable et Collection sont implémentées en tant que fonctions d'extension. Par exemple, la fonction map que nous avons utilisée au cours d'une étape précédente est une fonction d'extension de Iterable.

Dans notre code de classe Repository, nous allons ajouter plusieurs objets User à la liste _users. Ces appels peuvent être effectués de manière plus idiomatique grâce aux fonctions scope de Kotlin.

Pour limiter l'exécution du code au contexte d'un objet spécifique, sans devoir accéder à l'objet en fonction de son nom, Kotlin propose cinq fonctions scope : let, apply, with, run et also. Non seulement ces fonctions facilitent la lecture du code, mais elles le rendent aussi plus concis. Toutes les fonctions scope comprennent un destinataire (this), peuvent comporter un argument (it) et peuvent renvoyer une valeur.

Voici un aide-mémoire pratique qui vous aidera à mémoriser les conditions d'utilisation de chaque fonction :

6b9283d411fb6e7b.png

Étant donné que nous configurons l'objet _users dans notre Repository, nous pouvons utiliser la fonction apply pour rendre le code plus idiomatique :

init {
    val user1 = User("Jane", "")
    val user2 = User("John", null)
    val user3 = User("Anne", "Doe")

    _users.apply {
       // this == _users
       add(user1)
       add(user2)
       add(user3)
    }
 }

Dans cet atelier de programmation, nous avons passé en revue les notions élémentaires que vous devez connaître pour commencer à convertir du code Java en Kotlin. Cette conversion est indépendante de votre plate-forme de développement et garantit l'écriture d'un code Kotlin idiomatique.

Avec le code Kotlin idiomatique, les deux mots d'ordre sont efficacité et concision. Kotlin met ainsi à votre disposition une foule de fonctionnalités qui améliorent la sécurité, la lisibilité et la concision de votre code. Par exemple, nous pouvons même optimiser notre classe Repository en instanciant directement la liste _users avec des utilisateurs dans la déclaration, ce qui élimine le bloc init :

private val users = mutableListOf(User("Jane", ""), User("John", null), User("Anne", "Doe"))

Nous avons abordé un large éventail de sujets, de la gestion de la possibilité de valeur nulle aux fonctions d'extension en passant par les chaînes, les singletons, les collections, les fonctions de niveau supérieur, les propriétés et les fonctions scope. Nous avons converti deux classes Java en deux classes Kotlin qui se présentent désormais comme suit :

User.kt

data class User(var firstName: String?, var lastName: String?)

Repository.kt

val User.formattedName: String
    get() {
       return if (lastName != null) {
            if (firstName != null) {
                "$firstName $lastName"
            } else {
                lastName ?: "Unknown"
            }
        } else {
            firstName ?: "Unknown"
        }
    }

object Repository {

    private val _users = mutableListOf(User("Jane", ""), User("John", null), User("Anne", "Doe"))
    val users: List<User>
        get() = _users

    val formattedUserNames: List<String>
        get() = _users.map { user -> user.formattedName }
}

Voici un résumé des fonctionnalités Java et leurs équivalents en langage Kotlin :

Java

Kotlin

Objet final

Objet val

equals()

==

==

===

Classe contenant uniquement des données

Classe data

Initialisation dans le constructeur

Initialisation dans le bloc init

Champs et fonctions static

Champs et fonctions déclarés dans un companion object

Classe Singleton

object

Pour en savoir plus sur Kotlin et son utilisation sur votre plate-forme, consultez les ressources suivantes :