Android

Présentation du cours

Présentation personnelle

  • Ancien de l'ESGI (Architecture des logiciels)
     
  • Activités pro :
    Développeur mobile (Flutter & Android)
    CTO @ Como & CTO @ Flappy
    Formateur (Flutter, Git, Android, Firebase, Algo)
     
  • Adresse email : thomasecalle+prof@gmail.com

Présentation du cours

Règles générales concernant : 

  • les retards
  • l'attention en cours
  • les supports de cours
  • la notation
  • je prends vos feedback ! (thomasecalle+prof@gmail.com)

Prérequis

  • programmation orientée objet
     
  • bases de la programmation en général
     
  • appétences en UI

Jusqu'où irons-nous ?

  • apprentissage des bases de Kotlin
  • créer un projet Android
  • création de vue
  • navigation
  • apprentissage du layouting de base
  • appels réseaux
  • Architecture Components
  • etc.

Introduction à Android

Android, qu'est-ce que c'est ?

  • à l'origine une Startup Android 
  • rachetée en 2005 par Google
  • système d'exploitation basé sur le noyau linux
  • 80% des smartphones sont Android
  • mais aussi : tablettes, TV Android, objets connectés, smart watch, Chromebook, etc.

Android, qu'est-ce que c'est ?

Android n'est pas que un système d'exploitation

  • système d'exploitations
  • bibliothèques logicielles
  • environnement d'exécution (Dalvik puis ART)
  • framework (ce qui nous intéresse !)
  • lot d'applications de base

Environnement d'exécution

Jusqu'à la version 4.4 d'Android, la machine virtuelle Dalvik est utilisée pour interpréter du Java 

Cette machine virtuelle, un peu différente de la JVM (machine virtuelle d'Oracle) est spécialement conçue pour les téléphones de l'époque (beaucoup moins performants qu'aujourd'hui)

Environnement d'exécution

A partir de la version 5.0 d'Android, Dalvik est remplacée par ART (Android RunTime)

ART a été développé par Google pour pallier les problèmes de performances de Dalvik qui se faisait vieillissant

Versions d'Android

Versions d'Android

Sur ce tableau, il y a 2 informations très importantes :

  • la répartition (en %)
  • la version d'API associé

Mais pour comprendre leur importance, il faut comprendre ce qu'ils veulent dire et donc comment Android fonctionne

Fonctionnement d'Android

Android est avant tout un système d'exploitation

Cala signifie, pour faire trèèèès simple, qu'il y a un ensemble de programmes, de composants, ... bref de code Android dans votre téléphone

C'est ce qu'on appelle pour un langage de programmation une "bibliothèque standard" : un ensemble de variables, fonctions, etc. déjà en place pour aider le développeur

Fonctionnement d'Android

Avec Android, il y a également toute une bibliothèque graphique utilisée pour construire l'UI de nos applications

L'ensemble de la bibliothèque standard, bibliothèque graphique, etc. est ce qu'on appel l'API (Application Programing Interface) d'Android

Fonctionnement d'Android

Ainsi, les versions d'API d'Android correspondent à des mises à jour de cette API

Ajout de fonctions, mise à jours de composants graphiques, correction de bugs, etc.

Fonctionnement d'Android

BREF, un peu comme une version de PHP, de HTML ou autre quoi...

SAUF qu'il y a une différence majeure à prendre en compte !

Fonctionnement d'Android

L'ensemble de l'API Android est embarquée dans les smartphones !

Cela signifie que c'est lorsqu'un utilisateur met à jour son système d'exploitation qu'il met en fait à jour l'API Android utilisée par son Smartphone

Fonctionnement d'Android

Cela signifie donc que nous avons PLEINS de versions d'API d'Android dans la nature !

  • parce que certains ont des Smartphones trop vieux pour les mettre à jour (pays moins développés)
     
  • parce que les gens mettent beaucoup de temps à faire leurs mises à jour
     
  • parce qu'il y a une variété incroyable de téléphones différents sous Android

Fonctionnement d'Android

Résultat de ce constat :

Il serait impensable de développer une application Android en se basant sur le fait que les utilisateurs auront la dernière version !

Il faut donc en permanence penser aux utilisateurs avec de plus vieilles versions et parfois adapter son code en fonction !

Fonctionnement d'Android

Vous comprenez mieux l'utilité de ce tableau ?

Quels outils ?

Outils

Le premier outil indispensable pour Android est le SDK (Software Development Kit)

Il s'agit d'un ensemble d'outils utiles au développeur :

  • langage de programmation
  • compilateur
  • debogueur
  • etc.

Outils

Le second outil utile est un IDE

  • jusqu'à 2014 : Eclipse
  • après 2014 : Android Studio (IntelliJ)

En téléchargeant Android Studio, le SDK Android est compris dedans :)

Quel langage ?

Quel langage ?

On l'a dit, Android est de base prévu pour le langage Java

De très très nombreuses applications mobiles sont donc développées en Java

Mais ... l'entreprise JetBrain (à l'origine des IDE IntelliJ, Android Studio, etc.) travaillait depuis quelques années sur un langage nommé Kotlin

Quel langage ?

Kotlin est un langage de programmation qui permet de compiler directement pour la machine virtuelle Java

Cela permet notamment d'avoir une interroperabilité parfaite entre Java et Android

On peut écrire du kotlin et du Java dans un même projet, ça fonctionne !

Quel langage ?

Si vous ajoutez à ça le fait que Kotlin est peut-être l'un des 2 langages les plus modernes (avec Swift) , la grande majorité des dévelopeurs Java ont vu l'arrivée de Kotlin comme l'héritier et l'avenir de Java

Quel langage ?

Ainsi, et devant l'engouement général :

  • en 2017 : Google annonce que Kotlin est officiellement le second langage de programmation pris en charge par Android
     
  • 8 mai 2019 : Google annonce que Kotlin est officiellement LE langage recommandé pour développer en Android : remplace officiellement Java comme langage recommandé

Quel langage ?

Convertir un projet Java en Kotlin, bien que les langages soient interroperables, n'est pas forcément chose aisée

C'est pourquoi il est toujours possible de croiser des projets écrits en Java !

La plupart du temps, les développeurs maintiennent le Java, développe les nouvelles features en kotlin et convertissent le Java petit à petit

Quel langage ?

Mais nous allons quand même nous concentrer uniquement sur Kotlin parce que :

  • tous les projets from scratch sont écrits en kotlin !
     
  • le langage est bien plus moderne que les versions de Java supportées par Android

 

La face cachée des choix de Google

Kotlin, uniquement un choix de modernité ?

La face cachée

Bien que la modernité et l'efficacité de Kotlin soit un argument avéré, il n'est pas le seul qui a poussé Google à remplacer Java...

Il existe en effet un très gros conflit entre Oracle (société qui possède Java) et Google

La face cachée

En 2010, Oracle a assigné Google en justice, lui réclamant 9 milliard de dollars pour avoir utiliser des packages Java dans Android sans payer de droits de licences 

Ce conflit entre Oracle et Google dure depuis lors et est un cas très intéressant de notion de droits de propriété sur des codes sources, sur l'open source, etc.

Nul doute que la conclusion de ce conflit aura un impact énorme sur le monde du logiciel et la notion de droit d'auteur associée

La face cachée

Je vous invite à étudier de votre côté ce conflit s'il vous intéresse, car ce n'est pas réellement l'objet de notre cours

Mais ce conflit a quand même eu un impact énorme sur le développement Android ! 

En effet, Android ne peut plus supporter que la version 7 du JDK Java et quelques features de la version 8

La face cachée

Or le JDK de Java en est maintenant à la version 16...

Ainsi, l'arrivée de Kotlin était providentielle !

Kotlin permet à Google de se détacher de plus en plus de Java et donc d'Oracle tout en étant un langage extrêmement moderne (bien plus que les versions 7 et 8 de Java)

Apprenons le Kotlin

Apprenons le Kotlin

