Par Valentin Brosseau / Playmoweb
Point statistique :
À la base un OS pour appareil photo.
Linux ?
Construis autour du noyau Linux mais … c'est tout
L'isolation avant tout
Permissions d'accès
Deux types :
Trop pour certains… Mais le reflet de la réalité (foldable, 5G, Bluetooth LE, Bluetooth Mesh, NFC,…)
@Composable
fun Greeting(name: String) {
Text(text = "Hello $name!")
}
Le composant sont maintenant un principe de base de la conception d'interface utilisateur.
Des briques de base d'une interface.
Change la façon de faire des interfaces. Et uniquement ça.
(Nous avons toujours besoin de Kotlin, de Java, de Gradle, de l'Android SDK, etc.)
var counter by remember { mutableStateOf(0) }
Button(onClick = { counter++ }) {
Text("Clique => ${counter}")
}
Qu'observez-vous ? À votre avis, comment ça fonctionne ?
var isLogin by remember { mutableStateOf(false) }
if (isLogin) {
Text("Bienvenue")
} else {
Button(onClick = { isLogin = true }) {
Text("Se connecter")
}
}
Qu'observez-vous ? À votre avis, comment ça fonctionne ?
Column
: Alignement vertical.Row
: Alignement horizontal.Box
: Alignement libre.Button
: Un bouton.TextField
: Un champ de texte.Checkbox
: Une case à cocher.Switch
: Un interrupteur.Text
: Un texte.Beaucoup de composants sont déjà disponibles.
Repose sur le material design (nous y reviendrons)
var counter by remember { mutableStateOf(0) }
Column {
Button(onClick = { counter++ }) {
Text("Action")
}
AnimatedVisibility(visible = counter > 0) {
Text("Visible")
}
AnimatedContent(targetState = count) { targetState ->
Text(text = "Count: $targetState")
}
}
Le renouveau du développement Android
Arrêtons-nous un instant…
Jetbrains a développé Kotlin, Google l'a adopté. Les deux travaillent ensemble. Activement.
Le tout est Open Source.
Trois termes à retenir :
Compose multi-plateforme vous permettra donc de créer du code partagé entre vos applications Android, iOS, Desktop et Web.
Une dernière chose…
Jetpack Compose c'est jeune.
Il faut donc accepter que ça évolue vite / change / sois (parfois) instable.
Pour nous accompagner dans cette transition, Google a créé :
Accompanist est voué à disparaître. Car les fonctionnalités seront intégrées à Compose.
Deux façons de travailler :
stringResource(R.string.hello)
imageResource(R.drawable.image)
Qu'observez-vous ?
Column {
Text("Hello")
Text("World")
}
Row {
Text("Hello")
Text("World")
}
Column {
Row {
Text("Hello")
Text("World")
}
Row {
Text("Hello")
Text("World")
}
}
Box {
Text("Hello")
Text("World")
}
Nous construisons donc des grilles de composants.
Modifier pour réaliser ceci :
… quelques composants, et un peu de style.
Pour le style c'est plutôt automatique :
Nous avons à notre disposition un ensemble de composants « fonctionnels » qui vont nous permettre de créer les éléments de notre interface.
Text
: Un composant qui permet d'afficher du texte.Button
: Un composant qui permet d'afficher un bouton.Switch
: Un composant qui permet d'afficher un toggle (un bouton qui peut être activé ou désactivé).Image
: Un composant qui permet d'afficher une image.LazyColumn
: Un composant qui permet d'afficher une liste.Scaffold
: Un composant qui permet de créer une structure de base pour notre application (barre de navigation, - etc.).TopAppBar
: Un composant qui permet de créer une barre de navigation en haut de l'application.Card
: Un composant qui permet de créer une carte.IconButton
: Un composant qui permet de créer un bouton avec une icône.Nous avons également des composants qui sont là pour définir la structure de notre application :
Column
: Un composant qui permet de créer une colonne.Row
: Un composant qui permet de créer une ligne.Box
: Un composant qui permet de créer une boîte.Spacer
: Un composant qui permet de créer un espace entre deux éléments.Exemple :
Column {
Button(onClick = { /* Action */ }) {
Text("Cliquez ici")
}
Spacer(modifier = Modifier.weight(1f))
Text("Un texte")
}
Un peu d'explication… avant de continuer…
Button(onClick = { /* Action */ }) {
Text("Cliquez ici")
}
Ici, nous avons un bouton qui affiche un texte. Lorsque l'on clique dessus, une action est déclenchée.
Spacer(modifier = Modifier.weight(1f))
Un espace qui prend tout l'espace disponible. weight
est un pourcentage. Ici nous avons un poids de 1, donc il prend tout l'espace disponible.
Column {
Spacer(modifier = Modifier.weight(1f))
Text("Un texte")
}
À votre avis, que va-t-il se passer ?
Comment faire pour que le texte soit centré ?
Deux solutions :
horizontalAlignment = Alignment.CenterHorizontally
sur la Column
.textAlign = TextAlign.Center
sur le Text
. ⚠️ Attention, cela ne fonctionne que si votre Text
fait la largeur de l'écran.Column(
horizontalAlignment = Alignment.CenterHorizontally
) {
Spacer(modifier = Modifier.weight(1f))
Text("Un texte")
}
Comment faire pour que le texte soit centré verticalement ?
(PS : Il y a plusieurs solutions)
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Spacer(modifier = Modifier.weight(1f))
Text("Un texte")
Spacer(modifier = Modifier.weight(1f))
}
Column(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally)
{
Text("Un texte")
}
À votre avis, dans quels cas utiliser l'une ou l'autre des solutions ?
Pss : N'hésitez pas à consulter la documentation (mais également StackOverflow, ChatGPT, Claude, etc)
Modifier pour réaliser ceci :
Column(
modifier = Modifier.padding(innerPadding)
) {
Spacer(modifier = Modifier.weight(1f))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically
)
{
Greeting(
name = "Android",
)
}
Spacer(modifier = Modifier.weight(1f))
Row {
Button(onClick = { /*TODO*/ }) {
Text("Button 1")
}
Spacer(modifier = Modifier.weight(1f))
Button(onClick = { /*TODO*/ }) {
Text("Button 2")
}
}
}
Avant de continuer, une petite pause…
Nous allons en profiter pour créer un composant (vous aller voir c'est simple).
@Composable
fun VotreComposant(content: String) {
Text(text = content)
}
Et c'est tout… Je vous laisse essayer en créant un composant MyUI
qui représente votre petite interface.
En compose, nous parlons de Modifier
.
Les modifiers ont des méthodes pour modifier les composants (taille, couleur, etc.). Ils sont chaînables et varient en fonction du composant.
Text(
text = "Hello World",
modifier = Modifier
.padding(16.dp)
.background(Color.Blue)
.border(1.dp, Color.Black)
)
Modifier.fillMaxWidth() // Rempli la largeur
Modifier.fillMaxHeight() // Rempli la hauteur
Modifier.fillMaxSize() // Rempli la taille
Modifier.padding(16.dp) // Ajoute un padding de 16dp
Modifier.padding(16.dp, 8.dp) // Ajoute un padding de 16dp en largeur et 8dp en hauteur
Text(text = content, fontWeight = FontWeight.Light, fontSize = 10.sp)
Etc…
res/values/strings.xml
Accessible via stringResourcestringResource(R.string.un_texte)
.
strings.xml
👋 Pour la première fois, faisons-le ensemble
Via Android Studio bien évidemment. Et de préférence via l'éditeur XML
res/drawable/
R.drawable.nom_image
).Image(
painter = painterResource(R.drawable.nom_image),
contentDescription = "Une image",
modifier = Modifier.size(128.dp)
)
Placer l'image dans le dossier res/drawable/
. Puis ajouter le au-dessus de votre Text
qui est actuellement au centre de votre Column
.
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.playmoweb.demo.dmocourseseo">
<!-- Nouvelles permissions permettant de scanner en BLE Android après 11 -->
<uses-permission android:name="android.permission.BLUETOOTH_SCAN"
android:usesPermissionFlags="neverForLocation"
tools:targetApi="s" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<!-- Ancienne permission pour permettre l'usage du BLE Android avant 11 inclus -->
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
res
en détailR.drawable.…
)R.layout.…
)res
en détail 2Chaque dossier peut être redéfini en fonction de la résolution. (-hdpi
, -mdpi
, -xxhdpi
…)
Chaque dossier peut être redéfini en fonction… De la langue (values-fr/strings.xml
), de la configuration du « mobile », du thème sombre…
Pratiquons ensemble les ressources alternatives. En testant de dynamiser nos textes sans modifier le code.
Plusieurs solutions (Toast, Snackbar, Dialog)
Ajouter un Toast dans votre interface
// Récupération du context
val context = LocalContext.current
Toast.makeText(context, "Je suis un Toast", Toast.LENGTH_LONG).show();
⚠️ Avec la complétion d'Android Studio.
[https://developer.android.com/develop/ui/compose/components/snackbar]
Snackbar.LENGTH_SHORT
Snackbar.LENGTH_LONG
Snackbar.LENGTH_INDEFINITE
Un callback est une fonction qui est passée en paramètre d'une autre fonction.
fun doSomething(callback: () -> Unit) {
callback()
}
Fonctionne dans le code, mais également dans vos composants Compose.
@Composable
fun MyButton(onClick: () -> Unit) {
Button(onClick = onClick) {
Text("Cliquez ici")
}
}
onClick
: Clic sur un composant de type Button
.Modifier.clickable
: Clic sur tous les composants.Button(onClick = { /* Action */ }) {
Text("Cliquez ici")
}
L'action est déclenchée lors du clic sur le bouton. Simple, non ?
Si vous souhaitez par exemple afficher un Toast lors du clic sur un bouton :
val context = LocalContext.current
Button(onClick = {
Toast.makeText(context, "Clic sur le bouton", Toast.LENGTH_LONG).show()
}) {
Text("Cliquez ici")
}
Où sur une Image
:
val context = LocalContext.current
Image(
painter = painterResource(R.drawable.nom_image),
contentDescription = "Une image",
modifier = Modifier.size(128.dp).clickable {
Toast.makeText(context, "Clic sur l'image", Toast.LENGTH_LONG).show()
}
)
Ajouter une interaction sur votre bouton pour afficher un Toast.
Interaction « complexe » avec l'utilisateur (Choix…)
val context = LocalContext.current
AlertDialog(
onDismissRequest = { /* Action */ },
title = { Text("Titre") },
text = { Text("Contenu") },
confirmButton = {
Button(
onClick = {
Toast.makeText(context, "Clic sur le bouton", Toast.LENGTH_LONG).show()
}
) {
Text("Confirmer")
}
},
dismissButton = {
Button(
onClick = {
Toast.makeText(context, "Clic sur le bouton", Toast.LENGTH_LONG).show()
}
) {
Text("Annuler")
}
}
)
Ok mais… mais comment « déclencher » ce Dialog ?
var showDialog by remember { mutableStateOf(false) }
if (showDialog) {
// Afficher le Dialog
}
Button(onClick = { showDialog = true }) {
Text("Afficher le Dialog")
}
showDialog est un état qui permet de savoir si le Dialog doit être affiché ou non. Il est mutable, car il peut changer.
Ensemble « de règles » / de bonne pratique pour avoir des interfaces de qualités ou cohérentes.
Principe du Router en Web.
val navController = rememberNavController()
NavHost(
modifier = Modifier.padding(innerPadding),
navController = navController,
startDestination = "screen1"
) {
// Une page simple sans paramètre
composable("screen1") { Screen1(navController) }
// Une page avec un paramètre (ici un nom)
composable(
route = "screen2/{name}",
arguments = listOf(navArgument("name") { type = NavType.StringType })
) { backStackEntry -> Screen2(
navController,
name = backStackEntry.arguments?.getString("name") ?: ""
)
}
}
build.gradle
implementation("androidx.navigation:navigation-compose:2.7.7")
⚠️ N'oubliez pas de Sync
votre projet.
@Composable
fun Screen1(navController: NavController) {
Column {
Button(onClick = { navController.navigate("screen2/Valentin") }) {
Text("Bonjour Valentin")
}
}
}
@Composable
fun Screen2(navController: NavController, name: String) {
Column {
Text("Bonjour $name")
Button(onClick = { navController.popBackStack() }) {
Text("Retour")
}
}
}
ui/
: Les pages.home.kt
: La page d'accueil, logo + deux boutons.screen1.kt
: La première page.screen2.kt
: La seconde page.Screen 2 doit afficher le nom passé en paramètre.
Voir le support de cours
Le Scaffold est un composant qui permet de créer une structure de base pour votre application (barre de navigation, etc.).
Scaffold(
topBar = {
TopAppBar(
title = { Text("Ma liste") },
navigationIcon = {
IconButton(onClick = { navController.popBackStack() }) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = "Back"
)
}
})
}
) { innerPadding ->
Column(modifier = Modifier.padding(innerPadding)) {
// Contenu de la page
}
}
Mettre en place un Scaffold dans votre Screen1
et Screen2
.
« Jouons » avec les données…
Il faut comprendre ici que notre vue sera « recomposée » à chaque fois que nous allons mettre à jours nos données.
class Screen3ViewModel : ViewModel() {
// Liste de String.
// MutableStateFlow est un Flow qui peut être modifié.
val listFlow = MutableStateFlow(listOf<String>())
// Ajouter un élément
fun addElement(element: String) {
listFlow.value += element
}
// Supprimer un élément
fun removeElement(element: String) {
listFlow.value -= element
}
// Vider la liste
fun clearList() {
listFlow.value = emptyList()
}
}
@Composable
fun Screen3(
navController: NavController,
name: String,
viewModel: Screen2ViewModel = viewModel()
) {
// Liste dynamique de String
val list by viewModel.listFlow.collectAsStateWithLifecycle()
Column {
Text("Bonjour $name")
Button(onClick = { viewModel.addElement("Un élément") }) {
Text("Ajouter un élément")
}
Spacer(modifier = Modifier.weight(1f))
LazyColumn(modifier = Modifier.fillMaxSize()) {
items(list) { item ->
Text(item)
}
}
}
}
Flow
est un flux de données asynchrone.// Dans le ViewModel
val listFlow = MutableStateFlow(listOf<String>())
listFlow.value += "Un élément"
// Ou
val intFlow = MutableStateFlow(0)
intFlow.value += 1
// Dans le composant
val list by viewModel.listFlow.collectAsStateWithLifecycle()
.value = …
.collectAsStateWithLifecycle
.Screen3
.Screen3
.Android c'est très ouvert… Il faut s'imposer une organisation.
ui/
(pour l'interface)data/
(pour les données)remote/
(pour la partie accès au API http)Bien évidemment c'est un exemple
(Vous pouvez faire autrement…)
Adapters
)Vérifier l'organisation de votre projet initial.
Maintenant que nous avons une organisation, nous allons découper plus finement.
Plutôt qu'une simple liste de String, nous allons créer un élément de liste que nous allons répéter.
Card
qui contient un Titre, un Sous-Titre et une icône.Card
est répété pour chaque élément de la liste.Card
est cliquable.@Composable
fun ElementList(
title: String = "Mon titre",
content: String = "Mon contenu",
image: Int? = R.drawable.ic_launcher_foreground,
onClick: () -> Unit = {}
) {
Card(modifier = Modifier.fillMaxWidth().padding(5.dp), onClick = onClick) {
Row(modifier = Modifier.padding(5.dp), verticalAlignment = Alignment.CenterVertically) {
image?.let {
Image(modifier = Modifier.height(50.dp), painter = painterResource(id = it), contentDescription = content)
}
Column() {
Text(text = title)
Text(text = content, fontWeight = FontWeight.Light, fontSize = 10.sp)
}
}
}
}
ElementList
.Screen3
.Nous allons créer un composant MyScaffold
qui va nous permettre de créer un Scaffold avec une barre de navigation et un contenu.
Créer un composant générique qui contient un Scaffold avec une barre de navigation et un contenu.
(Voir support)
BLUETOOTH
: Permet d'activer le Bluetooth.BLUETOOTH_ADMIN
: Permet de gérer le Bluetooth.ACCESS_COARSE_LOCATION
: Permet d'accéder à la localisation approximative.ACCESS_FINE_LOCATION
: Permet d'accéder à la localisation précise.BLUETOOTH_SCAN
: Permet de scanner en BLE.BLUETOOTH_CONNECT
: Permet de se connecter en BLE.À votre avis, pourquoi ces permissions sont-elles nécessaires ?
Accompanist
pour les demandes de permissions.La suite dans le support…
Room
par exemple)int
, string
, …)Des questions ?