En cours de rédaction
Cours Google sur Kotlin avec Android: lien
Classes
Constructeur
Il y a 2 types de constructeurs : les primaires et secondaires :
class Heure_composant {
// mettre en pause le retour de l'heure
var Paused:Boolean = false
// Constructeur primaire
init {
println ("Constructeur primaire : Je me lance ...")
}
// Constructeur secondaire
constructor() {
println ("Constructeur Secondaire : je me lance ...")
}
}
Instenciation
val maj_heure_composant:Heure_composant = Heure_composant();
Héritage
Par défaut une classe est de type « final ». Pour qu’elle soit héritable il faut le spécifier avec le mot clef « open » devant « class »
open class Mother{
var name:String = "Mother"
}
class Daughter:Mother(){
var address:String = ""
}
Singleton
Très simple sous kotlin, il suffit d’utiliser le mot clef « objet » en lieu et place de « class » :
object mon_singleton {
var valeur = ""
}
fun main() {
val objet1:mon_singleton = mon_singleton
val objet2:mon_singleton = mon_singleton
println("objet1 : $objet1")
println("objet2 : $objet2")
objet1.valeur = "test singleton"
println("objet1 : ${objet1.valeur}")
println("objet2 : ${objet2.valeur}")
}
Résultat :
objet1 : mon_singleton@378fd1ac objet2 : mon_singleton@378fd1ac objet1 : test singleton objet2 : test singleton
Pas de constructeur mais utilisation de init possible en utilisant ‘object’
Si constructeur, code plus verbeux, exemple :
class MySingleton private constructor(private val param: String) {
companion object {
@Volatile
private var INSTANCE: MySingleton? = null
@Synchronized
fun getInstance(param: String): MySingleton = INSTANCE ?: MySingleton(param).also { INSTANCE = it }
}
fun printParam() {
print("Param: $param")
}
}
utilisation :
MySingleton.getInstance("something").printParam()
Fonctions
Récapitulatif sur la déclaration des fonctions, les paramètres, etc.
Déclaration
Simple fonction sans retour de paramètres et prenant une valeur en paramètre
fun ma_fonction(valeur1:String) {
println ("valeur1 : $valeur1")
}
ma_fonction("test")
Simple fonction avec valeur par defaut pour un paramètre :
fun ma_fonction(valeur1:String="") {
println ("valeur1 : $valeur1")
}
ma_fonction()
Principe des paramètres nommés quand la fonction est appelées :
fun ma_fonction(valeur1:String="", valeur2:String="") {
println ("valeur1 : $valeur1")
println ("valeur2 : $valeur2")
}
ma_fonction("val1","val2") // utilisation initiale comme en JAVA
ma_fonction() // comme les paramètres ont une valeur par défaut --> pas besoin de spécifier une valeur
ma_fonction(valeur2 = "val2") // utilisation via "paramètre nommé"
Fonction avec retour :
fun ma_fonction(valeur1:String="", valeur2:String=""): String {
// ...
return "$valeur1 --> $valeur2"
}
val val_finale:String = ma_fonction()
val val_finale2:String = ma_fonction(valeur1 = "val1", valeur2 = "val2")
Fonction en paramètre
fun ma_fonction(callback:()->Unit) {
println ("calculs ...")
callback()
println("fin")
}
// Utilisation
ma_fonction(callback = {
println (" intérieur callback")
})
// raccourci (possible car un seul paramètre attendant une fonction)
ma_fonction { println("intérieur callback") }
Fonction prenant une fonction en paramètre de manière optionnelle + surchage de fonction :
// fonction avec callback fonctionnel
fun ma_fonction( callbacks: (()->Unit)? = null ): String {
// ...
return ""
}
// fonction surcharge
fun ma_fonction (valeur1:String):String {
// ...
return ""
}
// Utilisation de la fonction de différentes manières
ma_fonction {
println ("callback ...")
}
ma_fonction()
ma_fonction(valeur1 = "test")
Lancer une activité et passage de paramètres
Sur première activité :
// Fetching the Local Context
val mContext = LocalContext.current
// déclaration intent
val intent = Intent(mContext,MainActivity2::class.java)
// ajout paramètre
intent.putExtra("valeur1","Valeur de passage")
// Lance l'activité
mContext.startActivity(intent)
Sur deuxième activité :
// récupération du contexte
val context = LocalContext.current
// récupération de l'intent de l'appli en cours
val intent = (context as MainActivity2).intent
// récupération de la donnée
val valeur:String? = intent.getStringExtra("valeur1")
Log
Utilisation de la classe utilitaire LOG pour éviter les println() …
fun code_click() {
Log.i("code_click","début code")
Log.d("code_click","debug 1")
Log.d("code_click","debug 2")
Log.w("code_click","proche manque ressource")
Log.e("code_click","Erreur ...")
Log.i("code_click","fin code")
}
@Composable
fun App_main13() {
Surface() {
Column() {
Row() {
Text(text = "Titre")
}
Row() {
Button(onClick = {
Log.i(TAG,"Click bouton")
Log.i("MonTAG","Click bouton")
code_click()
}) {
Text(text = "Cliquer moi pour log")
}
}
}
}
}
Résultat :
2022-06-27 16:25:05.915 17056-17069/com.example.test_compose7_navigation W/System: A resource failed to call close. 2022-06-27 16:25:25.894 17056-17056/com.example.test_compose7_navigation I/ContentValues: Click bouton 2022-06-27 16:25:25.894 17056-17056/com.example.test_compose7_navigation I/MonTAG: Click bouton 2022-06-27 16:25:25.894 17056-17056/com.example.test_compose7_navigation I/code_click: début code 2022-06-27 16:25:25.894 17056-17056/com.example.test_compose7_navigation D/code_click: debug 1 2022-06-27 16:25:25.894 17056-17056/com.example.test_compose7_navigation D/code_click: debug 2 2022-06-27 16:25:25.894 17056-17056/com.example.test_compose7_navigation W/code_click: proche manque ressource 2022-06-27 16:25:25.894 17056-17056/com.example.test_compose7_navigation E/code_click: Erreur ... 2022-06-27 16:25:25.894 17056-17056/com.example.test_compose7_navigation I/code_click: fin code
Liens :
- page officielle : https://developer.android.com/reference/kotlin/android/util/Log
- désactiver certains log en version release : https://thanhtungvo.medium.com/disable-debug-log-on-release-android-6a7f3ab86126
Thread, coroutine, channel et autre
Ressources :
- lien : https://kotlinlang.org/docs/coroutines-guide.html
- lien : https://www.baeldung.com/kotlin/threads-coroutines
- lien : https://borntocode.fr/android-introduction-aux-coroutines-de-kotlin/
- lien : https://www.baeldung.com/kotlin/channels
Le principe des Threads est toujours présent comme dans Java
val mon_thread = Thread{
println("Hello")
Thread.sleep(5000)
println("World")
}
mon_thread.start()
Une autre implémentation sous Kotlin est possible :
val mon_thread2 = thread(start = true) {
println("Hello2")
Thread.sleep(5000)
println("World2")
}
Le thread va s’executer directement. Bien que cette écriture soit concise, dans l’EDI Android, la variable « mon_thread2 » est marquée comme non utilisée alors que si en fait …

