Introduction à Kotlin : programmation Android pour les humains
Publié: 2022-03-11Dans un monde Android parfait, le langage principal de Java est vraiment moderne, clair et élégant. Vous pouvez écrire moins en faisant plus, et chaque fois qu'une nouvelle fonctionnalité apparaît, les développeurs peuvent l'utiliser simplement en augmentant la version dans Gradle. Ensuite, lors de la création d'une très belle application, elle apparaît entièrement testable, extensible et maintenable. Nos activités ne sont pas trop vastes et compliquées, nous pouvons changer les sources de données de la base de données au Web sans des tonnes de différences, et ainsi de suite. Ça sonne bien, non ? Malheureusement, le monde Android n'est pas cet idéal. Google vise toujours la perfection, mais nous savons tous que les mondes idéaux n'existent pas. Ainsi, nous devons nous aider dans ce grand voyage dans le monde Android.
Qu'est-ce que Kotlin et pourquoi devriez-vous l'utiliser ?
Donc, la première langue. Je pense que Java n'est pas le maître de l'élégance ou de la clarté, et il n'est ni moderne ni expressif (et je suppose que vous êtes d'accord). L'inconvénient est qu'en dessous d'Android N, on est encore limité à Java 6 (y compris quelques petites parties de Java 7). Les développeurs peuvent également attacher RetroLambda pour utiliser des expressions lambda dans leur code, ce qui est très utile lors de l'utilisation de RxJava. Au-dessus d'Android N, nous pouvons utiliser certaines des nouvelles fonctionnalités de Java 8, mais c'est toujours ce vieux Java lourd. Très souvent, j'entends les développeurs Android dire "J'aimerais qu'Android supporte un langage plus agréable, comme iOS le fait avec Swift". Et si je vous disais que vous pouvez utiliser un langage simple et très agréable, avec une sécurité nulle, des lambdas et de nombreuses autres fonctionnalités intéressantes ? Bienvenue à Kotlin.
Qu'est-ce que Kotlin ?
Kotlin est un nouveau langage (parfois appelé Swift pour Android), développé par l'équipe JetBrains, et est maintenant dans sa version 1.0.2. Ce qui le rend utile dans le développement Android, c'est qu'il se compile en bytecode JVM et peut également être compilé en JavaScript. Il est entièrement compatible avec Java, et le code Kotlin peut être simplement converti en code Java et vice versa (il existe un plugin de JetBrains). Cela signifie que Kotlin peut utiliser n'importe quel framework, bibliothèque, etc. écrit en Java. Sur Android, il s'intègre par Gradle. Si vous avez une application Android existante et que vous souhaitez implémenter une nouvelle fonctionnalité dans Kotlin sans réécrire toute l'application, commencez simplement à écrire dans Kotlin et cela fonctionnera.
Mais quelles sont les "grandes nouvelles fonctionnalités" ? Permettez-moi d'en énumérer quelques-uns :
Paramètres de fonction facultatifs et nommés
fun createDate(day: Int, month: Int, year: Int, hour: Int = 0, minute: Int = 0, second: Int = 0) { print("TEST", "$day-$month-$year $hour:$minute:$second") }Nous pouvons appeler la méthode createDate de différentes manières
createDate(1,2,2016) prints: '1-2-2016 0:0:0' createDate(1,2,2016, 12) prints: '1-2-2016 12:0:0' createDate(1,2,2016, minute = 30) prints: '1-2-2016 0:30:0'Sécurité nulle
Si une variable peut être nulle, le code ne compilera pas à moins que nous les forcions à le faire. Le code suivant contiendra une erreur - nullableVar peut être null :
var nullableVar: String? = “”; nullableVar.length;Pour compiler, il faut vérifier s'il n'est pas nul :
if(nullableVar){ nullableVar.length }Ou, plus court :
nullableVar?.lengthDe cette façon, si nullableVar est null, rien ne se passe. Sinon, nous pouvons marquer la variable comme n'acceptant pas la valeur null, sans point d'interrogation après le type :
var nonNullableVar: String = “”; nonNullableVar.length;Ce code compile, et si nous voulons affecter null à nonNullableVar, le compilateur affichera une erreur.
Il existe également un opérateur Elvis très utile :
var stringLength = nullableVar?.length ?: 0Ensuite, lorsque nullableVar est null (donc nullableVar?.length renvoie null), stringLength aura la valeur 0.
Variables mutables et immuables
Dans l'exemple ci-dessus, j'utilise var lors de la définition d'une variable. Ceci est modifiable, nous pouvons le réaffecter quand nous le voulons. Si nous voulons que cette variable soit immuable (dans de nombreux cas, nous devrions), nous utilisons val (comme valeur, pas comme variable) :
val immutable: Int = 1Après cela, le compilateur ne nous permettra pas de réaffecter à immutable.
Lambda
Nous savons tous ce qu'est un lambda, donc ici je vais juste montrer comment nous pouvons l'utiliser dans Kotlin :
button.setOnClickListener({ view -> Log.d("Kotlin","Click")})Ou si la fonction est le seul ou le dernier argument :
button.setOnClickListener { Log.d("Kotlin","Click")}Rallonges
Les extensions sont une fonctionnalité de langage très utile, grâce à laquelle nous pouvons "étendre" les classes existantes, même lorsqu'elles sont finales ou que nous n'avons pas accès à leur code source.
Par exemple, pour obtenir une valeur de chaîne à partir du texte d'édition, au lieu d'écrire à chaque fois editText.text.toString(), nous pouvons écrire la fonction :
fun EditText.textValue(): String{ return text.toString() }Ou plus court :
fun EditText.textValue() = text.toString()Et maintenant, avec chaque instance de EditText :
editText.textValue()Ou, nous pouvons ajouter une propriété renvoyant la même chose :
var EditText.textValue: String get() = text.toString() set(v) {setText(v)}Surcharge de l'opérateur
Parfois utile si nous voulons ajouter, multiplier ou comparer des objets. Kotlin permet la surcharge des opérateurs binaires (plus, moins, plusAssign, plage, etc.), des opérateurs de tableau (get, set, get range, set range), et des opérations égales et unaires (+a, -a, etc.)
Classe de données
De combien de lignes de code avez-vous besoin pour implémenter une classe User en Java avec trois propriétés : copy, equals, hashCode et toString ? À Kaotlin, vous n'avez besoin que d'une seule ligne :
data class User(val name: String, val surname: String, val age: Int)Cette classe de données fournit les méthodes equals(), hashCode() et copy(), ainsi que toString(), qui imprime User comme :
User(name=John, surname=Doe, age=23)Les classes de données fournissent également d'autres fonctions et propriétés utiles, que vous pouvez voir dans la documentation Kotlin.
Extensions Anko
Vous utilisez des extensions Butterknife ou Android, n'est-ce pas ? Et si vous n'avez même pas besoin d'utiliser cette bibliothèque, et après avoir déclaré des vues en XML, utilisez-la simplement à partir du code par son ID (comme avec XAML en C#):
<Button android: android:layout_width="match_parent" android:layout_height="wrap_content" /> loginBtn.setOnClickListener{}Kotlin a des extensions Anko très utiles, et avec cela, vous n'avez pas besoin de dire à votre activité ce qu'est loginBtn, il le sait simplement en « important » xml :
import kotlinx.android.synthetic.main.activity_main.*Il existe de nombreuses autres choses utiles dans Anko, notamment le démarrage d'activités, l'affichage de toasts, etc. Ce n'est pas l'objectif principal d'Anko - il est conçu pour créer facilement des mises en page à partir de code. Donc, si vous avez besoin de créer une mise en page par programmation, c'est la meilleure façon.
Ceci n'est qu'un aperçu de Kotlin. Je recommande la lecture du blog d'Antonio Leiva et de son livre - Kotlin for Android Developers, et bien sûr le site officiel de Kotlin.
Qu'est-ce que le MVP et pourquoi ?
Un langage agréable, puissant et clair ne suffit pas. Il est très facile d'écrire des applications désordonnées avec toutes les langues sans une bonne architecture. Les développeurs Android (principalement ceux qui débutent, mais aussi les plus avancés) donnent souvent à Activity la responsabilité de tout ce qui les entoure. Activité (ou Fragment, ou autre vue) télécharge les données, les envoie pour les enregistrer, les présente, répond aux interactions de l'utilisateur, modifie les données, gère toutes les vues enfants. . . et souvent bien plus. C'est trop pour des objets aussi instables comme les Activités ou les Fragments (il suffit de faire pivoter l'écran et l'Activité dit « Au revoir…. »).
Une très bonne idée est d'isoler les responsabilités des points de vue et de les rendre aussi stupides que possible. Les vues (activités, fragments, vues personnalisées ou tout ce qui présente des données à l'écran) doivent être uniquement responsables de la gestion de leurs sous-vues. Les vues doivent avoir des présentateurs, qui communiqueront avec le modèle et leur diront ce qu'ils doivent faire. Ceci, en bref, est le modèle Model-View-Presenter (pour moi, il devrait être nommé Model-Presenter-View pour montrer les connexions entre les couches).
"Hé, je connais quelque chose comme ça, et ça s'appelle MVC!" - n'avez-vous pas pensé? Non, MVP n'est pas la même chose que MVC. Dans le modèle MVC, votre vue peut communiquer avec le modèle. Lorsque vous utilisez MVP, vous n'autorisez aucune communication entre ces deux couches - la seule façon dont View peut communiquer avec Model est via Presenter. La seule chose que View sait sur Model peut être la structure de données. View sait comment, par exemple, afficher User, mais ne sait pas quand. Voici un exemple simple :
View sait "Je suis Activity, j'ai deux EditTexts et un Button. Lorsque quelqu'un clique sur le bouton, je dois le dire à mon présentateur et lui transmettre les valeurs de EditTexts. Et c'est tout, je peux dormir jusqu'à ce que le prochain clic ou le présentateur me dise quoi faire.
Le présentateur sait que quelque part se trouve une vue et il sait quelles opérations cette vue peut effectuer. Il sait également que lorsqu'il reçoit deux chaînes, il doit créer un utilisateur à partir de ces deux chaînes et envoyer les données au modèle à enregistrer, et si l'enregistrement réussit, indiquer à la vue "Afficher les informations de réussite".
Le modèle sait simplement où se trouvent les données, où elles doivent être enregistrées et quelles opérations doivent être effectuées sur les données.
Les applications écrites en MVP sont faciles à tester, à maintenir et à réutiliser. Un présentateur pur ne devrait rien savoir de la plate-forme Android. Il devrait s'agir d'une classe Java pure (ou Kotlin, dans notre cas). Grâce à cela, nous pouvons réutiliser notre présentateur dans d'autres projets. Nous pouvons également écrire facilement des tests unitaires, en testant séparément Model, View et Presenter.
Une petite digression : MVP devrait faire partie de l'architecture propre de l'oncle Bob pour rendre les applications encore plus flexibles et bien architecturées. J'essaierai d'écrire à ce sujet la prochaine fois.
Exemple d'application avec MVP et Kotlin
C'est assez de théorie, voyons un peu de code ! Bon, essayons de créer une application simple. L'objectif principal de cette application est de créer un utilisateur. Le premier écran aura deux EditTexts (Nom et Prénom) et un bouton (Enregistrer). Après avoir saisi le nom et le prénom et cliqué sur "Enregistrer", l'application doit afficher "L'utilisateur est enregistré" et passer à l'écran suivant, où le nom et le prénom enregistrés sont présentés. Lorsque le nom ou le prénom est vide, l'application ne doit pas enregistrer l'utilisateur et afficher une erreur indiquant ce qui ne va pas.
La première chose après la création du projet Android Studio est de configurer Kotlin. Vous devez installer le plug-in Kotlin et, après le redémarrage, dans Outils> Kotlin, vous pouvez cliquer sur "Configurer Kotlin dans le projet". IDE ajoutera des dépendances Kotlin à Gradle. Si vous avez un code existant, vous pouvez facilement le convertir en Kotlin par (Ctrl+Maj+Alt+K ou Code > Convertir le fichier Java en Kotlin). Si quelque chose ne va pas et que le projet ne se compile pas, ou que Gradle ne voit pas Kotlin, vous pouvez vérifier le code de l'application disponible sur GitHub.
Maintenant que nous avons un projet, commençons par créer notre première vue - CreateUserView. Cette vue devrait avoir les fonctionnalités mentionnées précédemment, nous pouvons donc écrire une interface pour cela :