Pour apprendre à coder en Kotlin (en dehors d'Android), vous pouvez utiliser :

Apprenons le Kotlin

Nous allons apprendre les BASES de Kotlin !

Hello world !

Hello world !

fun main(args: Array<String>) {
    print("Hello World !")
}

Les variables

Les variables

val a: Int = 42 
// Le type est déclaré après le nom
val b = 5       
// le type Int est inféré (Kotlin sait comprend que vous voulez mettre un Int)
val c: Int      
c = 3
// L'initialisation ayant lieu après, nous devons préciser le type

Les variables

VAL signifie, comme final en Java, que la référence de la variable est immutable

val languages = mutableListOf("Java", "JavaScript")

languages.add("Kotlin") 
// OK

languages = mutableListOf("Other", "List")
// PAS OK !!! la référence languages est immutable

Les variables

VAR signifie que la variable est mutable

var a = 3
a = 42
// OK, parce que a est mutable

val b = 3
b = 12
// PAS OK, parce que b est immutable

Les variables

CONST signifie que la variable est ce qu'on appelle une "compile-time constant"

const val CODE = 42

Les variables

Les Strings

Il y a 2 "types" de Strings :

  • les littéraux : "mon texte" (ceux de d'habitude)
  • les raws : """ mon texte """
val text = 
    """
Salut
Voici un test qui interprète les sauts 
de ligne !
    """

Les variables

Les Strings

Kotlin a un système de templating de strings

fun main() {
    var userName = "Thomas"
    hello(userName)
}

fun hello(name: String) {
    print("Hello $name")
}

Les variables

Les Strings

Kotlin a un système de templating de strings

fun main() {
    println("On peut aussi appeller une fonction ${test()}")
}

fun test(): Int {
    return 42
}

Les fonctions

Les fonctions

Une fonction de base en kotlin

fun main() {
    println(test())
}

fun test(): Int {
    return 42
}

Les fonctions

Un seul retour ? On peut raccourcir alors !

fun main() {
    println(test())
    println(multiply(3, 2))
}

fun test(): Int = 42

fun multiply(a: Int, b: Int): Int = a * b

Les fonctions

Paramètres nommés (et du coup on peut changer l'ordre !)

fun main() {
    hello("Thomas", "Ecalle")
    hello(lastName = "Ecalle", firstName = "Thomas")
}

fun hello(firstName: String, lastName: String) {
    println("Hello $firstName $lastName!")
}

Les fonctions

Valeurs par défaut

fun main() {
    hello()
    hello("Babar")
    hello(name = "Babar")
}

fun hello(name: String = "Babar") {
    println("Hello $name !")
}

Les fonctions

Fonction locales

fun main() {
    println(isMultipleOf8And2(8)) // true
    println(isMultipleOf8And2(5)) // false
    println(isMultipleOf8And2(16)) // true
}

fun isMultipleOf8And2(n: Int): Boolean {
    fun isMultipleOf(operand: Int): Boolean = n % operand == 0
    return isMultipleOf(2) && isMultipleOf(8)
}

Les fonctions

Extensions

Il est possible en Kotlin d'étendre le comportement d'une classe existante en lui ajoutant des fonctions

fun String.log(warning: Boolean = false) {
    val label = if (warning) "Warning" else "Info"
    println("$label : $this") 
}

fun main() {
    val text = "An incredible text"
    text.log()
    text.log(warning = true)
}

Les fonctions

Il est aussi possible en kotlin d'overrider le comportement des opérateurs !

fun main(args: Array<String>) {
    val p1 = Point(3, -8)
    val p2 = Point(2, 9)

    var sum = Point()
    sum = p1 + p2

    println("sum = (${sum.x}, ${sum.y})")
}

class Point(val x: Int = 0, val y: Int = 10) {
    operator fun plus(p: Point) : Point {
        return Point(x + p.x, y + p.y)
    }
}

Les structures de contrôle

fun main(args: Array<String>) {
    
val a = 3
val b = 42
    
var max = a 
if (a < b) max = b

var test: Int
if (a > b) {
    test = a
} else {
    test = b
}
 
val maximum = if (a > b) a else b

}

Les structures de contrôle

En Kotlin, il n'y a pas de Switch, mais When, extrêmement utile

fun main(args: Array<String>) {
    val x = 42
    
    when (x) {
    1 -> print("x == 1")
    2 -> print("x == 2")
    else -> {
        print("x is neither 1 nor 2")
	}
    }
}

Les structures de contrôle

fun main(args: Array<String>) {
	val x = 42
    
	when (x) {
		0, 1 -> print("x == 0 or x == 1")
		else -> print("otherwise")
	}
}

Les structures de contrôle

fun main(args: Array<String>) {
	val x = 42
    
	when (x) {
		in 1..10 -> print("x is in the range")
		in validNumbers -> print("x is valid")
		!in 10..20 -> print("x is outside the range")
		else -> print("none of the above")
	}
}

Les structures de contrôle

fun main(args: Array<String>) {
	val x = 42
	println(hasPrefix(x))
}

fun hasPrefix(x: Any) = when(x) {
	is String -> x.startsWith("prefix")
	else -> false
}

Les structures de contrôle

Les structures de contrôle

fun main(args: Array<String>) {
	val x = 42
	when {
		x.isOdd() -> print("x is odd")
		y.isEven() -> print("y is even")
		else -> print("x+y is even.")
	}
}

Les structures de contrôle

Les boucles FOR

fun main(args: Array<String>) {
    val array = arrayOf(3, 4, 18, 39)
    for (item: Int in array) {
		println(item)
    }
}

Les structures de contrôle

Les boucles FOR

fun main(args: Array<String>) {
    for (i in 1..3) {
		println(i)
	}
	for (i in 6 downTo 0 step 2) {
		println(i)
	}
}

Les structures de contrôle

Les boucles FOR

fun main(args: Array<String>) {
	val array = arrayOf(3, 4, 18, 39)
    
	for ((index, value) in array.withIndex()) {
		println("the element at $index is $value")
	}
}

Les classes

Les classes

Constructeur primaire

class Person constructor(firstName: String) {
    
}

class User(name: String){
    
}

fun main(args: Array<String>) {
    val user = User("Bob")
    val person = Person("Toto")
}

Les classes

le constructeur primaire ne peut contenir de code, il faut donc utiliser le bloc init

class InitOrderDemo(name: String) {
    
    init {
        println("First initializer block that prints ${name}")
    }
    
    
    init {
        println("Second initializer block that prints ${name.length}")
    }
}

fun main(args: Array<String>) {
    val a = InitOrderDemo("toto")
}

Les classes

class Customer(name: String) {
    val customerKey = name.toUpperCase()
}

fun main(args: Array<String>) {
    val customer = Customer("toto")
    println(customer.customerKey)
}

Les paramètres du constructeur primaire peuvent être utilisés directement

Les classes

class Customer(val name: String) {
    
}

fun main(args: Array<String>) {
    val customer = Customer("toto")
    println(customer.name)
}

Pour définir une propriété ET la binder avec les paramètres du constructeur, il y a une manière très simple :

Les classes

class Customer(private val name: String) {
    
}

fun main(args: Array<String>) {
    val customer = Customer("toto")
    println(customer.name)
}

On peut y définir leur visibilité aussi :

Provoque donc une erreur !

Les classes

Une classe peut avoir d'autres constructeurs

class Person(val name: String) {
    var children: MutableList<Person> = mutableListOf<Person>()
    constructor(name: String, parent: Person) : this(name) {
        parent.children.add(this)
    }
}

fun main(args: Array<String>) {
    val homer = Person("Homer")
    val bart = Person("Bart", parent = homer)
    println(homer.children)
}

Les classes

class Constructors {
    init {
        println("Init block")
    }

    constructor(i: Int) {
        println("Constructor")
    }
}

fun main(args: Array<String>) {
    val a = Constructors(42)
}

Les blocs init seront toujours appelés en premiers

Les classes

class User(val name: String = "Bob") {
    
}

fun main(args: Array<String>) {
    val bob = User()
    val jimmy = User("Jimmy")
    val michel = User(name = "Michel")
    println(bob.name)
    println(jimmy.name)
    println(michel.name)
}

On peut mettre des valeurs par défaut et les paramètres peuvent être nommés !

Les classes

L'héritage

  • toutes les classes héritent de Any (qui a equals(), hashCode() et toString() )
     
  • les classes sont final par défaut
     
  • il faut leur ajouter le mot clé open pour les rendre candidates à l'héritage

Les classes

L'héritage

enum class Speciality {
    KOTLIN,
    FLUTTER
}

open class Employee(val name: String) {
    fun sayHello() {
        println("Hello, my name is $name")
    }
}

class Developer(name: String, val speciality: Speciality): Employee(name) {
    
}

fun main(args: Array<String>) {
    val bob = Developer("Bob", Speciality.FLUTTER)
    bob.sayHello()
}

Les classes

L'héritage

open class Employee {
   constructor(lastName: String) {
       
   }
   constructor(firstName: String, lastName: String) {
       
   }
}

class Developer: Employee {
    constructor(lastName: String): super(lastName)
    constructor(firstName: String, lastName: String): super(firstName, lastName)
}

Les classes

L'héritage

Si une classe veut rendre overridable une méthode, elle doit le spécifier explicitement avec le mot open

Les classes

L'héritage

open class Employee(val name: String) {
    open fun sayHello() {
        println("Hello, my name is $name")
    }
}

class Developer(name: String): Employee(name) {
    override fun sayHello() {
        println("Hello, my name is $name and I am a developer")
    }
}

fun main(args: Array<String>) {
    val bob = Developer("Bob")
    val john = Employee("John")
    bob.sayHello()
    john.sayHello()
    
}

Les classes

L'héritage

class Person(val name: String) {
    var children: MutableList<Person> = mutableListOf<Person>()
    
    constructor(name: String, parent: Person) : this(name) {
        parent.children.add(this)
    }
    
    override fun toString(): String {
        return "$name (children = $children)"
    }
    
}

fun main(args: Array<String>) {
    val homer = Person("Homer")
    val bart = Person("Bart", parent = homer)
    val bartSon = Person("Bart son", parent = bart)
    println(homer.children)
}

Les classes

L'héritage

abstract class Employee(val name: String) {
    open fun sayHello() {
        println("Hello, my name is $name")
    }
}

class Developer(name: String): Employee(name) {
    override fun sayHello() {
        println("Hello, my name is $name and I am a developer")
    }
}

class Random(name: String): Employee(name) {
}

fun main(args: Array<String>) {
    val bob = Developer("Bob")
    val john = Random("John")
    bob.sayHello()
    john.sayHello()
}

Classes abstraites

Les classes

L'héritage

interface MyInterface {
    fun bar()
    fun foo() {
      println("Salut")
    }
}

class A: MyInterface {
    override fun bar() {
        println("A bar")
    }
}

class B: MyInterface {
    override fun bar() {
        println("B bar")
    }
    
    override fun foo() {
        println("Salut de B")
    }
}

fun main(args: Array<String>) {
    val a = A()
    val b = B()
    a.bar()
    a.foo()
    b.bar()
    b.foo()
}

Interfaces

Les classes

Les data class

Si vous êtes habitués à la programmation objet, vous savez qu'il y a un certain nombre de choses que l'on répète souvent lors de la création de classe 

  • overrider le toString
  • overrider le equals
  • overrider le hasCode
  • etc.

Les classes

Les data class

Kotlin a inventé les Data Class qui font tous le travail pour nous !!

Les classes

Sans data class

class User(val name: String, val age: Int) {
    override fun toString(): String {
        return "User { name: $name, age: $age}"
    }
    
    override fun equals(other: Any?): Boolean {
        return (other is User) && name == other.name && age == other.age
    } 
    
    override fun hashCode(): Int {
        return name.hashCode() + age.hashCode()
    } 
}

fun main(args: Array<String>) {
    val bob = User("Bob", 30)
    val bob2 = User("Bob", 30)
    println(bob)
    println(bob2)
    println(bob == bob2)
}

Les classes

Avec data class

data class User(val name: String, val age: Int)

fun main(args: Array<String>) {
    val bob = User("Bob", 30)
    val bob2 = User("Bob", 30)
    println(bob)
    println(bob2)
    println(bob == bob2)
}

Les classes

Destructuring

data class User(val name: String, val age: Int)

fun main(args: Array<String>) {
    val bob = User("Bob", 30)
    val bob2 = User("Bob", 30)
    
    val (name, age) = bob
    val (_, bob2Age) = bob2
    println(name)
    println(age)
    println(bob2Age)
}

Les nullables !

Les nullables !

Les types en Kotlin sont TOUS non nullable

Cela signifie qu'ils ne pourront jamais avoir NULL comme valeur

Cette fonctionnalité ravi tous ceux qui ont déjà eu affaire, en faisant de l'Android avec Java ou même d'autres frameworks, à la fameuse exception :  NullPointerException 

Les nullables !

Au delà d'éviter cette exception, ça permet aussi de réduire drastiquement tous les codes de nullité !

Il est tout de même parfois utile de rendre nullable un type, mais il faut alors le préciser explicitement avec le symbole "?"

Les nullables !

fun main(args: Array<String>) {
    var a: String = "toto"
    a = null
}

Null cannot be a value of a non-null type String

Les nullables !

fun main(args: Array<String>) {
    var a: String? = null
    println(a)
    a = "toto"
    println(a)
}

a est nullable (String?) et donc tout est ok ici 

Les nullables !

Pour éviter toute NullPointerException, tous les appels à des champs ou méthodes de nullable devront se faire ainsi :

data class User(val name: String)

fun main(args: Array<String>) {
    var bob: User? = null
    println(bob.name)      // Causerait une NullPointer !!
    println(bob?.name)     // Parfiat, n'afichera juste rien
    bob = User("Bob")
    println(bob?.name)     // Affiche le bon nom car bob est initialisé
}

Les nullables !

Ainsi on écrira souvent des choses comme ça :

bob?.department?.head?.name

Une telle chaîne renvoie null si un null a été trouvé 

Ca nous permet d'éviter bon nombre de tests  !

Les nullables !

L'opérateur ?: (Elvis operator) permet des ternaires raccourcies pour les nullables

fun main(args: Array<String>) {
    test(null)
    test(30)
}

fun test(a: Int?) {
    val value = a ?: 42
    println(value)
}

Les nullables !

L'opérateur !! permet de forcer un appel, une manière de dire que l'on est sûr que la référence n'est pas nulle

Cet opérateur est TRES dangereux, considérez que vous n'avez pas le droit de l'utiliser sauf cas extrême

Les nullables !

Par exemple, ceci serait impossible :

fun main(args: Array<String>) {
    var a: Int? = null
    a = 12
    test(a)
}

fun test(a: Int) {
    println(a)
}

Type mismatch: inferred type is Int? but Int was expected

Les nullables !

L'opérateur !! permet donc ce genre de choses :

fun main(args: Array<String>) {
    var a: Int? = null
    a = 12
    test(a!!)
}

fun test(a: Int) {
    println(a)
}

Très dangereux car propice à beaucoup d'erreurs non maîtrisées !

Les nullables !

Safe Cast

fun main(args: Array<String>) {
    var a: Int = 3
    var b: String = a as String
}

Ce code provoque une ClassCastException car le Int ne peut être caster ainsi en String

Les nullables !

Safe Cast

fun main(args: Array<String>) {
    var a: Int = 3
    var b: String? = a as? String
}

Les nullables nous permettent de rendre ça un peu plus sécurisant en assignant null si le cast ne fonctionne pas !

Les fonctions de haut niveau et les lambdas

Fonctions de haut niveau et lambdas

En Kotlin, les fonctions sont "de premier ordre"

Cela signifie qu'elle peuvent : 

  • être stockées dans des variables
  • être passées en paramètres d'autres fonctions
  • être retournées par d'autres fonctions

Fonctions de haut niveau et lambdas

Exemple de lambda :

fun main(args: Array<String>) {
	val square = { number: Int -> number * number }
	println(square(3))
}

Fonctions de haut niveau et lambdas

Voilà le genre de tricks qu'on peut faire !

fun main(args: Array<String>) {
	loopOn(10, {
		println("On middle !")
	})
}

fun loopOn(x: Int, onMiddle: () -> Unit) {
    for (i in 1..x) {
        if (i == x / 2) {
            onMiddle()
        } else {
            println(i)
        }
    }
}

Fonctions de haut niveau et lambdas

Petite astuce !

Si le dernier paramètre d'une fonction est une fonction, on peut simplifier l'écriture d'appel ainsi :

fun main(args: Array<String>) {
	loopOn(10) {
		println("On middle !")
	}
}

fun loopOn(x: Int, onMiddle: () -> Unit) {
    for (i in 1..x) {
        if (i == x / 2) {
            onMiddle()
        } else {
            println(i)
        }
    }
}

Les collections

Les collections

Les types de collections principales en Kotlin sont :

  • List -> collection d'éléments
  • Set -> collection d'éléments uniques
  • Map -> dictionnaire

Les collections

Pour chaque collection, vous avez une interface non-mutable, en "read-only" et une mutable 

fun main(args: Array<String>) {
	val list = mutableListOf("toto", "tata")
	list.add("titi")
	println(list)
    
    // Possible

	val list = listOf("toto", "tata")
	list.add("titi")
	println(list)
    
    // IMPOSSIBLE !
}

Les collections

Au delà de ce détail, les collections fonctionnent un peu comme vous en avez l'habitude

A savoir : deux listes sont égales si leur contenues sont égaux et de même taille

Les collections

Les maps

fun main(args: Array<String>) {
	val numbersMap = mapOf(
        "key1" to 1,
        "key2" to 2,
        "key3" to 3,
        "key4" to 1
    )
    println(numbersMap["key3"])
}

Les collections

Filtrage

fun main(args: Array<String>) {
    val numbers = listOf("one", "two", "three", "four")  
    val longerThan3 = numbers.filter { it.length > 3 }
    println(longerThan3)

    val numbersMap = mapOf(
        "key1" to 1,
        "key2" to 2,
        "key3" to 3,
        "key11" to 11
    )
    val filteredMap = numbersMap.filter { (key, value) -> key.endsWith("1") && value > 10}
    println(filteredMap)
}

Les collections

Plus & Minus

fun main(args: Array<String>) {
	val numbers = listOf("one", "two", "three", "four")

	val plusList = numbers + "five"
	val minusList = numbers - listOf("three", "four")
	println(plusList)
	println(minusList)
}

Les petits plus

Les petits plus

LET

Les petits plus

Let prend l'objet invoqué, le passe dans une lambda (it) , et renvoie le résultat de la lambda (le dernier élément)

data class User(var name: String)

fun main(args: Array<String>) {
    val user = User("Toto")
    val newName = user.let {
        it.name = "tata"
        it.name
    }
    
    println(newName)
}

Les petits plus

Très utile pour gérer les nullable !

data class User(var name: String)

fun main(args: Array<String>) {
    var user: User? = null
    
    user?.let { println(it) }
    
    user = User("Toto")
    
    user?.let { println(it) }
}

Les petits plus

ALSO

Les petits plus

Also permet comme son nom l'indique d'ajouter des instructions sur l'objet ciblé. Also renvoie l'objet ciblé

data class Person(var name: String, var tutorial : String)

fun main(args: Array<String>) {
    
	var person = Person("Anupam", "Kotlin")
	var l = person.let { it.tutorial = "Android" }
	var al = person.also { it.tutorial = "Android" }
    
	println(l)
    println(al)
    println(person)

}

Les petits plus

APPLY

Les petits plus

Apply fonctionne comme Also mais n'oblige pas l'utilisation du it

data class Person(var name: String, var t : String)

fun main(args: Array<String>) {
    
	var person = Person("Anupam", "Kotlin")

    person.apply { t = "Swift" }
    println(person)

    person.also { it.t = "Kotlin" }
    println(person)

}

Les petits plus

WITH

Les petits plus

With fonctionne un peu comme Apply mais avec une syntaxe différente

data class Person(var name: String, var tutorial : String)

fun main(args: Array<String>) {
    
	var person = Person("Anupam", "Kotlin")

	with(person)
	{
		name = "No Name"
		tutorial = "Kotlin tutorials"
	}
    
	println(person)

}

Créer sa première App

Créer sa première app

Créer sa première app

Explications à l'oral des choix suivants :

  • empty activity
  • nom application
  • package name
  • minimum SDK (la fameuse version minimale !)

Créer sa première app

Explications de ce qu'on voit sur Android Studio

Créer sa première app

Créer un émulateur

Créer sa première app

Lancer l'application

Créer sa première app

Explication de l'arborescence des fichiers

Créer sa première app

Dans un premier temps, il faut choisir la vue "Android" du projet, et non la vue "projet"

La vue "Android" affiche ne effet les fichiers d'une manière plus agréable et logique lorsqu'on développe

Créer sa première app

Comprendre Gradle

Gradle est ce qu'on appelle un "moteur de production"

Il permet de construire des projets en java, Kotlin et autres

Mise en place d'étapes de build, gestion de dépendances, ... Gradle est indispensable à un projet Android !

Créer sa première app

L'ensemble des fichiers gradle se trouvent dans le dossier Gradle Scripts

Créer sa première app

Les deux fichiers les plus importants sont 

  • build.gradle "niveau app"
  • build.gradle "niveau project"

 

Créer sa première app

build.gradle "project"

On y retrouve :

  • la version de Kotlin
  • les repositories de dépendances
  • les dépendances en therme de "plugins"

Créer sa première app

build.gradle "app"

On y retrouve :

  • les configs générales du projet
    • application id
    • min sdk version
    • version Code et version Name
  • les dépendances
  • l'application des plugins

Créer sa première app

Différence entre versionCode et versionName ?

Lorsque vous mettrez à jour votre application sur le Store, le versionCode de celle-ci doit TOUJOURS avoir été préalablement augmenté

Le versionCode est l'identifiant de la version. Il permet aux smartphones de vos utilisateurs de comprendre qu'il y a une nouvelle version et de leur proposer de l'installer

Le versionName lui n'est qu'un nom pour l'utilisateur. Il n'est pas obligé de changer même si c'est plutôt la logique de base

Créer sa première app

Le dossier "app"

Créer sa première app

Le Manifest

Le Manifest est la carte d'identité d'une application Android

On y retrouve le nom, l'icône, mais aussi les différentes "permissions dangereuses" que l'application utilisera

Ce fichier est interprété par le Store pour afficher des informations à l'utilisateur quant à l'utilisation de l'application

Créer sa première app

Le répertoire Java

On trouve dans ce répertoire tous les fichiers de code de notre application

Il se nomme "java" mais la classe MainActivity qui a été générée est bien un fichier Kotlin.. Kotlin et Java sont interroperables ! 

Créer sa première app

Le répertoire RES

Le répertoire res rassemble l'ensemble des ressources du projet :

  • drawable : l'ensemble des images du projet
  • layout: l'ensemble des fichiers de layout du projet
  • mipmap : essentiellement le l'icône de l'app
  • values: 
    • les couleurs
    • les Strings
    • les styles

Comprendre le layouting

Comprendre le Layouting

Analysons le fichier activity_main.xml dans les res/layout

Comprendre le Layouting

Comme beaucoup de frameworks existant, Android est basé sur une séparation du code "métier" et du layout associé

Comprendre le Layouting

Le layout en Android est composé de fichiers XML qui définissent l'ensemble de l'architecture des vues les unes avec les autres

Comprendre le Layouting

Mais alors... est-ce que ça veut dire qu'on va écrire du XML ???? 

Non, rassurez-vous.... il existe évidemment une interface graphique pour positionner les éléments les uns par rapport aux autres

Comprendre le Layouting

  • Code : le XML
  • Design : l'interface graphique associée
  • Split : les deux en parallèle

Comprendre le Layouting

Comprendre le Layouting

Avec l'interface graphique, on va pouvoir :

  • glisser/déposer des vues dans notre layout avec le panel de gauche
     
  • les positionner
     
  • changer leurs attributs avec le panel de droite
     
  • etc.

Comprendre le Layouting

Mais en fait.... on va pas utiliser l'interface graphique

Comprendre le Layouting

Bien que l'interface graphique se soit énormément amélioré par rapport au passé et notamment avec le ConstraintLayout, son utilisation reste la plupart du temps... une perte de temps

Les développeurs Android expérimentés n'utilisent presque jamais l'interface graphique

Une fois que l'on maîtrise l'édition du XMl, cela va BEAUCOUP plus vite et est beaucoup plus précis

Comprendre le Layouting

Nous allons apprendre à maîtriser le XML et voir que ce n'est pas si compliqué et que les outils d'Android Studio nous facilitent la vie

Comprendre le Layouting

Il existe principalement 2 types de composants XML :

  • les composants de "layouts"
    Ils servent à organiser les vues les unes par rapport aux autres
     
  • les composants View
    Ce sont les vues, du simple texte au bouton en passant par le champs de texte, etc.

Comprendre le Layouting

Il existe de nombreux composants de layouts :

  • RelativeLayout : éléments positionnés de manière.. relatives
     
  • LinearLayout : éléments en colonne ou en ligne selon l'orientation
     
  • ConstraintLayout : le plus utilisé, c'est le composant de layout le plus récent et peut à priori à lui tout seul être utilisé pour 99% des  cas

Comprendre le Layouting

Créons notre premier LinearLayout

contenu du fichier: 

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    android:layout_height="match_parent"
    android:layout_width="match_parent"
    xmlns:android="http://schemas.android.com/apk/res/android"
    />

Comprendre le Layouting

Il y a 2 attributs que doivent toujours impérativement avoir les composants XML :

  • android:layout_height : la hauteur du composant
  • android:layout_width : la largeur du composant

Différentes possibilités :

  • une valeur brute (par exemple 40dp)
  • wrap_content : prend la taille de ses enfants
  • match_parent : prend la taille du parent (tout l'écran pour le composant racine)

Comprendre le Layouting

Par exemple ici, ajoutons une couleur de fond à notre LinearLayout :

<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_height="match_parent"
    android:layout_width="match_parent"
    android:background="@color/colorPrimary"
    />

Comprendre le Layouting

On peut voir d'ailleurs la manière d'aller chercher une couleur dans les ressources

Il suffit pour cela d'indiquer :

  • @color : précisant qu'on va chercher dans les resources de couleurs
  • colorPrimary : le nom de la couleur

Comprendre le Layouting

On retrouve toutes nos couleurs dans le fichier associé !

<?xml version="1.0" encoding="utf-8"?>
<resources>
  <color name="colorPrimary">#6200EE</color>
  <color name="colorPrimaryDark">#3700B3</color>
  <color name="colorAccent">#03DAC5</color>
</resources>

Comprendre le Layouting

Petite exercice :

  • rajouter une couleur dans les ressources
     
  • utiliser cette couleur comme couleur de fond à la place

Comprendre le Layouting

Ajoutons maintenant une texte à notre layout

La vue correspondant à un texte s'appelle TextView

<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_height="match_parent"
    android:layout_width="match_parent"
    android:background="@color/background"
    >

  <TextView
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:text="Hello world !"
      />
</LinearLayout>

Comprendre le Layouting

Pourquoi AndroidStudio souligne mon texte ?

Un des critères important en développement est l'internationalisation des Strings

En effet, si l'utilisateur change la langue de son téléphone, nous aimerions que cela modifie automatiquement les chaînes de caractères de l'application

Pour faire cela, on utilise un fichier de Strings dans les ressources du projet !

Comprendre le Layouting

<resources>
  <string name="app_name">Test1</string>
</resources>

En soulignant le texte de notre TextView, AndroidStudio nous informe qu'il serait plus pertinent de mettre le texte dans les ressources

Comprendre le Layouting

Petit exercice :

  • renseignez une chaîne correspondant au titre dans les ressources
     
  • utilisez-le à la place de celui écrit en dur

Astuce : inspirez-vous de ce qu'on a fait pour les couleurs

Comprendre le Layouting

Nous allons maintenant afficher un sous-titre

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_height="match_parent"
    android:layout_width="match_parent"
    android:background="@color/background"
    android:orientation="vertical"
    >

  <TextView
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:text="@string/title"
      />

  <TextView
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:text="@string/subtitle"
      />

</LinearLayout>

Comprendre le Layouting

Notez le android:orientation="vertical"

Cet attribut permet de comprendre la disposition des vues enfant du LinearLayout : verticale ou horizontale

Comprendre le Layouting

Comment centrer un peu mieux nos vues ?

On utilise pour cela l'attribut gravity sur le LinearLayout

<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_height="match_parent"
    android:layout_width="match_parent"
    android:background="@color/background"
    android:orientation="vertical"
    android:gravity="center"
    >

Comprendre le Layouting

Notez que ce genre d'écriture est possible

android:gravity="bottom|center"

Comprendre le Layouting

Comment mettre des marges ou padding entre les éléments ?

La plupart des vues et layouts ont ces attributs, à vous des les utiliser au mieux !

android:padding="20dp"
android:layout_margin="10dp"

Comprendre le Layouting

Notez d'ailleurs qu'il existe un moyen de faire en sorte que les dimensions (ce genre de valeurs 20dp, etc.) soient renseignées dans des ressources

Pour ce faire, on crée un fichier de ressource nommé dimens

Comprendre le Layouting

Voilà ce qui est généré

<?xml version="1.0" encoding="utf-8"?>
<resources>
</resources>

Ca marche finalement comme les autres ressources, sauf que ce sont des dimen et non des string ou color

<?xml version="1.0" encoding="utf-8"?>
<resources>
  <dimen name="generalPadding">20dp</dimen>
</resources>

Comprendre le Layouting

Et voici comment l'utiliser !

android:padding="@dimen/generalPadding"

Comprendre le Layouting

Petit exercice :

Ecrire le XML permettant d'arriver à ce résultat :

  • un bouton est affiché grâce au composant xml ... Button !
     
  • un layout peut contenir d'autres layouts
     
  • .. et voilà ! vous savez déjà tout pour faire cet écran

Le ConstraintLayout

ConstraintLayout

Nous avons vu dans le chapitre précédent les bases du layouting et notamment l'utilisation du LinearLayout

Il existe d'autres composants de layouts permettant de positionner les éléments les uns par rapports aux autres (nous avons mentionné par exemple le RelativeLayout)

Seulement, depuis maintenant quelques années, il existe un composant qui surpasse largement les autres et qui est presque à 100% utilisé, quel que soit le besoin

ConstraintLayout

Le ConstraintLayout

ConstraintLayout

Dans ConstraintLayout, il y a Constraints : contraintes

L'idée de ce composant est en effet de permettre à toutes les vues de se contraindre les unes par rapport aux autres

ConstraintLayout

Le ConstraintLayout nous permet d'aplatir la hiérarchie des vues enfants

Fini les 4 Linear dans un Linear lui même composé de X Linear, etc.

Le ConstraintLayout a bien d'autres caractéristiques qui le rendent passionnant, et nous allons voir ça ensemble

ConstraintLayout

Tout d'abord, pour placer une vue dans un ConstraintLayout, il faut impérativement lui appliquer des contraines

C'est logique en fait, il faut que la vue ait assez de contraintes pour savoir où se placer dans son élément parent

ConstraintLayout

<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    >

  <TextView
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:text="@string/title"
      />
</androidx.constraintlayout.widget.ConstraintLayout>

AndroidStudio gueule ici car notre TextView n'est soumise à aucune contrainte, ça n'est pas logique, il ne sait pas où la placer

ConstraintLayout

Pour appliquer une contrainte, on utiliser les attributs suivants :

  • app:layoutConstraintBottom_toBottomOf
  • app:layoutConstaintTop_toTopOf
  • etc.

En gros il faut les lire comme :

  • le bas de ma vue est contraint au bas de tel élément
  • le haut de ma vue est contraint au ... de tel élément
  • le droite de ma vue est contraint au .. (gauche|droit) de tel élément
  • etc.

ConstraintLayout

Ainsi, pour centrer notre TextView dans son parent.. rien de plus simple !

<TextView
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:text="@string/title"
      app:layout_constraintTop_toTopOf="parent"
      app:layout_constraintBottom_toBottomOf="parent"
      app:layout_constraintEnd_toEndOf="parent"
      app:layout_constraintStart_toStartOf="parent"
      />

ConstraintLayout

Petite astuce :

Commencez à taper par exemple "botbot" dans les attributs, et vous verrez très vitre proposé le constraintBottom_toBottomOf !

Vous verrez qu'écrire les contraintes est très rapide en XML une fois que vous avez cette astuce en tête !

ConstraintLayout

Petite astuce 2 :

Vous avez peut-être remarqué qu'en terme de contraintes horizontal, il y a 2 notions différentes :

  • right et left
  • start et end

A votre avis, quelle est la différence ?

ConstraintLayout

Petit exercice :

  • ajoutez un bouton en bas comme ceci
     
  • n'utilisez que des contraintes
     
  • pour l'écart avec le bas, on mettra un padding sur le ConstraintLayout

ConstraintLayout

Pour le moment, je peux placer les vues les unes par rapports à l'élément parent

Comment les placer les unes par rapport aux autres ?

Il suffit de donner un identifiant à nos vues !

L'identifiant est d'ailleurs ce qui permettra plus tard de cibler une vue dans notre code Kotlin, mais on y reviendra !

ConstraintLayout

Voici comment déclarer un identifiant

 <TextView
      android:id="@+id/title"
      // autres attributs ...
      />

Notez le "@+id" signifiant que l'on crée cet identifiant (qui doit être unique dans le layout courant !)

ConstraintLayout

Lorsqu'on voudra faire référence à cette vue, on aura qu'à écrire ça ainsi :

@id/title

Par exemple  :

<Button
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:text="Boutton"
      app:layout_constraintBottom_toBottomOf="parent"
      app:layout_constraintEnd_toEndOf="parent"
      app:layout_constraintStart_toStartOf="parent"
      app:layout_constraintTop_toBottomOf="@id/title"
      />

ConstraintLayout

Petit exercice :

Grâce au principe des identifiants, concevez l'écran suivant

ConstraintLayout

En contraignant toutes nos vues les unes par rapport aux autres de cette manière, nous avons crée sans le savoir une chaîne

Plus exactement une chaîne verticale ici

Il existe pleins de comportement qu'un' chaîne peut adopter, notamment son chainStyle

ConstraintLayout

Il existe les chainStyle suivants : vertical et horizontal

layout_constraintVertical_chainStyle
layout_constraintHorizontal_chainStyle

Un chainStyle va indiquer aux éléments d'une chaîne de quelle manière ils doivent se répartir l'espace disponible

ConstraintLayout

Seul l'élément racine de la chaîne a besoin d'avoir cet attribut !

<Button
      android:id="@+id/button1"
      // Autres attributs ...
      app:layout_constraintBottom_toTopOf="@id/button2"
      app:layout_constraintEnd_toEndOf="parent"
      app:layout_constraintStart_toStartOf="parent"
      app:layout_constraintTop_toTopOf="parent"
      />

  <Button
      android:id="@+id/button2"
      // Autres attributs ...
      app:layout_constraintBottom_toTopOf="@id/button3"
      app:layout_constraintEnd_toEndOf="parent"
      app:layout_constraintStart_toStartOf="parent"
      app:layout_constraintTop_toBottomOf="@id/button1"
      />

  <Button
      android:id="@+id/button3"
      // Autres attributs ...
      app:layout_constraintBottom_toBottomOf="parent"
      app:layout_constraintEnd_toEndOf="parent"
      app:layout_constraintStart_toStartOf="parent"
      app:layout_constraintTop_toBottomOf="@id/button2"
      />

Seul le button1 en aura besoin ici !

ConstraintLayout

le chainStyle peut prendre 3 valeurs :

  • packed
    les vues sont rapprochées les unes des autres 
  • spread
    les vues (et les extérieurs) sont espacés de manière égale
  • spread_inside
    les espaces internes entre les vues sont égaux et plus grands, rapprochant les vues du bords

ConstraintLayout

packed

spread

spread_inside

ConstraintLayout

Notion de Bias

Lorsque l'élément parent a davantage d'espace disponible que le demande l'élément enfant, l'élément enfant est centré

<Button
      android:id="@+id/button1"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:text="Boutton 1"
      app:layout_constraintBottom_toBottomOf="parent"
      app:layout_constraintEnd_toEndOf="parent"
      app:layout_constraintStart_toStartOf="parent"
      app:layout_constraintTop_toTopOf="parent"
      />

ConstraintLayout

Notion de Bias

On dit qu'il a un Bias de 50% car il est à 50% verticalement et horizontalement

Il est possible de modifier ce bias pour le placer de manière plus précise, on utilise alors :

app:layout_constraintVertical_bias
app:layout_constraintHorizontal_bias

ConstraintLayout

Notion de Bias

Par exemple, pour placer le bouton à 30% verticalement et 70% horizontalement :

<Button
      android:id="@+id/button1"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:text="Boutton 1"
      app:layout_constraintBottom_toBottomOf="parent"
      app:layout_constraintEnd_toEndOf="parent"
      app:layout_constraintStart_toStartOf="parent"
      app:layout_constraintTop_toTopOf="parent"
      app:layout_constraintVertical_bias=".3"
      app:layout_constraintHorizontal_bias=".7"
      />

ConstraintLayout

Notion de Bias

Lorsqu'on a créée une chaîne, mettre un bias sur le premier maillon provoque l'effet sur tous, car les autres sont positionnés en fonction du maillon.. tout est logique 

ConstraintLayout

Notion de Bias

Lorsqu'on a créée une chaîne, mettre un bias sur le premier maillon provoque l'effet sur tous lorsqu'il est placé sur l'axe principal

Sinon, chaque élément peut avoir son propre bias sur l'axe secondaire

ConstraintLayout

<Button
      android:id="@+id/button1"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:text="Boutton 1"
      app:layout_constraintBottom_toTopOf="@id/button2"
      app:layout_constraintEnd_toEndOf="parent"
      app:layout_constraintStart_toStartOf="parent"
      app:layout_constraintTop_toTopOf="parent"
      app:layout_constraintVertical_chainStyle="packed"
      app:layout_constraintVertical_bias=".2"
      />

  <Button
      android:id="@+id/button2"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:text="Boutton 2"
      app:layout_constraintBottom_toTopOf="@id/button3"
      app:layout_constraintEnd_toEndOf="parent"
      app:layout_constraintStart_toStartOf="parent"
      app:layout_constraintTop_toBottomOf="@id/button1"
      />

  <Button
      android:id="@+id/button3"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:text="Boutton 3"
      app:layout_constraintBottom_toBottomOf="parent"
      app:layout_constraintEnd_toEndOf="parent"
      app:layout_constraintStart_toStartOf="parent"
      app:layout_constraintTop_toBottomOf="@id/button2"
      />

ConstraintLayout

<Button
      android:id="@+id/button1"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:text="Boutton 1"
      app:layout_constraintBottom_toTopOf="@id/button2"
      app:layout_constraintEnd_toEndOf="parent"
      app:layout_constraintHorizontal_bias=".8"
      app:layout_constraintStart_toStartOf="parent"
      app:layout_constraintTop_toTopOf="parent"
      app:layout_constraintVertical_chainStyle="packed"
      />

  <Button
      android:id="@+id/button2"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:text="Boutton 2"
      app:layout_constraintBottom_toTopOf="@id/button3"
      app:layout_constraintEnd_toEndOf="parent"
      app:layout_constraintHorizontal_bias=".2"
      app:layout_constraintStart_toStartOf="parent"
      app:layout_constraintTop_toBottomOf="@id/button1"
      />

  <Button
      android:id="@+id/button3"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:text="Boutton 3"
      app:layout_constraintBottom_toBottomOf="parent"
      app:layout_constraintEnd_toEndOf="parent"
      app:layout_constraintHorizontal_bias=".8"
      app:layout_constraintStart_toStartOf="parent"
      app:layout_constraintTop_toBottomOf="@id/button2"
      />

ConstraintLayout

Petit exercice :

Codez l'écran ci-dessous en utilisant les concepts de chainStyle et de Bias

ConstraintLayout

Notion de Poids

Lorsque différents éléments se partagent l'espace de leur parent, il est possible de leur attribuer un poids

Le parent accordera alors un espace proportionnel au poids attribué à chaque enfant

Pour ceux qui connaissent FlexBox en CSS, c'est un peu le même principe

ConstraintLayout

Notion de Poids

<RelativeLayout
      android:id="@+id/container1"
      android:layout_width="0dp"
      android:layout_height="0dp"
      android:background="@color/colorPrimary"
      app:layout_constraintBottom_toTopOf="@id/container2"
      app:layout_constraintEnd_toEndOf="parent"
      app:layout_constraintStart_toStartOf="parent"
      app:layout_constraintTop_toTopOf="parent"
      app:layout_constraintVertical_weight="2"
      />

  <RelativeLayout
      android:id="@+id/container2"
      android:layout_width="0dp"
      android:layout_height="0dp"
      android:background="@color/colorAccent"
      app:layout_constraintBottom_toBottomOf="parent"
      app:layout_constraintEnd_toEndOf="parent"
      app:layout_constraintStart_toStartOf="parent"
      app:layout_constraintTop_toBottomOf="@id/container1"
      app:layout_constraintVertical_weight="1"
      />

ConstraintLayout

Notion de Guidelines

Les Guidelines dans un ConstraintLayout sont comme des lignes invisibles qui permettent de mieux délimiter des zones précises auxquelles les enfants vont se contraindre

ConstraintLayout

Notion de Guidelines

Les 2 attributs importants sont :

android:orientation

pour savoir si c'est une ligne horizontale ou verticale

app:layout_constraintGuide_percent

pour la positionner

ConstraintLayout

Petit exercice

Réalisez l'écran suivant

Lier la logique et l'UI

Lier la logique et l'UI

Pour coder la logique de notre écran, nous allons nous intéresser à la MainActivity qui tient notre layout

class MainActivity : AppCompatActivity()
{
  override fun onCreate(savedInstanceState: Bundle?)
  {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
  }
}

Lier la logique et l'UI

Voyez une Activity comme un élément de base d'écran Android

On pourrait faire le rapprochement :

"une activity = un écran"

On verra plus tard que ce n'est pas exactement ça, mais partons de ce principe pour le moment

Lier la logique et l'UI

Une activité que l'on crée par défaut doit hériter de AppCompatActivity

Nous avons alors accès à pleins de caractéristiques et notamment une méthode overridable : onCreate()

C'est dans cette méthode que nous allons appeler la méthode setContentView pour indiquer le Layout

Lier la logique et l'UI

Pour récupérer la référence d'une vue dans notre activité, il suffit d'utiliser la méthode findViewById<T?>()

Exemple, considérons que l'on a un TextView dans notre écran écrit ainsi :

<TextView
      android:id="@+id/title"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:textColor="@color/black"
      app:layout_constraintBottom_toBottomOf="parent"
      app:layout_constraintEnd_toEndOf="parent"
      app:layout_constraintStart_toStartOf="parent"
      app:layout_constraintTop_toTopOf="parent"
      />

Lier la logique et l'UI

Voici comment récupérer la référence puis la modifier depuis l'activité

class MainActivity : AppCompatActivity()
{

  override fun onCreate(savedInstanceState: Bundle?)
  {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    val title = findViewById<TextView?>(R.id.title)
    title?.text = "Hello"
  }
}

Lier la logique et l'UI

Libre à vous ensuite d'utiliser les API des différentes vues pour interagir avec !

Par exemple : écouter le click d'un Button comme celui-ci

<Button
      android:id="@+id/button"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:textColor="@color/black"
      android:text="toto"
      app:layout_constraintBottom_toBottomOf="parent"
      app:layout_constraintEnd_toEndOf="parent"
      app:layout_constraintStart_toStartOf="parent"
      app:layout_constraintTop_toTopOf="parent"
      />

Lier la logique et l'UI

Tout d'abord, on récupère la référence

class MainActivity : AppCompatActivity()
{

  override fun onCreate(savedInstanceState: Bundle?)
  {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    val button = findViewById<Button?>(R.id.button)
    button?.text = "babar"
  }
}

Lier la logique et l'UI

Ensuite, il est possible d'ajouter un ClickListener

Un ClickListener est une implémentation de l'interface du même nom qui sera appelé lors du click sur le bouton

button?.setOnClickListener(this)

Nous pouvons par exemple passer au bouton la référence de notre activité pour lui donner la responsabilité de la gestion du click

Lier la logique et l'UI

Il est par contre évidemment nécéssaire de faire implémenter l'interface par l'activité

class MainActivity : AppCompatActivity(), View.OnClickListener
override fun onClick(view: View?)
{
	TODO("Not yet implemented")
}

Lier la logique et l'UI

Testons ça !

override fun onClick(view: View?)
{
	Log.d("MonTag",  "Salut !")
}

Lier la logique et l'UI

Il est possible d'utiliser la gestion des callbacks de Kotlin pour écrire les ClickListener d'une manière un peu plus "sexy"

button?.setOnClickListener {
  Log.d("MonTag", "Salut !")
}

Lier la logique et l'UI

Enfin, il est aussi possible d'indiquer la callback du bouton.. directement dans le XML !

<Button
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:text="click"
      android:onClick="test"
      />

Lier la logique et l'UI

Il faut alors que cette fonction soit dans l'activité, et prenne une vue en paramètre :

fun test(view: View)
{

}

Lier la logique et l'UI

Petit exercice

  • Mettre en place 2 boutons et affecter un comportement (log basique par exemple) différent selon le bouton cliqué
  • N'utiliser qu'un seul et même listener pour les 2 boutons 

Lier la logique et l'UI

Petite Astuce

En Kotlin, plus exactement grâce aux Kotlin Extensions, il y a un petit sucre syntaxique qui nous permet d'outrepasser le findViewById 

En effet :

  • plus la peine de créer un champs de la classe
  • plus la peine d'utiliser le findViewById
  • la vue est directement accessible

Lier la logique et l'UI

class MainActivity : AppCompatActivity()
{
  private var button: Button? = null

  override fun onCreate(savedInstanceState: Bundle?)
  {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    button = findViewById(R.id.button)
    button?.setOnClickListener {
      Log.d("MonTag", "Salut !")
    }
  }
}

devient :

class MainActivity : AppCompatActivity()
{
  override fun onCreate(savedInstanceState: Bundle?)
  {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    button?.setOnClickListener {
      Log.d("MonTag", "Salut !")
    }
  }
}

Lier la logique et l'UI

Petit exercice

Réaliser l'écran suivant :

  • écrire du texte (EditText)
  • cliquer sur le bouton
  • le text écrit est affiché

Cycle de vie

Cycle de vie

Une application mobile doit pouvoir être utilisée de plein de manières différentes

  • on peut la mettre en tâche de fond
  • on peut lire une notification alors que l'app tourne
  • on peut appliquer une rotation de l'écran
  • on peut réduire la fenêtre (multi-window)
  • le système peut décider de stopper une app en fond qui consomme de la batterie
  • etc. 

Cycle de vie

Tous ces différents cas génèrent de nombreux changements dans l'application !

Pour bien maîtriser son application de A à Z, il est impératif de maîtriser son cycle de vie

Il existe de nombreuses callbacks dans l'activité qui nous permettent d'écouter les changements du cycle de vie

Cycle de vie

Le cycle de vie d'une activité passe par 6 étapes qui ont chacune une callback associée

  • onCreate()
  • onStart()
  • onResume()
  • onPause()
  • onStop()
  • onDestroy()

Cycle de vie

Cycle de vie

Petit exercice

Mettez en place des logs dans les différentes callbacks de votre activité et testez le cycle de vie

Cycle de vie

Petit exercice 2

  • placer un EditText et un Button sur votre écran
  • écrire quelque chose dans l'EditText
  • au click sur le Button, le texte renseigné dans l'EditText doit être retranscrit dans une TextView sur l'écran
  • appliquer une rotation à l'écran
  • que remarquez-vous ?

Cycle de vie

Vous l'avez sûrement remarqué, le texte dans la TextView a disparu !

En effet, si on regarde les logs, la rotation détruit complètement l'activité ( onDestroy() )et repasse ensuite dans tout le cycle de vie de création

Il arrive donc que l'on perde l'état de l'application en fonction de ce genre d'évènements... d'où la nécessité de connaître le cycle de vie

Cycle de vie

Il existe toutefois des solutions pour garder l'état précédent et nous allons voir ça

Cycle de vie

onCreate est un peu la callback de base

Y faire ce qui a attrait à toute la vie de l'activité :

  • mise en place layout
  • lien avec un ViewModel (on verra ça plus tard)
  • etc.

Gestion d'état

Cycle de vie

onCreate prend un paramètre

override fun onCreate(savedInstanceState: Bundle?)

En fait ce Bundle est un dictionnaire contenant le précédent état de l'application

C'est grâce à ce Bundle que l'on va pouvoir gérer les changements d'états

Gestion d'état

Cycle de vie

Mais alors, quand pouvons nous remplir ce dictionnaire ?

override fun onSaveInstanceState(outState: Bundle)

Grâce à la callback 

qui nous permet de sauvegarder l'état avant la destruction de l'activité

Gestion d'état

Cycle de vie

Voici par exemple comment sauvegarder la valeur de notre TextView

override fun onSaveInstanceState(outState: Bundle)
{
    outState.putString("MY_AWESOME_KEY", text?.text.toString())
    super.onSaveInstanceState(outState)
}

Gestion d'état

Cycle de vie

Gestion d'état

De retour dans notre onCreate, nous pouvons utiliser le bundle :

override fun onCreate(savedInstanceState: Bundle?)
{
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    savedInstanceState?.get("MY_AWESOME_KEY")?.let { previousText ->
      text?.text = previousText as String
    }

    // ClickListener button..
}

Cycle de vie

Pour finir :

La notion de cycle de vie est très importante !

La mettre de côté, c'est avoir potentiellement beaucoup de problèmes et de comportements inatendus dans l'application

Cycle de vie

Petite astuce finale :

Même si il est important de savoir gérer la rotation de l'écran et les problèmes engendrés, il arrive souvent qu'une application n'ait besoin que d'un affichage en portrait

Il existe bien sûr une manière de préciser à notre activité une orientation forcée

Cycle de vie

Ca se passe dans le Manifest !

<activity 
        android:name=".MainActivity"
        android:configChanges="orientation"
        android:screenOrientation="portrait"
        >
  • android:configChanges = "c'est moi qui gère l'orientation"
     
  • android:screenorientation = l'orientation souhaitée

Navigation

(bases)

Navigation (bases)

Commençons par créer une nouvelle activité

Navigation (bases)

Notez que, dans le Manifest, une balise avec votre nouvelle activité est apparue :

<activity android:name=".SecondActivity">

TOUTE activité doit obligatoirement être renseignée dans le Manifest, sans quoi votre application crashera

Navigation (bases)

Notez également que, c'est dans cette balise que l'on indique celle qui sera lancé en première :

<intent-filter>
	<action android:name="android.intent.action.MAIN" />

	<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>

Navigation (bases)

Modifions légèrement le layout de la seconde activité

 <TextView
      android:id="@+id/title"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:text="Seconde Activité !"
      app:layout_constraintBottom_toBottomOf="parent"
      app:layout_constraintEnd_toEndOf="parent"
      app:layout_constraintStart_toStartOf="parent"
      app:layout_constraintTop_toTopOf="parent"
      />

  <Button
      android:id="@+id/back"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:text="Retour"
      android:layout_marginTop="30dp"
      app:layout_constraintEnd_toEndOf="parent"
      app:layout_constraintStart_toStartOf="parent"
      app:layout_constraintTop_toBottomOf="@id/title"
      />

Navigation (bases)

De retour dans notre activité principale, nous allons tenter de naviguer vers la seconde !

Nous allons pour cela utiliser la méthode startActivity

startActivity prend un seul paramètre qui est un Intent

Intent signifie "intention" -> nous allons préciser quelle est notre intention, c'est à dire aller vers la seconde activité

Navigation (bases)

button?.setOnClickListener {
      val intent = Intent(this, SecondActivity::class.java)
      startActivity(intent)
}

Voici comment définir un Intent de la manière la plus simple

On lui donne le contexte de départ, la classe d'arrivée, et c'est bon !

On passe ensuite cet Intent à notre méthode, et on navigue !

Navigation (bases)

Une fois sur la seconde activité, comment retourner en arrière ? 

Il "suffit" de finir l'activité !

Les activités sont en fait empilées dans la "Stack de navigation"

Finir la seconde revient à la retirer de la Stack et donc à revenir à la précédente

Navigation (bases)

override fun onCreate(savedInstanceState: Bundle?)
{
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_second)
    back?.setOnClickListener(this)
}

override fun onClick(view: View?)
{
    if (view == back)
    {
      finish()
    }
}

Navigation (bases)

Comment passer des paramètres ?

Nous pouvons passer un argument dans l'intent de cette manière :

val intent = Intent(this, SecondActivity::class.java)
intent.putExtra("key", "value")
startActivity(intent)

Navigation (bases)

Si on veut passer plusieurs paramètres, on utilise un Bundle

val intent = Intent(this, SecondActivity::class.java)

val params = Bundle()
params.putString("firstKey", "firstValue")
params.putInt("secondKey", 42)
intent.putExtras(params)
      
startActivity(intent)

Navigation (bases)

Comment lire les paramètres ?

On peut à tout moment récupérer l'intent qui a déclenché l'activité

On peut ensuite récupérer les extras si il y'en a et les utiliser !

Navigation (bases)

Comment lire les paramètres ?

override fun onCreate(savedInstanceState: Bundle?)
{
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_second)
    back?.setOnClickListener(this)

    intent.extras?.get("firstKey")?.let {
      text?.text = it as String
    }
}

Navigation (bases)

Start Activity For Result

Il est parfois intéressant pour une activité d'en lancer une seconde et d'attendre un résultat de celle-ci

Pour cela, on utilise startActivityForResult qui prend, en plus de l'intent, le code de requête (qui sera utilisé pour lire le résultat)

 startActivityForResult(intent, 32)

Navigation (bases)

Start Activity For Result

L'activité suivante peut alors renseigné un résultat avant de se finir :

val resultIntent = Intent()
resultIntent.putExtra("some_key", "Mon résultat")
setResult(32, resultIntent)
finish()

Navigation (bases)

Start Activity For Result

L'activité principale doit alors implémenter la callback suivante pour récupérer les résultats :

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?)
{
    super.onActivityResult(requestCode, resultCode, data)
    if (requestCode == 32)
    {
      val result = data?.extras?.get("some_key") as String?
      text?.text = result
    }
}

Navigation (bases)

Petit exercice

  • premier écran demandant le nom de l'utilisateur
  • on clique sur OK, ça envoie sur un second écran
  • le second écran affiche le nom saisie et demande cette fois le poids
  • une fois le poids renseigné, on clique sur valider et on revient sur le premier écran qui affiche ensuite le nom et le poids

Navigation (bases)

Petite variante : le pattern Factory

Un pattern souvent utilisé en programmation mobile est le pattern Factory

Nous pouvons l'utiliser pour rendre un peu plus sexy et maintenable toute notre logique de navigation 

Navigation (bases)

Ainsi, une activité définirait elle-même la manière dont elle est instanciée et appelée :

companion object
{
    private val TITLE_KEY: String = "titleKey"

    fun navigateTo(context: Context, title: String)
    {
      val intent = Intent(context, Activity2::class.java).apply {
        putExtra(TITLE_KEY, title)
      }
      context.startActivity(intent)
    }
}

Navigation (bases)

Ce qui permet :

  • d'encapsuler toute logique de création à un seul endroit
     
  • d'encapsuler toute logique de clés de paramètres
     
  • les appelants n'ont pas à connaître la logique
     
  • si celle-ci change, elle ne changera que dans la factory

Navigation (bases)

Côté Activité appelante, ça donne ça :

Activity2.navigateTo(this, "SALUT")

Les Fragments

(The old way..)

Les Fragments

Les Fragments sont des morceaux d'interfaces et de logiqe interchangeables et modulaires de l'application

Là où on partait du principe qu'une activité est un écran, un Fragment peut-être un écran entier ou un bout d'écran

Un Fragment ne peut pas exister en dehors du contexte d'une activité

Les Fragments

A l'origine les Fragments ont été crées pour le développement sur tablette, pour mieux gérer le responsive (version d'api 11, HoneyComb)

