
Contact Picker Android 17 di Flutter: saatnya mengganti READ_CONTACTS
- David Serrano Canales
- 19 Oktober 2026
Daftar Isi
Android 17 membawa perubahan penting dalam cara kita membaca kontak di perangkat: sekarang kita bisa memanggil pemilih sistem untuk memilih kontak dan mengimpornya ke aplikasi kita, tanpa perlu mendeklarasikan izin baca READ_CONTACTS di manifest.
Karena itu, Google mengumumkan pembatasan baru: izin READ_CONTACTS kini hanya untuk aplikasi yang memang tidak bisa berfungsi hanya dengan pemilih sistem. Artinya, kalau aplikasimu mengimpor kontak dengan cara yang sederhana, kamu harus memakai pemilih itu. Kalau izinnya benar-benar vital agar aplikasimu jalan, kamu harus mengirim deklarasi di Play Console yang menjelaskan kenapa pemilih sistem tidak cukup. Kebijakan ini berlaku mulai 27 Januari 2027.
Catatannya, ini berlaku untuk aplikasi yang menargetkan targetSdk 37 atau lebih tinggi. Kalau aplikasimu masih menargetkan versi sebelumnya, kebijakan ini belum menyentuhmu, meski cepat atau lambat akan kena begitu kamu menaikkan target.
Di artikel ini saya akan coba memberimu panduan untuk memahami apa implikasinya, bagaimana ini memengaruhimu, dan apakah kamu benar-benar perlu terus memakai izin itu atau pemilih sistem sudah cukup. Setelah itu, saya akan memberi beberapa contoh kode untuk menunjukkan bagaimana saya sendiri mengimplementasikan pemilih sistem di Android 17+ pada aplikasi saya.
Info
📽 Versi video tersedia di YouTube
Apakah kebijakan kontak baru Google Play memengaruhi aplikasimu?
Sebagian besar aplikasi yang mengakses kontak akan tetap berjalan dengan baik memakai pemilih sistem, tanpa perlu mendeklarasikan izin READ_CONTACTS.
Kalau aplikasimu mengakses kontak sesekali saja, misalnya untuk mengimpornya ke dalam aplikasi, atau mungkin untuk mengambil satu data spesifik tentang sebuah kontak pada saat tertentu, maka pemilih sistem sudah lebih dari cukup.
Bedanya ada di cakupan: dengan izin kamu membaca seluruh buku alamat, sedangkan dengan pemilih kamu hanya menerima kontak yang dipilih pengguna, dan di atas itu kamu bisa meminta hanya bidang yang kamu perlukan. Aksesnya juga sementara, jadi habis begitu kamu selesai membaca hasil pilihannya.
Ambil contoh aplikasi saya, Pelacak Utang. Aplikasi tempat kamu membuat kontak lalu menetapkan utang dan pembayaran untuk mereka. Kontak bisa dibuat langsung di dalam aplikasi, atau kamu bisa memilih untuk mengimpornya dari buku alamat perangkat supaya lebih praktis. Use case saya persis sebuah eksekusi sekali jalan: «ambil kontak dari buku alamat perangkat ke dalam aplikasi».
Pemilih sistem cocok sekali untuk saya, saya tidak butuh akses berulang tanpa henti ke buku alamat; saya hanya butuh akses sesaat, pada momen tertentu, untuk menarik kontaknya dan menyimpannya di dalam aplikasi.
⬆️ Pemilih yang kamu lihat di video ini adalah pemilih native baru dari Android 17.
Kasus yang memang masih membenarkan izin itu adalah yang membutuhkan buku alamat lengkap secara berulang, bukan pilihan sesaat: aplikasi telepon atau perpesanan, identifikasi penelepon, atau pencadangan dan sinkronisasi kontak. Kalau aplikasimu bukan yang seperti itu, kemungkinan besar pemilih sistem sudah memadai.
Mengimplementasikan Contact Picker Android 17 di Flutter
Ambil contoh sebuah aplikasi uji sederhana yang sekadar menampilkan daftar kontak dari buku alamat:
Kalau kita mau mengambil kontak dengan cara «konvensional», kita bisa memakai plugin seperti flutter_contacts:
final deviceContacts = await FlutterContacts.getAll(
properties: {ContactProperty.photoThumbnail},
);
Kode ini akan memberi kita daftar kontak lengkap dengan nama dan gambarnya. Tapi sebelumnya kamu harus meminta izin ke pengguna, kalau tidak panggilan ini tidak akan mengembalikan apa pun:
final status = await FlutterContacts.permissions.request(PermissionType.read);
Sekarang mari lakukan penyesuaian yang diperlukan untuk Android 17+. Yang pertama adalah mendeklarasikan izin READ_CONTACTS hanya untuk perangkat dengan Android 16 ke bawah:
<uses-permission
android:name="android.permission.READ_CONTACTS"
android:maxSdkVersion="36" />
Kita juga perlu mengompilasi dengan SDK Android 17, karena kelas ContactsPickerSessionContract tidak ada di versi sebelumnya. Flutter masih memakai 36 secara default, jadi kita harus menyetelnya manual di build.gradle.kts aplikasi:
android {
compileSdk = 37
defaultConfig {
targetSdk = 37
}
}
Meluncurkan pemilih dari Kotlin dengan ACTION_PICK_CONTACTS
Sebenarnya kita bisa memakai plugin untuk ini, tapi karena pengerjaannya cukup sederhana, saya lebih suka implementasi native supaya tidak bergantung pada satu plugin lagi. Sisi native dan sisi Dart berkomunikasi lewat method channel, dan dari Kotlin-lah kita memanggil pemilih sistem, mengaturnya untuk pilihan ganda seperti ini:
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 itu adalah batas jumlah pilihan, yang sampai ke kita dari Dart lewat method channel.
Begitu pengguna selesai, pemilih tidak mengembalikan kontaknya secara langsung, melainkan sebuah session URI yang menunjuk ke hasil pilihan. Kita harus menanyakannya lewat ContentResolver, dan di sinilah detail pentingnya: kursor memberi kita satu baris untuk setiap bidang yang diminta, bukan satu baris per kontak, jadi barisnya harus dikelompokkan:
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.
}
}
}
Ingat bahwa kueri ini adalah I/O, jadi sebaiknya keluarkan dari thread utama sebelum menjawab Dart.
Dengan kode ini kita bisa mendapatkan kontak beserta nama dan fotonya; tidak perlu apa-apa lagi untuk contoh di atas.
Membaca kontak terpilih di Dart
Berikutnya, di kode Dart kita, mari definisikan sebuah metode yang memberi tahu apakah kita harus memakai pemilih sistem atau plugin tradisional. Untuk itu kita perlu tahu versi Android saat ini, dan itu bisa dilakukan dengan 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;
}
Dengan ini, kita sudah bisa mengambil kontak lewat mekanisme yang tepat:
Future<List<AppContact>> loadContacts() async {
if (await _isSystemPickerAvailable()) {
return await _pickContactsWithSystemPicker();
}
return await _readContactsWithPermission();
}
Di Android 17+, kita ambil kontaknya dari sisi native lalu kita 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;
}
Satu hal terakhir yang perlu diingat: pemilih itu dibuka oleh pengguna, bukan oleh aplikasi. Kamu tidak bisa meluncurkannya sendiri untuk menyegarkan kontak di latar belakang, jadi kalau kamu butuh hasil pilihannya nanti, simpan saja.
Di sini saya hanya menunjukkan bagian-bagian kuncinya, tapi untuk memahami pengerjaannya lebih baik kamu bisa melihat proyek lengkapnya di sini.
Kesimpulan
Di artikel ini kita sudah melihat sekilas kebijakan baru Google Play soal izin membaca kontak, dan saya menunjukkan sebuah pendekatan sederhana yang saya ikuti sendiri untuk bekerja dengan kontak di aplikasi-aplikasi saya.
Kalau kamu merasa benar-benar membutuhkan izin kontak, ingat bahwa kamu harus mengisi formulir khusus di Google Play sebelum kebijakannya berlaku, pada 27 Januari 2027.
Sampai jumpa lain kali!