Mais il existe en plus de principe de coroutines, préconisé par KOTLIN, plus sûr et demandant moins de ressouces mémoire :
runBlocking {
val job = launch{
println("${Thread.currentThread()} has run.")
delay(5000)
println("Hello world")
}
}
Dans cet exemple, le code appelant sera en attente que les lignes ci-dessus soit entièrement executées … ce qui ne correspondant pas à l’effet du code avec les Threads.
Il faut donc passer via Async :
val tache = GlobalScope.async {
println("Hello3")
delay(5000)
println("World3")
}
runBlocking {
val job = launch {
tache
}
}
et pour faire en sorte que le processus parent attende :
val tache = GlobalScope.async () {
println("Hello3")
delay(5000)
println("World3")
return@async "traitement fini"
}
runBlocking {
val res = tache.await()
println("resultat : ${res}")
}
Exemple plus complet alliant coroutine, channel et mise à jour de composant graphique :
Un channel permet de discuter entre coroutine. C’est par ce procédé que la mise à jour de l’heure est « mise en pause » au sein de la coroutine « MAJ_heure » (pas le job qui tourne tout le temps, mais juste la transmission de l’information de l’heure au sein avant appel du callback)
// Permet de communiquer entre job
val channel = Channel<String>()
// Fonction qui va envoyer l'heure via callable
suspend fun MAJ_heure(callbacks_heure: ((heure:String) -> Unit)?) {
var paused:Boolean = false
// boucle sans fin (pas bien :) )
while (true) {
// channel pas vide ?
if (!channel.isEmpty) {
val va_tmp = channel.receive()
// logique de pause
if (va_tmp=="pause_paspause") { paused = !paused}
if (va_tmp=="1") { paused = true}
if (va_tmp=="0") { paused = false}
}
// un petit délai de pause
delay(50)
// heure en string
val cal = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
Calendar.getInstance()
} else {
TODO("VERSION.SDK_INT < N")
}
val heure = cal.get(Calendar.HOUR_OF_DAY)
val minute = cal.get(Calendar.MINUTE)
val seconde = cal.get(Calendar.SECOND)
val milliseconde = cal.get(Calendar.MILLISECOND)
// si callback défini
if (callbacks_heure != null) {
// pas en pause
if (paused==false) {
// envoi heure au callback
callbacks_heure("$heure : $minute : $seconde : $milliseconde")
}
}
}
}
@SuppressLint("CoroutineCreationDuringComposition")
@Composable
fun App_main10() {
var heure:String by remember { mutableStateOf("12:00:00")}
// lance actualisation variable heure
val job = GlobalScope.launch {
var maj_heure2 = MAJ_heure {
try{
heure=it
} catch (e:Exception) {
}
}
}
// Affichage global
Surface() {
Column(modifier = Modifier.fillMaxWidth().padding(10.dp)) {
Row{
Text ("Bienvenue sur l'appli qui tue \nElle va vous donner l'heure ...")
}
Spacer(modifier = Modifier.height(10.dp))
Row() {
Text (" --> Heure : $heure ")
}
Spacer(modifier = Modifier.height(10.dp))
Row() {
Button(
onClick = {
GlobalScope.launch {
channel.send("pause_paspause")
}
}) {
Text("Pause / pas pause Affichage")
}
}
}
}
}
Ce qui donne :

