Refactoriser vers 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.

Ce dont vous avez besoin

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é.

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;
    }

}

En fonction du type de projet, importez androidx.annotation.Nullable si vous utilisez un projet Android ou org.jetbrains.annotations.Nullable autrement.

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 refactoriser automatiquement du code Java en code Kotlin, mais parfois, un peu d'aide est nécessaire. Nous effectuerons d'abord cette opération, puis nous examinerons le code refactorisé pour comprendre comment et pourquoi il a été refactorisé de cette façon.

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).

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 refactorisation 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("John", "Doe")

Les paramètres de fonction peuvent être nommés lors de l'appel de fonctions:

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")

Avant de continuer, assurez-vous que votre classe User est bien de type data. Transformons 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 :

  • Un bloc init a été ajouté (Repository.kt#L50)
  • Le champ static fait désormais partie d'un bloc companion object (Repository.kt#L33)
  • La liste des users peut avoir une valeur nulle, car l'objet n'a pas été instancié au moment de la déclaration (Repository.kt#L7).
  • La méthode getFormattedUserNames() est maintenant une propriété appelée formattedUserNames (Repository.kt#L11)
  • L'itération sur la liste d'utilisateurs (qui faisait initialement partie de getFormattedUserNames() ) présente une syntaxe différente de celle en Java (Repository.kt#L15).

Avant d'aller plus loin, nous allons faire un peu de ménage dans le code. Nous constatons 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 null, dites-nous qu'elle ne peut pas contenir d'utilisateurs avec une valeur null. Nous allons donc procéder comme suit :

  • Nous allons supprimer le ? dans User? à l'intérieur de la déclaration de type users.
  • getUsers doit renvoyer List<User>?

Le convertisseur automatique se divise également inutilement en deux lignes, dans les déclarations de variables des variables utilisateur et de celles définies dans le bloc init. Ajoutons chaque déclaration de variable sur une seule ligne. Voici à quoi doit ressembler ce code:

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)
    }
}

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 staticKotlin

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éé et déplacé le champ INSTANCE ici.

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 l'objet associé, puis remplacez la définition de classe par object Repository.

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 users = Repository.users

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 de données 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 suit:

 
for ((firstName, lastName) in users!!) {
       val name: String?

       if (lastName != null) {
          if (firstName != null) {
                name = "$firstName $lastName"
          } else {
                name = lastName
          }
       } else if (firstName != null) {
            name = firstName
       } else {
            name = "Unknown"
       }
       userNames.add(name)
}

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. Pour toutes les utilisations de l'objet users, l'opérateur d'assertion "non nul" !! est utilisé. Elle convertit toute variable en un type "non nul" et renvoie une exception si la valeur est 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. Nous pouvons donc instancier directement l'objet 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(), fournir le type d'élément de liste, supprimer l'appel de constructeur ArrayList du bloc init, et supprimer la déclaration de type explicite de la propriété users.

private val users = mutableListOf<User>()

Nous avons également remplacé "var" 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).

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.

val userNames: MutableList<String?> = ArrayList(users.size)
for ((firstName, lastName) in users) {
    ...
}

De plus, comme la variable"users"est déjà initialisée, nous devons supprimer l'initialisation du bloc init:

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

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

É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. Puisque nous souhaitons afficher "Unknown" si l'un de ces noms est manquant, nous pouvons le remplacer par une valeur non nulle en supprimant ? de la déclaration du type.

val name: String

Si la valeur de lastName est nulle, name correspond à firstName ou à "Unknown" :

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

Ceci 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, user.firstName est renvoyé s'il n'est pas nul. Si user.firstName est nul, l'expression renvoie la valeur de droite, "Unknown" :

if (lastName != null) {
    ...
} else {
    name = 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.

Le convertisseur automatique met à jour la concaténation du prénom et du nom afin de référencer le nom de la variable directement dans la chaîne à l'aide du symbole $ et d'insérer l'expression entre { } .

// Java
name = user.getFirstName() + " " + user.getLastName();

// Kotlin
name = "${user.firstName} ${user.lastName}"

Dans le code, remplacez la concaténation de chaîne par:

name = "$firstName $lastName"

Dans le langage Kotlin (if), when, for et while sont des expressions. Elles renvoient une valeur. Votre IDE affiche même un avertissement indiquant que l'attribution doit être retirée de if :

Suivons la suggestion de l'IDE et supprimez l'attribution des deux instructions if. La dernière ligne de l'instruction if sera attribuée. Comme ceci, il est évident que la seule fonction de ce bloc est d'initialiser la valeur du nom:

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

Ensuite, vous recevrez un avertissement vous informant que la déclaration name peut être associée à l'affectation. Vous pouvez également appliquer ce paramètre. Étant donné que le type de la variable de nom peut être déduit, nous pouvons supprimer la déclaration de type explicite. Voici à quoi ressemble maintenant notre propriété formattedUserNames:

val formattedUserNames: List<String?>
   get() {
       val userNames: MutableList<String?> = ArrayList(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
   }

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
            }
        }

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 ci-dessous.

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. Ajoutez maintenant 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

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éfinie directement dans Collections.kt à partir de la bibliothèque standard.

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. Par conséquent, ils ont une affinité avec ce type.

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 l'utilisateur, et non une fonctionnalité de la classe Repository, nous allons utiliser la propriété d'extension. Le fichier Repository ressemble maintenant à ceci:

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 utilisateur à la liste _users. Ces appels peuvent être effectués de manière plus idiomatique grâce aux fonctions scope.

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 a créé cinq fonctions scope : let, apply, with, run et also. courtes et performantes, toutes ces fonctions ont un récepteur (this), peuvent avoir un argument (it) et peuvent renvoyer une valeur. Vous devez choisir celle qui vous convient le mieux, en fonction de vos objectifs.

Voici un aide-mémoire pour vous aider à vous souvenir de ceci:

É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 à refactoriser votre code Java en Kotlin. Cette refactorisation est indépendante de votre plate-forme de développement et contribue à garantir que le code que vous écrivez est 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

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 :