Cet article porte sur certains des aspects les plus utiles du langage Kotlin lorsque vous développez pour Android.
Utiliser des fragments
Les sections suivantes utilisent des exemples de Fragment
pour présenter certaines des meilleures fonctionnalités de Kotlin.
Héritage
Dans Kotlin, vous pouvez déclarer une classe avec le mot clé class
. Dans l'exemple suivant, LoginFragment
est une sous-classe de Fragment
. Vous pouvez indiquer l'héritage en plaçant l'opérateur :
entre la sous-classe et son parent :
class LoginFragment : Fragment()
Dans cette déclaration de classe, LoginFragment
est chargé d'appeler le constructeur de sa super-classe, Fragment
.
Dans LoginFragment
, vous pouvez ignorer un certain nombre de rappels de cycle de vie pour répondre aux changements d'état dans votre Fragment
. Pour remplacer une fonction, utilisez le mot clé override
, comme illustré dans l'exemple suivant :
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.login_fragment, container, false)
}
Pour référencer une fonction dans la classe parente, utilisez le mot clé super
, comme illustré dans l'exemple suivant :
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
}
Possibilité de valeur nulle et initialisation
Dans les exemples précédents, certains paramètres des méthodes remplacées comportent un type suivi d'un point d'interrogation ?
. Cela indique que les arguments transmis pour ces paramètres peuvent avoir une valeur nulle. Assurez-vous de gérer la possibilité de valeur nulle en toute sécurité.
Dans Kotlin, vous devez initialiser les propriétés d'un objet lorsque vous le déclarez.
Cela signifie que lorsque vous obtenez une instance d'une classe, vous pouvez immédiatement référencer l'une de ses propriétés accessibles. Les objets View
dans un Fragment
ne sont toutefois pas prêts à être gonflés avant d'appeler Fragment#onCreateView
. Vous devez donc reporter l'initialisation de la propriété pour une View
.
lateinit
vous permet de reporter l'initialisation de la propriété. Lorsque vous utilisez lateinit
, vous devez initialiser votre propriété dès que possible.
L'exemple suivant montre comment utiliser lateinit
pour attribuer des objets View
dans onViewCreated
:
class LoginFragment : Fragment() {
private lateinit var usernameEditText: EditText
private lateinit var passwordEditText: EditText
private lateinit var loginButton: Button
private lateinit var statusTextView: TextView
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
usernameEditText = view.findViewById(R.id.username_edit_text)
passwordEditText = view.findViewById(R.id.password_edit_text)
loginButton = view.findViewById(R.id.login_button)
statusTextView = view.findViewById(R.id.status_text_view)
}
...
}
Conversion SAM
Vous pouvez écouter les événements de clic dans Android en implémentant l'interface OnClickListener
. Les objets Button
contiennent une fonction setOnClickListener()
qui intègre une implémentation de OnClickListener
.
OnClickListener
comporte une seule méthode abstraite, onClick()
, que vous devez implémenter. Étant donné que setOnClickListener()
utilise toujours un OnClickListener
comme argument et que OnClickListener
comporte toujours la même méthode abstraite, cette implémentation peut être représentée à l'aide d'une fonction anonyme dans Kotlin. Ce processus est appelé conversion SAM (Single-abstraction-Method, ou méthode abstraite unique en français).
Cette conversion peut rendre votre code considérablement plus clair. L'exemple suivant montre comment utiliser la conversion SAM pour implémenter un OnClickListener
pour un Button
:
loginButton.setOnClickListener {
val authSuccessful: Boolean = viewModel.authenticate(
usernameEditText.text.toString(),
passwordEditText.text.toString()
)
if (authSuccessful) {
// Navigate to next screen
} else {
statusTextView.text = requireContext().getString(R.string.auth_failed)
}
}
Le code de la fonction anonyme transmise à setOnClickListener()
s'exécute lorsqu'un utilisateur clique sur le bouton loginButton
.
Objets compagnons
Les objets compagnons fournissent un mécanisme permettant de définir des variables ou des fonctions liées conceptuellement à un type, mais pas à un objet particulier. Les objets compagnons fonctionnent de la même manière que le mot clé static
Java pour les variables et les méthodes.
Dans l'exemple suivant, TAG
est une constante String
. Vous n'avez pas besoin d'une instance unique de String
pour chaque instance de LoginFragment
. Vous devriez donc la définir dans un objet compagnon :
class LoginFragment : Fragment() {
...
companion object {
private const val TAG = "LoginFragment"
}
}
Vous pouvez définir TAG
au niveau supérieur du fichier, mais le fichier peut également comporter un grand nombre de variables, de fonctions et de classes également définies au niveau supérieur. Les objets compagnons permettent d'associer des variables, des fonctions et la définition de classe sans faire référence à une instance particulière de cette classe.
Délégation de propriété
Lorsque vous initialisez des propriétés, vous pouvez répéter certains des modèles les plus courants d'Android, tels que l'accès à un ViewModel
dans un Fragment
. Pour éviter un excès de code en double, vous pouvez utiliser la syntaxe de délégation de propriété de Kotlin.
private val viewModel: LoginViewModel by viewModels()
La délégation de propriété fournit une implémentation courante que vous pouvez réutiliser dans votre application. Android KTX vous fournit des délégués de propriété.
viewModels
, par exemple, récupère un élément ViewModel
limité au Fragment
actuel.
La délégation de propriété utilise la réflexion, ce qui ralentit les performances. En contrepartie, la syntaxe est plus concise et permet de gagner du temps de développement.
Possibilité de valeur nulle
Kotlin fournit des règles strictes concernant les valeurs nulles qui préservent la sûreté du typage dans l'ensemble de votre application. En Kotlin, les références à des objets ne peuvent pas contenir de valeurs nulles par défaut. Pour attribuer une valeur nulle à une variable, vous devez déclarer un type de variable pouvant avoir une valeur nulle en ajoutant ?
à la fin du type de base.
Par exemple, l'expression suivante est non conforme dans Kotlin. name
est de type String
et ne peut pas avoir une valeur nulle :
val name: String = null
Pour autoriser une valeur nulle, vous devez utiliser un type de String
pouvant avoir une valeur nulle, soit String?
, comme illustré dans l'exemple suivant :
val name: String? = null
Interopérabilité
Les règles strictes de Kotlin rendent votre code plus sûr et plus concis. Elles réduisent le risque de générer une erreur NullPointerException
, qui entraînerait le plantage de votre application. De plus, elles réduisent le nombre de vérifications de valeur nulle que vous devez effectuer dans votre code.
Souvent, vous devez également appeler du code autre que Kotlin lorsque vous écrivez une application Android, car la plupart des API Android sont écrites dans le langage de programmation Java.
La possibilité de valeur nulle est un domaine clé où Java et Kotlin diffèrent dans leur comportement. Java est moins strict sur la syntaxe prenant en charge la possibilité de valeur nulle.
Par exemple, la classe Account
comporte plusieurs propriétés, dont une propriété String
appelée name
. Java ne dispose pas des règles de Kotlin sur la possibilité de valeur nulle. Il s'appuie plutôt sur des annotations de possibilité de valeur nulle facultatives pour déclarer explicitement si vous pouvez attribuer une valeur nulle.
Étant donné que le framework Android est principalement écrit en Java, vous pouvez rencontrer ce scénario lorsque vous faites appel à des API sans annotation de possibilité de valeur nulle.
Types de plates-formes
Si vous utilisez Kotlin pour référencer un membre name
non annoté défini dans une classe Account
de Java, le compilateur ne sait pas si la String
est mappée à une String
ou une String?
dans Kotlin. Cette ambiguïté est représentée par un type de plate-forme, String!
.
String!
n'a pas de signification particulière pour le compilateur Kotlin. String!
peut représenter String
ou String?
, et le compilateur vous permet d'attribuer une valeur de l'un ou l'autre de ces types. Notez que vous risquez de générer une erreur NullPointerException
si vous représentez le type en tant que String
et que vous attribuez une valeur nulle.
Pour remédier à ce problème, vous devez utiliser des annotations de possibilité de valeur nulle lorsque vous écrivez du code en Java. Ces annotations aident les développeurs Java et Kotlin.
Par exemple, voici la classe Account
telle qu'elle est définie dans Java :
public class Account implements Parcelable {
public final String name;
public final String type;
private final @Nullable String accessId;
...
}
L'une des variables de membre, accessId
, est annotée avec @Nullable
, ce qui indique qu'elle peut contenir une valeur nulle. Kotlin traitera ensuite accessId
en tant que String?
.
Pour indiquer qu'une variable ne peut jamais avoir une valeur nulle, utilisez l'annotation @NonNull
:
public class Account implements Parcelable {
public final @NonNull String name;
...
}
Dans ce scénario, name
est considéré comme une String
ne pouvant pas avoir une valeur nulle dans Kotlin.
Les annotations de possibilité de valeur nulle sont incluses dans toutes les nouvelles API Android et dans de nombreuses API Android existantes. De nombreuses bibliothèques Java ont ajouté des annotations de possibilité de valeur nulle pour mieux répondre aux besoin des développeurs Kotlin et Java.
Gérer la possibilité de valeur nulle
Si vous avez des doutes sur un type Java, considérez qu'il peut avoir une valeur nulle.
Par exemple, le membre name
de la classe Account
n'est pas annoté. Vous devez donc supposer qu'il s'agit d'une String
pouvant avoir une valeur nulle.
Si vous souhaitez réduire name
afin que sa valeur ne contienne pas d'espace de début ou de fin, vous pouvez utiliser la fonction trim
de Kotlin. Il existe plusieurs méthodes pour réduire une String?
de manière sécurisée. L'une d'entre elles consiste à utiliser l'opérateur d'assertion "non nul", !!
, comme illustré dans l'exemple suivant :
val account = Account("name", "type")
val accountName = account.name!!.trim()
L'opérateur !!
traite tout ce qui se trouve à gauche comme une valeur non nulle. Dans ce cas, vous traitez name
comme une String
non nulle. Si le résultat de l'expression de gauche est nul, votre application génère une erreur NullPointerException
.
Cet opérateur est simple et rapide, mais il doit être utilisé avec parcimonie, car il peut introduire des erreurs NullPointerException
dans votre code.
Un choix plus sûr consiste à utiliser l'opérateur d'appel sécurisé ?.
, comme illustré dans l'exemple suivant :
val account = Account("name", "type")
val accountName = account.name?.trim()
En utilisant l'opérateur d'appel sécurisé, si name
a une valeur non nulle, le résultat de name?.trim()
est une valeur de nom sans espace de début ni de fin. Si name
a une valeur nulle, le résultat de name?.trim()
est null
. Cela signifie que votre application ne peut jamais générer d'erreur NullPointerException
lors de l'exécution de cette instruction.
Bien que l'opérateur d'appel sécurisé vous épargne une erreur NullPointerException
éventuelle, il transmet une valeur nulle à l'instruction suivante. Vous pouvez gérer les cas de valeur nulle immédiatement en utilisant un opérateur Elvis (?:
), comme illustré dans l'exemple suivant :
val account = Account("name", "type")
val accountName = account.name?.trim() ?: "Default name"
Si le résultat de l'expression de gauche de l'opérateur Elvis est nul, la valeur figurant à droite est définie sur accountName
. Cette technique est utile pour obtenir une valeur par défaut qui serait sinon nulle.
Vous pouvez également utiliser l'opérateur Elvis pour renvoyer un résultat à partir d'une fonction de manière anticipée, comme illustré dans l'exemple suivant :
fun validateAccount(account: Account?) {
val accountName = account?.name?.trim() ?: "Default name"
// account cannot be null beyond this point
account ?: return
...
}
Modifications apportées aux API Android
Les API Android sont de plus en plus compatibles avec Kotlin. De nombreuses API Android parmi les plus courantes, y compris AppCompatActivity
et Fragment
, contiennent des annotations de possibilité de valeur nulle. Certains appels tels que Fragment#getContext
offrent des alternatives compatibles avec Kotlin.
Par exemple, l'accès au Context
d'un Fragment
est presque toujours non nul, car la plupart des appels que vous effectuez dans un Fragment
sont effectués alors que le Fragment
est associé à une Activity
(une sous-classe de Context
). Cela dit, Fragment#getContext
ne renvoie pas toujours une valeur non nulle, car il existe des scénarios dans lesquels un Fragment
n'est pas associé à une Activity
. Ainsi, le type renvoyé de Fragment#getContext
peut avoir une valeur nulle.
Étant donné que le Context
renvoyé par Fragment#getContext
peut avoir une valeur nulle (et est annoté en tant que @Nullable), vous devez le traiter en tant que Context?
dans votre code Kotlin.
Cela signifie qu'il faut appliquer l'un des opérateurs mentionnés précédemment pour traiter la possibilité de valeur nulle avant d'accéder à ses propriétés et fonctions. Pour certains de ces scénarios, Android contient des API alternatives qui offrent cette possibilité.
Fragment#requireContext
, par exemple, renvoie un Context
non nul et génère une erreur IllegalStateException
s'il est appelé en cas de Context
nul. De cette façon, vous pouvez traiter le Context
renvoyé comme non nul sans avoir besoin d'opérateurs d'appel sécurisé ou de solutions de contournement.
Initialisation des propriétés
Dans Kotlin, les propriétés ne sont pas initialisées par défaut. Elles doivent l'être lorsque leur classe englobante est initialisée.
Vous pouvez initialiser des propriétés de différentes façons. L'exemple suivant montre comment initialiser une variable index
en lui attribuant une valeur dans la déclaration de classe :
class LoginFragment : Fragment() {
val index: Int = 12
}
Cette initialisation peut également être définie dans un bloc d'initialisation :
class LoginFragment : Fragment() {
val index: Int
init {
index = 12
}
}
Dans les exemples ci-dessus, index
est initialisé lorsqu'un LoginFragment
est construit.
Cependant, il se peut que certaines propriétés ne puissent pas être initialisées lors de la construction de l'objet. Par exemple, vous pouvez vouloir référencer une View
à partir d'un Fragment
, ce qui signifie que le modèle doit d'abord être gonflé. Le gonflement n'a pas lieu lorsqu'un Fragment
est construit. En revanche, il a lieu lorsque vous appelez Fragment#onCreateView
.
Une façon d'éviter ce problème est de déclarer la vue comme pouvant avoir une valeur nulle et de l'initialiser dès que possible, comme illustré dans l'exemple suivant :
class LoginFragment : Fragment() {
private var statusTextView: TextView? = null
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
statusTextView = view.findViewById(R.id.status_text_view)
statusTextView?.setText(R.string.auth_failed)
}
}
Bien que cela fonctionne comme prévu, vous devez maintenant gérer la possibilité de valeur nulle de la View
chaque fois que vous la référencez. Nous vous recommandons d'utiliser lateinit
pour l'initialisation de View
, comme illustré dans l'exemple suivant :
class LoginFragment : Fragment() {
private lateinit var statusTextView: TextView
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
statusTextView = view.findViewById(R.id.status_text_view)
statusTextView.setText(R.string.auth_failed)
}
}
Le mot clé lateinit
vous permet d'éviter d'initialiser une propriété lors de la construction d'un objet. Si votre propriété est référencée avant d'être initialisée, Kotlin génère une erreur UninitializedPropertyAccessException
. Par conséquent, veillez à initialiser votre propriété dès que possible.