Autre solution plus simple et sans passer par un channel pour mettre en pause l’affichage de l’heure :
Création d’une classe dédiée :
package com.example.test_compose7_navigation.Classes
import android.icu.util.Calendar
import android.os.Build
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
class Heure_composant ( var callbacks_heure:(heure:String)->Unit={}) {
var Paused:Boolean = false // pour mettre en pause le retour de l'heure
init {
Demmarer()
}
// lance job qui donner l'heure via le callback
fun Demmarer() {
val job = GlobalScope.launch {
// boucle sans fin (pas bien :) )
while (true) {
// un petit délai de pause
delay(50)
// heure en string
val cal = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
Calendar.getInstance()
} else {
TODO("VERSION.SDK_INT < N")
}
val heure = cal.get(Calendar.HOUR_OF_DAY)
val minute = cal.get(Calendar.MINUTE)
val seconde = cal.get(Calendar.SECOND)
val milliseconde = cal.get(Calendar.MILLISECOND)
// Affichage en oause ?
if (!Paused) {
// envoi heure au callback
callbacks_heure("$heure : $minute : $seconde : $milliseconde")
}
}
}
}
fun Pause_pas_pause() {
Paused=!Paused
}
}
Utilisation de la classe dans le « composable »
@Composable
fun App_main10() {
var heure:String by remember { mutableStateOf("12:00:00")}
// lance actualisation variable heure
val maj_heure_composant:Heure_composant = Heure_composant() {
try {
heure=it
} catch (e:Exception) {
}
}
// Affichage global
Surface() {
Column(modifier = Modifier.fillMaxWidth().padding(10.dp)) {
Row{
Text ("Bienvenue sur l'appli qui tue \nElle va vous donner l'heure ...")
}
Spacer(modifier = Modifier.height(10.dp))
Row() {
Text (" --> Heure : $heure ")
}
Spacer(modifier = Modifier.height(10.dp))
Row() {
Button(
onClick = {
maj_heure_composant.Pause_pas_pause()
}) {
Text("Pause / pas pause Affichage")
}
}
}
}
}
Compose : récupérer le contexte de l’application
val context = LocalContext.current
Compose : mise en forme générale
Utiliser les composables
- Column
- Row
- Spacer
Column(
modifier = Modifier
.border(width = 2.dp, color = Color(0xFF0000FF))
.padding(5.dp)
) {
Row( modifier = Modifier
.border(width = 1.dp, color = Color(0xFF00FF00))
.padding(2.dp)) {
Text(text = "AAA", modifier = Modifier.border(width = 1.dp, color = Color(0xFFD1AA4F)))
Text(text = "BBB")
}
Row(modifier = Modifier
.border(width = 1.dp, color = Color(0xFF00FF00))
.padding(2.dp)) {
Text(text = "CCC")
Text(text = "DDD")
Column(modifier = Modifier
.border(width = 1.dp, color = Color(0xFF0000FF))
.padding(2.dp)) {
Text(text = "EEEE")
Spacer(modifier = Modifier.height(5.dp).background(Color(0xFF676767)))
Text(text = "FFFF")
}
}
Row (modifier = Modifier
.border(width = 1.dp, color = Color(0xFF00FF00))
.padding(2.dp)){
Text(text = "GGG")
}
}
Ce qui donne

