Android 17 Contact Picker in Flutter: Zeit, READ_CONTACTS zu ersetzen

Android 17 Contact Picker in Flutter: Zeit, READ_CONTACTS zu ersetzen

Inhaltsverzeichnis

Android 17 bringt eine wichtige Änderung beim Auslesen der Kontakte auf dem Gerät mit sich: Ab sofort lässt sich ein System-Picker aufrufen, um Kontakte auszuwählen und in unsere App zu importieren, ohne die Leseberechtigung READ_CONTACTS im Manifest deklarieren zu müssen.

Deshalb hat Google neue Einschränkungen angekündigt: Die Berechtigung READ_CONTACTS bleibt denjenigen Apps vorbehalten, die mit dem System-Picker allein nicht funktionieren können. Das heißt: Wenn deine App Kontakte auf einfache Weise importiert, wirst du den Picker verwenden müssen. Ist die Berechtigung für deine App unverzichtbar, musst du eine Deklaration in der Play Console einreichen und begründen, warum der Picker dir nicht reicht. Die Richtlinie tritt am 27. Januar 2027 in Kraft.

Das Ganze gilt allerdings für Apps, die auf targetSdk 37 oder höher zielen. Zielt deine noch auf eine frühere Version, betrifft dich die Richtlinie vorerst nicht — sie holt dich aber ein, sobald du das Target anhebst.

In diesem Artikel versuche ich, dir einen Leitfaden an die Hand zu geben: was das bedeutet, wie es dich betrifft, und ob du die Berechtigung wirklich weiter brauchst oder ob dir der System-Picker reicht. Danach zeige ich dir ein paar Codebeispiele, wie ich den System-Picker unter Android 17+ in meinen eigenen Apps umgesetzt habe.

Info

📽 Videoversion auf YouTube verfügbar

Betrifft die neue Kontakte-Richtlinie von Google Play deine App?

Die meisten Apps, die auf Kontakte zugreifen, werden mit dem System-Picker weiterhin einwandfrei funktionieren, ohne die Berechtigung READ_CONTACTS deklarieren zu müssen.

Wenn deine App punktuell auf Kontakte zugreift, etwa um sie in die App zu importieren, oder um zu einem bestimmten Zeitpunkt eine einzelne Angabe zu einem Kontakt zu holen, dann reicht der System-Picker mehr als aus.

Der Unterschied liegt im Umfang: Mit der Berechtigung hast du das gesamte Adressbuch gelesen, mit dem Picker bekommst du dagegen nur die Kontakte, die der Nutzer auswählt — und du kannst obendrein nur die Felder anfordern, die dich interessieren. Der Zugriff ist außerdem temporär und läuft aus, sobald du die Auswahl fertig gelesen hast.

Nehmen wir als Beispiel meine App Schuldenkontrolle. Eine App, in der du Kontakte anlegst und ihnen Schulden und Zahlungen zuordnest. Kontakte lassen sich intern anlegen, oder du importierst sie der Bequemlichkeit halber aus dem Adressbuch des Geräts. Mein Use Case ist genau eine punktuelle Ausführung von „die Kontakte aus dem Adressbuch des Geräts in die App holen“.

Der System-Picker passt für mich perfekt, ich brauche keinen dauerhaften, wiederkehrenden Zugriff auf das Adressbuch; ich brauche nur einen punktuellen Zugriff zu einem bestimmten Zeitpunkt, um die Kontakte zu holen und in der App zu speichern.

⬆️ Der Picker in diesem Video ist der neue native Picker von Android 17.

Die Fälle, die die Berechtigung weiterhin rechtfertigen, sind die, die das vollständige Adressbuch wiederkehrend brauchen statt einer punktuellen Auswahl: Telefon- oder Messaging-Apps, Anruferkennung, oder Backup und Synchronisierung von Kontakten. Passt deine App in keine dieser Kategorien, reicht dir der Picker höchstwahrscheinlich.

Den Android 17 Contact Picker in Flutter umsetzen