Depuis... les Fragments sont très (trop?) utilisés, pour un peu tout et n'importe quoi

Certains font même des applications "Single Activity" avec une seule activité et uniquement des Fragments

Les Fragments

Comment créer un Fragment

  • il faut d'abord une activité
     
  • un fragment remplace une partie d'un layout (écran entier ou non)
     
  • on crée donc un layout avec un ID qui permettra de le cibler

Les Fragments

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/container"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    />

ici, notre fragment prendra tout l'écran

Les Fragments

Ensuite, nous allons créer un Fragment

Les Fragments

Ca nous a générer pas mal de code, mais rien de sorcier !

Android est parti du principe que notre Fragment prendrait 2 paramètres (on peut en ajouter ou en retirer, c'est juste un exemple, un template)

Les Fragments

Une Factory a été créée pour faciliter la création d'une nouvelle instance et le passage de paramètres

Les Fragments

Dans le OnCreate, le code généré récupère les potentiels arguments passés et les lient aux champs du fragment

Les Fragments

Ici, on indique quel est le layout associé (il a été généré aussi)

Les Fragments

Si on voulait afficher dans une textview la valeur de "param1", on le ferait dans le onViewCreated()

override fun onViewCreated(view: View, savedInstanceState: Bundle?)
  {
    super.onViewCreated(view, savedInstanceState)
    titleTextview?.text = param1
  }

Les Fragments

Un Fragment ... a aussi un cycle de vie !

En fait il a les même callback que l'activité + certaines propres à l'activité elle même

Pour rappel, un Fragment n'a de raison d'être qu'en étant attaché à une activité

Les Fragments

Cycle de vie d'un fragment

Les Fragments

On remarque les nouvelles callback :

  • onAttach lorsque le Fragment est attaché à l'activité
  • onCreateView lorsqu'on crée la vue
  • onActivityCreated lorsque l'activité est créée
  • onDestroyView lorsque la vue du Fragment est détruite
  • onDetach lorsque le Fragment se détache de l'activité

Les Fragments

Maintenant que le Fragment est crée, on peut aller dans l'activité pour l'appeller

override fun onCreate(savedInstanceState: Bundle?)
{
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    supportFragmentManager.beginTransaction()
      .replace(R.id.container, AFragment.newInstance("Param 1", "Param 2"))
      .commitNow()
}

Les Fragments

Il est possible, plutôt que d'utiliser un layout, de le remplacer par un Fragment. En effet, on peut directement renseigner un Fragment ainsi dans l'activité :

<?xml version="1.0" encoding="utf-8"?>
<fragment xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/fragment_fixe"
    android:name="com.example.test1.AFragment"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />

Mais nous préfèrerons la méthode du layout.. plus customisable !

Les Fragments

Le FragmentManager va nous permettre de faire pas mal d'actions sur les fragments (transitions, retour en arrière, etc.)

Les Fragments

Comment discuter entre le Fragment et l'activité ?

Les Fragments

Comment discuter entre le Fragment et l'activité ?

Le Fragment a accès à un champs activity permettant, en castant le champs, de faire appel à une méthode spécifique de l'activité parente

(activity as MainActivity).aMethodInMainActivity()

Créons une méthode "aMethodInMainActivity" dans la MainActivity

Le Fragment peut alors l'appeler ainsi

Les Fragments

Quels sont les problèmes de cette technique ?

  • couplage très fort entre le Fragment et l'activité !
  • si le cast échoue, exception !

Nous préfèrerons utiliser le système d'interface, en imposant à une activité parente de l'implémenter

Les Fragments

Imaginons cette interface :

interface AFragmentInterface
{
  fun anAwesomeMethodCalledFromAFragment(value: String?)
}

Le Fragment aura dont un champs de type AFragmentInterface

class AFragment : Fragment()
{
  private var aFragmentInterface: AFragmentInterface? = null
  
//....

Les Fragments

Nous allons maintenant nous intéresser à la callback onAttach du Fragment

Cette méthode est appelée quand le Fragment est attaché à l'activité, et nous permet ainsi de nous assurer que celle-ci implémente bien l'interface et, si c'est le cas, la renseigner 

override fun onAttach(context: Context)
  {
    super.onAttach(context)
    try
    {
      aFragmentInterface = activity as AFragmentInterface
    } catch (exception: ClassCastException)
    {
      Log.e("error", "${activity?.packageName} must implement AFragmentInterface")
    }
  }

Les Fragments

De son côté, l'activité n'a plus qu'à implémenter l'interface !

class MainActivity : AppCompatActivity(), AFragmentInterface
override fun anAwesomeMethodCalledFromAFragment(value: String?)
{
    Log.d("toto", "Je suis l'activité, le fragment m'a envoyé ça : $value")
}

Les Fragments

Enfin, imaginons que le fragment doivent envoyer une info lors du clique sur la TextView :

titleTextview?.setOnClickListener {
      aFragmentInterface?.anAwesomeMethodCalledFromAFragment("YO")
    }

Voilà comment "proprement" faire discuter les fragments et les activités !

Les Fragments

Il y a toutefois encore un soucis de couplage à mon sens dans cette logique.

  • Sauriez-vous expliquer pourquoi ?
  • Comment pourrions-nous améliorer ça ?

Les Fragments

En utilisant le principe du pattern Factory !

Nous profitons du fait qu'il soit déjà en place par défaut lors de l'instanciation d'un Fragment, et on l'utilise pour forcer l'envoie de l'interface

Il s'agit donc d'imposer l'envoie d'une instance d'interface lors de la création de l'instance de Fragment. Que cette instance soit l'activité elle-même, une lambda.. peu importe !

Les Fragments

Ca donnerait donc, côté Fragment :

companion object
  {
    @JvmStatic
    fun newInstance(param1: String, param2: String, aFragmentInterface: AFragmentInterface) =
      AFragment().apply {
        arguments = Bundle().apply {
          putString(ARG_PARAM1, param1)
          putString(ARG_PARAM2, param2)
        }
        setFragmentInterface(aFragmentInterface)
      }
  }
fun setFragmentInterface(aFragmentInterface: AFragmentInterface)
{
    this.aFragmentInterface = aFragmentInterface
}

Les Fragments

Et côté Activity :

AFragment.newInstance("param1", "param2", this)

Les Fragments

Naviguer entre les fragments

Les Fragments

Naviguer entre les fragments

Créons un second Fragment, rien de spécial, juste un texte centré par exemple

class BFragment : Fragment()
{

  override fun onCreateView(
    inflater: LayoutInflater, container: ViewGroup?,
    savedInstanceState: Bundle?
  ): View?
  {
    return inflater.inflate(R.layout.fragment_b, container, false)
  }

  companion object
  {
    @JvmStatic
    fun newInstance() = BFragment()
  }
}

Les Fragments

Du côté de l'activité

On va réagir à la callback du Fragment en naviguant vers ce nouveau BFragment

override fun anAwesomeMethodCalledFromAFragment(value: String?)
{
    // ICI !
}

Les Fragments

Et voilà !

override fun anAwesomeMethodCalledFromAFragment(value: String?)
{
    supportFragmentManager.beginTransaction()
      .replace(R.id.container, BFragment.newInstance())
      .commitNow()
}

Les Fragments

Petit exercice final :

  • créer une activity composée de 2 Fragments, un en haut et un en bas
     
  • en bas, il y a un EditText pour récupérer un email et un bouton de validation
     
  • lorsque l'utilisateur clique sur le bouton, l'activity doit être prévenue et afficher l'email saisi dans le Fragment du haut 

Les Fragments

ça sert à quoi ?

Les Fragments, ça sert à quoi ?

Je vous l'ai dit dans le chapitre précédent, les Fragments ont été conçus à la base lors de l'arrivée des tablettes dans le monde Android

L'objectif était d'avoir des bouts d'interface modulables pour pouvoir s'adapter aux différentes tailles d'écrans

Ok... mais qu'est-ce que ça veut dire ?

Les Fragments, ça sert à quoi ?

Tout d'abord, il faut savoir qu'il est possible de créer des fichiers de layouts différents selon l'orientation du téléphone ou encore selon la taille de l'écran

Le fonctionnement est un peu le même que pour les ressources de l'application

Les Fragments, ça sert à quoi ?

Par exemple, créons un layout pour le téléphone en Landscape (paysage)

Pour ce faire, lors de la création de notre layout, il nous suffit de préciser comme dossier de layout le dossier : layout-land

Celui-ci sera généré s'il n'existe pas

Les Fragments, ça sert à quoi ?

Les Fragments, ça sert à quoi ?

Attention à bien spécifier le même nom que notre layout d'activité ! L'objectif est d'avoir 2 layouts différents selon l'orientation

Nous pouvons voir nettement qu'il existe maintenant 2 layouts activity_main.xml mais l'un est de base tandis que l'autre est "land"

Les Fragments, ça sert à quoi ?

A partir de maintenant, Android switchera automatiquement de layout selon l'orientation de votre device !

Les Fragments, ça sert à quoi ?

Faisons le test, en mettant l'activité à blanc en portrait et rouge en paysage !

Lancez l'application, appliquez une rotation... ça fonctionne !

Les Fragments, ça sert à quoi ?

Nous pouvons donc faire des layouts différents selon le mode portrait ou le mode paysage.. mais pas seulement !

Il est aussi possible de désigner une taille minimum et cela grâce au qualifieur sw<N>dp

Où l'on remplace N par la taille souhaitée

Les Fragments, ça sert à quoi ?

Par exemple :

  • layout-sw600dp (tablettes 7 pouces, 600dp et plus)
  • layout-sw700dp (tablettes 10 pouces, 700dp et plus)

Ce sont les 2 plus utilisées mais vous pouvez bien sûr mettre vos propres règles

Les Fragments, ça sert à quoi ?

Ainsi, si on voulait gérer : le mode portrait et le mode paysage pour les téléphones en dessous de 600dp, les tablettes 7 pouces et les tablettes 8 pouces :

Les Fragments, ça sert à quoi ?

Ca fait du boulot pour gérer tous les cas !

Sachez d'ailleurs que cette règle des "qualifieurs" marche aussi pour les autres ressources comme les dimens etc.

Les Fragments, ça sert à quoi ?

Et les Fragments dans tout ça ?

Les Fragments, ça sert à quoi ?

Comment feriez-vous si vous deviez créer ce genre de vue ?

Les Fragments, ça sert à quoi ?

Pour faire ce genre de vue, on va être obligés d'utiliser des Fragments ! Nous allons jouer à la fois sur les layouts correspondants au landscape ou non, puis nous jouerons avec le concept de Fragment pour afficher les contenu

Les Fragments, ça sert à quoi ?

Mais avant ça, et comme on veut afficher une liste... faisons un petit cours sur l'affichage de listes en Android

Les Listes

Les listes

Les listes font parties des éléments primordiaux d'une application mobile

Connaissez-vous beaucoup d'applications mobiles sans liste ?

Les listes

En Android, vous avez 2 composants principaux

  • la ListView
    • avant Lollipop
    • dur à configurer précisément
       
  • la RecyclerView
    • beaucoup plus performant
    • meilleur architecture par défaut (ViewHolder pattern)

Les listes

En pratique il y a plus de différences, mais retenez simplement que les développements récents sur Android privilégient largement l'utilisation de la RecyclerView

Les listes

Mettre en place une RecyclerView peut parfois paraître verbeux et "compliqué"

On va voir qu'il n'en est rien !

Les listes

Une RecyclerView est composé de 4 éléments :

  • un LayoutManager qui indique le positionnement des vues les unes par rapport aux autres
     
  • un (ou des) ViewHolder qui représentent le rendu graphique d'un élément
     
  • un Adapter qui est responsable de gérer la liste d'items
     
  • une liste d'objets métiers

Les listes

Petit aparté, à votre avis, qu'est-ce que le Recyclage d'une liste ?

Les listes

Il arrive qu'une application mobile ait un nombre très important d'éléments à afficher dans une liste

Imaginer devoir afficher une liste de 1000 items, avec chacun une UI peaufinée, une animation, etc.

Le device doit alors faire tellement de calculs et tellement utiliser la RAM, que le scroll de la liste peut donner des sensations de lag très vite

Les listes

Le principe du recyclage, c'est de dire que seules les vues visibles à l'écran (+ quelques extras) seront réellement chargée en mémoire

Lorsque l'utilisateur scroll, les vues sortantes de l'écran sont détruites, et les vues suivantes sont chargées

Les listes

Ainsi, on ne charge les vues que lorsqu'on en a besoin, et on recycle les ViewHolder pour ne pas avoir à en créer 1000 mais 4 ou 5 seulement

Les listes

Le recyclage a vraiment grandement améliorer l'affichage des listes sur Mobile et permet un comportement beaucoup plus fluide qu'avant

Créons notre première liste !

Les listes

Contrairement à la ListView qui est contenue de base dans Android depuis la première version, la RecyclerView est une dépendance externe

Pour ajouter cette dépendance, nous allons dans le fichier build.gradle (app)

Dans la section "dependencies", on ajoute :

implementation 'androidx.recyclerview:recyclerview:1.0.0'

Les listes

On oublie pas de "sync" pour que Gradle aille nous chercher la bibliothèque et s'assure que tout va bien

On laisse tourner, et quand c'est fini on peut reprendre !

Les listes

On peut maintenant utiliser la RecyclerView dans notre XML ! Comme ceci par exemple :

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_height="match_parent"
    android:layout_width="match_parent"
    >

  <androidx.recyclerview.widget.RecyclerView
      android:id="@+id/recyclerView"
      android:layout_width="match_parent"
      android:layout_height="match_parent"
      />

</androidx.constraintlayout.widget.ConstraintLayout>

Les listes

Tout d'abord, créons nous une liste d'éléments à afficher

data class User(val firstName: String, val lastName: String, val description: String)
class MainActivity : AppCompatActivity()
{

	private val users = listOf(
      User("John", "Cena", "Lorem ipsum"),
      User("Homer", "Simpson", "Lorem ipsum"),
      User("Bob", "Dylan", "Lorem ipsum"),
      User("Rick", "Morty", "Lorem ipsum"),
      User("Marcel", "Pagnol", "Lorem ipsum"),
      User("Asterix", "& Obelix", "Lorem ipsum"),
      User("Kaaris", "Sevran", "Lorem ipsum"),
      User("Provençal", "Le Gaullois", "Lorem ipsum")
  	)
  
  // .. reste de code de l'activité ..

Les listes

Nous pouvons directement créer un Layout propre à un élément de la liste

Les listes

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="100dp"
    android:padding="20dp"
    >

  <TextView
      android:id="@+id/name"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:textSize="24sp"
      android:textStyle="bold"
      app:layout_constraintBottom_toTopOf="@id/description"
      app:layout_constraintStart_toStartOf="parent"
      app:layout_constraintTop_toTopOf="parent"
      app:layout_constraintVertical_chainStyle="packed"
      tools:text="Gérard Depardieu"
      />

  <TextView
      android:id="@+id/description"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:textSize="18sp"
      app:layout_constraintBottom_toBottomOf="parent"
      app:layout_constraintStart_toStartOf="parent"
      app:layout_constraintTop_toBottomOf="@id/name"
      tools:text="Lorem ipsum dolor sit amet"
      />

</androidx.constraintlayout.widget.ConstraintLayout>

user_item.xml

Les listes

Il nous faut ensuite créer un ViewHolder

La classe ViewHolder sera en charge de lier l'UI d'un élément de la liste avec l'objet métier correspondant

Ce sont ces fameux ViewHolder qui sont recylcés !

Un ViewHolder c'est :

  • hériter de Recycler.ViewHolder
  • avoir un layout associé
  • une méthode pour binder l'objet métier et la vue

Les listes

class UserViewHolder(inflater: LayoutInflater, parent: ViewGroup) :
  RecyclerView.ViewHolder(
    inflater.inflate(R.layout.user_item, parent, false)
  )
{

  private var name: TextView? = null
  private var description: TextView? = null


  init
  {
    name = itemView.findViewById(R.id.name)
    description = itemView.findViewById(R.id.description)
  }

  fun bind(user: User)
  {
    name?.text = "${user.firstName} ${user.lastName}"
    description?.text = user.description
  }

}

Les listes

Maintenant, nous devons nous occuper de l'Adapter

L'Adapter est une classe héritante de RecyclerView.Adapter qui se chargera de gérer les ViewHolders en fonction de la liste des objets métiers à afficher

Les listes

class MyAdapter(private val users: List<User>) : RecyclerView.Adapter<UserViewHolder>()
{

  override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserViewHolder
  {
    val inflater = LayoutInflater.from(parent.context)
    return UserViewHolder(inflater, parent)
  }

  override fun onBindViewHolder(holder: UserViewHolder, position: Int)
  {
    val user: User = users[position]
    holder.bind(user)
  }

  override fun getItemCount(): Int = users.size

}

Les listes

override fun getItemCount(): Int = users.size

Ca paraît sorcier, mais ça l'est pas !

Faisons un petit zoom sur les méthodes :

Ici, on indique à l'adapter le nombre d'item qu'il aura à gérer, à priori le même nombre d'items qu'il y a dans la liste

Les listes

override fun onBindViewHolder(holder: UserViewHolder, position: Int)
{
    val user: User = users[position]
    holder.bind(user)
}

Ici, la callback nous donne un ViewHolder (du type tenu par l'Adapter) et un Int correspondant à l'index de l'item dans la liste

Nous pourrions avoir des comportements différents selon l'item, mais ici nous bindons à chaque fois le UserViewHolder et l'objet User associé

Les listes

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserViewHolder
{
    val inflater = LayoutInflater.from(parent.context)
    return UserViewHolder(inflater, parent)
}

Ici, c'est la méthode qui est appelé lorsqu'Android doit afficher un ViewHolder à l'écran et qu'il lui demande de se déssiner

Les listes

Actuellement nous avons :

  • les objets métiers (liste des User)
  • le ViewHolder
  • l'Adapter

Il nous manque simplement le LayoutManager

Les listes

Pour le coup, il est rare que vous ayez besoin d'hériter de LayoutManager car les 2 qui existent par défaut remplissent la plupart des cas d'utilisation :

  • LinearLayoutManager pour rendre les vues de manière verticales ou horizontales
     
  • GridLayoutManager pour rendre les vues sous forme de grille

Les listes

Nous pouvons désormais assembler tous nos morceaux du puzzle et faire notre RecyclerView !

Nous allons fournir à notre RecyclerView un LayoutManager, un Adapter, et nous fournirons à celui-ci la liste des données à afficher :

Les listes

class MainActivity : AppCompatActivity()
{
  private val users = listOf(
    User("John", "Cena", "Lorem ipsum"),
    User("Homer", "Simpson", "Lorem ipsum"),
    User("Bob", "Dylan", "Lorem ipsum"),
    User("Rick", "Morty", "Lorem ipsum"),
    User("Marcel", "Pagnol", "Lorem ipsum"),
    User("Asterix", "& Obelix", "Lorem ipsum"),
    User("Kaaris", "Sevran", "Lorem ipsum"),
    User("Provençal", "Le Gaullois", "Lorem ipsum")
  )

  override fun onCreate(savedInstanceState: Bundle?)
  {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    recyclerView?.apply {
      layoutManager = LinearLayoutManager(this@MainActivity)
      adapter = MyAdapter(users)
    }
  }
}

Les listes

Comme nous sommes dans un "apply", un simple "this" ferait référence à la recyclerView, c'est pourquoi on utilise ce petit trick Kotlin : this@MainActivity

Les listes

Petit exercice :

Comment ferais-je si je souhaitais réagir au clic sur l'un des éléments de la liste ?

Essayer de trouver par vous-même une façon de faire cela et nous corrigerons ensemble ensuite

La "réaction" peut être un simple Log, il faut juste savoir comment réagir au clic

Les appels réseaux

Les appels réseaux

Une application mobile doit, la plupart du temps, se brancher à une API afin de récupérer des informations depuis internet pour les afficher

Il est donc primordial, en tant que développeur Android, de savoir réaliser un appel réseau

Les appels réseaux

Nous allons aller étape par étape pour comprendre comment cela fonctionne en Android

Tout d'abord, il va falloir que notre application puisse avoir accès à Internet

Ca peut paraître idiot, mais il s'agit là d'une permission "dangereuse"

Les appels réseaux

Pour rappel, les permissions dangereuses sont un ensemble de permissions que le développeur doit déclarer dans son Manifest car elles seront indiquées à l'utilisateur dans la fiche de Store de l'application

Une permission dangereuse doit ensuite faire l'objet d'une autorisation de la part de l'utilisateur : les fameuses popup "autorisez-vous l'utilisation du GPS", etc.

Les appels réseaux

La permission concernant Internet est un peu particulière

Bien qu'il faille la préciser quand même dans le Manifest, Android ne force plus le développeur à demander l'autorisation

Les appels réseaux

La première étape est donc tout de même de préciser ces permissions :

<manifest>

...

<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.INTERNET" />

<application>

...

</application>

</manifest

Les appels réseaux

INTERNET : nous sert à faire des appels réseaux

 

ACCESS_NETWORK_STATE : nous sert à connaître l'état du réseau

Les appels réseaux

Avant d'aller plus loin, il est important de comprendre un concept nouveau en développement Android : la notion de "Thread de l'UI" ou encore "Main Thread"

La totalité des calculs permettants de rendre l'UI de votre application est réalisé sur un unique Thread qui est donc ce fameux Thread de l'UI

Les appels réseaux

Afin de garantir une fluidité graphique optimale à l'utilisateur, le système Android interdit CATEGORIQUEMENT tout processus considéré comme "long" à s'effectuer sur ce Thread

En effet, cela aurait pour effet de potentiellement ralentir ce Thread, potentiellement avoir un impact sur l'UI et ça... Android le refuse, et c'est non contournable

Les appels réseaux

Il est donc primordial, lorsque vous voulez faire des appels réseaux, de gérer ceux-ci à l'aide d'un autre Thread que celui de l'UI

Et quand vous développiez votre application Android en Java.... c'est là que ça se compliquait !

Les appels réseaux

Et ce pour 2 raisons :

  1. Parce que celui qui se targuerait de maîtriser sereinement et complètement les Thread serait soit malhonnête, soit idiot
     
  2. Parce que, même si Android Java a fait un effort pour faciliter cette gestion de Thread, le code à écrire restait très verbeux et compliqué à mettre en place

Les appels réseaux

Pour les plus curieux, qui voudraient se renseigner sur ce qu'on faisait en Java, cela s'appelait des AsyncTask

Attention, je rappelle que nous sommes bloqués sur Android à Java 7 (plus quelques fonctionnalités de Java 8)

Il serait trop facile, et surtout idiot, de tirer la conclusion que Java n'était pas un bon langage

Les appels réseaux

Quoi qu'il en soit, Kotlin, EL GRANDE Kotlin est arrivé

Attention, Kotlin ou pas, l'utilisation du Thread de l'UI pour faire un appel réseau est toujours interdit, faut pas rêver

Seulement Kotlin, comme à son habitude, peut grandement, grandement nous faciliter la vie

Les appels réseaux

C'est parti pour faire notre premier appel réseau

Les appels réseaux

Nous allons pour le moment nous baser sur cette requête à réaliser en GET :

Les appels réseaux

La manière la plus basique de requêter une URL et de récupérer son contenu sous forme de String est d'utiliser la classe Url proposée par Kotlin

On peut ainsi, dans son Activity, écrire ça par exemple :

override fun onCreate(savedInstanceState: Bundle?)
{
  super.onCreate(savedInstanceState)
  setContentView(R.layout.activity_a)

  val url = URL("https://next.json-generator.com/api/json/get/VydTXyeqY?delay=2000")
  val stringResponse = url.readText()
}

Les appels réseaux

Lancez donc votre application...

Les appels réseaux

L'application CRASH !

Les appels réseaux

Si vous enquêtez dans la StackTrace, vous allez retrouver cette Exception, à l'origine de notre problème :

android.os.NetworkOnMainThreadException

Je crois que le nom de l'exception se suffit à lui-même et que si vous avez bien compris le début du cours, vous comprenez pourquoi elle a été lancée

Les appels réseaux

Il va donc nous falloir être capable de réaliser cet appel en dehors du Thread principal

Les appels réseaux

Je vais être clair avec vous dès maintenant :

La gestion fine des threads en Kotlin n'est pas du tout un sujet trivial

Nous allons voir dans ce cours, puisque c'est un cours d'initiation, la partie émergée de l'iceberg

Nous allons voir des solutions "magiques", mais comprenez bien qu'elles cachent une grande complexité

Les appels réseaux

Pour les plus courageux et les plus curieux d'entre vous, si jamais vous cherchez à comprendre le fond du fond de la gestion de "threads" en Kotlin, je vous invite à vous renseigner sur le principe de COROUTINES en Kotlin

C'est un sujet passionnant mais attention : faut pas s'y attaquer en tremblant des genoux

Les appels réseaux

Sinon, pour la magie, ça se passe ici !

Nous allons utiliser une bibliothèque externe pour nous aider avec les Threads : Anko

Vous trouverez le Github d'Anko à cette URL : 

Les appels réseaux

"Monsieur, pourquoi il y a marqué qu'Anko est déprécié ? On apprend des outils dépassés nous ?"

Les appels réseaux

Non.

Anko a été créé à la base pour être un DSL (Domain Specific Language) de Kotlin et améliorer l'écriture des Layout

Anko n'est plus maintenu et considéré par ses mainteneurs comme "déprécié" car ceux-ci estiment que ce rôle là n'a plus lieux d'être avec l'existence de JetPack Compose (le "framework" Android pour écrire son application en Declarative UI, à la manière de Swift UI ou Flutter)

Les appels réseaux

Bref

Nous, on ne va utiliser Anko que pour une seule raison, 0.00005% de ce que c'est

Donc pas de panique ! En plus, c'est juste pour vous montrer mais après on apprendra encore une meilleure manière !

Les appels réseaux

On peut donc passer à l'installation de la bibliothèque

Dans votre build.gradle (de l'app), dans la section "dependencies", ajoutez :

implementation "org.jetbrains.anko:anko:0.10.8"

On oublie pas le Gradle Sync, et on est bon !

Les appels réseaux

Revenons maintenant dans notre Activity

Voici le petit miracle que nous permet de faire Anko :

override fun onCreate(savedInstanceState: Bundle?)
{
  super.onCreate(savedInstanceState)
  setContentView(R.layout.activity_a)

  doAsync {
    val url = URL("https://next.json-generator.com/api/json/get/VydTXyeqY?delay=2000")
    val stringResponse = url.readText()

    uiThread {
      Log.d("toto", "Response = $stringResponse")
    }
  }
}

Les appels réseaux

Les fonctions sont plutôt très claires, mais du coup :

  • doAsync prend une callback qu'Anko va jouer sur un autre Thread
  • uiThread prend une callback qu'Anko ne jouera qu'une fois que l'appel sera fini, et qui sera lancée sur le Thread de l'UI

Les appels réseaux

Regardez le log généré

Félicitations, vous venez de faire votre premier appel réseau !

Les appels réseaux

Bon, maintenant, il nous reste à PARSER ce résultat en objet métier (en objet Kotlin, Pojo, Modèle, bref, le nom qui vous plaît)

Pour ce faire, nous allons déjà créer une classe qui correspond :

data class User(val firstName: String, val lastName: String, val address: String)

Les appels réseaux

Ensuite, nous allons utiliser l'une des 2 bibliothèques les plus utilisées sur Android pour parser du Json qui sont :

  • Gson
  • Jackson

Nous choisirons Gson (choix parfaitement arbitraire, les 2 se ressemblant énormément)

Les appels réseaux

Hop, on met la dépendance dans le build.gradle :

implementation 'com.google.code.gson:gson:2.8.6'

Et on oublie pas le Gradle Sync !

Les appels réseaux

Et là... ça se complique.

Gson est un outil ultra puissant qui permet de customiser très efficacement votre parsing

Capable de reconnaître des Enums et de bien d'autres folies qui sont souvent négligées par ce genre d'outils

Mais du coup, il paye cette puissance avec un coût.. Gson est dur à utiliser

Les appels réseaux

Et non ! Pas du tout en fait, voilà :

val user = Gson().fromJson(stringResponse, User::class.java)

Pas plus, pas moins, merci Gson.

Attention, je ne rigolais pas sur la puissance de l'outil par contre, il est vraiment génial !

Mais il se paye aussi le luxe d'être simple d'utilisation :)

Les appels réseaux

Ainsi :

override fun onCreate(savedInstanceState: Bundle?)
{
  super.onCreate(savedInstanceState)
  setContentView(R.layout.activity_a)

  doAsync {
    val url = URL("https://next.json-generator.com/api/json/get/VydTXyeqY?delay=2000")
    val stringResponse = url.readText()

    val user = Gson().fromJson(stringResponse, User::class.java)

    uiThread {
      Log.d("toto", "user = $user")
    }
  }
}

Les appels réseaux

On log bien le toString de la data class :

D/toto: user = User(firstName=Chuck, lastName=Bartowski, address=Burbank)

On a correctement parsé notre appel !

Les appels réseaux

Ok, maintenant, comment gérer l'absence de réseau ?

Les appels réseaux

Nous allons partir du principe que nous demanderons à Android si le téléphone a accès à internet avant de faire l'appel

Dans un premier temps, on va encapsuler l'appel réseau et le déclencher lors d'un évènement

Au clic sur une TextView par exemple

Les appels réseaux

On peut donc imaginer quelque chose comme :

override fun onCreate(savedInstanceState: Bundle?)
  {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_a)

    textView?.setOnClickListener(this)
  }

  override fun onClick(view: View?)
  {
    doAsync {
      val url = URL("https://next.json-generator.com/api/json/get/VydTXyeqY?delay=2000")
      val stringResponse = url.readText()

      val user = Gson().fromJson(stringResponse, User::class.java)

      uiThread {
        Log.d("toto", "user = $user")
      }
    }
  }

Les appels réseaux

Histoire de voir graphiquement le process, rajoutons ceci :

override fun onCreate(savedInstanceState: Bundle?)
  {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_a)

    textView?.setOnClickListener(this)
  }

  override fun onClick(view: View?)
  {
    textView?.text = "Loading"
    doAsync {
      val url = URL("https://next.json-generator.com/api/json/get/VydTXyeqY?delay=2000")
      val stringResponse = url.readText()

      val user = Gson().fromJson(stringResponse, User::class.java)

      uiThread {
        Log.d("toto", "user = $user")
        textView?.text = user.firstName
      }
    }
  }

Les appels réseaux

Nous allons ensuite créer cette méthode permettant de renvoyer un booléen indiquant si nous avons du réseau ou non :

private fun isNetworkConnected(): Boolean 
{
  //1 On recupère le "ConnectivityManager", le nom est assez parlant...
  val connectivityManager = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager 
  //2 On récupère le réseau actif (Wifi, 4G, etc.)
  val activeNetwork = connectivityManager.activeNetwork
  //3 On demande d'analyser ses capacités actuelles
  val networkCapabilities = connectivityManager.getNetworkCapabilities(activeNetwork)
  //4 Enfin, on regarde si il en a et si elles lui permettent d'accéder à internet. On retour donc ça
  return networkCapabilities != null 
  && networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) 
}

Les appels réseaux

Allez-y, mettez donc ça dans votre Activity...

Ca ne fonctionne pas.

Android Studio n'est pas content.

Arrivez-vous à comprendre l'erreur ?

Les appels réseaux

connectivityManager.activeNetwork

C'est ce petit bout de code qui nous embête

La raison :

Call require API level 23 (current min is 21)

Les appels réseaux

Ca y est, enfin !

On a notre premier problème de version minimum d'API Android

Ca paraissait hyper simple de récupérer l'état du réseau hein ?

Bah en fait... ça n'est simple que depuis l'API 23 !

Les appels réseaux

Avant la version d'API 23, c'était une toute autre histoire

Alors, comment fait-on ?

Les appels réseaux

Une solution pourrait être de faire 2 comportements différents selon la version de SDK de l'utilisateur

On coderait alors la version après API 23 et celle avant

private fun isNetworkConnected(): Boolean
{
  val connectivityManager = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager

  return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
  {
    val activeNetwork =  connectivityManager.activeNetwork
    val networkCapabilities = connectivityManager.getNetworkCapabilities(activeNetwork)
    networkCapabilities != null && networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
  }
  else
  {
    TODO("VERSION.SDK_INT < M")
    true
  }
}

Les appels réseaux

Voilà, vous comprenez un peu mieux ce dont je vous parlais au début

Une autre solution serait de se dire que de nombreux développeurs ont déjà dû faire ça et en faire une bibliothèque

Les appels réseaux

En voici une que j'ai développé dans le cadre de mon travail de développeur dans une agence

Servez-vous en si besoin, c'est gratos, c'est pour moi

(après, il doit y en avoir plein d'autres, et sûrement encore mieux)

Les appels réseaux

Mais nous, on va en rester là

On va donc rester sur notre code avec le IF et le bête "return true" dans le cas de version < 23

Les appels réseaux

Adaptons un peu notre code d'appel réseau en fonction :

override fun onClick(view: View?)
{
  if (isNetworkConnected())
  {
    textView?.text = "Loading"
    doAsync {
      val url = URL("https://next.json-generator.com/api/json/get/VydTXyeqY?delay=2000")
      val stringResponse = url.readText()

      val user = Gson().fromJson(stringResponse, User::class.java)

      uiThread {
        Log.d("toto", "user = $user")
        textView?.text = user.firstName
      }
    }
  }
  else
  {
    textView?.text = "No network !"
  }
}

Les appels réseaux

Allez-y, faites des tests en mettant en mode avion, etc !

Si votre téléphone ou émulateur a une version d'API > 23, vous verrez l'erreur !

Les appels réseaux

En théorie, on pourrait s'arrêter là

C'était un gros chapitre, et on a vu pleins de trucs

Les appels réseaux

Mais, gérer une communication avec une API, on le sait, c'est pas toujours aussi simple que faire un bête GET sans paramètres sur une route toute simple

Une vraie application, de taille conséquente, se doit d'utiliser des bibliothèques plus robustes, plus pertinentes

Les appels réseaux

On pourrait évidemment tout faire avec ce que nous propose Kotlin

Mais comme dans toutes les technos, vous imaginez bien que des gens l'ont déjà fait et vous propose une bibliothèque sur un plateau d'argent

Les appels réseaux

Cette bibliothèque magique, dans le cadre des appels réseaux, c'est :

Retrofit

Retrofit, c'est THE client HTTP en Android, ni plus ni moins 

Ne pas utiliser Retrofit, c'est faire du feu avec des silex alors qu'on vous tend un lance-flamme 

Les appels réseaux

Nous n'allons qu'effleurer Retrofit, nous contenter de l'essentiel, mais libre à vous de voir à quel point c'est puissant et immense :

Les appels réseaux

Alors comment ça marche ?

Les appels réseaux

Tout d'abord, ajoutons la dépendance :

implementation 'com.squareup.retrofit2:retrofit:2.9.0'

Retrofit peut automatiser le Parsing si on lui donne l'adapter qui va bien

Et il existe des adapters Retrofit pour toutes les bibliothèques les plus connues de parsing Android

Les appels réseaux

Et donc il y en a un pour Gson ! Ajoutons-le :

implementation 'com.squareup.retrofit2:converter-gson:2.6.3'

On peut maintenant virer notre ancienne dépendance Gson :

implementation 'com.google.code.gson:gson:2.8.6'

On Gradle Sync, et on est prêts !

(d'ailleurs, on peut virer Anko aussi maintenant !)

Les appels réseaux

Retrofit a comme gros point positif d'automatiser énormément de chose !

Par exemple, l'ensemble de nos requêtes seront définies dans une Interface de manière extrêmement propre et précise

Les appels réseaux

Voici un exemple dans notre cas :

interface MyApiService
{
  @GET("json/get/VydTXyeqY?delay=2000")
  fun retrieveUser(): Call<User>
}

Aussi simple que ça.

Les appels réseaux

  • L'annotation GET permet de renseigner le EndPoint de la requête en question
     
  • La fonction est la fonction qui sera générée
     
  • L'objet Call<User> indique que le fonction créera un appel sensé récupérer un User (voyez ça comme une Promise en javascript ou Future dans d'autres langages)

Les appels réseaux

Ici, ça ne nous intéresse pas, mais sachez qu'il y a toutes les customisations que vous pouvez imaginer

Regardez juste sur la Home de la doc à quel point c'est facile :)

Les appels réseaux

Il nous faut ensuite créer l'objet Retrofit qui sera à générer une fois pour toute et dans lequel nous pouvons indiquer notre Parser :

val retrofit = Retrofit.Builder()
      .baseUrl("https://next.json-generator.com/api/")
      .addConverterFactory(GsonConverterFactory.create())
      .build()

Les appels réseaux

Encapsulons le dans un Singleton pour être sûr qu'il ne soit instancié qu'une fois :

object ApiRepository
{
  private var apiService: MyApiService? = null

  init
  {
    val retrofit = Retrofit.Builder()
      .baseUrl("https://next.json-generator.com/api/")
      .addConverterFactory(GsonConverterFactory.create())
      .build()

    apiService = retrofit.create(MyApiService::class.java)
  }
}

Les appels réseaux

Oui, c'est aussi simple que ça un Singleton en Kotlin.

Maintenant, on ajoute à notre singleton une méthode pour récupérer l'utilisateur

object ApiRepository
{
  private var apiService: MyApiService? = null

  init
  {
    val retrofit = Retrofit.Builder()
      .baseUrl("https://next.json-generator.com/api/")
      .addConverterFactory(GsonConverterFactory.create())
      .build()

    apiService = retrofit.create(MyApiService::class.java)
  }

  fun retrieveUser(callback: Callback<User>)
  {
    val call = apiService?.retrieveUser()
    call?.enqueue(callback)
  }
}

Les appels réseaux

fun retrieveUser(callback: Callback<User>)
{
  val call = apiService?.retrieveUser()
  call?.enqueue(callback)
}

Sur la première ligne : on crée l'appel

Sur la seconde ligne : on lui abonne une callback qui sera appellée en cas d'échec ou réussite

Les appels réseaux

ENFIN, on peut faire l'appel !

Pour ce faire, rdv dans l'Activity, effacez tout les trucs d'avant et on refait ça proprement :

override fun onClick(view: View?)
{
  textView?.text = "Loading"
  ApiRepository.retrieveUser(this)
}

Les appels réseaux

Dans ce cas, j'ai choisi de faire implémenter l'interface par mon Activity, elle doit donc implémenter les deux méthodes de l'interface :

override fun onFailure(call: Call<User>, t: Throwable)
{
  textView?.text = "Error : ${t.message}"
}

override fun onResponse(call: Call<User>, response: Response<User>)
{
  textView?.text = "Code ${response.code()}, User = ${response.body()}"
}

Les appels réseaux

Si j'avais voulu ne pas faire porter par mon Activity, j'aurais du faire une classe anonyme permettant d'implémenter les 2 méthodes, comme ça :

ApiRepository.retrieveUser(object: Callback<User> {
      override fun onFailure(call: Call<User>, t: Throwable)
      {
        textView?.text = "Error : ${t.message}"
      }

      override fun onResponse(call: Call<User>, response: Response<User>)
      {
        textView?.text = "Code ${response.code()}, User = ${response.body()}"
      }

    })

Les appels réseaux

FINI !

Bravo, c'était pas un chapitre simple

Si vous l'avez suivi jusqu'au bout (je vous conseille de le relire quand même), vous avez maintenant toutes les billes pour faire CE QUE VOUS VOULEZ !

Les appels réseaux

Evidemment, on peut toujours aller plus loin

Pour les plus curieux d'entre-vous, je réitère, renseignez-vous sur les Coroutines en Kotlin

Vous y trouverez des astuces pour alléger encore plus le code et éviter d'avoir 36.000 CallBack Retrofit partout par exemple...

Mais je vous laisse regarder ça, c'est bien trop avancé pour ce cours déjà bien rempli :)

Les appels réseaux

Petit exercice final sur les appels réseau !

  • Afficher un bouton au centre de l'écran
  • Au click, récupérer la liste sur le EndPoint suivant :
  • Durant l'appel, indiquer à l'utilisateur ce que vous faites (rdv prochaine slide pour savoir faire un Loader)
  • Une fois l'appel fini, affichez la liste
  • Si il y a une erreur, afficher une erreur

Les appels réseaux

Voici un exemple de composant XML pour faire un "Loader" très commun sur Android :

<androidx.core.widget.ContentLoadingProgressBar
      android:id="@+id/loader"
      android:layout_width="50dp"
      android:layout_height="50dp"
      android:layout_marginTop="60dp"
      app:layout_constraintBottom_toBottomOf="parent"
      app:layout_constraintEnd_toEndOf="parent"
      app:layout_constraintStart_toStartOf="parent"
      app:layout_constraintTop_toTopOf="parent"
      style="?android:attr/progressBarStyleLarge"
      />

SahredPreferences

SharedPreferences

Ce petit chapitre va nous permettre de faire connaissance avec un concept assez connu sur Android : les Shared Preferences

Il s'agit de données que l'on va pouvoir stocker sous forme de clés/valeurs dans la mémoire du téléphone

SharedPreferences

Grâce aux SharedPreferences, nous allons pouvoir écrire dans un fichier XML qui va être stocké sur un espace mémoire dédié à notre application et qui survivra aux interruptions/relances de l'application

A la création du fichier, nous pouvons décider si nous souhaitons le rendre publique  ou privé

Un fichier de préférences publique.. pourra être accessible des autres applications du téléphone

SharedPreferences

Attention, les données stockées dans les SharedPreferences sont stockées dans les "datas" du téléphone, et pas dans le "cache"

Les "datas" peuvent être supprimées bien plus facilement par le système, pour diverses raisons, et ne doivent donc pas contenir de données critiques pour le fonctionnement de l'application

SharedPreferences

Comment ça fonctionne ?

On récupère une instance des SharedPreferences

val sharedPreferences = getSharedPreferences(PREFERENCES_FILE_NAME, Context.MODE_PRIVATE)

On peut aussi récupérer un fichier spécifique à l'activité en cours (et donc non partagé avec les autres activités) avec : 

val sharedPreferences = getPreferences(Context.MODE_PRIVATE)

SharedPreferences

Ensuite, on se met soit en mode Editor pour écrire :

sharedPreferences.edit().putString(MY_KEY, value).apply()

Soit on lit directement une valeur (on précise une valeur par défaut si il ne trouve rien dans le fichier)

sharedPreferences.getString(MY_KEY, null)

SharedPreferences

Exemples d'utilisation ?

  • Stocker un token d'authentification pour les appels API
  • Stocker un booléen pour l'affichage d'une vue rare (popup de message unique, OnBoarding, etc.)

SharedPreferences

Exercice

  • créer un écran basique avec un EditText et un boutton
     
  • au click sur le bouton, stocker la valeur de l'EditText dans les SharedPreferences
     
  • au lancement de l'écran, si une précédente valeur avait été sauvegardée, afficher là de base dans l'EditText

Base de Données Locale : ROOM

Room

Chaque application Android peut avoir accès à une Base de Données SQLLite sur le téléphone de l'utilisateur

Cette base de données permet de stocker dans le "cache" des données sous formes de tables SQL et c'est très pratique notamment pour mettre en place une logique de cache d'appels réseaux

Room

Avant, utiliser la BDD SQLLite n'était pas forcément aisé

Depuis, Android a sorti Room, une bibliothèque permettant de grandement faciliter le dialogue entre votre code et la BDD

Attention, gérer un cache et une BDD locale reste souvent plus compliqué que l'on ne croit, à utiliser donc avec parcimonie

Room

Mise en place :

  • Bibliothèque Room
implementation "androidx.room:room-runtime:2.2.6"
  • Annotation Processor
kapt "androidx.room:room-compiler:2.2.6"
  • Ensemble de coroutines et "petits plus" 
implementation "androidx.room:room-ktx:2.2.6"

Room

En haut de votre app/build.gradle, ajoutez ceci pour que KAPT soit appliqué :

apply plugin: 'kotlin-kapt'

Room

La communication avec la BDD va se faire de manière asynchrone

Nous allons donc devoir utiliser un peu les coroutines, même si nous ne nous pencherons une fois de plus pas trop en détail dessus

implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.0'

Room

Voici les 3 éléments principaux dans Room :

  • la Database
     
  • les DAO (Data Access Objects)
     
  • les Entities

Room

Database

Il s'agit, comme son nom l'indique, du point d'accès principal à la BDD SQLLite

Nous allons y déclarer les différentes Tables (Entities) et DAO

Room

Entity

Une Entity n'est rien d'autre que la représentation d'une Table au sens SQL du terme

Il s'agit simplement d'une classe annotée de @Entity et qui va nous permettre de déclarer ses champs, etc.

Room

DAO

Ce sont les interfaces qui vont nous permettre de définir les méthodes de communication avec les Entities

Room

OK, c'est parti pour un petit exemple !

Nous allons partir sur une seule table pour le moment, nous stockerons les données d'utilisateurs

Room

Première étape, nous allons définir notre @Entity

@Entity
data class User(
    @PrimaryKey(autoGenerate = true) val uid: Long? = null,
    @ColumnInfo(name = "first_name") val firstName: String?,
    @ColumnInfo(name = "last_name") val lastName: String?
)

Room

Comme vous pouvez le voir, Room est essentiellement basé sur le principe des annotations !

Ici par exemple, nous avons une annotation @PrimaryKey pour définir la clé primaire ainsi que @ColumnInfo pour donner des informations spécifiques sur le champs

Room

Concernant la Primary Key

Nous utilisons ici le autoGenerate = true pour ne pas avoir à nous en occuper, mais vous pouvez évidemment l'enlever et avoir votre propre logique (venant de l'API)

Besoin d'avoir une clé primaire composée ? Ca se passe dans l'annotation Entity alors :

@Entity(primaryKeys = arrayOf("firstName", "lastName"))

Room

Dans l'annotation Entity, on peut également préciser un nom spécifique (sinon, c'est celui de la classe)

