1. Introduction
Au fil des décennies, les programmeurs ont développé plusieurs fonctionnalités du langage de programmation pour vous aider à mieux coder, en utilisant moins de code pour exprimer une même idée, en ayant recours à des concepts abstraits pour exprimer des idées complexes ou encore en écrivant du code qui empêche les autres développeurs de faire des erreurs. Le langage Kotlin ne fait pas exception et plusieurs fonctionnalités permettent aux développeurs d'écrire un code plus expressif.
Malheureusement, ces dernières ne sont pas forcément accessibles si vous débutez en programmation. Même si elles semblent utiles, l'étendue de leur utilité et les problèmes qu'elles permettent de résoudre ne sont pas toujours évidents. Vous avez probablement déjà vu certaines fonctionnalités utilisées dans Compose et dans d'autres bibliothèques.
Bien que rien ne puisse remplacer l'expérience, cet atelier de programmation vous présente plusieurs concepts Kotlin pour vous aider à structurer des applications plus volumineuses, notamment :
- les génériques ;
- les différents types de classes (classes d'énumération et classes de données) ;
- les objets singleton et compagnons ;
- les propriétés et fonctions des extensions ;
- les fonctions de portée.
À la fin de cet atelier de programmation, vous devriez mieux comprendre le code étudié durant ce cours, et avoir appris quelques situations au cours desquelles vous pourriez rencontrer ou utiliser ces concepts dans vos propres applications.
Conditions préalables
- Avoir une bonne connaissance des concepts de programmation orientée objet, y compris l'héritage.
- Savoir définir et implémenter des interfaces.
Points abordés
- Comment définir un paramètre de type générique pour une classe ?
- Comment instancier une classe générique ?
- Quand utiliser des classes de données et d'énumération ?
- Comment définir un paramètre de type générique devant implémenter une interface ?
- Comment utiliser les fonctions de portée pour accéder aux propriétés et aux méthodes de classes ?
- Comment définir des objets singleton et compagnons pour une classe ?
- Comment étendre des classes existantes avec de nouvelles propriétés et méthodes ?
Ce dont vous avez besoin
- Un navigateur Web ayant accès à Kotlin Playground
2. Créer une classe réutilisable avec des génériques
Imaginons que vous codiez une application pour un quiz en ligne, semblable aux quiz de ce cours. Les questions peuvent prendre plusieurs formes, telles que des textes à trous ou encore des "vrai ou faux". Une question individuelle peut être représentée par une classe, avec plusieurs propriétés.
Le texte d'une question peut être représenté par une chaîne. Les questions du quiz doivent également représenter la réponse. Toutefois, différents types de questions (vrai ou faux, par exemple) peuvent avoir besoin de représenter la réponse à l'aide d'un autre type de données. Définissons trois types de questions différents.
- Question à trous : la réponse est un mot représenté par une donnée de type
String
. - Question de type Vrai ou faux : la réponse est représentée par une donnée de type
Boolean
. - Problèmes mathématiques: la réponse est une valeur numérique. La réponse à un problème arithmétique simple est représentée par une donnée de type
Int
.
De plus, les questions de notre exemple, quel que soit leur type, auront un indice de difficulté. L'indice de difficulté est représenté par une chaîne avec trois valeurs possibles : "easy"
(facile), "medium"
(intermédiaire) ou "hard"
(difficile).
Définissez des classes pour chaque type de question du quiz :
- Accédez à Kotlin Playground.
- Au-dessus de la fonction
main()
, définissez une classe nomméeFillInTheBlankQuestion
pour les questions à trous, composée d'une propriétéString
pourquestionText
(texte de la question), d'une propriétéString
pouranswer
(réponse) et d'une propriétéString
pourdifficulty
(difficulté).
class FillInTheBlankQuestion(
val questionText: String,
val answer: String,
val difficulty: String
)
- Sous la classe
FillInTheBlankQuestion
, définissez une autre classe nomméeTrueOrFalseQuestion
pour les questions de type Vrai ou faux, composée d'une propriétéString
pourquestionText
, d'une propriétéBoolean
pouranswer
et d'une propriétéString
pourdifficulty
.
class TrueOrFalseQuestion(
val questionText: String,
val answer: Boolean,
val difficulty: String
)
- Enfin, sous les deux autres classes, définissez une classe
NumericQuestion
, composée d'une propriétéString
pourquestionText
, d'une propriétéInt
pouranswer
et d'une propriétéString
pourdifficulty
.
class NumericQuestion(
val questionText: String,
val answer: Int,
val difficulty: String
)
- Regardez le code que vous avez écrit. Avez-vous remarqué la répétition ?
class FillInTheBlankQuestion(
val questionText: String,
val answer: String,
val difficulty: String
)
class TrueOrFalseQuestion(
val questionText: String,
val answer: Boolean,
val difficulty: String
)
class NumericQuestion(
val questionText: String,
val answer: Int,
val difficulty: String
)
Les trois classes ont exactement les mêmes propriétés : questionText
, answer
et difficulty
. La seule différence réside dans le type de données de la propriété answer
. Vous pensez peut-être que la solution évidente est de créer une classe parente avec questionText
et difficulty
, et que chaque sous-classe définisse la propriété de answer
.
Toutefois, l'utilisation de l'héritage pose le même problème que ci-dessus. Chaque fois que vous ajoutez un nouveau type de question, vous devez ajouter une propriété de answer
. La seule différence réside dans le type de données. Il est également étrange d'avoir une classe parente Question
sans propriété de réponse.
Lorsque vous voulez qu'une propriété ait différents types de données, le sous-classement n'est pas la solution. Kotlin propose plutôt des types génériques qui vous permettent d'avoir une seule propriété avec différents types de données, selon les cas d'utilisation.
Qu'est-ce qu'un type de données générique ?
Les types génériques (ou génériques) permettent à un type de données, tel qu'une classe, de spécifier un type de données d'espace réservé inconnu pouvant être utilisé avec ses propriétés et méthodes. Qu'est-ce que cela signifie exactement ?
Dans l'exemple ci-dessus, au lieu de définir une propriété de réponse pour chaque type de données possible, vous pouvez créer une classe unique pour représenter n'importe quelle question et utiliser un nom d'espace réservé pour le type de données de la propriété answer
. Le type de données réel (String
, Int
, Boolean
, etc.) est spécifié lorsque la classe est instanciée. Chaque fois que le nom de l'espace réservé est utilisé, le type de données transmis à la classe est utilisé à la place. La syntaxe permettant de définir un type générique pour une classe est présentée ci-dessous :
Un type de données générique est fourni lors de l'instanciation d'une classe. Il doit donc être défini dans la signature de la classe. Le nom de la classe est suivi d'un chevron ouvrant (<
), puis d'un nom d'espace réservé pour le type de données, et enfin d'un chevron fermant (>
).
Vous pouvez ensuite utiliser le nom de l'espace réservé chaque fois que vous utilisez un type de données réel dans la classe, pour une propriété par exemple.
Le processus est le même que pour toute autre déclaration de propriété, à la différence que le nom de l'espace réservé est utilisé à la place du type de données.
Comment votre classe peut-elle savoir quel type de données utiliser ? Le type de données utilisé pour le type générique est transmis comme un paramètre entre chevrons lorsque vous instanciez la classe.
Le nom de la classe est suivi d'un chevron ouvrant (<
), puis du type de données réel (String
, Boolean
, Int
, etc.), et enfin d'un chevron fermant. (>
). Le type de données de la valeur que vous transmettez pour la propriété générique doit correspondre à celui entre les chevrons. La propriété "answer" sera générique, ce qui vous permettra d'utiliser une classe pour représenter tout type de question, que la réponse soit String
, Boolean
ou Int
, ou tout autre type de données.
Refactoriser votre code pour utiliser des génériques
Refactorisez votre code pour utiliser une classe unique nommée Question
avec une propriété de réponse générique.
- Supprimez les définitions de classe pour
FillInTheBlankQuestion
,TrueOrFalseQuestion
etNumericQuestion
. - Créez une classe nommée
Question
.
class Question()
- Après le nom de la classe, mais avant les parenthèses, ajoutez un paramètre de type générique à l'aide des chevrons gauche et droit. Appelez le type générique
T
.
class Question<T>()
- Ajoutez les propriétés
questionText
,answer
etdifficulty
. La propriétéquestionText
doit être de typeString
. La propriétéanswer
doit être de typeT
, car son type de données est spécifié lors de l'instanciation de la classeQuestion
. La propriétédifficulty
doit être de typeString
.
class Question<T>(
val questionText: String,
val answer: T,
val difficulty: String
)
- Pour voir comment cela fonctionne avec plusieurs types de questions (questions à trous, vrai ou faux, etc.), créez trois instances de la classe
Question
dansmain()
, comme ci-dessous.
fun main() {
val question1 = Question<String>("Quoth the raven ___", "nevermore", "medium")
val question2 = Question<Boolean>("The sky is green. True or false", false, "easy")
val question3 = Question<Int>("How many days are there between full moons?", 28, "hard")
}
- Exécutez votre code pour vous assurer que tout fonctionne correctement. Vous devriez maintenant avoir trois instances de la classe
Question
, chacune avec des types de données différents pour la réponse, au lieu de trois classes différentes ou de recourir à l'héritage. Si vous souhaitez gérer des questions avec un autre type de réponse, vous pouvez réutiliser la même classeQuestion
.
3. Utiliser une classe d'énumération
Dans la section précédente, vous avez défini une propriété de difficulté, avec trois valeurs possibles : "easy", "medium" et "hard". Cela fonctionne, mais quelques problèmes persistent.
- Si vous avez mal orthographié l'une des trois chaînes possibles, vous risquez d'introduire des bugs.
- Si les valeurs changent, par exemple
"medium"
, est renommé"average"
, vous devrez alors mettre à jour toutes les utilisations de la chaîne. - Rien ne vous empêche, vous ou un autre développeur, d'utiliser accidentellement une chaîne différente qui n'est pas l'une des trois valeurs valides.
- Le code est plus difficile à gérer si vous ajoutez des niveaux de difficulté supplémentaires.
Kotlin vous aide à résoudre ces problèmes avec un type de classe spécial : la classe d'énumération. Une classe d'énumération permet de créer des types avec un ensemble limité de valeurs possibles. Par exemple, les quatre directions cardinales (Nord, Sud, Est et Ouest) peuvent être représentées par une classe d'énumération. Utiliser d'autres directions n'est pas nécessaire (et le code ne devrait pas le permettre). La syntaxe d'une classe d'énumération est présentée ci-dessous.
Chaque valeur possible d'une énumération est appelée constante d'énumération. Les constantes d'énumération sont placées entre accolades et séparées par une virgule. Par convention, les constantes s'écrivent en majuscules.
Pour faire référence à des constantes d'énumération, vous devez utiliser l'opérateur point.
Utiliser une constante d'énumération
Modifiez votre code de sorte à utiliser une constante d'énumération, plutôt qu'une String
, pour représenter la difficulté.
- Sous la classe
Question
, définissez une classeenum
nomméeDifficulty
.
enum class Difficulty {
EASY, MEDIUM, HARD
}
- Dans la classe
Question
, modifiez le type de données de la propriétédifficulty
en remplaçantString
parDifficulty
.
class Question<T>(
val questionText: String,
val answer: T,
val difficulty: Difficulty
)
- Lors de l'initialisation des trois questions, transmettez la constante d'énumération pour la difficulté.
val question1 = Question<String>("Quoth the raven ___", "nevermore", Difficulty.MEDIUM)
val question2 = Question<Boolean>("The sky is green. True or false", false, Difficulty.EASY)
val question3 = Question<Int>("How many days are there between full moons?", 28, Difficulty.HARD)
4. Utiliser une classe de données
Bon nombre des classes avec lesquelles vous avez travaillé jusqu'à présent, telles que les sous-classes de Activity
, ont plusieurs méthodes pour effectuer différentes actions. Ces classes ne se limitent pas à représenter des données. Elles contiennent de nombreuses fonctionnalités.
Les classes telles que la classe Question
ne contiennent quant à elles que des données. Elles ne possèdent aucune méthode permettant d'effectuer une action et peuvent être définies comme étant une classe de données. Définir une classe comme une classe de données permet au compilateur Kotlin de formuler certaines hypothèses et d'implémenter automatiquement certaines méthodes. Par exemple, toString()
est appelé en arrière-plan par la fonction println()
. Lorsque vous utilisez une classe de données, toString()
et d'autres méthodes sont implémentées automatiquement en fonction des propriétés de la classe.
Pour définir une classe de données, il vous suffit d'ajouter le mot clé data
devant class
.
Convertir Question
en classe de données
Tout d'abord, vous verrez ce qui se passe lorsque vous essayez d'appeler une méthode telle que toString()
sur une classe qui n'est pas une classe de données. Puis, vous convertirez Question
en une classe de données, de sorte que chaque méthode soit implémentée par défaut.
- Dans
main()
, affichez le résultat de l'appel de la méthodetoString()
surquestion1
.
fun main() {
val question1 = Question<String>("Quoth the raven ___", "nevermore", Difficulty.MEDIUM)
val question2 = Question<Boolean>("The sky is green. True or false", false, Difficulty.EASY)
val question3 = Question<Int>("How many days are there between full moons?", 28, Difficulty.HARD)
println(question1.toString())
}
- Exécutez votre code. La sortie affiche le nom de la classe et un identifiant unique pour l'objet.
Question@37f8bb67
- Transformez
Question
en une classe de données en utilisant le mot clédata
.
data class Question<T>(
val questionText: String,
val answer: T,
val difficulty: Difficulty
)
- Exécutez à nouveau votre code. En en faisant une classe de données, Kotlin peut déterminer comment afficher les propriétés de la classe lors de l'appel à
toString()
.
Question(questionText=Quoth the raven ___, answer=nevermore, difficulty=MEDIUM)
Lorsqu'une classe est définie comme une classe de données, les méthodes suivantes sont implémentées :
equals()
hashCode()
: vous rencontrerez cette méthode lorsque vous utiliserez certains types de collections.toString()
componentN()
:component1()
,component2()
, etc.copy()
5. Utiliser un objet singleton
Il existe de nombreux scénarios dans lesquels vous voulez qu'une classe n'ait qu'une seule instance. Par exemple pour :
- afficher les statistiques du joueur dans un jeu mobile ;
- interagir avec un seul périphérique (par exemple, envoyer du contenu audio via une enceinte) ;
- connecter un objet à une source de données distante (telle qu'une base de données Firebase) ;
- s'authentifier, lorsqu'un seul utilisateur ne peut se connecter à la fois.
Dans les scénarios ci-dessus, vous auriez probablement dû utiliser une classe. Cependant, vous n'aurez à instancier qu'une seule instance de cette classe. S'il n'y a qu'un seul appareil physique ou si un seul utilisateur s'est connecté à la fois, il n'y aura aucune raison de créer plusieurs instances. Le fait d'avoir deux objets qui accèdent simultanément au même périphérique peut entraîner un comportement très étrange et des bugs.
Vous pouvez indiquer clairement dans votre code qu'un objet ne doit avoir qu'une seule instance en le définissant comme un singleton. Un singleton est une classe qui ne peut avoir qu'une seule instance. Kotlin propose une construction spéciale, appelée object, qui peut être utilisée pour créer une classe singleton.
Définir un objet singleton
La syntaxe d'un objet est semblable à celle d'une classe. Utilisez simplement le mot clé object
au lieu de class
. Un objet singleton ne peut pas avoir de constructeur, car vous ne pouvez pas créer d'instances directement. Toutes les propriétés sont définies entre accolades et reçoivent une valeur initiale.
Certains exemples donnés plus tôt peuvent ne pas sembler évidents, surtout si vous n'avez pas encore travaillé avec des périphériques spécifiques ou si vous n'avez pas encore été confronté à l'authentification dans vos applications. Toutefois, vous verrez des objets singleton à mesure que vous poursuivez votre développement Android. Voyons un exemple concret qui utilise un objet pour l'état de l'utilisateur et dans lequel une seule instance est nécessaire.
Pour un quiz, il serait intéressant de pouvoir suivre la progression de l'étudiant en ayant accès au nombre de questions auxquelles il a répondu. Vous n'aurez besoin que d'une instance de cette classe. Par conséquent, au lieu de la déclarer en tant que classe, déclarez-la en tant qu'objet singleton.
- Créez un objet nommé
StudentProgress
.
object StudentProgress {
}
- Dans cet exemple, nous supposons qu'il y a 10 questions au total et que trois d'entre elles ont reçu une réponse jusqu'à présent. Ajoutez deux propriétés
Int
:total
avec une valeur de10
etanswered
avec une valeur de3
.
object StudentProgress {
var total: Int = 10
var answered: Int = 3
}
Accéder à un objet singleton
Vous vous souvenez que vous ne pouvez pas créer directement une instance d'un objet singleton ? Comment pouvez-vous alors accéder à ses propriétés ?
Étant donné qu'il n'existe qu'une seule instance de StudentProgress
à la fois, vous accédez à ses propriétés en faisant référence au nom de l'objet lui-même, suivi de l'opérateur point (.
), puis du nom de la propriété.
Mettez à jour votre fonction main()
pour accéder aux propriétés de l'objet singleton.
- Dans
main()
, ajoutez un appel àprintln()
qui génère les questionsanswered
ettotal
à partir de l'objetStudentProgress
.
fun main() {
...
println("${StudentProgress.answered} of ${StudentProgress.total} answered.")
}
- Exécutez votre code pour vérifier que tout fonctionne correctement.
... 3 of 10 answered.
Déclarer des objets en tant qu'objets compagnons
Les classes et les objets dans Kotlin peuvent être définis à l'intérieur d'autres types et constituent un excellent moyen d'organiser votre code. Vous pouvez définir un objet singleton dans une autre classe à l'aide d'un objet compagnon. Un objet compagnon vous permet d'accéder, à partir de la classe, à ses propriétés et méthodes, pour une syntaxe plus concise. Cet accès est possible seulement si les propriétés et méthodes de l'objet appartiennent à cette classe.
Pour déclarer un objet compagnon, il vous suffit d'ajouter le mot clé companion
devant object
.
Vous allez créer une nouvelle classe appelée Quiz
pour stocker les questions du quiz et faire de StudentProgress
un objet compagnon de la classe Quiz
.
- Sous l'énumération
Difficulty
, définissez une nouvelle classe nomméeQuiz
.
class Quiz {
}
- Déplacez
question1
,question2
etquestion3
depuismain()
vers la classeQuiz
. Vous devez également supprimerprintln(question1.toString())
, si ce n'est pas déjà fait.
class Quiz {
val question1 = Question<String>("Quoth the raven ___", "nevermore", Difficulty.MEDIUM)
val question2 = Question<Boolean>("The sky is green. True or false", false, Difficulty.EASY)
val question3 = Question<Int>("How many days are there between full moons?", 28, Difficulty.HARD)
}
- Déplacez l'objet
StudentProgress
dans la classeQuiz
.
class Quiz {
val question1 = Question<String>("Quoth the raven ___", "nevermore", Difficulty.MEDIUM)
val question2 = Question<Boolean>("The sky is green. True or false", false, Difficulty.EASY)
val question3 = Question<Int>("How many days are there between full moons?", 28, Difficulty.HARD)
object StudentProgress {
var total: Int = 10
var answered: Int = 3
}
}
- Marquez l'objet
StudentProgress
avec le mot clécompanion
.
companion object StudentProgress {
var total: Int = 10
var answered: Int = 3
}
- Mettez à jour l'appel à
println()
pour référencer les propriétés avecQuiz.answered
etQuiz.total
. Même si ces propriétés sont déclarées dans l'objetStudentProgress
, elles sont accessibles grâce à la notation par points en n'utilisant que le nom de la classeQuiz
.
fun main() {
println("${Quiz.answered} of ${Quiz.total} answered.")
}
- Exécutez votre code pour en vérifier la sortie.
3 of 10 answered.
6. Étendre les classes avec de nouvelles propriétés et méthodes
Lorsque vous utilisez Compose, vous avez peut-être remarqué une syntaxe intéressante lorsque vous spécifiez la taille des éléments d'interface utilisateur. Les types numériques, comme Double
, semblent avoir des propriétés telles que dp
et sp
qui spécifient les dimensions.
Pourquoi les concepteurs du langage Kotlin incluent-ils des propriétés et des fonctions sur les types de données intégrés, en particulier pour la création d'une UI Android ? Ont-ils prédit l'avenir ? Kotlin était-il conçu pour être utilisé avec Compose avant même que ce dernier n'existe ?
Bien sûr que non ! Lorsque vous saisissez une classe, vous ne savez généralement pas exactement comment un autre développeur l'utilisera ou prévoit de l'utiliser dans son application. Il est impossible de prédire tous les cas d'utilisation futurs et c'est pourquoi il est déconseillé d'ajouter des données inutiles à votre code.
Le langage Kotlin permet aux autres développeurs d'étendre les types de données existants, en ajoutant des propriétés et des méthodes accessibles à l'aide d'une syntaxe à points, comme s'ils faisaient partie de ce type de données. Un développeur qui n'a pas travaillé sur les types à virgule flottante dans Kotlin, par exemple quelqu'un qui crée la bibliothèque Compose, peut décider d'ajouter des propriétés et des méthodes propres aux dimensions de l'interface utilisateur.
Comme vous avez vu cette syntaxe au cours des deux premières unités dédiées à l'apprentissage de Compose, il est maintenant temps de découvrir son fonctionnement. Vous allez ajouter des propriétés et des méthodes pour étendre les types existants.
Ajouter une propriété d'extension
Pour définir une propriété d'extension, ajoutez le nom du type et un opérateur point (.
) devant le nom de la variable.
Vous allez refactoriser le code de la fonction main() pour afficher la progression du quiz avec une propriété d'extension.
- Sous la classe
Quiz
, définissez une propriété d'extension deQuiz.StudentProgress
nomméeprogressText
de typeString
.
val Quiz.StudentProgress.progressText: String
- Définissez un getter pour la propriété d'extension qui renvoie la même chaîne que celle utilisée précédemment dans
main()
.
val Quiz.StudentProgress.progressText: String
get() = "${answered} of ${total} answered"
- Remplacez le code de la fonction
main()
par le code qui afficheprogressText
. Comme il s'agit d'une propriété d'extension de l'objet compagnon, vous pouvez y accéder grâce à la notation par points, en utilisant le nom de la classe,Quiz
.
fun main() {
println(Quiz.progressText)
}
- Exécutez votre code pour vérifier qu'il fonctionne.
3 of 10 answered.
Ajouter une fonction d'extension
Pour définir une fonction d'extension, ajoutez le nom du type et un opérateur point (.
) avant le nom de la fonction.
Vous allez ajouter une fonction d'extension pour afficher la progression du quiz sous forme d'une barre de progression. Étant donné que vous ne pouvez pas créer de barre de progression dans Kotlin Playground, vous allez afficher une barre de progression de style rétro grâce à du texte !
- Ajoutez une fonction d'extension à l'objet
StudentProgress
nomméprintProgressBar()
. La fonction ne doit pas comporter de paramètres ni de valeur de retour.
fun Quiz.StudentProgress.printProgressBar() {
}
- Affichez le caractère
▓
, le nombre correspondant àanswered
, en utilisantrepeat()
. Cette partie foncée de la barre de progression représente le nombre de questions auxquelles vous avez répondu. Utilisezprint()
, car vous ne voulez pas saisir de nouvelle ligne après chaque caractère.
fun Quiz.StudentProgress.printProgressBar() {
repeat(Quiz.answered) { print("▓") }
}
- Affichez le caractère
▒
, le nombre correspondant à la différence entretotal
etanswered
, en utilisantrepeat()
. Cette partie claire représente les questions restantes dans la barre de processus.
fun Quiz.StudentProgress.printProgressBar() {
repeat(Quiz.answered) { print("▓") }
repeat(Quiz.total - Quiz.answered) { print("▒") }
}
- Affichez une nouvelle ligne à l'aide de
println()
sans argument, puis affichezprogressText
.
fun Quiz.StudentProgress.printProgressBar() {
repeat(Quiz.answered) { print("▓") }
repeat(Quiz.total - Quiz.answered) { print("▒") }
println()
println(Quiz.progressText)
}
- Mettez à jour le code dans
main()
pour appelerprintProgressBar()
.
fun main() {
Quiz.printProgressBar()
}
- Exécutez votre code pour en vérifier la sortie.
▓▓▓▒▒▒▒▒▒▒ 3 of 10 answered.
Tout ceci est-il obligatoire ? Absolument pas. Toutefois, les propriétés et méthodes d'extension vous permettent de partager davantage votre code avec d'autres développeurs. Avoir recours à une syntaxe utilisant des points pour d'autres types peut faciliter la lecture de votre code, à la fois pour vous, mais aussi pour les autres développeurs.
7. Réécrire les fonctions d'extension à l'aide d'interfaces
Sur la page précédente, vous avez vu comment ajouter des propriétés et des méthodes à l'objet StudentProgress
sans avoir à inclure directement du code, à l'aide des propriétés et des fonctions d'extension. Bien qu'il s'agisse d'un excellent moyen d'ajouter des fonctionnalités à une classe déjà définie, il n'est pas toujours nécessaire d'étendre une classe si vous avez accès au code source. De même, vous ne savez pas toujours quelle implémentation adopter. Vous savez seulement qu'une certaine méthode ou propriété doit exister. Si plusieurs classes doivent avoir les mêmes propriétés et méthodes supplémentaires, éventuellement avec un comportement différent, vous pouvez définir ces propriétés et méthodes à l'aide d'une interface.
Supposons qu'en plus des quiz, vous disposiez de classes pour des sondages, les étapes d'une recette ou toute autre donnée ordonnée pouvant utiliser une barre de progression. Vous pouvez définir ce que l'on appelle une "interface" afin de spécifier les méthodes et/ou les propriétés que chacune de ces classes doit inclure.
Une interface est définie à l'aide du mot clé interface
, suivi d'un nom UpperCamelCase (majuscule à la première lettre), puis d'une accolade ouvrante et d'une accolade fermante. Entre les accolades, vous pouvez définir n'importe quelle signature de méthode ou propriété "get-only" que toute classe conforme à l'interface doit implémenter.
Une interface est un contrat. On considère qu'une classe qui se conforme à une interface étend l'interface. Une classe peut déclarer qu'elle souhaite étendre une interface à l'aide du signe deux-points (:
), suivi d'une espace, puis du nom de l'interface.
De son côté, la classe doit implémenter toutes les propriétés et méthodes spécifiées dans l'interface. Cela vous permet de vous assurer facilement que toute classe devant étendre l'interface implémente exactement les mêmes méthodes avec la même signature de méthode. Si vous modifiez l'interface de quelque manière que ce soit, par exemple en ajoutant ou en supprimant des propriétés ou des méthodes, ou en changeant la signature d'une méthode, le compilateur exige que vous mettiez à jour toute classe qui étend l'interface. De la sorte, votre code reste cohérent et plus facile à gérer.
Les interfaces permettent de modifier le comportement des classes qui les étendent. Il revient à chaque classe de fournir l'implémentation.
Voyons comment réécrire la barre de progression pour qu'elle utilise une interface et comment faire en sorte que la classe Quiz étende cette interface.
- Au-dessus de la classe
Quiz
, définissez une interface nomméeProgressPrintable
. Nous avons choisi le nomProgressPrintable
, car il permet aux classes qui l'étendent d'afficher une barre de progression.
interface ProgressPrintable {
}
- Dans l'interface
ProgressPrintable
, définissez une propriété nomméeprogressText
.
interface ProgressPrintable {
val progressText: String
}
- Modifiez la déclaration de la classe
Quiz
pour qu'elle étende l'interfaceProgressPrintable
.
class Quiz : ProgressPrintable {
...
}
- Dans la classe
Quiz
, ajoutez une propriété nomméeprogressText
de typeString
, comme spécifié dans l'interfaceProgressPrintable
. Étant donné que la propriété provient deProgressPrintable
, faites précéderval
du mot clé de remplacement.
override val progressText: String
- Copiez le "getter" de propriété de l'ancienne propriété d'extension
progressText
.
override val progressText: String
get() = "${answered} of ${total} answered"
- Supprimez l'ancienne propriété d'extension
progressText
.
Code à supprimer :
val Quiz.StudentProgress.progressText: String
get() = "${answered} of ${total} answered"
- Dans l'interface
ProgressPrintable
, ajoutez une méthode nomméeprintProgressBar
qui n'accepte aucun paramètre et qui n'a pas de valeur renvoyée.
interface ProgressPrintable {
val progressText: String
fun printProgressBar()
}
- Dans la classe
Quiz
, ajoutez la méthodeprintProgressBar()
à l'aide du mot cléoverride
.
override fun printProgressBar() {
}
- Déplacez le code de l'ancienne fonction d'extension
printProgressBar()
vers le nouvel élémentprintProgressBar()
à partir de l'interface. Modifiez la dernière ligne pour faire référence à la nouvelle variableprogressText
dans l'interface en supprimant la référence àQuiz
.
override fun printProgressBar() {
repeat(Quiz.answered) { print("▓") }
repeat(Quiz.total - Quiz.answered) { print("▒") }
println()
println(progressText)
}
- Supprimez la fonction d'extension
printProgressBar()
. Cette fonctionnalité appartient désormais à la classeQuiz
qui étendProgressPrintable
.
Code à supprimer :
fun Quiz.StudentProgress.printProgressBar() {
repeat(Quiz.answered) { print("▓") }
repeat(Quiz.total - Quiz.answered) { print("▒") }
println()
println(Quiz.progressText)
}
- Mettez à jour le code dans
main()
. Comme la fonctionprintProgressBar()
est désormais une méthode de la classeQuiz
, vous devez d'abord instancier un objetQuiz
, puis appelerprintProgressBar()
.
fun main() {
Quiz().printProgressBar()
}
- Exécutez votre code. La sortie reste inchangée, mais votre code est désormais plus modulaire. À mesure que vos codebases se développent, vous pouvez facilement ajouter des classes conformes à la même interface pour réutiliser le code sans hériter d'une super-classe.
▓▓▓▒▒▒▒▒▒▒ 3 of 10 answered.
Il existe de nombreux cas d'utilisation d'interfaces permettant de structurer votre code. Vous allez commencer à les utiliser fréquemment dans les modules courants. Voici quelques exemples d'interfaces que vous pourrez rencontrer lorsque vous utiliserez Kotlin.
- Injection manuelle de dépendances : créez une interface définissant toutes les propriétés et méthodes de la dépendance. Imposez l'interface comme type de données de la dépendance (activité, scénario de test, etc.) afin qu'une instance de toute classe mettant en œuvre cette interface puisse être utilisée. Cela vous permet de permuter les implémentations sous-jacentes.
- Simulation des tests automatisés : la classe fictive et la classe réelle sont conformes à la même interface.
- Accès aux mêmes dépendances dans une application multiplateforme Compose : par exemple, créez une interface fournissant un ensemble commun de propriétés et de méthodes pour Android et pour ordinateur, même si l'implémentation sous-jacente diffère pour chaque plate-forme.
- Plusieurs types de données dans Compose, tels que
Modifier
, sont des interfaces. Cela vous permet d'ajouter des modificateurs sans avoir à accéder au code source sous-jacent ni à le modifier.
8. Utiliser des fonctions de portée pour accéder aux propriétés et aux méthodes de classe
Comme vous l'avez déjà vu, Kotlin inclut de nombreuses fonctionnalités permettant de rendre votre code plus concis.
Parmi elles, vous découvrirez les fonctions de portée à mesure que vous poursuivez votre apprentissage. Les fonctions de portée vous permettent d'accéder rapidement aux propriétés et aux méthodes d'une classe sans avoir à accéder plusieurs fois au nom de la variable. Qu'est-ce que cela signifie exactement ? Prenons un exemple.
Éliminer les références d'objets répétitives avec des fonctions de portée
Les fonctions de portée sont des fonctions de niveau supérieur qui vous permettent d'accéder aux propriétés et aux méthodes d'un objet sans faire référence au nom de l'objet. Ces fonctions sont appelées "fonctions de portée", car le corps de la fonction transmise reprend la portée de l'objet avec lequel la fonction est appelée. Par exemple, certaines fonctions de portée vous permettent d'accéder aux propriétés et aux méthodes d'une classe, comme si ces fonctions étaient définies comme une méthode de cette classe. Cela peut rendre votre code plus lisible en vous permettant d'omettre le nom de l'objet lorsqu'il est redondant.
Pour mieux illustrer cela, nous allons examiner quelques fonctions de portée que vous étudierez plus tard dans le cours.
Remplacer les noms d'objets longs à l'aide de let()
La fonction let()
vous permet de faire référence à un objet dans une expression lambda à l'aide de l'identifiant it
, au lieu du nom réel de l'objet. Cela peut vous permettre d'éviter d'utiliser un nom d'objet long et plus descriptif lorsque vous accédez à plusieurs propriétés. La fonction let()
est une fonction d'extension qui peut être appelée sur n'importe quel objet Kotlin à l'aide de la notation par points.
Essayez d'accéder aux propriétés de question1
, question2
et question3
avec let()
:
- Ajoutez une fonction à la classe
Quiz
nomméeprintQuiz()
.
fun printQuiz() {
}
- Ajoutez le code suivant pour afficher les éléments
questionText
,answer
etdifficulty
de la question. Même si vous accédez à plusieurs propriétés pourquestion1
,question2
etquestion3
, le nom complet de la variable est utilisé à chaque fois. Si le nom de la variable a changé, vous devez mettre à jour chaque occurrence.
fun printQuiz() {
println(question1.questionText)
println(question1.answer)
println(question1.difficulty)
println()
println(question2.questionText)
println(question2.answer)
println(question2.difficulty)
println()
println(question3.questionText)
println(question3.answer)
println(question3.difficulty)
println()
}
- Utilisez un appel à la fonction
let()
pour encadrer le code accédant aux propriétésquestionText
,answer
etdifficulty
dequestion1
,question2
etquestion3
. Remplacez le nom de la variable dans chaque expression lambda par cet appel.
fun printQuiz() {
question1.let {
println(it.questionText)
println(it.answer)
println(it.difficulty)
}
println()
question2.let {
println(it.questionText)
println(it.answer)
println(it.difficulty)
}
println()
question3.let {
println(it.questionText)
println(it.answer)
println(it.difficulty)
}
println()
}
- Mettez à jour le code dans
main()
pour créer une instance de la classeQuiz
nomméequiz
.
fun main() {
val quiz = Quiz()
}
- Appelez
printQuiz()
.
fun main() {
val quiz = Quiz()
quiz.printQuiz()
}
- Exécutez votre code pour vérifier que tout fonctionne correctement.
Quoth the raven ___ nevermore MEDIUM The sky is green. True or false false EASY How many days are there between full moons? 28 HARD
Appeler les méthodes d'un objet sans variable avec apply()
L'une des fonctionnalités intéressantes des fonctions de portée est que vous pouvez les appeler sur un objet avant même qu'il ne soit attribué à une variable. Par exemple, la fonction apply()
est une fonction d'extension qui peut être appelée sur un objet à l'aide de la notation par points. La fonction apply()
renvoie également une référence à cet objet pour qu'il puisse être stocké dans une variable.
Mettez à jour le code dans main()
pour appeler la fonction apply()
.
- Lors de la création de l'instance de la classe
Quiz
, appelezapply()
après la parenthèse fermante. Vous pouvez omettre les parenthèses lorsque vous appelezapply()
et utiliser la syntaxe lambda de fin.
val quiz = Quiz().apply {
}
- Déplacez l'appel à
printQuiz()
dans l'expression lambda. Vous n'avez plus besoin de référencer la variablequiz
ni d'utiliser la notation par points.
val quiz = Quiz().apply {
printQuiz()
}
- La fonction
apply()
renvoie l'instance de la classeQuiz
, mais comme vous ne l'utilisez plus, supprimez la variablequiz
. Avec la fonctionapply()
, vous n'avez même pas besoin d'une variable pour appeler des méthodes sur l'instance deQuiz
.
Quiz().apply {
printQuiz()
}
- Exécutez votre code. Notez que vous avez pu appeler cette méthode sans aucune référence à l'instance de
Quiz
. La fonctionapply()
a renvoyé les objets qui étaient stockés dansquiz
.
Quoth the raven ___ nevermore MEDIUM The sky is green. True or false false EASY How many days are there between full moons? 28 HARD
Bien que l'utilisation des fonctions de portée ne soit pas obligatoire pour obtenir la sortie souhaitée, les exemples ci-dessus montrent comment elles peuvent rendre votre code plus concis et éviter toute répétition d'un même nom de variable.
Le code ci-dessus n'est qu'un exemple. Nous vous encourageons à ajouter des favoris et à consulter la documentation relative aux fonctions de portée, dont vous découvrirez l'utilisation plus tard dans le cours.
9. Résumé
Vous venez de voir plusieurs nouvelles fonctionnalités Kotlin en action. Les génériques permettent de transmettre des types de données en tant que paramètres aux classes. Les classes d'énumération définissent quant à elles un ensemble limité de valeurs possibles, tandis que les classes de données permettent de générer automatiquement des méthodes utiles pour les classes.
Vous avez également vu comment créer un objet singleton (limité à une instance), comment en faire un objet compagnon d'une autre classe et comment étendre des classes existantes avec de nouvelles propriétés "get-only" et de nouvelles méthodes. Enfin, vous avez vu comment les fonctions de portée peuvent simplifier la syntaxe pour accéder aux propriétés et aux méthodes.
Ces concepts seront abordés dans les prochaines unités, à mesure que vous en apprendrez plus sur Kotlin, le développement Android et Compose. À présent, vous comprenez mieux comment fonctionnent ces concepts et comment ils peuvent améliorer la réutilisation et la lisibilité de votre code.