
Contact Picker d'Android 17 en Flutter: hora de substituir READ_CONTACTS
- David Serrano Canales
- 19 d’octubre del 2026
Taula de continguts
Android 17 porta un canvi important pel que fa a la lectura dels contactes del dispositiu: ara és possible invocar un selector del sistema per triar contactes i importar-los a la nostra app sense necessitat de declarar el permís de lectura READ_CONTACTS al nostre manifest.
Arran d’això, Google ha anunciat noves restriccions: el permís READ_CONTACTS queda reservat a aquelles apps que no puguin funcionar només amb el selector del sistema. Això implica que, si la teva app importa contactes de manera simple, hauràs d’utilitzar el selector. Si el permís et resulta imprescindible perquè l’app funcioni, hauràs d’enviar una declaració a Play Console justificant per què el selector no et serveix. La política entra en vigor el 27 de gener del 2027.
Això sí, això s’aplica a les apps que apuntin a targetSdk 37 o superior. Si la teva encara apunta a una versió anterior, la política no t’afecta de moment, tot i que t’acabarà afectant tan bon punt pugis el target.
En aquest article intentaré donar-te una guia per entendre què implica això, com t’afectarà, i si realment necessites continuar fent servir el permís o amb el selector del sistema ja en tens prou. Després, et donaré alguns exemples de codi per ensenyar-te com he implementat jo mateix el selector del sistema a Android 17+ a les meves pròpies apps.
Info
📽 Versió en vídeo disponible a YouTube
T’afecta la nova política de contactes de Google Play?
La majoria d’apps que accedeixen a contactes podran continuar funcionant perfectament amb el selector del sistema sense necessitat de declarar el permís READ_CONTACTS.
Si la teva aplicació accedeix als contactes de manera puntual, per exemple per importar-los dins l’app, o potser per obtenir una dada específica sobre un contacte en un moment determinat, llavors el selector del sistema és més que suficient.
La diferència és l’abast: amb el permís llegies l’agenda sencera, mentre que amb el selector només reps els contactes que l’usuari tria, i a més pots demanar únicament els camps que t’interessen. L’accés també és temporal, de manera que s’esgota tan bon punt acabes de llegir la selecció.
Posem com a exemple la meva aplicació Control de Deutes. Una app on crees contactes, i els assignes deutes i pagaments. Els contactes es poden crear de manera interna, o bé pots triar importar-los de l’agenda del dispositiu per a més comoditat. El meu use case és precisament una execució puntual d’«obtenir els contactes de l’agenda del dispositiu dins l’app».
El selector del sistema em serveix perfectament, no necessito accés recurrent sense fi a l’agenda; només necessito un accés puntual en un moment determinat per endur-me els contactes i guardar-me’ls dins l’app.
⬆️ El selector que veus en aquest vídeo és el nou selector natiu d’Android 17.
Els casos que sí que continuen justificant el permís són els que necessiten l’agenda completa de manera recurrent, i no una selecció puntual: apps de telèfon o missatgeria, identificació de trucades, o còpies de seguretat i sincronització de contactes. Si la teva app no encaixa en res d’això, el més probable és que el selector et valgui.
Implementar el Contact Picker d’Android 17 en Flutter
Posem com a exemple una app de proves senzilla on simplement llistem els contactes de l’agenda:
Si volem obtenir els contactes de manera «convencional», podem utilitzar un plugin com flutter_contacts:
final deviceContacts = await FlutterContacts.getAll(
properties: {ContactProperty.photoThumbnail},
);
Aquest codi ens donarà un llistat de contactes amb el nom i la imatge. Això sí, abans cal demanar-li el permís a l’usuari, o la crida no ens tornarà res:
final status = await FlutterContacts.permissions.request(PermissionType.read);
Ara fem les adaptacions necessàries per adaptar-ho a Android 17+. El primer de tot és declarar el permís de READ_CONTACTS únicament per a dispositius amb Android 16 o inferior:
<uses-permission
android:name="android.permission.READ_CONTACTS"
android:maxSdkVersion="36" />
També necessitem compilar contra el SDK d’Android 17, perquè la classe ContactsPickerSessionContract no existeix en versions anteriors. Flutter encara fa servir el 36 per defecte, així que cal fixar-ho a mà al build.gradle.kts de l’app:
android {
compileSdk = 37
defaultConfig {
targetSdk = 37
}
}
Llançar el selector des de Kotlin amb ACTION_PICK_CONTACTS
Després, podríem utilitzar algun plugin per a això, però atès que el desenvolupament és força senzill, jo prefereixo una implementació nativa per estalviar-me dependre d’un altre plugin. La part nativa i la part Dart es comuniquen amb un method channel, i és des de Kotlin on invoquem el selector del sistema, configurant-lo per a selecció múltiple de la manera següent:
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)
Aquest call.arguments as Int és el límit de selecció, que ens arriba des de Dart a través del method channel.
Quan l’usuari acaba, el selector no ens torna els contactes directament, sinó una session URI que apunta a la selecció. Cal consultar-la amb el ContentResolver, i aquí hi ha el detall important: el cursor ens dona una fila per cada camp sol·licitat, no una per contacte, així que toca agrupar les files:
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.
}
}
}
Tingues en compte que aquesta consulta és I/O, així que convé treure-la del fil principal abans de respondre a Dart.
Amb aquest codi podem obtenir els contactes amb el seu nom i la seva foto, no ens cal res més per a l’exemple anterior.
Llegir els contactes seleccionats en Dart
Després, al nostre codi Dart, definim un mètode que ens indicarà si hem d’utilitzar el selector del sistema o el plugin tradicional. Per fer-ho, hem de conèixer la versió d’Android actual, i ho podem fer amb el 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;
}
Amb això, ja podem procedir a obtenir els contactes amb el mecanisme apropiat:
Future<List<AppContact>> loadContacts() async {
if (await _isSystemPickerAvailable()) {
return await _pickContactsWithSystemPicker();
}
return await _readContactsWithPermission();
}
A Android 17+, obtenim els contactes des de la banda nativa i els parsegem:
/// 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;
}
Una última cosa a tenir en compte: el selector l’obre l’usuari, no l’app. No pots llançar-lo pel teu compte per refrescar els contactes en segon pla, així que si necessites la selecció més endavant, guarda-te-la.
Aquí només t’he mostrat les parts clau, però per entendre millor el desenvolupament pots veure el projecte complet aquí.
Conclusió
En aquest article hem vist de manera molt ràpida la nova política de Google Play pel que fa al permís de lectura de contactes, i t’he mostrat un approach simple que he seguit jo per treballar amb contactes a les meves aplicacions.
En cas que consideris que necessites sí o sí el permís de contactes, recorda que has d’emplenar el formulari dedicat a això a Google Play abans que la política entri en vigor, el 27 de gener del 2027.
Fins a la propera!