@Entity(tableName = "user")

Concernant le @ColumnInfo, si on veut un autre nom que celui du champs, on peut l'overrider :

@ColumnInfo(name = "first_name") val firstName: String?,

Room

Pour plus d'infos :

Room

Déclarons maintenant notre DAO !

Pour rappel, le DAO c'est l'interface qui déclare les méthodes de communication applicables sur notre Entity

Room

@Dao
interface UserDao
{
  @Insert
  fun insert(user: User): Long
}

Comme vous le voyez, on est toujours sur un concept d'annotations

On a besoin de rien de plus ! L'Annotation Processor va se charger de tout générer pour nous !

La requête nous renverra l'ID généré, d'où le type de retour !

Room

Mettons un peu plus de méthodes !

@Dao
interface UserDao
{
  @Query("SELECT * FROM user")
  fun getAll(): List<User>

  @Query("SELECT * FROM user WHERE uid IN (:userIds)")
  fun loadAllByIds(userIds: IntArray): List<User>

  @Query(
    "SELECT * FROM user " +
        "WHERE first_name LIKE :first" +
        " AND last_name LIKE :last"
  )
  fun findByName(first: String, last: String): List<User>

  @Insert
  fun insertAll(users: List<User>): List<Long>

  @Insert
  fun insert(user: User): Long