Compose : LazyColumn ou LazyRow (et Item, Divider)
Équivalent du recylcerView
Ressources :
var liste_texte = mutableListOf<String>("")
for (i in 1..1000) {
liste_texte.add("texte n°$i")
}
Surface() {
LazyColumn() {
for ((index,value) in liste_texte.withIndex()) {
if ( index % 9 == 1 ) {
item {
Spacer(modifier = Modifier
.background(Color(0x467C9261))
.fillMaxWidth()
.height(10.dp)
)
Divider()
}
}
item {
Text(text = value)
Divider()
}
}
}
}
Ce qui donne :

Compose : Card
Référence :
Surface( modifier = Modifier.fillMaxWidth()) {
Column( modifier = Modifier.padding(10.dp)) {
Card(elevation = 10.dp, modifier = Modifier.padding(4.dp)) {
Row( verticalAlignment = Alignment.CenterVertically) {
Icon(imageVector = Icons.Default.Info, contentDescription = "", modifier = Modifier.padding(4.dp))
Text ("Mon Card", modifier = Modifier.padding(8.dp))
}
}
}
}
Ce qui donne :

Compose : SnackBarHost (équivalent à Toast)
Cas n°1 (sans personnalisation de la bulle)
val scope = rememberCoroutineScope()
var snackbarHostState by remember {mutableStateOf(SnackbarHostState())}
Surface() {
Column(modifier = Modifier
.fillMaxWidth()
.padding(10.dp)) {
Button(onClick = { /*TODO*/
scope.launch {
snackbarHostState.showSnackbar("Message vers utilisateur")
}
}
) {
Text(text = "Cliquer moi pour un snack ...")
}
}
SnackbarHost(hostState = snackbarHostState)
}
Ce qui donne :

Cas n°2 :
val scope = rememberCoroutineScope()
var snackbarHostState by remember {mutableStateOf(SnackbarHostState())}
Surface() {
Column(modifier = Modifier
.padding(10.dp))
{
Button(onClick = {
// on lance le snackbar
scope.launch {
snackbarHostState.showSnackbar("", duration = SnackbarDuration.Short)
}
}) {
Text(text = "Cliquer moi pour un snack ...")
}
}
}
SnackbarHost(
hostState = snackbarHostState) {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier.fillMaxSize()
) {
Card(
shape = RoundedCornerShape(8.dp),
border = BorderStroke(2.dp, Color.White),
modifier = Modifier.padding(10.dp),
elevation = 10.dp
) {
Row(
modifier = Modifier.padding(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(text = "Message bulle pour utilisateur")
Spacer(modifier = Modifier.width(10.dp))
Button(onClick =
{ // on force l'arret du snackBar ...
snackbarHostState.currentSnackbarData?.dismiss()
}) {
Text ("OK !")
}
}
}
}
}
Ce qui donne :

