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
etrun
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).
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 appelleequals()
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 modificateurfun
. - La méthode
getFormattedUserNames()
est maintenant une propriété appeléeformattedUserNames
. - 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 bloccompanion 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
?
dansUser?
à l'intérieur de la déclaration de typeusers
. - Nous allons supprimer le
?
dansUser?
pour le type renvoyégetUsers()
, afin qu'il renvoieList<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 String
s 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 :
É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 | Objet |
|
|
|
|
Classe contenant uniquement des données | Classe |
Initialisation dans le constructeur | Initialisation dans le bloc |
Champs et fonctions | Champs et fonctions déclarés dans un |
Classe Singleton |
|
Pour en savoir plus sur Kotlin et son utilisation sur votre plate-forme, consultez les ressources suivantes :
- Kotlin Koans
- Tutoriels Kotlin
- Android Kotlin Fundamentals (Principes de base d'Android en langage Kotlin)
- Kotlin Bootcamp for Programmers (Formation Kotlin pour les programmeurs)
- Kotlin for Java developers (Kotlin pour les développeurs Java) : cours gratuit en mode Audit