  @Update
  fun updateUser(user: User)

  @Delete
  fun delete(user: User)

  @Query("DELETE FROM user")
  fun deleteAll()
}

Room

Enfin, nous pouvons créer notre DataBase !

Pour cela nous allons créer une classe Abstraite annotée de @Database et qui devra donner la liste des Entities ainsi que des DAO associés

Room

@Database(entities = [User::class], version = 1)
abstract class MyDatabase : RoomDatabase()
{
  abstract fun userDao(): UserDao
}

Nous allons maintenant en faire un Singleton pour récupérer une instance de la Database une seule fois :

Room

@Database(entities = [User::class], version = 1)
abstract class MyDatabase : RoomDatabase()
{
  abstract fun userDao(): UserDao

  companion object
  {

    private var database: MyDatabase? = null

    fun instance(context: Context): MyDatabase?
    {
      if (database != null)
      {
        return database
      }

      database = Room.databaseBuilder(
        context,
        MyDatabase::class.java, "database-name"
      ).build()

      return database
    }
  }
}

Room

On est bon ! On va pouvoir maintenant utiliser notre BDD !

Pour se faire, je vais mettre un peu de code dans une Activity créée pour l'occasion, mais pas forcément de code graphique pour le moment, on va regarder les Logs

Room

Les appels à l'instance de la BDD sont asynchrones, il faut donc pouvoir gérer l'asynchronisme dans notre manière de communiquer avec la BDD