Compose : EditText
Ressources :
- https://www.goodrequest.com/blog/jetpack-compose-basics-text-input
- https://material.io/components/text-fields
Les champs d’édition sont :
- TextField
- OutlinedTextField
Exemple :
var text by remember { mutableStateOf("")}
var is_error by remember {mutableStateOf(false)}
var image_vector_error by remember { mutableStateOf(Icons.Default.Check)}
OutlinedTextField(
value = text,
// permet d'éditer ...
onValueChange = {
// MAJ texte
text = it
// Détecte une erreur ...
is_error = if (text.equals("erreur")) true else false
if (is_error) {
image_vector_error = Icons.Default.Warning
} else {
image_vector_error = Icons.Default.Check
}
},
// Titre du champ
label = {Text("Nom")},
isError = is_error,
modifier = Modifier.padding( top =10.dp, start = 10.dp, bottom = 4.dp),
// Texte complémenataire si pas de texte saii
placeholder = {
Text(text = "Saisir votre nom ...")
},
// Composant avant Texte (Image, texte, autres...)
leadingIcon = { Icon(imageVector = Icons.Default.Info, contentDescription = "") },
// Composant après texte (Image, texte, autres...)
trailingIcon = { Icon(imageVector = image_vector_error, contentDescription = "") },
)
// si erreur --> affiche texte explication en dessous du TextField
if (is_error) {
Text(
text = "Erreur dans la saisie ...",
color = MaterialTheme.colors.error,
style = MaterialTheme.typography.caption,
modifier = Modifier.padding(start = 20.dp)
)
}
Ce qui donne :




Compose : bouton (Button, OutlinedButton, TextButton)
3 types de bouton d’après material (v2)
Column(modifier = Modifier.padding(10.dp)) {
Row() {
Button(onClick = { /*TODO*/ }) {
Text("Boutton")
}
Spacer (modifier= Modifier.width(10.dp) )
Button(onClick = { /*TODO*/ }) {
Text("Boutton")
Icon(imageVector = Icons.Default.Close, contentDescription = "",
modifier = Modifier.padding(start = 4.dp))
}
}
OutlinedButton(onClick = { /*TODO*/ }) {
Text("Outlined Boutton")
}
TextButton(onClick = { /*TODO*/ }) {
Text("Texte Boutton")
}
}
ce qui donne :

Compose : TopAppBar
2 implémentation possibles.
Exemple avec une
Scaffold(
topBar = {
TopAppBar(
title = {
Text("Discute avec ta banque")
},
navigationIcon = {
IconButton(onClick = { /*TODO*/ }) {
Icon(Icons.Default.ArrowBack,"")
}
},
actions = {
IconButton(onClick = { /*TODO*/ }) {
Icon(Icons.Default.Search,"")
}
IconButton(onClick = { /*TODO*/ }) {
Icon(Icons.Default.List,"")
}
}
)
}
) {
Column {
LazyColumn() {
for (i in 1..40) {
item {
Text ("Contenu appli $i.")
}
}
}
}
}
Ce qui donne :

Compose : AlertDialog
2 implémentations …
code avec une :
var afficher_dialogue by remember {mutableStateOf(false)}
Column() {
Button(onClick = {
afficher_dialogue = true
}) {
Text("AlertDIalog")
}
if (afficher_dialogue) {
AlertDialog(
onDismissRequest =
{ //Quand on clique en dehors de la boite de dialogue
// Permet de gérer le modal ou pas ...
afficher_dialogue = false
},
title = {
Text(text = "Bienvenue")
},
text = {
Text("Bienvenue sur la nouvelle boite dialogue")
},
confirmButton = {
Button(onClick = { afficher_dialogue = false },) {
Text("OK")
}
},
dismissButton = {
OutlinedButton(onClick = { /*TODO*/ },) {
Text("Annuler")
}
}
)
}
}
Ce qui donne :