interface CreateUserView : View { fun showEmptyNameError() /* show error when name is empty */ fun showEmptySurnameError() /* show error when surname is empty */ fun showUserSaved() /* show user saved info */ fun showUserDetails(user: User) /* show user details */ }Comme vous pouvez le voir, Kotlin est similaire à Java dans la déclaration des fonctions. Ce sont toutes des fonctions qui ne retournent rien, et les dernières ont un paramètre. C'est la différence, le type de paramètre vient après le nom. L'interface View n'est pas d'Android - c'est notre interface simple et vide :
interface ViewL'interface de Basic Presenter doit avoir une propriété de type View, et au moins une méthode (onDestroy par exemple), où cette propriété sera définie sur null :
interface Presenter<T : View> { var view: T? fun onDestroy(){ view = null } }Ici, vous pouvez voir une autre fonctionnalité de Kotlin - vous pouvez déclarer des propriétés dans les interfaces et également y implémenter des méthodes.
Notre CreateUserView doit communiquer avec CreateUserPresenter. La seule fonction supplémentaire dont ce présentateur a besoin est saveUser avec deux arguments de chaîne :
interface CreateUserPresenter<T : View>: Presenter<T> { fun saveUser(name: String, surname: String) }Nous avons également besoin de la définition du modèle - il est mentionné plus tôt dans la classe de données :
data class User(val name: String, val surname: String)Après avoir déclaré toutes les interfaces, nous pouvons commencer à implémenter.
CreateUserPresenter sera implémenté dans CreateUserPresenterImpl :
class CreateUserPresenterImpl(override var view: CreateUserView?): CreateUserPresenter<CreateUserView> { override fun saveUser(name: String, surname: String) { } }La première ligne, avec la définition de classe :
CreateUserPresenterImpl(override var view: CreateUserView?)Est un constructeur, nous l'utilisons pour attribuer une propriété de vue, définie dans l'interface.
MainActivity, qui est notre implémentation CreateUserView, a besoin d'une référence à CreateUserPresenter :
class MainActivity : AppCompatActivity(), CreateUserView { private val presenter: CreateUserPresenter<CreateUserView> by lazy { CreateUserPresenterImpl(this) } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) saveUserBtn.setOnClickListener{ presenter.saveUser(userName.textValue(), userSurname.textValue()) /*use of textValue() extension, mentioned earlier */ } } override fun showEmptyNameError() { userName.error = getString(R.string.name_empty_error) /* it's equal to userName.setError() - Kotlin allows us to use property */ } override fun showEmptySurnameError() { userSurname.error = getString(R.string.surname_empty_error) } override fun showUserSaved() { toast(R.string.user_saved) /* anko extension - equal to Toast.makeText(this, R.string.user_saved, Toast.LENGTH_LONG) */ } override fun showUserDetails(user: User) { } override fun onDestroy() { presenter.onDestroy() } }Au début du cours, nous avons défini notre présentateur :
private val presenter: CreateUserPresenter<CreateUserView> by lazy { CreateUserPresenterImpl(this) }Il est défini comme immuable (val) et est créé par un délégué paresseux, qui sera affecté la première fois qu'il sera nécessaire. De plus, nous sommes sûrs qu'il ne sera pas nul (pas de point d'interrogation après définition).
Lorsque l'utilisateur clique sur le bouton Enregistrer, View envoie des informations à Presenter avec les valeurs EditTexts. Lorsque cela se produit, l'utilisateur doit être enregistré, nous devons donc implémenter la méthode saveUser dans Presenter (et certaines des fonctions du modèle):
override fun saveUser(name: String, surname: String) { val user = User(name, surname) when(UserValidator.validateUser(user)){ UserError.EMPTY_NAME -> view?.showEmptyNameError() UserError.EMPTY_SURNAME -> view?.showEmptySurnameError() UserError.NO_ERROR -> { UserStore.saveUser(user) view?.showUserSaved() view?.showUserDetails(user) } } }Lorsqu'un utilisateur est créé, il est envoyé à UserValidator pour vérifier sa validité. Ensuite, selon le résultat de la validation, la méthode appropriée est appelée. La construction when() {} est identique à switch/case en Java. Mais c'est plus puissant - Kotlin permet d'utiliser non seulement enum ou int dans 'case', mais aussi des plages, des chaînes ou des types d'objets. Il doit contenir toutes les possibilités ou avoir une expression else. Ici, il couvre toutes les valeurs UserError.
En utilisant view?.showEmptyNameError() (avec un point d'interrogation après view), nous sommes protégés de NullPointer. La vue peut être annulée dans la méthode onDestroy, et avec cette construction, rien ne se passera.
Lorsqu'un modèle utilisateur n'a pas d'erreurs, il indique à UserStore de l'enregistrer, puis demande à View d'afficher le succès et d'afficher les détails.
Comme mentionné précédemment, nous devons implémenter certaines choses de modèle :
enum class UserError { EMPTY_NAME, EMPTY_SURNAME, NO_ERROR } object UserStore { fun saveUser(user: User){ //Save user somewhere: Database, SharedPreferences, send to web... } } object UserValidator { fun validateUser(user: User): UserError { with(user){ if(name.isNullOrEmpty()) return UserError.EMPTY_NAME if(surname.isNullOrEmpty()) return UserError.EMPTY_SURNAME } return UserError.NO_ERROR } }La chose la plus intéressante ici est UserValidator. En utilisant le mot objet, nous pouvons créer une classe singleton, sans nous soucier des threads, des constructeurs privés, etc.
Prochaine chose - dans la méthode validateUser(user), il y a l'expression with(user) {}. Le code dans ce bloc est exécuté dans le contexte de l'objet, transmis avec le nom et le prénom sont les propriétés de l'utilisateur.
Il y a aussi une autre petite chose. Tout le code ci-dessus, de enum à UserValidator, la définition est placée dans un seul fichier (la définition de la classe User est également ici). Kotlin ne vous oblige pas à avoir chaque classe publique dans un seul fichier (ou à nommer la classe exactement comme fichier). Ainsi, si vous avez de courts morceaux de code associés (classes de données, extensions, fonctions, constantes - Kotlin ne nécessite pas de classe pour la fonction ou la constante), vous pouvez le placer dans un seul fichier au lieu de le diffuser dans tous les fichiers du projet.
Lorsqu'un utilisateur est enregistré, notre application doit l'afficher. Nous avons besoin d'une autre vue - il peut s'agir de n'importe quelle vue Android, d'une vue personnalisée, d'un fragment ou d'une activité. J'ai choisi Activité.
Alors, définissons l'interface UserDetailsView. Il peut afficher l'utilisateur, mais il devrait également afficher une erreur lorsque l'utilisateur n'est pas présent :
interface UserDetailsView { fun showUserDetails(user: User) fun showNoUserError() }Ensuite, UserDetailsPresenter. Il doit avoir une propriété utilisateur :
interface UserDetailsPresenter<T: View>: Presenter<T> { var user: User? }Cette interface sera implémentée dans UserDetailsPresenterImpl. Il doit remplacer la propriété de l'utilisateur. Chaque fois que cette propriété est attribuée, l'utilisateur doit être actualisé sur la vue. Nous pouvons utiliser un paramètre de propriété pour cela :
class UserDetailsPresenterImpl(override var view: UserDetailsView?): UserDetailsPresenter<UserDetailsView> { override var user: User? = null set(value) { field = value if(field != null){ view?.showUserDetails(field!!) } else { view?.showNoUserError() } } }L'implémentation de UserDetailsView, UserDetailsActivity, est très simple. Comme précédemment, nous avons un objet présentateur créé par chargement différé. L'utilisateur à afficher doit être transmis via l'intention. Il y a un petit problème avec cela pour le moment, et nous le résoudrons dans un instant. Lorsque nous avons un utilisateur d'intention, View doit l'attribuer à son présentateur. Après cela, l'utilisateur sera actualisé à l'écran ou, s'il est nul, l'erreur apparaîtra (et l'activité se terminera - mais le présentateur ne le sait pas):
class UserDetailsActivity: AppCompatActivity(), UserDetailsView { private val presenter: UserDetailsPresenter<UserDetailsView> by lazy { UserDetailsPresenterImpl(this) } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_user_details) val user = intent.getParcelableExtra<User>(USER_KEY) presenter.user = user } override fun showUserDetails(user: User) { userFullName.text = "${user.name} ${user.surname}" } override fun showNoUserError() { toast(R.string.no_user_error) finish() } override fun onDestroy() { presenter.onDestroy() } }Le passage d'objets via des intentions nécessite que cet objet implémente l'interface Parcelable. C'est un travail très "sale". Personnellement, je déteste faire cela à cause de tous les CRÉATEURS, propriétés, sauvegarde, restauration, etc. Heureusement, il existe un plugin approprié, Parcelable pour Kotlin. Après l'avoir installé, nous pouvons générer Parcelable en un seul clic.
La dernière chose à faire est d'implémenter showUserDetails(user : User) dans notre MainActivity :
override fun showUserDetails(user: User) { startActivity<UserDetailsActivity>(USER_KEY to user) /* anko extension - starts UserDetailsActivity and pass user as USER_KEY in intent */ }Et c'est tout.
Nous avons une application simple qui enregistre un utilisateur (en fait, il n'est pas enregistré, mais nous pouvons ajouter cette fonctionnalité sans toucher au présentateur ou à la vue) et le présente à l'écran. À l'avenir, si nous voulons changer la façon dont l'utilisateur est présenté à l'écran, par exemple de deux activités à deux fragments dans une activité, ou deux vues personnalisées, les modifications ne concerneront que les classes View. Bien sûr, si nous ne modifions pas la fonctionnalité ou la structure du modèle. Le présentateur, qui ne sait pas exactement ce qu'est la vue, n'aura besoin d'aucune modification.
Et après?
Dans notre application, Presenter est créé chaque fois qu'une activité est créée. Cette approche, ou son contraire, si Presenter doit persister à travers les instances d'activité, fait l'objet de nombreuses discussions sur Internet. Pour moi, cela dépend de l'application, de ses besoins et du développeur. Parfois, il vaut mieux détruire le présentateur, parfois non. Si vous décidez d'en conserver un, une technique très intéressante consiste à utiliser LoaderManager pour cela.
Comme mentionné précédemment, MVP devrait faire partie de l'architecture propre de l'oncle Bob. De plus, les bons développeurs devraient utiliser Dagger pour injecter les dépendances des présentateurs aux activités. Cela aide également à maintenir, tester et réutiliser le code à l'avenir. Actuellement, Kotlin fonctionne très bien avec Dagger (avant la sortie officielle, ce n'était pas si facile), ainsi qu'avec d'autres bibliothèques Android utiles.
Emballer
Pour moi, Kotlin est un super langage. C'est moderne, clair et expressif tout en étant développé par des gens formidables. Et nous pouvons utiliser n'importe quelle nouvelle version sur n'importe quel appareil et version Android. Tout ce qui me met en colère contre Java, Kotlin s'améliore.
Bien sûr, comme je l'ai dit, rien n'est idéal. Kotlin présente également certains inconvénients. Les dernières versions du plugin gradle (principalement alpha ou beta) ne fonctionnent pas bien avec ce langage. Beaucoup de gens se plaignent que le temps de construction est un peu plus long que Java pur, et les apks ont quelques Mo supplémentaires. Mais Android Studio et Gradle continuent de s'améliorer et les téléphones ont de plus en plus d'espace pour les applications. C'est pourquoi je pense que Kotlin peut être un langage très agréable pour chaque développeur Android. Essayez-le et partagez dans la section des commentaires ci-dessous ce que vous en pensez.
Le code source de l'exemple d'application est disponible sur Github : github.com/tomaszczura/AndroidMVPKotlin
