À propos de cet atelier de programmation
1. Bienvenue !
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.
Ce dont vous avez besoin
2. Configuration
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;
}
}
3. Déclarer la possibilité de valeur nulle, val, var et des classes de données
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 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
4. Arguments par défaut et arguments nommés
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")
5. Initialisation de l'objet, objet associé et singleton
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 bloccompanion 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éeformattedUserNames
(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
?
dansUser?
à l'intérieur de la déclaration de typeusers
. getUsers
doit renvoyerList<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 static
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éé 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)
}
6. Gérer la possibilité de valeur nulle
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"
}
7. Modèles de chaîne et expression if
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.
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
}
8. Opérations sur les collections
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"
}
}
}
9. Propriétés et propriétés de support
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)
}
}
10. Fonctions et propriétés de niveau supérieur et d'extension
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
.
11. Fonctions de limitation : let, apply, with, run, also
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)
}
}
12. Synthèse
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 | 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
- Developing Android apps with Kotlin (Développer des applications Android avec Kotlin) : cours offert
- Kotlin Bootcamp for Programmers (Formation Kotlin pour les programmeurs)
- Kotlin for Java developers (Kotlin pour les développeurs Java) : cours offert en mode Audit