Nous allons rester sur notre habitude des callbacks pour le moment, libre à vous d'aller plus loin dans votre compréhension des Coroutines pour voir ce qu'on peut faire !

Room

Voici par exemple ici comment récupérer tous les utilisateurs :

private fun retrieveUsers(callback: (List<User>) -> Unit)
{
	GlobalScope.launch {
		val database = MyDatabase.instance(applicationContext)
		callback.invoke(database?.userDao()?.getAll() ?: emptyList())
	}
}

GlobalScope.launch nous permet de lancer un "scope" de coroutine, voyez ça comme une sorte "d'autre Thread"

Room

Voici une autre fonction par exemple pour insérer une liste d'utilisateurs

private fun insertUsers(users: List<User>, callback: (List<Long>) -> Unit)
  {
    GlobalScope.launch {
      val database = MyDatabase.instance(applicationContext)
      val ids = database?.userDao()?.insertAll(users)
      callback.invoke(ids)
    }
  }

Room

Lancez un premier coups en insérant des données :

insertUsers(
      listOf(
        User(firstName = "Michael", lastName = "Jordan"),
        User(firstName = "Steve", lastName = "Jobs"),
        User(firstName = "Elon", lastName = "Musk")
      )
    )

Room

Puis lancez une seconde fois en lisant votre BDD !

