Android 17 Contact Picker ve Flutter: READ_CONTACTS'ı değiştirme vakti

Android 17 Contact Picker ve Flutter: READ_CONTACTS'ı değiştirme vakti

İçindekiler

Android 17, cihazdaki kişileri okuma konusunda önemli bir değişiklik getiriyor: artık manifest dosyamızda READ_CONTACTS okuma iznini tanımlamadan, kişileri seçip uygulamamıza aktarmak için bir sistem seçici çağırabiliyoruz.

Bu nedenle Google yeni kısıtlamalar duyurdu: READ_CONTACTS izni artık yalnızca sistem seçicisiyle çalışamayan uygulamalara ayrılıyor. Yani uygulaman kişileri basit bir şekilde içe aktarıyorsa, seçiciyi kullanmak zorunda kalacaksın. İzin uygulamanın çalışması için vazgeçilmezse, seçicinin neden yetmediğini açıklayan bir beyanı Play Console’a göndermen gerekecek. Politika 27 Ocak 2027’de yürürlüğe giriyor.

Şunu da ekleyelim: bu, targetSdk 37 veya üstünü hedefleyen uygulamalar için geçerli. Seninki hâlâ daha eski bir sürümü hedefliyorsa politika şimdilik seni etkilemiyor, ama target’ı yükselttiğin anda seni yakalayacak.

Bu yazıda sana bunun ne anlama geldiğini, seni nasıl etkileyeceğini ve izni kullanmaya gerçekten devam etmen gerekip gerekmediğini, yoksa sistem seçicisinin yeterli olup olmadığını anlaman için bir rehber vermeye çalışacağım. Sonrasında, Android 17+ üzerinde sistem seçicisini kendi uygulamalarımda nasıl uyguladığımı gösteren birkaç kod örneği vereceğim.

Info

📽 Video versiyonu YouTube‘da mevcut

Google Play’in yeni kişiler politikası uygulamanı etkiliyor mu?

Kişilere erişen uygulamaların çoğu, READ_CONTACTS iznini tanımlamaya gerek kalmadan sistem seçicisiyle gayet güzel çalışmaya devam edecek.

Uygulaman kişilere ara sıra erişiyorsa — örneğin onları uygulamaya aktarmak için ya da belirli bir anda bir kişiyle ilgili tek bir veriyi almak için — sistem seçicisi fazlasıyla yeterli.

Fark kapsamda: izinle tüm rehberi okuyordun, seçiciyle ise yalnızca kullanıcının seçtiği kişileri alıyorsun ve üstelik sadece ilgilendiğin alanları isteyebiliyorsun. Erişim ayrıca geçici; seçimi okumayı bitirir bitirmez sona eriyor.

Örnek olarak kendi uygulamamı, Borç Takipçisi uygulamasını ele alalım. Kişiler oluşturup onlara borç ve ödeme atadığın bir uygulama. Kişiler uygulama içinde oluşturulabiliyor ya da kolaylık olsun diye cihazın rehberinden içe aktarılabiliyor. Benim use case’im tam olarak «cihazın rehberindeki kişileri uygulamanın içine al» şeklinde tek seferlik bir işlem.

Sistem seçicisi bana kusursuz uyuyor; rehbere sürekli ve sonu gelmeyen bir erişime ihtiyacım yok. Sadece belirli bir anda, kişileri alıp uygulamanın içine kaydetmek için tek seferlik bir erişim yetiyor.

⬆️ Bu videoda gördüğün seçici, Android 17’nin yeni native seçicisi.

İzni hâlâ gerçekten haklı çıkaran durumlar, tek seferlik bir seçim yerine rehberin tamamına düzenli olarak ihtiyaç duyanlar: telefon veya mesajlaşma uygulamaları, arayan kimliği, ya da kişilerin yedeklenmesi ve senkronizasyonu. Uygulaman bunlardan birine girmiyorsa, büyük ihtimalle seçici sana yeter.

Android 17 Contact Picker’ı Flutter’da uygulamak

Örnek olarak, yalnızca rehberdeki kişileri listeleyen basit bir test uygulaması ele alalım:

Örnek uygulamadaki kişi listesi

Kişileri «klasik» yoldan almak istersek, flutter_contacts gibi bir eklenti kullanabiliriz:

final deviceContacts = await FlutterContacts.getAll(
  properties: {ContactProperty.photoThumbnail},
);

