
Contact Picker w Androidzie 17 i Flutter: czas zastąpić READ_CONTACTS
- David Serrano Canales
- 19 października 2026
Spis treści
Android 17 przynosi ważną zmianę w odczycie kontaktów z urządzenia: od teraz można wywołać systemowy selektor, żeby wybrać kontakty i zaimportować je do naszej aplikacji, bez deklarowania uprawnienia do odczytu READ_CONTACTS w manifeście.
W związku z tym Google ogłosiło nowe ograniczenia: uprawnienie READ_CONTACTS zostaje zarezerwowane dla tych aplikacji, które nie są w stanie działać z samym selektorem systemowym. Oznacza to, że jeśli twoja aplikacja importuje kontakty w prosty sposób, będziesz musiał użyć selektora. Jeśli uprawnienie jest niezbędne, żeby aplikacja działała, będziesz musiał wysłać deklarację w Play Console z uzasadnieniem, dlaczego selektor ci nie wystarcza. Zasady wchodzą w życie 27 stycznia 2027.
Przy czym dotyczy to aplikacji celujących w targetSdk 37 lub wyższy. Jeśli twoja celuje jeszcze we wcześniejszą wersję, zasady na razie cię nie dotyczą, choć dopadną cię, gdy tylko podniesiesz target.
W tym artykule spróbuję dać ci przewodnik, żebyś zrozumiał, co to oznacza, jak cię dotknie i czy naprawdę musisz dalej używać uprawnienia, czy wystarczy ci selektor systemowy. Potem pokażę ci kilka przykładów kodu, jak sam zaimplementowałem selektor systemowy na Androidzie 17+ we własnych aplikacjach.
Info
📽 Wersja wideo dostępna na YouTube
Czy nowe zasady Google Play dotyczące kontaktów mają wpływ na twoją aplikację?
Większość aplikacji, które sięgają po kontakty, będzie dalej działać bez zarzutu z selektorem systemowym, bez deklarowania uprawnienia READ_CONTACTS.
Jeśli twoja aplikacja sięga po kontakty doraźnie, na przykład żeby je zaimportować do aplikacji albo pobrać konkretną informację o kontakcie w danym momencie, to selektor systemowy w zupełności wystarczy.
Różnica leży w zakresie: z uprawnieniem czytałeś całą książkę adresową, a z selektorem dostajesz wyłącznie kontakty wybrane przez użytkownika — i w dodatku możesz poprosić tylko o te pola, które cię interesują. Dostęp jest też tymczasowy, więc wygasa, gdy tylko skończysz czytać zaznaczenie.
Weźmy za przykład moją aplikację Kontroler Długów. Aplikację, w której tworzysz kontakty i przypisujesz im długi oraz płatności. Kontakty można utworzyć wewnętrznie albo zaimportować z książki adresowej urządzenia, dla wygody. Mój use case to dokładnie jednorazowe wykonanie „pobierz kontakty z książki adresowej urządzenia do aplikacji”.
Selektor systemowy sprawdza się u mnie doskonale, nie potrzebuję bezterminowego, powtarzalnego dostępu do książki adresowej; potrzebuję tylko doraźnego dostępu w określonym momencie, żeby pobrać kontakty i zapisać je w aplikacji.
⬆️ Selektor, który widzisz na tym filmie, to nowy natywny selektor Androida 17.
Przypadki, które nadal uzasadniają uprawnienie, to te wymagające pełnej książki adresowej w sposób powtarzalny, a nie jednorazowego wyboru: aplikacje telefonu albo komunikatory, identyfikacja rozmówcy, czy kopie zapasowe i synchronizacja kontaktów. Jeśli twoja aplikacja nie należy do tej kategorii, selektor najpewniej ci wystarczy.
Implementacja Contact Pickera z Androida 17 we Flutterze
Weźmy za przykład prostą aplikację testową, która po prostu wypisuje kontakty z książki adresowej:
Jeśli chcemy pobrać kontakty w „klasyczny” sposób, możemy użyć wtyczki takiej jak flutter_contacts:
final deviceContacts = await FlutterContacts.getAll(
properties: {ContactProperty.photoThumbnail},
);
Ten kod da nam listę kontaktów z nazwą i zdjęciem. Trzeba jednak najpierw poprosić użytkownika o uprawnienie, bo inaczej wywołanie nic nie zwróci:
final status = await FlutterContacts.permissions.request(PermissionType.read);
Teraz zróbmy zmiany potrzebne do dostosowania tego do Androida 17+. Przede wszystkim zadeklarujmy uprawnienie READ_CONTACTS tylko dla urządzeń z Androidem 16 lub starszym:
<uses-permission
android:name="android.permission.READ_CONTACTS"
android:maxSdkVersion="36" />
Musimy też kompilować z SDK Androida 17, bo klasa ContactsPickerSessionContract nie istnieje we wcześniejszych wersjach. Flutter domyślnie używa jeszcze 36, więc trzeba to ustawić ręcznie w build.gradle.kts aplikacji:
android {
compileSdk = 37
defaultConfig {
targetSdk = 37
}
}
Uruchomienie selektora z Kotlina przez ACTION_PICK_CONTACTS
Moglibyśmy użyć do tego jakiejś wtyczki, ale skoro implementacja jest dość prosta, wolę natywne rozwiązanie, żeby nie uzależniać się od kolejnej wtyczki. Strona natywna i strona Dart rozmawiają ze sobą przez method channel, a to z Kotlina wywołujemy selektor systemowy, konfigurując go do wyboru wielu kontaktów w taki sposób:
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)
To call.arguments as Int to limit zaznaczenia, który dociera do nas z Darta przez method channel.
Kiedy użytkownik skończy, selektor nie zwraca nam kontaktów bezpośrednio, tylko session URI wskazujące na zaznaczenie. Trzeba je odpytać przez ContentResolver i tu jest ważny szczegół: kursor daje nam jeden wiersz na każde żądane pole, a nie jeden na kontakt, więc trzeba pogrupować wiersze:
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.
}
}
}
Pamiętaj, że to zapytanie to I/O, więc warto zdjąć je z głównego wątku, zanim odpowiemy Dartowi.
Tym kodem pobierzemy kontakty z nazwą i zdjęciem, nic więcej nie jest nam potrzebne do powyższego przykładu.
Odczyt wybranych kontaktów w Darcie
Następnie w naszym kodzie Dart zdefiniujmy metodę, która powie nam, czy mamy użyć selektora systemowego, czy klasycznej wtyczki. Do tego musimy znać aktualną wersję Androida, a możemy to zrobić wtyczką 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;
}
Mając to, możemy już pobrać kontakty właściwym mechanizmem:
Future<List<AppContact>> loadContacts() async {
if (await _isSystemPickerAvailable()) {
return await _pickContactsWithSystemPicker();
}
return await _readContactsWithPermission();
}
Na Androidzie 17+ pobieramy kontakty ze strony natywnej i je parsujemy:
/// 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;
}
I jeszcze jedno, o czym warto pamiętać: selektor otwiera użytkownik, a nie aplikacja. Nie uruchomisz go na własną rękę, żeby odświeżyć kontakty w tle, więc jeśli zaznaczenie przyda ci się później, zapisz je.
Pokazałem ci tu tylko kluczowe fragmenty, ale żeby lepiej zrozumieć całość, możesz zobaczyć pełny projekt tutaj.
Podsumowanie
W tym artykule bardzo szybko przejrzeliśmy nowe zasady Google Play dotyczące uprawnienia do odczytu kontaktów i pokazałem ci proste podejście, które sam stosuję, żeby pracować z kontaktami w swoich aplikacjach.
Jeśli uznasz, że uprawnienie do kontaktów jest ci absolutnie niezbędne, pamiętaj, że musisz wypełnić dedykowany formularz w Google Play, zanim zasady wejdą w życie 27 stycznia 2027.
Do następnego razu!