
Contact Picker do Android 17 em Flutter: hora de substituir READ_CONTACTS
- David Serrano Canales
- 19 de outubro de 2026
Índice
O Android 17 traz uma mudança importante no que toca à leitura dos contactos do dispositivo: agora é possível invocar um seletor do sistema para escolher contactos e importá-los para a nossa app, sem necessidade de declarar a permissão de leitura READ_CONTACTS no nosso manifesto.
Por causa disso, a Google anunciou novas restrições: a permissão READ_CONTACTS fica reservada para aquelas apps que não conseguem funcionar apenas com o seletor do sistema. Isto implica que, se a tua app importa contactos de forma simples, vais ter de usar o seletor. Se a permissão te for imprescindível para a app funcionar, terás de enviar uma declaração na Play Console a justificar porque é que o seletor não te serve. A política entra em vigor a 27 de janeiro de 2027.
Dito isto, aplica-se às apps que apontem para targetSdk 37 ou superior. Se a tua ainda aponta para uma versão anterior, a política não te afeta para já, embora te acabe por afetar assim que subires o target.
Neste artigo vou tentar dar-te um guia para perceberes o que isto implica, como é que te vai afetar, e se precisas mesmo de continuar a usar a permissão ou se o seletor do sistema já te chega. Depois, vou dar-te alguns exemplos de código para te mostrar como implementei eu próprio o seletor do sistema no Android 17+ nas minhas apps.
Info
📽 Versão em vídeo disponível no YouTube
A nova política de contactos da Google Play afeta a tua app?
A maioria das apps que acedem a contactos vai continuar a funcionar perfeitamente com o seletor do sistema, sem precisar de declarar a permissão READ_CONTACTS.
Se a tua aplicação acede aos contactos de forma pontual, por exemplo para os importar dentro da app, ou talvez para obter um dado específico sobre um contacto num momento determinado, então o seletor do sistema é mais do que suficiente.
A diferença está no alcance: com a permissão lias a agenda inteira, enquanto que com o seletor só recebes os contactos que o utilizador escolhe, e ainda por cima podes pedir apenas os campos que te interessam. O acesso também é temporário, por isso esgota-se assim que acabas de ler a seleção.
Vamos pegar como exemplo na minha aplicação Controle de Dívidas. Uma app onde crias contactos e lhes atribuis dívidas e pagamentos. Os contactos podem ser criados internamente, ou então podes optar por importá-los da agenda do dispositivo, por comodidade. O meu use case é precisamente uma execução pontual de «ir buscar os contactos da agenda do dispositivo para dentro da app».
O seletor do sistema serve-me na perfeição, não preciso de acesso recorrente e sem fim à agenda; só preciso de um acesso pontual, num momento determinado, para trazer os contactos e guardá-los dentro da app.
⬆️ O seletor que vês neste vídeo é o novo seletor nativo do Android 17.
Os casos que continuam mesmo a justificar a permissão são os que precisam da agenda completa de forma recorrente, e não de uma seleção pontual: apps de telefone ou mensagens, identificação de chamadas, ou cópias de segurança e sincronização de contactos. Se a tua app não encaixa em nada disso, o mais provável é que o seletor te chegue.
Implementar o Contact Picker do Android 17 em Flutter
Vamos pegar como exemplo numa app de testes simples que se limita a listar os contactos da agenda:
Se quisermos obter os contactos da forma «convencional», podemos usar um plugin como o flutter_contacts:
final deviceContacts = await FlutterContacts.getAll(
properties: {ContactProperty.photoThumbnail},
);
Este código vai dar-nos uma lista de contactos com o nome e a imagem. No entanto, primeiro é preciso pedir a permissão ao utilizador, ou a chamada não nos devolve nada:
final status = await FlutterContacts.permissions.request(PermissionType.read);
Agora vamos fazer as adaptações necessárias para o Android 17+. A primeira coisa é declarar a permissão READ_CONTACTS apenas para dispositivos com Android 16 ou inferior:
<uses-permission
android:name="android.permission.READ_CONTACTS"
android:maxSdkVersion="36" />
Também precisamos de compilar contra o SDK do Android 17, porque a classe ContactsPickerSessionContract não existe em versões anteriores. O Flutter ainda usa o 36 por omissão, por isso é preciso fixá-lo à mão no build.gradle.kts da app:
android {
compileSdk = 37
defaultConfig {
targetSdk = 37
}
}
Lançar o seletor a partir do Kotlin com ACTION_PICK_CONTACTS
Depois, podíamos usar algum plugin para isto, mas dado que o desenvolvimento é bastante simples, eu prefiro uma implementação nativa para evitar depender de mais um plugin. A parte nativa e a parte Dart comunicam através de um method channel, e é a partir do Kotlin que invocamos o seletor do sistema, configurando-o para seleção múltipla da seguinte forma:
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)
Esse call.arguments as Int é o limite de seleção, que nos chega do Dart através do method channel.
Quando o utilizador termina, o seletor não nos devolve os contactos diretamente, mas sim uma session URI que aponta para a seleção. É preciso consultá-la com o ContentResolver, e aqui está o detalhe importante: o cursor dá-nos uma linha por cada campo pedido, e não uma por contacto, por isso há que agrupar as linhas:
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.
}
}
}
Tem em conta que esta consulta é I/O, por isso convém tirá-la da thread principal antes de responder ao Dart.
Com este código conseguimos obter os contactos com o seu nome e a sua foto, não precisamos de mais nada para o exemplo anterior.
Ler os contactos selecionados em Dart
Depois, no nosso código Dart, vamos definir um método que nos indica se devemos usar o seletor do sistema ou o plugin tradicional. Para isso, temos de saber a versão do Android atual, e podemos fazê-lo com o 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;
}
Com isto, já podemos obter os contactos pelo mecanismo apropriado:
Future<List<AppContact>> loadContacts() async {
if (await _isSystemPickerAvailable()) {
return await _pickContactsWithSystemPicker();
}
return await _readContactsWithPermission();
}
No Android 17+, obtemos os contactos do lado nativo e fazemos o 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;
}
Uma última coisa a ter em conta: o seletor é aberto pelo utilizador, não pela app. Não o podes lançar por tua conta para atualizar os contactos em segundo plano, por isso se precisares da seleção mais tarde, guarda-a.
Aqui só te mostrei as partes principais, mas para perceberes melhor o desenvolvimento podes ver o projeto completo aqui.
Conclusão
Neste artigo vimos de forma muito rápida a nova política da Google Play relativamente à permissão de leitura de contactos, e mostrei-te um approach simples que segui eu próprio para trabalhar com contactos nas minhas aplicações.
Caso consideres que precisas mesmo da permissão de contactos, lembra-te de que tens de preencher o formulário dedicado na Google Play antes de a política entrar em vigor, a 27 de janeiro de 2027.
Até à próxima!