Prérequis: Terminez totalement le TP précédent
app/build.gradle
: on aura besoin de la lib de base et et de l'extension spécifique pour Jetpack ComposeImageView
qui affichera l'avatar de l'utilisateur dans le layout de la liste (à coté de votre TextView
par ex)onResume
, récupérez une référence à cette vue puis utilisez Coil pour afficher une image en passant une URL de votre choix, par exemple:imageView.load("https://goo.gl/gEgYUd")
user
UserActivity
et ajoutez la dans le manifestCompose
:setContent {
var bitmap: Bitmap? by remember { mutableStateOf(null) }
var uri: Uri? by remember { mutableStateOf(null) }
Column {
AsyncImage(
modifier = Modifier.fillMaxHeight(.2f),
model = bitmap ?: uri,
contentDescription = null
)
Button(
onClick = {},
content = { Text("Take picture") }
)
Button(
onClick = {},
content = { Text("Pick photo") }
)
}
}
Activity
quand on clique sur l'ImageView
du premier écran (pas besoin de launcher ici car on attends pas de résultat: startActivity(intent)
)TakePicturePreview
qui retourne un Bitmap
(c'est à dire une image stockée dans une variable, pas dans un fichier, donc en qualité limitée):val takePicture = rememberLauncherForActivityResult(TakePicturePreview()) {
bitmap = it
}
UserWebService
, ajouter une nouvelle méthode (cette route n'est pas documentée à ma connaissance donc c'est cadeau):@Multipart
@POST("sync/v9/update_avatar")
suspend fun updateAvatar(@Part avatar: MultipartBody.Part): Response<User>
MultipartBody.Part
qui permet d'envoyer des fichiers en HTTP:private fun Bitmap.toRequestBody(): MultipartBody.Part {
val tmpFile = File.createTempFile("avatar", "jpg")
tmpFile.outputStream().use { // *use*: open et close automatiquement
this.compress(Bitmap.CompressFormat.JPEG, 100, it) // *this* est le bitmap ici
}
return MultipartBody.Part.createFormData(
name = "avatar",
filename = "avatar.jpg",
body = tmpFile.readBytes().toRequestBody()
)
}
takePicture
, envoyez l'image au serveur avec updateAvatar
, en convertissant bitmap
avec toRequestBody
avantonResume
de TaskListFragment
, afficher l'avatar renvoyé depuis le serveur afin de le voir sur l'écran principal:imageView.load(user.avatar) {
error(R.drawable.ic_launcher_background) // image par défaut en cas d'erreur
}
On va maintenant permettre à l'utilisateur d'uploader une image enregistrée sur son téléphone
Pour simplifier on utilisera PhotoPicker
private fun Uri.toRequestBody(context: Context): MultipartBody.Part {
val fileInputStream = context.contentResolver.openInputStream(this)!!
val fileBody = fileInputStream.readBytes().toRequestBody()
return MultipartBody.Part.createFormData(
name = "avatar",
filename = "avatar.jpg",
body = fileBody
)
}
Jusqu'ici, vous avez probablement utilisé un Android en version 10 ou plus récente: la gestion de l'accès aux fichiers est simplifiée tant qu'on utilise les dossiers partagés (Images, Videos, etc) Mais pour gérer les versions plus anciennes, il faut demander la permission READ_EXTERNAL_STORAGE
avant d'accéder au fichiers:
Manifest.xml
:<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
launcher
avec le contrat RequestPermission()
Manifest.permission.READ_EXTERNAL_STORAGE
) et dans sa callback, utilisez le launcher précédentif (Build.VERSION.SDK_INT >= 29) { ... }
Actuellement, la qualité d'image récupérée de l'appareil photo est très faible (car passée en Bitmap
dans la RAM), pour changer cela il faut utiliser le contrat TakePicture
qui écrit dans un fichier passé au launcher par une Uri
// propriété: une URI dans le dossier partagé "Images"
private val captureUri by lazy {
contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, ContentValues())
}
// launcher
val takePicture = rememberLauncherForActivityResult(TakePicture()) { success ->
if (success) uri = captureUri
// compléter
}
// utilisation
takePicture.launch(captureUri)
UserViewModel
: il ne doit plus y avoir de calls API directement dans UserActivity
UserActivity
, permettre d'afficher et éditer le nom d'utilisateurdata class
afin de serializer le body de la requête correctement pour l'API et d'ajouter une méthode à UserWebService
:@POST("sync/v9/sync")
suspend fun update(@Body userUpdate: UserUpdate): Response<Unit>
User
: il faut créer un objet Commands
de type user_update
private fun pickPhotoWithPermission() {
val storagePermission = Manifest.permission.READ_EXTERNAL_STORAGE
val permissionStatus = checkSelfPermission(storagePermission)
val isAlreadyAccepted = permissionStatus == PackageManager.PERMISSION_GRANTED
val isExplanationNeeded = shouldShowRequestPermissionRationale(storagePermission)
when {
isAlreadyAccepted -> // lancer l'action souhaitée
isExplanationNeeded -> // afficher une explication
else -> // lancer la demande de permission et afficher une explication en cas de refus
}
}
private fun showMessage(message: String) {
Snackbar.make(findViewById(android.R.id.content), message, Snackbar.LENGTH_LONG).show()
}
Pour faire encore mieux, vous pouvez aussi afficher un message avec AlertDialog en Compose et continuer le flow en fonction de la réponse de l'utilisateur.
Permettez à l'utilisateur de faire un export texte de ses tâches dans un backup.csv
, contenant chaque tâche ligne par ligne et s'enregistrera dans les fichiers du téléphone.
Permettez à l'utilisateur de créer des tâches depuis un fichier .csv
.
// revoir toute la fin: demander les permissions pour l'export, pas pour les photos