system_contact_picker 0.0.3 copy "system_contact_picker: ^0.0.3" to clipboard
system_contact_picker: ^0.0.3 copied to clipboard

Permissionless native Flutter contact picker using Android system pickers and iOS ContactsUI.

example/lib/main.dart

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:system_contact_picker/system_contact_picker.dart';

void main() {
  runApp(const ContactPickerExampleApp());
}

class ContactPickerExampleApp extends StatelessWidget {
  const ContactPickerExampleApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.teal),
        useMaterial3: true,
      ),
      home: const ContactPickerExamplePage(),
    );
  }
}

class ContactPickerExamplePage extends StatefulWidget {
  const ContactPickerExamplePage({super.key});

  @override
  State<ContactPickerExamplePage> createState() =>
      _ContactPickerExamplePageState();
}

class _ContactPickerExamplePageState extends State<ContactPickerExamplePage> {
  final _picker = const SystemContactPicker();
  ContactPickerCapabilities? _capabilities;
  List<PickedContact> _contacts = const <PickedContact>[];
  String? _error;
  bool _isBusy = false;

  @override
  void initState() {
    super.initState();
    _loadCapabilities();
  }

  Future<void> _loadCapabilities() async {
    try {
      final capabilities = await _picker.getCapabilities();
      if (!mounted) return;
      setState(() => _capabilities = capabilities);
    } catch (_) {
      // Widget tests run without a platform implementation.
    }
  }

  Future<void> _pickField(ContactField field) async {
    await _run(() {
      return _picker.pickContacts(fields: <ContactField>{field});
    });
  }

  Future<void> _pickMultiple() async {
    await _run(() {
      return _picker.pickContacts(
        allowMultiple: true,
        limit: 5,
        fields: const <ContactField>{ContactField.phone, ContactField.email},
      );
    });
  }

  Future<void> _pickPhoneOnly() async {
    await _pickField(ContactField.phone);
  }

  Future<void> _pickRichContact() async {
    await _run(() {
      return _picker.pickContacts(
        fields: <ContactField>{...ContactField.values},
      );
    });
  }

  Future<void> _run(Future<List<PickedContact>> Function() action) async {
    if (_isBusy) return;
    setState(() {
      _isBusy = true;
      _error = null;
    });
    try {
      final contacts = await action();
      if (!mounted) return;
      setState(() {
        _contacts = contacts;
      });
    } on PlatformException catch (error) {
      if (!mounted) return;
      setState(() => _error = '${error.code}: ${error.message}');
    } catch (error) {
      if (!mounted) return;
      setState(() => _error = error.toString());
    } finally {
      if (mounted) setState(() => _isBusy = false);
    }
  }

  bool _supportsFields(Iterable<ContactField> fields) {
    final capabilities = _capabilities;
    return capabilities != null &&
        fields.every(capabilities.supportedFields.contains);
  }

  String _pickerMode(ContactPickerCapabilities capabilities) {
    if (capabilities.platform == 'android') {
      if (capabilities.usesAndroid17ContactPicker) {
        return 'Android 17+ privacy-preserving Contact Picker';
      }
      return 'Android 9–16 permissionless legacy picker';
    }
    return 'iOS ContactsUI picker';
  }