Nehmen wir als Beispiel eine einfache Test-App, die lediglich die Kontakte aus dem Adressbuch auflistet:

Kontaktliste in der Beispiel-App

Wollen wir die Kontakte auf dem „klassischen“ Weg holen, können wir ein Plugin wie flutter_contacts verwenden:

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

Dieser Code liefert uns eine Liste von Kontakten mit Namen und Bild. Allerdings musst du den Nutzer vorher um die Berechtigung bitten, sonst gibt der Aufruf nichts zurück:

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

Jetzt nehmen wir die nötigen Anpassungen für Android 17+ vor. Als Erstes deklarieren wir die Berechtigung READ_CONTACTS nur noch für Geräte mit Android 16 oder niedriger:

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

Außerdem müssen wir gegen das Android-17-SDK kompilieren, denn die Klasse ContactsPickerSessionContract existiert in früheren Versionen nicht. Flutter verwendet standardmäßig noch die 36, wir müssen das also von Hand in der build.gradle.kts der App setzen:

android {
    compileSdk = 37

    defaultConfig {
        targetSdk = 37
    }
}

Den Picker aus Kotlin mit ACTION_PICK_CONTACTS starten

Man könnte dafür zwar ein Plugin nehmen, aber da die Umsetzung recht überschaubar ist, bevorzuge ich eine native Implementierung, um mir die Abhängigkeit von noch einem Plugin zu sparen. Die native Seite und die Dart-Seite reden über einen Method Channel miteinander, und aus Kotlin heraus rufen wir den System-Picker auf und konfigurieren ihn für Mehrfachauswahl:

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)

Dieses call.arguments as Int ist das Auswahllimit, das uns aus Dart über den Method Channel erreicht.

Ist der Nutzer fertig, gibt uns der Picker die Kontakte nicht direkt zurück, sondern eine Session-URI, die auf die Auswahl zeigt. Die müssen wir mit dem ContentResolver abfragen — und hier kommt das Entscheidende: Der Cursor liefert uns eine Zeile pro angefordertem Feld, nicht eine pro Kontakt. Wir müssen die Zeilen also gruppieren:

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

Denk daran, dass diese Abfrage I/O ist: Sie gehört vom Main Thread heruntergeholt, bevor wir Dart antworten.

Mit diesem Code bekommen wir die Kontakte mit Namen und Foto, mehr brauchen wir für das obige Beispiel nicht.

Die ausgewählten Kontakte in Dart auslesen

Als Nächstes definieren wir in unserem Dart-Code eine Methode, die uns sagt, ob wir den System-Picker oder das klassische Plugin verwenden sollen. Dafür müssen wir die aktuelle Android-Version kennen, was mit dem Plugin device_info_plus geht:

static const _systemPickerMinAndroidSdk = 37;

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

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

Damit können wir die Kontakte nun über den passenden Mechanismus holen:

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

  return await _readContactsWithPermission();
}

Unter Android 17+ holen wir die Kontakte von der nativen Seite und parsen sie:

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

Noch eine letzte Sache: Der Picker wird vom Nutzer geöffnet, nicht von der App. Du kannst ihn nicht eigenmächtig starten, um die Kontakte im Hintergrund zu aktualisieren — wenn du die Auswahl später brauchst, speichere sie also.

Hier habe ich dir nur die wesentlichen Teile gezeigt; um die Umsetzung besser zu verstehen, kannst du dir das vollständige Projekt hier ansehen.

Fazit

In diesem Artikel haben wir uns die neue Richtlinie von Google Play zur Leseberechtigung für Kontakte sehr knapp angesehen, und ich habe dir einen einfachen Ansatz gezeigt, dem ich selbst folge, um in meinen Apps mit Kontakten zu arbeiten.

Falls du der Meinung bist, dass du die Kontakte-Berechtigung unbedingt brauchst, denk daran, dass du das entsprechende Formular bei Google Play ausfüllen musst, bevor die Richtlinie am 27. Januar 2027 in Kraft tritt.

Bis zum nächsten Mal!