
Contact Picker d'Android 17 avec Flutter : il est temps de remplacer READ_CONTACTS
- David Serrano Canales
- 19 octobre 2026
Table des matières
Android 17 apporte un changement important sur la lecture des contacts de l’appareil : il est désormais possible d’appeler un sélecteur système pour choisir des contacts et les importer dans notre app, sans avoir à déclarer la permission de lecture READ_CONTACTS dans notre manifeste.
De ce fait, Google a annoncé de nouvelles restrictions : la permission READ_CONTACTS est désormais réservée aux apps qui ne peuvent pas fonctionner avec le seul sélecteur système. Ce qui veut dire que, si ton app importe des contacts de façon simple, tu vas devoir utiliser le sélecteur. Si la permission t’est indispensable pour que l’app fonctionne, tu devras envoyer une déclaration dans la Play Console expliquant pourquoi le sélecteur ne te suffit pas. La politique entre en vigueur le 27 janvier 2027.
Cela dit, cela concerne les apps qui ciblent targetSdk 37 ou supérieur. Si la tienne cible encore une version antérieure, la politique ne t’affecte pas pour l’instant, même si elle finira par te rattraper dès que tu monteras la cible.
Dans cet article je vais essayer de te donner un guide pour comprendre ce que cela implique, en quoi cela va t’affecter, et si tu as vraiment besoin de continuer à utiliser la permission ou si le sélecteur système te suffit. Ensuite, je te donnerai quelques exemples de code pour te montrer comment j’ai moi-même implémenté le sélecteur système sur Android 17+ dans mes propres apps.
Info
📽 Version vidéo disponible sur YouTube
La nouvelle politique sur les contacts de Google Play concerne-t-elle ton app ?
La plupart des apps qui accèdent aux contacts vont continuer à fonctionner parfaitement avec le sélecteur système, sans avoir besoin de déclarer la permission READ_CONTACTS.
Si ton application accède aux contacts de façon ponctuelle, par exemple pour les importer dans l’app, ou peut-être pour récupérer une donnée précise sur un contact à un moment donné, alors le sélecteur système est largement suffisant.
La différence, c’est la portée : avec la permission tu lisais tout le carnet d’adresses, alors qu’avec le sélecteur tu ne reçois que les contacts choisis par l’utilisateur, et en plus tu peux ne demander que les champs qui t’intéressent. L’accès est aussi temporaire, il expire dès que tu as fini de lire la sélection.
Prenons comme exemple mon application Contrôle de la Dette. Une app où tu crées des contacts, et où tu leur affectes des dettes et des paiements. Les contacts peuvent être créés en interne, ou bien tu peux choisir de les importer depuis le carnet d’adresses de l’appareil, par commodité. Mon use case, c’est précisément une exécution ponctuelle de « récupérer les contacts du carnet d’adresses dans l’app ».
Le sélecteur système me convient parfaitement, je n’ai pas besoin d’un accès récurrent et sans fin au carnet d’adresses ; j’ai seulement besoin d’un accès ponctuel, à un moment donné, pour récupérer les contacts et les stocker dans l’app.
⬆️ Le sélecteur que tu vois dans cette vidéo est le nouveau sélecteur natif d’Android 17.
Les cas qui justifient encore la permission sont ceux qui ont besoin du carnet d’adresses complet de façon récurrente, et non d’une sélection ponctuelle : apps de téléphonie ou de messagerie, identification d’appel, ou sauvegarde et synchronisation de contacts. Si ton app ne rentre pas dans ce genre de cas, il y a de fortes chances que le sélecteur fasse l’affaire.
Implémenter le Contact Picker d’Android 17 avec Flutter
Prenons comme exemple une app de test toute simple qui se contente de lister les contacts du carnet d’adresses :
Si on veut récupérer les contacts de façon « classique », on peut utiliser un plugin comme flutter_contacts :
final deviceContacts = await FlutterContacts.getAll(
properties: {ContactProperty.photoThumbnail},
);
Ce code nous donnera une liste de contacts avec leur nom et leur image. Cela dit, il faut d’abord demander la permission à l’utilisateur, sinon l’appel ne nous renverra rien :
final status = await FlutterContacts.permissions.request(PermissionType.read);
Passons maintenant aux adaptations nécessaires pour Android 17+. La première chose à faire est de déclarer la permission READ_CONTACTS uniquement pour les appareils sous Android 16 ou inférieur :
<uses-permission
android:name="android.permission.READ_CONTACTS"
android:maxSdkVersion="36" />
Il faut aussi compiler avec le SDK d’Android 17, parce que la classe ContactsPickerSessionContract n’existe pas dans les versions antérieures. Flutter utilise encore le 36 par défaut, il faut donc le fixer à la main dans le build.gradle.kts de l’app :
android {
compileSdk = 37
defaultConfig {
targetSdk = 37
}
}
Lancer le sélecteur depuis Kotlin avec ACTION_PICK_CONTACTS
Ensuite, on pourrait utiliser un plugin pour ça, mais étant donné que le développement est assez simple, je préfère une implémentation native pour m’éviter de dépendre d’un plugin de plus. La partie native et la partie Dart communiquent via un method channel, et c’est depuis Kotlin qu’on appelle le sélecteur système, en le configurant pour la sélection multiple comme ceci :
val intent = Intent(ContactsPickerSessionContract.ACTION_PICK_CONTACTS).apply {
putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
putExtra(
ContactsPickerSessionContract.EXTRA_PICK_CONTACTS_SELECTION_LIMIT,
call.arguments as Int,
)
// Read back only the fields below instead of every data field.
putExtra(
ContactsPickerSessionContract.EXTRA_PICK_CONTACTS_MATCH_ALL_DATA_FIELDS,
false,
)
putStringArrayListExtra(
ContactsPickerSessionContract.EXTRA_PICK_CONTACTS_REQUESTED_DATA_FIELDS,
arrayListOf(
ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE,
ContactsContract.CommonDataKinds.Photo.CONTENT_ITEM_TYPE,
),
)
}
startActivityForResult(intent, PICK_CONTACTS_REQUEST_CODE)
Ce call.arguments as Int, c’est la limite de sélection, qui nous arrive depuis Dart via le method channel.
Quand l’utilisateur a terminé, le sélecteur ne nous renvoie pas les contacts directement, mais une session URI qui pointe vers la sélection. Il faut l’interroger avec le ContentResolver, et voilà le détail important : le curseur nous donne une ligne par champ demandé, et non une par contact, il faut donc regrouper les lignes :
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
val sessionUri = data?.data
if (resultCode != Activity.RESULT_OK || sessionUri == null) {
// The user left the picker without confirming a selection.
return
}
val projection = arrayOf(
ContactsContract.Contacts.LOOKUP_KEY,
ContactsContract.Contacts.DISPLAY_NAME_PRIMARY,
ContactsContract.Data.MIMETYPE,
ContactsContract.CommonDataKinds.Photo.PHOTO,
)
contentResolver.query(sessionUri, projection, null, null, null)?.use { cursor ->
while (cursor.moveToNext()) {
// Group the rows by LOOKUP_KEY, and keep the blob of the row whose
// MIMETYPE is Photo.CONTENT_ITEM_TYPE as the contact photo.
}
}
}
Garde à l’esprit que cette requête, c’est de l’I/O : mieux vaut la sortir du thread principal avant de répondre à Dart.
Avec ce code on peut récupérer les contacts avec leur nom et leur photo, on n’a besoin de rien d’autre pour l’exemple ci-dessus.
Lire les contacts sélectionnés côté Dart
Ensuite, dans notre code Dart, définissons une méthode qui nous indiquera s’il faut utiliser le sélecteur système ou le plugin classique. Pour cela, il nous faut connaître la version d’Android en cours, ce qu’on peut faire avec le plugin device_info_plus :
static const _systemPickerMinAndroidSdk = 37;
Future<bool> _isSystemPickerAvailable() async {
if (!Platform.isAndroid) return false;
final androidInfo = await DeviceInfoPlugin().androidInfo;
return androidInfo.version.sdkInt >= _systemPickerMinAndroidSdk;
}
Avec ça, on peut maintenant récupérer les contacts avec le bon mécanisme :
Future<List<AppContact>> loadContacts() async {
if (await _isSystemPickerAvailable()) {
return await _pickContactsWithSystemPicker();
}
return await _readContactsWithPermission();
}
Sur Android 17+, on récupère les contacts côté natif et on les parse :
/// Opens the system Contact Picker. Returns an empty list if the user leaves
/// without selecting anything.
Future<List<AppContact>> _pickContactsWithSystemPicker() async {
final response = await _channel.invokeMapMethod<String, Object?>(
_methodPickContacts,
_maxSelectedContacts,
);
if (response == null || response['wasCancelled'] == true) {
return const [];
}
final contacts = <AppContact>[];
for (final rawContact in response['contacts'] as List<Object?>) {
final contact = rawContact as Map<Object?, Object?>;
final name = contact['name'] as String;
if (name.isEmpty) continue;
contacts.add(
AppContact(
id: contact['id'] as String,
name: name,
photo: contact['photo'] as Uint8List?,
),
);
}
return contacts;
}
Une dernière chose à garder à l’esprit : le sélecteur est ouvert par l’utilisateur, pas par l’app. Tu ne peux pas le lancer de ton côté pour rafraîchir les contacts en arrière-plan, donc si tu as besoin de la sélection plus tard, stocke-la.
Je ne t’ai montré ici que les parties essentielles, mais pour mieux comprendre le développement tu peux consulter le projet complet ici.
Conclusion
Dans cet article, on a vu très rapidement la nouvelle politique de Google Play concernant la permission de lecture des contacts, et je t’ai montré une approche simple que j’ai suivie moi-même pour travailler avec les contacts dans mes applications.
Si tu estimes que la permission contacts t’est absolument nécessaire, n’oublie pas que tu dois remplir le formulaire dédié dans Google Play avant l’entrée en vigueur de la politique, le 27 janvier 2027.
À la prochaine !