  @override
  Widget build(BuildContext context) {
    final capabilities = _capabilities;
    return Scaffold(
      appBar: AppBar(title: const Text('System Contact Picker')),
      body: ListView(
        padding: const EdgeInsets.all(16),
        children: <Widget>[
          if (capabilities == null)
            const LinearProgressIndicator()
          else
            Card(
              child: Padding(
                padding: const EdgeInsets.all(12),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: <Widget>[
                    Text(
                      _pickerMode(capabilities),
                      style: Theme.of(context).textTheme.titleMedium,
                    ),
                    const SizedBox(height: 6),
                    Text(
                      'Platform: ${capabilities.platform}'
                      '${capabilities.androidSdkInt == null ? '' : ' · API ${capabilities.androidSdkInt}'}\n'
                      'Contacts permission: not required\n'
                      'Selection: ${capabilities.supportsMultiple ? 'single or multiple' : 'one contact and one field'}\n'
                      'Supported fields: ${ContactField.values.where(capabilities.supportedFields.contains).map((field) => field.name).join(', ')}',
                    ),
                  ],
                ),
              ),
            ),
          const SizedBox(height: 16),
          Text(
            'Permissionless examples',
            style: Theme.of(context).textTheme.titleSmall,
          ),
          const SizedBox(height: 8),
          Wrap(
            spacing: 8,
            runSpacing: 8,
            children: <Widget>[
              FilledButton(
                onPressed:
                    !_isBusy &&
                        _supportsFields(<ContactField>[ContactField.phone])
                    ? _pickPhoneOnly
                    : null,
                child: const Text('Pick phone'),
              ),
              OutlinedButton(
                onPressed:
                    !_isBusy &&
                        _supportsFields(<ContactField>[ContactField.email])
                    ? () => _pickField(ContactField.email)
                    : null,
                child: const Text('Pick email'),
              ),
              OutlinedButton(
                onPressed:
                    !_isBusy &&
                        _supportsFields(<ContactField>[
                          ContactField.postalAddress,
                        ])
                    ? () => _pickField(ContactField.postalAddress)
                    : null,
                child: const Text('Pick address'),
              ),
              OutlinedButton(
                onPressed:
                    !_isBusy &&
                        _supportsFields(<ContactField>[ContactField.name])
                    ? () => _pickField(ContactField.name)
                    : null,
                child: const Text('Pick name'),
              ),
            ],
          ),
          if (capabilities?.supportsMultiple ?? false) ...<Widget>[
            const SizedBox(height: 16),
            Text(
              'Multi-contact selection',
              style: Theme.of(context).textTheme.titleSmall,
            ),
            const SizedBox(height: 8),
            Wrap(
              spacing: 8,
              runSpacing: 8,
              children: <Widget>[
                FilledButton.tonal(
                  onPressed:
                      !_isBusy &&
                          _supportsFields(<ContactField>[
                            ContactField.phone,
                            ContactField.email,
                          ])
                      ? _pickMultiple
                      : null,
                  child: const Text('Pick up to 5 contacts'),
                ),
                FilledButton.tonal(
                  onPressed: !_isBusy && _supportsFields(ContactField.values)
                      ? _pickRichContact
                      : null,
                  child: const Text('Pick all fields'),
                ),
              ],
            ),
          ] else if (capabilities != null) ...<Widget>[
            const SizedBox(height: 12),
            Card(
              child: Padding(
                padding: EdgeInsets.all(12),
                child: Text(
                  'Multiple contact selection is unavailable on Android 9–16. '
                  'Google’s permissionless system picker supports it starting with '
                  'Android 17 (API 37). This device can select one contact at a time.',
                ),
              ),
            ),
          ],
          if (_isBusy) ...<Widget>[
            const SizedBox(height: 16),
            const LinearProgressIndicator(),
          ],
          if (_error != null) ...<Widget>[
            const SizedBox(height: 16),
            Text(
              _error!,
              style: TextStyle(color: Theme.of(context).colorScheme.error),
            ),
          ],
          const SizedBox(height: 16),
          for (final contact in _contacts) ContactTile(contact: contact),
        ],
      ),
    );
  }
}

class ContactTile extends StatelessWidget {
  const ContactTile({required this.contact, super.key});

  final PickedContact contact;

  @override
  Widget build(BuildContext context) {
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(12),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: <Widget>[
            Text(
              contact.displayName.isEmpty ? '(No name)' : contact.displayName,
              style: Theme.of(context).textTheme.titleMedium,
            ),
            for (final phone in contact.phones) Text('Phone: ${phone.value}'),
            for (final email in contact.emails) Text('Email: ${email.value}'),
            for (final address in contact.postalAddresses)
              Text('Address: ${address.formatted ?? address.street ?? ''}'),
            for (final organization in contact.organizations)
              Text('Organization: ${organization.company ?? ''}'),
            for (final website in contact.websites)
              Text('Website: ${website.value}'),
            for (final relation in contact.relations)
              Text('Relation: ${relation.value}'),
            for (final event in contact.events) Text('Event: ${event.value}'),
            for (final nickname in contact.nicknames)
              Text('Nickname: ${nickname.value}'),
            if (contact.thumbnail != null) const Text('Photo: available'),
          ],
        ),
      ),
    );
  }
}
2
likes
160
points
1.73k
downloads

Documentation

API reference

Publisher

verified publishersyedmubashirali.com

Weekly Downloads

Permissionless native Flutter contact picker using Android system pickers and iOS ContactsUI.

Repository (GitHub)
View/report issues

License

MIT (license)

Dependencies

flutter, plugin_platform_interface

More

Packages that depend on system_contact_picker

Packages that implement system_contact_picker