//    insertUsers(
//      listOf(
//        User(firstName = "Michael", lastName = "Jordan"),
//        User(firstName = "Steve", lastName = "Jobs"),
//        User(firstName = "Elon", lastName = "Musk")
//      )
//    )

    retrieveUsers { users ->
      users.forEach {
        Log.d("toto", "user found : $it")
      }
    }

Room

Petit plus avec Android Studio !

Vous devriez trouver normalement dans un des onglets autour de votre IDE un nommé "Database Inspector"

Lorsque l'app tourne sur votre Device, ouvrez cet onglet, et vous voilà avec une vue sur votre BDD, rien que ça ! 

Room

Félicitations !

Vous voilà déjà bien avancés dans votre apprentissage de Room

Bon.. j'imagine que vous voulez quand même savoir comment on peut gérer des relations entres Entities, car c'est finalement le GROS truc qui manque dans ce cours sur Room

Room

Ok, mettons en place des relations

Tout d'abord, il est possible d'ajouter des Embedded relations

Il s'agit de créer une nouvelle Entity mais dont les champs ne serrons que de nouvelles colonnes de l'Entity qui l'embed

Room

Par exemple, donnons une adresse à nos Users et découpons là dans une Entity à part Embedded

@Entity(tableName = "user")
data class User(
  @PrimaryKey(autoGenerate = true) val uid: Long? = null,
  @ColumnInfo(name = "first_name") val firstName: String?,
  @ColumnInfo(name = "last_name") val lastName: String?,
  @Embedded val address: Address?
)


data class Address(
  val street: String?,
  val state: String?,
  val city: String?,
  @ColumnInfo(name = "post_code") val postCode: Int
)

Room

Dans cet exemple, les champs de Adress ne seront en fait que des nouvelles colonnes dans la table "user"

Utiliser une relation @Embedded permet de rentre le code des Entities plus propre

Allez-y, relancez votre application et regardez dans le Database Inspector !

Room

ERREUR !

Vous voilà sûrement tombé sur une erreur de ce type :

Room cannot verify the data integrity.
Looks like you've changed schema but forgot to update the version number.

Room

En effet, vous êtes en train de manipuler une vraie BDD SQL, et comme toute BDD, il faut faire attention aux migrations de structure !

Première étape, il est impératif d'incrémenter la version de la BDD à chaque changement de structure, sinon ça pète une erreur

@Database(entities = [User::class], version = 2)
abstract class MyDatabase : RoomDatabase()

Room

Deuxième étape,  il faut préciser à SQLLite comment il doit se comporter au niveau de la migration

En effet, que faire si l'app existait déjà, avec des données, et que la nouvelle structure a changé ??

Room

Pour régler ça, il y a les Scripts de Migration

Nous pouvons créer une classe héritant de Migration qui permettra de lancer toutes les commandes SQL qui nous semblent importantes pour l'intégrité des données

Cette classe devra préciser la version de base ainsi que celle d'arrivée, on aura donc X classes de Migration au fur et à mesure de l'avancée de l'application !

Room

Voici un exemple :

val MIGRATION_1_2 = object : Migration(1, 2) {
      override fun migrate(database: SupportSQLiteDatabase) {
        database.execSQL("Ma Query SQL")
      }
    }
database = Room.databaseBuilder(
        context,
        MyDatabase::class.java, "database-name"
      )
        .addMigrations(MIGRATION_1_2, MIGRATION_2_3, ...)
        .build()

Ce qui donne :

Room

Voilà une des raisons pour lesquelles je précisais au début que gérer un VRAI cache mobile, ça peut être très compliqué

Surtout qu'un utilisateur mobile ne fait pas forcément toutes les MAJ ! Rien ne l'empêche d'avoir sauté la version 2 de votre app et de directement télécharger la 3