Compose : Ressource Texte, Image, Couleur
@Composable
fun App_main12() {
Surface() {
Column() {
Row() {
val ressources = LocalContext.current.resources
Image(
painter = painterResource(id = R.drawable.img),
contentDescription = null,
modifier = Modifier.width(30.dp).padding(2.dp)
)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
Text(
text = ressources.getString(R.string.titre),
color = Color(ressources.getColor(R.color.couleur_titre,null))
)
} else {
Text(
text = ressources.getString(R.string.titre),
color = Color(ressources.getColor(R.color.couleur_titre))
)
}
}
}
}
}
Liens :
- présentation ANDROID : https://developer.android.com/jetpack/compose/resources
Compose : les états dans les éléments de type Composable
1 Faire au maximum des composants sans états (StateLess) :
- pour pouvoir tout re-initialiser si besoin
2 Utiliser un modelView le plus possible dans l’activité ce qui simplifiera grandement la gestion des composants et sous composants
liens utiles :
- https://www.composables.co/blog/state
- permet de comprendre pourquoi il faut faire du StateLess
- explique le bug de MAJ des BasicTextField
Android/Kotlin : écriture fichier
Premier cas simple (écriture fichier dans le répertoire privé de l’app)
Fonctions permettant la lecture ou l’écriture dans un fichier
fun ecrire_text(context:Context,personne:Personne, callback_texte:(texte_retour:String)->Unit={}) {
val fileoutputStream:FileOutputStream
val nom_fichier ="test.txt"
fileoutputStream = context.openFileOutput(nom_fichier, Context.MODE_PRIVATE)
fileoutputStream.write(personne.toString().toByteArray())
callback_texte("Ecriture fichier effctuée")
}
fun lire_text(context:Context, callback_texte:(texte_retour:String)->Unit={}) {
var fileInputStream:FileInputStream?=null
val nom_fichier ="test.txt"
// fichier existant !
val file_exist = context.getFileStreamPath(nom_fichier).exists() // va vérifier le fichier en private
if (file_exist) {
fileInputStream = context.openFileInput(nom_fichier)
var inputStreamReader: InputStreamReader = InputStreamReader(fileInputStream)
val bufferedReader: BufferedReader = BufferedReader(inputStreamReader)
val stringBuilder: StringBuilder = StringBuilder()
var text: String? = null
while ({ text = bufferedReader.readLine(); text }() != null) {
stringBuilder.append(text)
}
callback_texte(stringBuilder.toString())
} else {
callback_texte("Fichier inexistant")
}
}
Code compose associé :
@Composable
fun App_main11() {
var texte_resultat by remember{ mutableStateOf("")}
val context = LocalContext.current
val personne1 = Personne(
Id=1,
Nom = "lulu",
Premon = "lolo",
Age = 19
)
Surface {
Column(modifier = Modifier.padding(10.dp)) {
Row() {
Text(text = "Test Ecriture/Lecture Fichier")
}
Spacer(modifier = Modifier.height(10.dp))
Row() {
Button(onClick = {
//Ecroture fichier
ecrire_text(context,personne1) {
texte_resultat = it
}
}) {
Text(text = "Ecrire")
}
}
Spacer(modifier = Modifier.height(10.dp))
Row() {
Button(onClick = {
//lecture fichier
lire_text(context) {
texte_resultat=it
}
}) {
Text(text = "lire")
}
}
Spacer(modifier = Modifier.height(10.dp))
Row() {
Text(text = texte_resultat)
}
}
}
}
Ce qui donne :

Deuxième cas manipulant des listes d’objets + GSON (écriture fichier dans le répertoire privé de l’app)
fun ecrire_text(context:Context,personne:Personne, callback_texte:(texte_retour:String)->Unit={}) {
val fileoutputStream:FileOutputStream
val nom_fichier ="test.txt"
// Liste d'objet de type personne
var liste_personne:MutableList<Personne> = mutableListOf()
liste_personne.add(personne)
liste_personne.add(personne)
liste_personne.add(personne)
// liste d'objets en texte JSON
val json_string = Gson().toJson(liste_personne)
// enregistrement fichier (partie privée de l'appli)
fileoutputStream = context.openFileOutput(nom_fichier, Context.MODE_PRIVATE)
fileoutputStream.write(json_string.toByteArray())
callback_texte("Ecriture fichier effctuée\n $json_string")
fileoutputStream.close()
}
fun lire_text(context:Context, callback_texte:(texte_retour:String)->Unit={}) {
var fileInputStream:FileInputStream?=null
val nom_fichier ="test.txt"
val file_exist = context.getFileStreamPath(nom_fichier).exists()
// fichier existant ?
if (file_exist) {
// Récupération contenu fichier
fileInputStream = context.openFileInput(nom_fichier)
var inputStreamReader: InputStreamReader = InputStreamReader(fileInputStream)
val bufferedReader: BufferedReader = BufferedReader(inputStreamReader)
val stringBuilder: StringBuilder = StringBuilder()
var text: String? = null
while ({ text = bufferedReader.readLine(); text }() != null) {
stringBuilder.append(text)
}
callback_texte(stringBuilder.toString())
// passage de texte JSON en liste de personnes
val json_str:String = stringBuilder.toString()
val stype = object : TypeToken<List<Personne>> () {}.type
val liste_personne:List<Personne> = Gson().fromJson<List<Personne>>(json_str,stype)
println(json_str)
// Affichage liste
for (une_personne in liste_personne) {
println ("--> $une_personne")
}
} else {
callback_texte("Fichier inexistant")
}
}
Ce qui donne :

