Utiliser les modèles Kotlin courants avec Android

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.