Vous voyez le problème ? Il faudrait donc aussi prévoir une migration de 1 à 3...

Room

Après, il est toujours possible de dire à Android : 

Tu CLEAR tout à chaque changement de version de la BDD

C'est violent, mais ça facilite la vie ! Nous partirons donc pour ça pour nos exemples :

database = Room.databaseBuilder(
        context,
        MyDatabase::class.java, "database-name"
      )
        .fallbackToDestructiveMigration()
        .build()

Room

Maintenant ça devrait marcher ! 

Lancez l'app et regardez votre Database Inspector

Room

Les relations ONE TO ONE

Imaginons une relation One To One telle que, dans une app type Spotify, un utilisateur puisse avoir une "Library" correspondant à ses sons à lui en tant que musicien

Le musicien a alors UNE "library" et celle-ci appartient à UN musicien

Room

Voilà comment on représenterait ça :

@Entity
data class Library(
  @PrimaryKey val libraryId: Long,
  val userOwnerId: Long
)

Une Entity Library :

Un Model qui fait le lien :

data class UserAndLibrary(
  @Embedded val user: User,
  @Relation(
    parentColumn = "uid",
    entityColumn = "userOwnerId"
  )
  val library: Library
)

Room

On en profite pour créer une LibraryDao

@Dao
interface LibraryDao
{
  @Insert
  fun insert(library: Library): Long

  @Insert
  fun insertAll(libraries: List<Library>): List<Long>
}

Room

ATTENTION

On oublie pas, dès lors qu'on ajoute une Entity, ça signifie :

  • incrémentation de la version de la BDD
  • ajout de cette Entity dans la liste dans l'annotation @Database

Room

Voici comment on pourrait lister ces nouveaux Modèles de donner par exemple (mettons cette Query dans notre UserDao) :

@Transaction
@Query("SELECT * FROM user")
fun getUsersAndLibraries(): List<UserAndLibrary>

Le @Transaction permet à Room de savoir qu'il va devoir faire 2 requêtes et lier le tout

Room

Voici un exemple de création de User et de Libraries liées

private fun insertUsersAndLLibraries()
  {
    val randomAddress = Address(
      street = "Rue du Kotlin",
      state = "France",
      city = "Paris",
      postCode = 75001
    )

    insertUsers(
      listOf(
        User(firstName = "Michael", lastName = "Jordan", address = randomAddress),
        User(firstName = "Steve", lastName = "Jobs", address = randomAddress),
        User(firstName = "Elon", lastName = "Musk", address = randomAddress)
      )
    ) { identifiers ->

      val libraries = mutableListOf<Library>()

      identifiers?.forEach { id ->
        libraries.add(Library(userOwnerId = id))
      }

      GlobalScope.launch {
        val database = MyDatabase.instance(applicationContext)
        database?.libraryDao()?.insertAll(libraries)
      }
    }
  }

Room

Les relations One To Many

On reste dans notre app de musique, et on considère qu'un utilisateur peut avoir 0 ou plusieurs Playlist

Room

On commence par créer une nouvelle Entity

on oublie pas les règles quand on change quelque chose dans la BDD !

@Entity
data class Playlist(
  @PrimaryKey val playlistId: Long,
  val userCreatorId: Long,
  val playlistName: String
)

Room

De nouveau, on crée un modèle représentant la relation

data class UserWithPlaylists(
  @Embedded val user: User,
  @Relation(
    parentColumn = "uid",
    entityColumn = "userCreatorId"
  )
  val playlists: List<Playlist>
)

Room

Et c'est reparti !

Vous connaissez déjà les étapes suivantes :

  • créer un DAO pour Playlist (la mettre dans @Database)
  • renseigner une Query dans User pour récupérer une liste de UserWithPlaylits
  • Mettre en place le code pour enregistrer/lire les données !

Room

Enfin, la relation MANY TO MANY

Dans notre exemple d'application de musique, imaginez les Playlist et les Song

Une Playlist a plusieurs Songs et une Song peut appartenir à plusieurs Playlist

Room

Niveau structure, ça change un peu car il nous faut mettre en place une Table de jointure entre les deux Entity :

@Entity
data class Playlist(
  @PrimaryKey val playlistId: Long,
  val userCreatorId: Long,
  val playlistName: String
)

@Entity
data class Song(
  @PrimaryKey val songId: Long,
  val songName: String,
  val artist: String
)

@Entity(primaryKeys = ["playlistId", "songId"])
data class PlaylistSongCrossRef(
  val playlistId: Long,
  val songId: Long
)

Room

Ensuite, on peut avoir 2 types de modèles différents, selon la requête qu'on veut faire :

data class PlaylistWithSongs(
  @Embedded val playlist: Playlist,
  @Relation(
    parentColumn = "playlistId",
    entityColumn = "songId",
    associateBy = Junction(PlaylistSongCrossRef::class)
  )
  val songs: List<Song>
)

data class SongWithPlaylists(
  @Embedded val song: Song,
  @Relation(
    parentColumn = "songId",
    entityColumn = "playlistId",
    associateBy = Junction(PlaylistSongCrossRef::class)
  )
  val playlists: List<Playlist>
)

Room

Enfin, côté DAO, voici ce que ça donnerait comme requêtes :

@Transaction
@Query("SELECT * FROM Playlist")
fun getPlaylistsWithSongs(): List<PlaylistWithSongs>

@Transaction
@Query("SELECT * FROM Song")
fun getSongsWithPlaylists(): List<SongWithPlaylists>

Room

Pour finir ! (parce que là c'est long)

Comment faire si vous voulez, en une seule requête, récupérer tous les User, avec toutes leurs Playlist et, pour chacune, toutes leurs Song ?

Room

Vu que nous avons déjà un modèle représentant la jointure PlaylistWithSongs, il nous suffit d'en créer un nouveau chez User, comme ceci :

data class UserWithPlaylistsAndSongs(
  @Embedded val user: User,
  @Relation(
    entity = Playlist::class,
    parentColumn = "uid",
    entityColumn = "userCreatorId"
  )
  val playlists: List<PlaylistWithSongs>
)

Room

Vous comprenez pourquoi ?

Nous lions le User avec un modèle qui lie déjà les Playlist et les Song ! Ca donne ça en gros :

Room

Niveau requête, ça donnerait donc un truc "simple" comme ça :

@Transaction
@Query("SELECT * FROM User")
fun getUsersWithPlaylistsAndSongs(): List<UserWithPlaylistsAndSongs>

Attention aux perfs quand même ! A priori il n'y a pas vraiment de raisons de faire d'aussi grandes Query, c'est juste pour l'exemple que je vous le montre

Room

FINI !

Félicitations, si vous avez tenu jusque-là, vous savez désormais bien vous débrouiller avec Room ! :)

Ne reste plus qu'à pratiquer ! Gérer du caching n'est pas du tout chose aisée mais, si c'est bien fait, ça peut vraiment être génial 

Aller plus loin ?

Android Architecture Components

Nous avons pu voir dans ce cours toutes les bases nécessaires à la création d'applications Android

En théorie, rien ne vous empêche maintenant de réaliser une application rivalisant avec les meilleures

Nous n'avons pas réellement pu aller très loin dans la customisation graphique et l'intégration avancée, mais vos pourrez progresser sur ce sujet de votre côté avec les bases que vous avez

Aller plus loin ?

Android Architecture Components

Il reste toutefois des sujets intéressants pour ceux qui voudraient aller un peu plus loin

En effet, certains sujets sur la création de l'architecture d'une application Android ne sont pas simples : 

  • la navigation
  • la gestion des fragments, activités
  • la gestion du cycle de vie
  • la transmission d'informations entre fragments
  • etc.

Aller plus loin ?

Android Architecture Components

Ces problèmes d'architecture.... ça fait un bail que les développeurs l'ont relevé !

Ils soulevaient en effet le problème d'un manque de prise de position d'Android quant au type d'architecture à utiliser

Aller plus loin ?

Android Architecture Components

Google a alors tapé très fort en fin 2017 en annonçant les :

Android Architecture Components

Aller plus loin ?

Android Architecture Components

Aller plus loin ?

Android Architecture Components

Le principe phare des Architecture Components était de séparer de manière nette la logique de l'UI 

Les Activités / Fragments ne devraient plus avoir qu'à mettre à jour l'UI !

Toute la logique métier devrait être déportée ailleurs

Aller plus loin ?

Android Architecture Components

On y gagnerait en ayant des Activités / Fragments bien plus lisibles et simples (uniquement du graphique)

Mais aussi en ayant du code métier séparé, dans des fichiers Kotlin sans liens avec l'UI et donc bien plus testables

Aller plus loin ?

Android Architecture Components

Les 2 composants les plus intéressants pour une telle architecture sont :

  • les ViewModel
  • les LiveData

Aller plus loin ?

Android Architecture Components

ViewModel

Un ViewModel, ça va être une classe Kotlin qui va nous permettre d'encapsuler toute la logique métier propre à un/des Fragments / Activités

Les différents Fragments / Activités n'auront alors plus qu'à utiliser une instance de ce ViewModel pour accéder aux données

Aller plus loin ?

Android Architecture Components

ViewModel

Le ViewModel a également un avantage COLOSSAL... c'est un LyfeCycleAware component

Ca signifie que le ViewModel gère POUR VOUS les changements d'états du cycle de vie ! Et croyez moi, il vous économise beaucoup, beaucoup de boulot !

Aller plus loin ?

Android Architecture Components

LiveData

Bien que le ViewModel, de par sa gestion automatique du cycle de vie, est déjà une révolution, il est souvent associé aux LiveData

Les LiveData sont des objets basés sur le pattern Observer qui vont vous permettre de mettre en place une programmation reactive assez simplement dans votre application

Aller plus loin ?

Android Architecture Components

LiveData

Ainsi, vos controlleurs (Fragments/Activity) vont pouvoir s'abonner aux LiveData du ViewModel, et réagir en fonction des changemelntds

Aller plus loin ?

Android Architecture Components

Testons un peu ça !

Aller plus loin ?

Android Architecture Components

On se crée un petit ViewModel qui va être chargé de récupérer la liste des Users venant d'une API (on reprend ce qu'on a vu précédemment avec Retrofit)

Aller plus loin ?

Android Architecture Components

package com.example.mastertest2.viewmodels

import android.util.Log
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response

class MyViewModel : ViewModel(), Callback<List<User>>
{
  val users = MutableLiveData<List<User>>()

  fun loadUsers()
  {
    ApiRepository.retrieveUser(this)
  }

  override fun onFailure(call: Call<List<User>>, t: Throwable)
  {
    Log.e("toto", "Une erreur est survenue, il faut en faire quelque chose !")
  }

  override fun onResponse(call: Call<List<User>>, response: Response<List<User>>)
  {
    users.value = response.body()
  }
}

Aller plus loin ?

Android Architecture Components

val users = MutableLiveData<List<User>>()

Ici, on déclare notre LiveData (mutable) qui va être la donnée que l'on va observer tout le long du processus et sur laquelle les controlleurs (Fragments/Activity) vont se baser pour l'UI

Aller plus loin ?

Android Architecture Components

fun loadUsers()
{
	ApiRepository.retrieveUser(this)
}

Ici, on déclare une méthode que le contrôleur pourra appeler et qui lancera toute la logique nécessaire

Aller plus loin ?

Android Architecture Components

override fun onFailure(call: Call<List<User>>, t: Throwable)
{
	Log.e("toto", "Une erreur est survenue, il faut en faire quelque chose !")
}

override fun onResponse(call: Call<List<User>>, response: Response<List<User>>)
{
	users.value = response.body()
}

Ces callbacks on les connaît déjà, mais notez le "users.value" qui signifie qu'on change la valeur de la LiveData et donc qu'on veut notifier les observers

Aller plus loin ?

Android Architecture Components

Une fois qu'on a fait le ViewModel, on peut jouer avec nos Activity / Fragments !

Histoire d'automatiser la gestion des ViewModel sous forme de Singleton, on va tirer une nouvelle petite dépendance pratique : 

implementation "androidx.activity:activity-ktx:1.1.0"

Aller plus loin ?

Android Architecture Components

Ensuite, on a plus qu'à faire référence au ViewModel dans notre activité

override fun onCreate(savedInstanceState: Bundle?)
{
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_my_view_model)

    val myViewModel: MyViewModel by viewModels()
}

Aller plus loin ?

Android Architecture Components

Notez le :

val myViewModel: MyViewModel by viewModels()

C'est à ça que nous sert la dépendance que l'on vient de tirer :)

Aller plus loin ?

Android Architecture Components

Ensuite, nous pouvons lancer la recherche, puis OBSERVER le résultat !

override fun onCreate(savedInstanceState: Bundle?)
{
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_my_view_model)

    val myViewModel: MyViewModel by viewModels()

    myViewModel.loadUsers()
    myViewModel.users.observe(this, Observer<List<User>>{ users ->
     Log.d("toto", "Voici les users : $users")
    })
}

Aller plus loin ?

Android Architecture Components

Et voilà le travail !

Votre Activity / Fragment n'est plus du tout responsable de la logique de votre application !

Aller plus loin ?

Android Architecture Components

Comment gérer le chargement ou les erreurs ?

A vous de voir, mais le composant tenu par une LiveData peut être un poil amélioré :

enum class Status {
  LOADING,
  SUCCESS,
  ERROR
}

data class UsersModel(
  val status: Status,
  val users: List<User> = listOf(),
  val errorMessage: String = ""
)

Aller plus loin ?

Android Architecture Components

Ainsi, côté ViewModel ça donnerait ça :

class MyViewModel : ViewModel(), Callback<List<User>>
{
  val usersModel = MutableLiveData<UsersModel>()

  fun loadUsers()
  {
    usersModel.value = UsersModel(status = Status.LOADING)
    ApiRepository.retrieveUser(this)
  }

  override fun onFailure(call: Call<List<User>>, t: Throwable)
  {
    usersModel.value = UsersModel(status = Status.ERROR, errorMessage = "WOW, super erreur là, attention")
  }

  override fun onResponse(call: Call<List<User>>, response: Response<List<User>>)
  {
    usersModel.value = UsersModel(status = Status.SUCCESS, users = response.body() ?: emptyList())
  }
}

Aller plus loin ?

Android Architecture Components

Enfin, côté Activity / Fragment, on peut du coup gérer TOUS les cas, hyper facilement :

myViewModel.usersModel.observe(this, Observer<UsersModel>{ userModel ->
  when(userModel.status)
  {
       Status.SUCCESS -> Log.d("toto", "on affiche la liste à l'utilisateur")
       Status.LOADING -> Log.d("toto", "on affiche notre plus beau Loader")
       Status.ERROR -> Log.d("toto", "on affiche une erreur de qualité supérieure")
  }
})

Aller plus loin ?

Android Architecture Components

Encore plus fort, vous vous souvenez de l'exemple sur le Cycle de vie avec l'EditText ?

Pour rappel, nous avions un EditText, un Button et une TextView. Au clic sur le Button, on plaçait la valeur de l'EditText dans la TextView

Aller plus loin ?

Android Architecture Components

Nous remarquions alors que, à la rotation de l'écran, nous perdions la valeur car la vue se reconstruisait depuis zéro, car elle était passée dans le OnDestroy

Essayons de refaire cette logique, mais avec ViewModel et LiveData cette fois...

Aller plus loin ?

Android Architecture Components

Côté ViewModel, on est très basique :

class MyEditTextViewModel : ViewModel()
{
  val result = MutableLiveData<String>()

  fun setResult(value: String) {
    result.value = value
  }
}

Aller plus loin ?

Android Architecture Components

Côté Activity, ça donne ça :

private val myViewModel: MyEditTextViewModel by viewModels()

override fun onCreate(savedInstanceState: Bundle?)
{
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_my_view_model)

    myViewModel.result.observe(this, Observer<String> { newValue ->
      result.text = newValue
    })

    validate?.setOnClickListener {
      myViewModel.setResult(myEditText?.text.toString())
    }
}

Aller plus loin ?

Android Architecture Components

Et voilà !

Vous pouvez faire toutes les rotations que vous voulez, le texte ne disparaît pas ! Et ça c'est parce que le ViewModel est conscient du Life Cycle et gère tout ça pour vous !

Et ça, croyez moi, c'est loin, loin ... LOIN d'être anodin !

Aller plus loin ?

Android Architecture Components

Pour finir, voici ce que Google préconise donc comme architecture pour les applications Android modernes, un mélange de ViewModel, LiveData et de pattern Repository :

Aller plus loin ?

Android Architecture Components

Aller plus loin ?

Android Architecture Components

Toutes les applications ne nécessitent peut-être pas une architecture monstrueuse, et il faut adapter votre code à votre contexte, ça c'est certain

Cependant, j'espère que ce petit chapitre vous a convaincu sur l'utilité de mettre en place ViewModel et LiveData pour découpler un maximum votre logique de votre UI ainsi que pour vous faciliter la vie côté cycle de vie !

Android

By Ecalle Thomas

Android

  • 1,215