
Contact Picker в Android 17 и Flutter: пора заменить READ_CONTACTS
- David Serrano Canales
- 19 октября 2026 г.
Содержание
Android 17 приносит важное изменение в том, как мы читаем контакты устройства: теперь можно вызвать системный выбор контактов, чтобы выбрать контакты и импортировать их в наше приложение, не объявляя разрешение на чтение READ_CONTACTS в манифесте.
В связи с этим Google объявил новые ограничения: разрешение READ_CONTACTS теперь остаётся за теми приложениями, которые не могут работать с одним только системным выбором. Это значит, что если твоё приложение импортирует контакты простым способом, тебе придётся использовать системный выбор. Если же разрешение тебе жизненно необходимо, придётся отправить декларацию в Play Console с обоснованием, почему системного выбора тебе не хватает. Политика вступает в силу 27 января 2027 года.
Правда, это касается приложений с targetSdk 37 или выше. Если твоё всё ещё нацелено на более раннюю версию, политика пока тебя не затрагивает — но догонит, как только ты поднимешь target.
В этой статье я постараюсь дать тебе ориентир: что всё это означает, как это тебя затронет и действительно ли тебе нужно и дальше использовать разрешение или системного выбора уже достаточно. Потом покажу несколько примеров кода — как я сам реализовал системный выбор на Android 17+ в своих приложениях.
Info
📽 Видеоверсия доступна на YouTube
Затрагивает ли новая политика Google Play по контактам твоё приложение?
Большинство приложений, которые работают с контактами, продолжат прекрасно работать через системный выбор, без объявления разрешения READ_CONTACTS.
Если твоё приложение обращается к контактам разово — например, чтобы импортировать их внутрь приложения или получить конкретные данные о контакте в определённый момент, — то системного выбора более чем достаточно.
Разница в охвате: с разрешением ты читал всю адресную книгу, а с системным выбором получаешь только те контакты, которые выбрал пользователь, и вдобавок можешь запросить лишь интересующие тебя поля. Доступ при этом временный и истекает, как только ты дочитал выборку.
Возьмём для примера моё приложение Учёт долгов. Приложение, где ты создаёшь контакты и назначаешь им долги и платежи. Контакты можно создать внутри приложения, а можно для удобства импортировать из адресной книги устройства. Мой use case — это как раз разовое выполнение «забрать контакты из адресной книги устройства внутрь приложения».
Системный выбор мне подходит идеально: мне не нужен бесконечный повторяющийся доступ к адресной книге, мне нужен лишь разовый доступ в определённый момент, чтобы забрать контакты и сохранить их внутри приложения.
⬆️ Выбор контактов, который ты видишь на этом видео, — это новый нативный механизм Android 17.
Случаи, которые действительно оправдывают разрешение, — это те, где полная адресная книга нужна регулярно, а не разовая выборка: приложения для звонков и мессенджеры, определитель номера, резервное копирование и синхронизация контактов. Если твоё приложение не из этой категории, скорее всего, системного выбора тебе хватит.
Реализация Contact Picker из Android 17 во Flutter
Возьмём для примера простое тестовое приложение, которое просто выводит список контактов из адресной книги:
Если мы хотим получить контакты «классическим» способом, можно воспользоваться плагином вроде flutter_contacts:
final deviceContacts = await FlutterContacts.getAll(
properties: {ContactProperty.photoThumbnail},
);
Этот код вернёт нам список контактов с именем и изображением. Правда, сначала нужно запросить разрешение у пользователя, иначе вызов ничего не вернёт:
final status = await FlutterContacts.permissions.request(PermissionType.read);
Теперь внесём изменения, необходимые для Android 17+. Первым делом объявим разрешение READ_CONTACTS только для устройств с Android 16 и ниже:
<uses-permission
android:name="android.permission.READ_CONTACTS"
android:maxSdkVersion="36" />
Ещё нам нужно компилироваться с SDK Android 17, потому что класса ContactsPickerSessionContract в более ранних версиях не существует. Flutter по умолчанию всё ещё использует 36, так что это придётся прописать вручную в build.gradle.kts приложения:
android {
compileSdk = 37
defaultConfig {
targetSdk = 37
}
}
Запуск системного выбора из Kotlin через ACTION_PICK_CONTACTS
Дальше можно было бы взять какой-нибудь плагин, но раз реализация довольно простая, я предпочитаю нативный вариант, чтобы не зависеть ещё от одного плагина. Нативная часть и часть на Dart общаются через method channel, и именно из Kotlin мы вызываем системный выбор, настраивая его на множественное выделение вот так:
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)
Этот call.arguments as Int — лимит выбора, который приходит к нам из Dart через method channel.
Когда пользователь закончил, системный выбор возвращает нам не сами контакты, а session URI, указывающий на выборку. Её нужно запросить через ContentResolver, и вот важная деталь: курсор отдаёт нам по строке на каждое запрошенное поле, а не по строке на контакт, так что строки придётся группировать:
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.
}
}
}
Учти, что этот запрос — это I/O, так что его стоит убрать с главного потока, прежде чем отвечать Dart.
С этим кодом мы получаем контакты с именем и фотографией, больше для примера выше ничего не нужно.
Чтение выбранных контактов в Dart
Затем в нашем коде на Dart определим метод, который подскажет, использовать системный выбор или классический плагин. Для этого нужно знать текущую версию Android, а сделать это можно плагином 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;
}
С этим мы уже можем получать контакты подходящим способом:
Future<List<AppContact>> loadContacts() async {
if (await _isSystemPickerAvailable()) {
return await _pickContactsWithSystemPicker();
}
return await _readContactsWithPermission();
}
На Android 17+ получаем контакты с нативной стороны и разбираем их:
/// 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;
}
И последнее, о чём стоит помнить: системный выбор открывает пользователь, а не приложение. Ты не можешь запустить его сам, чтобы обновить контакты в фоне, так что если выборка понадобится позже — сохрани её.
Здесь я показал только ключевые части, но чтобы лучше разобраться в реализации, можешь посмотреть полный проект здесь.
Заключение
В этой статье мы очень быстро разобрали новую политику Google Play в отношении разрешения на чтение контактов, и я показал тебе простой подход, которому следую сам, чтобы работать с контактами в своих приложениях.
Если ты считаешь, что разрешение на контакты тебе абсолютно необходимо, помни, что нужно заполнить специальную форму в Google Play до того, как политика вступит в силу 27 января 2027 года.
До следующего раза!