imageView.load("https://goo.gl/gEgYUd")
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") }
        )
    }
}
val takePicture = rememberLauncherForActivityResult(TakePicturePreview()) {
    bitmap = it
}
@Multipart
@POST("sync/v9/update_avatar")
suspend fun updateAvatar(@Part avatar: MultipartBody.Part): Response<User>
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()
    )
}
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(): MultipartBody.Part {
    val fileInputStream = 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:

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

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 = capturedUri
}

// utilisation
takePicture.launch(captureUri)
@PATCH("sync/v9/sync")
suspend fun update(@Body userUpdate: UserUpdate): Response<Unit>
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