Troisième cas : écriture fichier dans répertoire externe
En Java, pour écrire sur un répertoire « public » comme « Mes documents » ou « Download » il était possible de le faire via « context.getExternalStoragePublicDirectory ».
Or en kotlin, cette fonction n’est pas disponible, voire depréciée depuis Android Q.
Nous allons voir comment créer un fichier et y insérer du contenu avant toute chose :
val nom_fichier = "fichier_externe.txt"
val path = context.getFilesDir().getAbsolutePath()
val fichier = File(path, nom_fichier)
println("path : " + fichier.path)
println("absoluteFile : " + fichier.absoluteFile)
println("absolutePath : " + fichier.absolutePath)
println("canonicalFile : " + fichier.canonicalFile)
println("canonicalPath : " + fichier.canonicalPath)
println("")
// création du fichier
val isNewFileCreated :Boolean = fichier.createNewFile()
println (" isNewFileCreated : $isNewFileCreated")
// fichier déja crée ?
if (!isNewFileCreated) {
// suppression du fichier
val fic_efface = fichier.delete()
println("fichier effacé : $fic_efface" )
}
println ("Fichier existant : " + fichier.exists())
println ("Re création du fichier : " + fichier.createNewFile())
println ("Possibilité d'écrire dans le fichier : " + fichier.canWrite())
// Ecriture via "PrintWriter"
println (" ")
println ("Ecriture via printWriter")
println ("taille fichier : " + fichier.length())
fichier.printWriter().use {
out-> out.println("test")
}
println ("taille fichier : " + fichier.length())
println ("Re création du fichier : " + fichier.createNewFile())
// Ecriture via "writeText
println (" ")
println ("Ecriture via writeText")
println ("taille fichier : " + fichier.length())
fichier.writeText("ma ligne de texte")
println ("taille fichier : " + fichier.length())
fichier.writeText("ma ligne de texte")
println ("taille fichier : " + fichier.length())
// Ecriture via "Files.write"
println (" ")
println ("Ecriture via Files.write ")
val contenu = " Du texte à GOGO de GOGO..."
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
Files.write(fichier.toPath(),contenu.toByteArray(), StandardOpenOption.WRITE)
}
println ("taille fichier : " + fichier.length())
Autre exemple plus court sauvegardant une liste d’objet en format JSON
// Liste d'objets de type personne
var liste_personne:MutableList<Personne> = mutableListOf()
liste_personne.add(personne)
liste_personne.add(personne)
liste_personne.add(personne)
// liste d'objets en texte JSON
val json_string = Gson().toJson(liste_personne)
val path = context.getFilesDir().getAbsolutePath()
val fichier = File(path, nom_fichier)
fichier.createNewFile()
fichier.writeBytes(json_string.toByteArray())
println ("taille fichier : " + fichier.length())
et le code permettant de lire le fichier créé :
val nom_fichier = "fichier_externe.txt"
val path = context.getFilesDir().getAbsolutePath()
val fichier = File(path, nom_fichier)
if (fichier.exists()) {
val texte_fichier = fichier.readText()
println("tmp : $texte_fichier")
// passage de texte JSON en liste de personnes
val stype = object : TypeToken<List<Personne>> () {}.type
val liste_personne:List<Personne> = Gson().fromJson<List<Personne>>(texte_fichier,stype)
// Affichage liste
for (une_personne in liste_personne) {
println ("--> $une_personne")
}
} else {
println("Fichier inexistant")
}
Prise en compte vers un répertoire externe :
/////////////////////////////////
// Ecriture dans Download
val chemin_download = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
println(" chemin_download : $chemin_download ")
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
//// valable API <= 28 (nécessite de définir les permissions de lecture dans AndroidManifest.xml)
val fichier2 = File(chemin_download, nom_fichier)
try {
fichier2.createNewFile()
fichier2.writeBytes(json_string.toByteArray())
} catch (e:Exception) {
}
} else {
//// valable API >= 29
// Création du fichier dans le stockage interne
// Via le resolver copie du fichier vers le répertoire externe
val une_url = URL(Uri.fromFile(fichier).toString())
val mime_type = "text/plain"
val contentValues = ContentValues().apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, nom_fichier)
put(MediaStore.MediaColumns.MIME_TYPE, mime_type)
put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS)
}
val resolver = context.contentResolver
val uri = resolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues)
if (uri != null) {
une_url.openStream().use { input ->
resolver.openOutputStream(uri).use { output ->
input.copyTo(output!!, DEFAULT_BUFFER_SIZE)
}
}
}
}
et pour lire le fichier
fun lire_text_externe(context:Context, callback_texte:(texte_retour:String)->Unit={}) {
val nom_fichier = "fichier_externe.txt"
val chemin_download = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
val fichier = File(chemin_download, nom_fichier)
var texte_fichier = ""
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
//// valable API <= 28 (nécessite de définir les permissions de lecture dans AndroidManifest.xml)
if (fichier.exists()) {
texte_fichier = fichier.readText()
} else {
println("Fichier inexistant")
}
} else {
//// valable API >= 29
// Via le contentResolver
val chemin_download = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
val fichier = File(chemin_download, nom_fichier)
val resolver = context.contentResolver
val inputStream:InputStream? = resolver.openInputStream(Uri.fromFile(fichier))
val reader = BufferedReader(inputStream?.reader())
texte_fichier = reader.readText()
}
if (!texte_fichier.equals("")) {
println("texte_fichier : $texte_fichier")
// passage de texte JSON en liste de personnes
val stype = object : TypeToken<List<Personne>> () {}.type
val liste_personne:List<Personne> = Gson().fromJson<List<Personne>>(texte_fichier,stype)
// Affichage liste
for (une_personne in liste_personne) {
println ("--> $une_personne")
}
} else {
println ("pas de contenu existant")
}
}
Ressources:
- les inputStream : https://www.baeldung.com/kotlin/inputstream-to-string
- utilisation du mediastore pour écrire : https://medium.com/@thuat26/how-to-save-file-to-external-storage-in-android-10-and-above-a644f9293df2
- les types mimes : https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types
Compose : Bannière PUB
A ce jour et par défaut il n’y a pas de composants avec Compose pour ajouter une bannière pub. Il faut ajouter manuellement une vue via AndroidView.
/**
* Composable dédié à la pub de l'application
* @param size Taille du bloc publicitaire
* @param id Numéro identification de l'appli pour la pub
*/
@Composable
fun BannerAds(size:AdSize, id:String) {
AndroidView(
factory = { context ->
// Creates custom view
AdView(context).apply {
this.setAdSize(size)
this.adUnitId = id
this.loadAd(AdRequest.Builder().build())
}
}
)
}
ce qui donne :

Resources :
- https://viede.dev/jetpack-compose-admob-banner-adview/
- https://blog.blundellapps.co.uk/using-admob-banner-ads-in-a-compose-layout/
Voir …
- voir les Composables ;
- leçons d’utilisation par google : lien
- les composants disponibles et manière de les programmer :
- la récupération des ressources iages, icônes, etc..
- les variables remember et rememberState
- les threads
- thread vs coroutines : lien
- le positionnement des composables (en dehors des fichiers « main »)
- le principe d’animation basique
- présentation google : lien
- l’intégration de composable tiers
- l’utilisation en kotlin de bibliothèques écrites en Java (graph et PDF)
- revoir comment déclarer une classe « singleton » ;la navigation entre les vues
- les accès aux fichiers
- les autorisations (Internet, accès disques, etc)
- les transformations objet vers JSON et inversement