Bu kod bize kişilerin adını ve resmini içeren bir liste verecek. Ancak önce kullanıcıdan izin istemen gerekiyor, yoksa çağrı bize hiçbir şey döndürmez:

final status = await FlutterContacts.permissions.request(PermissionType.read);

Şimdi Android 17+ için gereken uyarlamaları yapalım. İlk iş, READ_CONTACTS iznini yalnızca Android 16 ve altındaki cihazlar için tanımlamak:

<uses-permission
  android:name="android.permission.READ_CONTACTS"
  android:maxSdkVersion="36" />

Ayrıca Android 17 SDK’sıyla derlememiz gerekiyor, çünkü ContactsPickerSessionContract sınıfı önceki sürümlerde yok. Flutter hâlâ varsayılan olarak 36 kullanıyor, bu yüzden uygulamanın build.gradle.kts dosyasında elle ayarlamamız şart:

android {
    compileSdk = 37

    defaultConfig {
        targetSdk = 37
    }
}

Seçiciyi Kotlin’den ACTION_PICK_CONTACTS ile başlatmak

Bunun için bir eklenti kullanabilirdik ama geliştirmesi oldukça basit olduğundan, bir eklentiye daha bağımlı olmamak adına native bir uygulamayı tercih ediyorum. Native taraf ile Dart tarafı bir method channel üzerinden konuşuyor ve sistem seçicisini Kotlin’den çağırıp çoklu seçim için şöyle yapılandırıyoruz:

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)

Şu call.arguments as Int, Dart’tan method channel aracılığıyla bize ulaşan seçim limiti.

Kullanıcı işini bitirdiğinde seçici bize kişileri doğrudan vermiyor, seçime işaret eden bir session URI veriyor. Bunu ContentResolver ile sorgulamamız gerekiyor ve işte önemli ayrıntı: imleç bize kişi başına değil, istenen her alan için bir satır veriyor; yani satırları gruplamak gerekiyor:

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.
        }
    }
}

Şunu unutma: bu sorgu I/O işi, dolayısıyla Dart’a cevap vermeden önce ana iş parçacığından çıkarmakta fayda var.

Bu kodla kişileri adlarıyla ve fotoğraflarıyla alabiliyoruz; yukarıdaki örnek için başka bir şeye ihtiyacımız yok.

Seçilen kişileri Dart tarafında okumak

Ardından Dart kodumuzda, sistem seçicisini mi yoksa klasik eklentiyi mi kullanmamız gerektiğini söyleyecek bir metot tanımlayalım. Bunun için mevcut Android sürümünü bilmemiz lazım; bunu da device_info_plus eklentisiyle yapabiliriz:

static const _systemPickerMinAndroidSdk = 37;

Future<bool> _isSystemPickerAvailable() async {
  if (!Platform.isAndroid) return false;

  final androidInfo = await DeviceInfoPlugin().androidInfo;
  return androidInfo.version.sdkInt >= _systemPickerMinAndroidSdk;
}

Bununla artık kişileri doğru mekanizmayla alabiliriz:

Future<List<AppContact>> loadContacts() async {
  if (await _isSystemPickerAvailable()) {
    return await _pickContactsWithSystemPicker();
  }

  return await _readContactsWithPermission();
}

Android 17+ üzerinde kişileri native taraftan alıp ayrıştırıyoruz:

/// 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;
}

Akılda tutulması gereken son bir şey: seçiciyi kullanıcı açar, uygulama değil. Kişileri arka planda tazelemek için onu kendi başına başlatamazsın; dolayısıyla seçime sonradan ihtiyacın olacaksa, kaydet.

Burada sana yalnızca kilit kısımları gösterdim, ama geliştirmeyi daha iyi anlamak için projenin tamamını buradan inceleyebilirsin.

Sonuç

Bu yazıda Google Play’in kişileri okuma iznine dair yeni politikasına çok hızlı bir göz attık ve uygulamalarımda kişilerle çalışmak için kendi izlediğim basit bir yaklaşımı gösterdim.

Kişiler iznine kesinlikle ihtiyacın olduğunu düşünüyorsan, politika 27 Ocak 2027’de yürürlüğe girmeden önce Google Play’deki ilgili formu doldurman gerektiğini unutma.

Bir dahaki sefere görüşmek üzere!