flutter_contacts_pro 3.0.0
flutter_contacts_pro: ^3.0.0 copied to clipboard
Fast, complete contact management for Android, iOS & macOS — all fields, groups, accounts, vCards, native dialogs, listeners, SIM contacts, and number blocking.
example/lib/main.dart
// Comprehensive example demonstrating flutter_contacts plugin usage.
// Shows CRUD operations, native dialogs, permissions, and contact management.
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_contacts_pro/flutter_contacts.dart';
void main() => runApp(
MaterialApp(
theme: ThemeData(useMaterial3: true, colorSchemeSeed: Colors.blue),
home: const ContactListPage(),
),
);
class ContactListPage extends StatefulWidget {
const ContactListPage({super.key});
@override
State<ContactListPage> createState() => _ContactListPageState();
}
class _ContactListPageState extends State<ContactListPage> {
List<Contact>? _contacts;
StreamSubscription? _sub;
bool _denied = false;
bool _loading = true;
@override
void initState() {
super.initState();
_init();
}
@override
void dispose() {
_sub?.cancel();
super.dispose();
}
Future<void> _init() async {
final status = await FlutterContacts.permissions.request(
PermissionType.readWrite,
);
if (status != PermissionStatus.granted &&
status != PermissionStatus.limited) {
return setState(() {
_denied = true;
_loading = false;
});
}
// Listen for contact changes
_sub = FlutterContacts.onContactChange.listen((changes) {
for (final change in changes) {
debugPrint('Contact ${change.type.name}: ${change.contactId}');
}
_load();
});
_load();
}
Future<void> _load() async {
setState(() => _loading = true);
try {
final contacts = await FlutterContacts.getAll(
properties: {
ContactProperty.name,
ContactProperty.phone,
ContactProperty.email,
ContactProperty.photoThumbnail,
},
);
setState(() {
_contacts = contacts;
_loading = false;
});
} catch (e) {
debugPrint('Error loading contacts: $e');
setState(() => _loading = false);
}
}
Future<void> _createContactWithNativeDialog() async {
// Create a contact with pre-filled data
final newContact = Contact(
name: Name(first: 'John', last: 'Doe'),
phones: [Phone(number: '+1234567890')],
emails: [Email(address: 'john.doe@example.com')],
);
// Show native contact creator dialog
final contactId = await FlutterContacts.native.showCreator(
contact: newContact,
);
if (contactId != null && mounted) {
// Show success message
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Contact created successfully!'),
backgroundColor: Colors.green,
),
);
// Optionally open the contact viewer
await FlutterContacts.native.showViewer(contactId);
// Reload contacts
_load();
} else if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Contact creation cancelled'),
backgroundColor: Colors.orange,
),
);
}
}
Future<void> _pickContact() async {
final contactId = await FlutterContacts.native.showPicker();
if (contactId != null && mounted) {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => ContactDetailPage(contactId: contactId),
),
);
}
}
void _openContactDetail(String contactId) {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => ContactDetailPage(contactId: contactId),
),
);
}
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(
title: const Text('Contacts'),
actions: [
IconButton(
icon: const Icon(Icons.search),
tooltip: 'Pick Contact',
onPressed: _pickContact,
),
],
),
body: _denied
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.block, size: 64, color: Colors.red),
const SizedBox(height: 16),
const Text(
'Contact permission not granted',
style: TextStyle(fontSize: 18),
),
const SizedBox(height: 8),
ElevatedButton(
onPressed: () async {
await FlutterContacts.permissions.openSettings();
},
child: const Text('Open Settings'),
),
],
),
)
: _loading
? const Center(child: CircularProgressIndicator())
: _contacts == null || _contacts!.isEmpty
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.contacts, size: 64, color: Colors.grey),
const SizedBox(height: 16),
const Text(
'No contacts found',
style: TextStyle(fontSize: 18, color: Colors.grey),
),
],
),
)
: RefreshIndicator(
onRefresh: _load,
child: ListView.builder(
itemCount: _contacts!.length,
itemBuilder: (_, i) {
final contact = _contacts![i];
return ListTile(
leading: CircleAvatar(
backgroundImage: contact.photo?.thumbnail != null
? MemoryImage(contact.photo!.thumbnail!)
: null,
child: contact.photo?.thumbnail == null
? const Icon(Icons.person)
: null,
),
title: Text(contact.displayName ?? '(No name)'),
subtitle: contact.phones.isNotEmpty
? Text(contact.phones.first.number)
: contact.emails.isNotEmpty
? Text(contact.emails.first.address)
: null,
// The 'favorite' getter does not exist on Contact, so we remove it.
trailing: null,
onTap: () => _openContactDetail(contact.id!),
);
},
),
),
floatingActionButton: FloatingActionButton.extended(
onPressed: _createContactWithNativeDialog,
icon: const Icon(Icons.add),
label: const Text('Create Contact'),
),
);
}
class ContactDetailPage extends StatelessWidget {
final String contactId;
const ContactDetailPage({super.key, required this.contactId});
Future<Contact?> _load() =>
FlutterContacts.get(contactId, properties: ContactProperties.all);
Future<void> _editContact(BuildContext context, Contact contact) async {
final updatedId = await FlutterContacts.native.showEditor(contactId);
if (updatedId != null && context.mounted) {
Navigator.pop(context);
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (_) => ContactDetailPage(contactId: updatedId),
),
);
}
}
Future<void> _viewContact(BuildContext context) async {
await FlutterContacts.native.showViewer(contactId);
}
Future<void> _deleteContact(BuildContext context) async {
final confirmed = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('Delete Contact'),
content: const Text('Are you sure you want to delete this contact?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: const Text('Cancel'),
),
TextButton(
onPressed: () => Navigator.pop(context, true),
style: TextButton.styleFrom(foregroundColor: Colors.red),
child: const Text('Delete'),
),
],
),
);
if (confirmed == true && context.mounted) {
try {
await FlutterContacts.delete(contactId);
if (context.mounted) {
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Contact deleted'),
backgroundColor: Colors.green,
),
);
}
} catch (e) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Error deleting contact: $e'),
backgroundColor: Colors.red,
),
);
}
}
}
}
@override
Widget build(BuildContext context) => FutureBuilder<Contact?>(
future: _load(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Scaffold(body: Center(child: CircularProgressIndicator()));
}
final contact = snapshot.data;
if (contact == null) {
return Scaffold(
appBar: AppBar(title: const Text('Contact')),
body: const Center(child: Text('Contact not found')),
);
}
final photo = contact.photo?.fullSize ?? contact.photo?.thumbnail;
return Scaffold(
appBar: AppBar(
title: Text(contact.displayName ?? 'Contact'),
actions: [
IconButton(
icon: const Icon(Icons.visibility),
tooltip: 'View',
onPressed: () => _viewContact(context),
),
IconButton(
icon: const Icon(Icons.edit),
tooltip: 'Edit',
onPressed: () => _editContact(context, contact),
),
IconButton(
icon: const Icon(Icons.delete),
tooltip: 'Delete',
onPressed: () => _deleteContact(context),
),
],
),
body: ListView(
children: [
// Photo section
if (photo != null)
Container(
height: 200,
width: double.infinity,
decoration: BoxDecoration(
image: DecorationImage(
image: MemoryImage(photo),
fit: BoxFit.cover,
),
),
)
else
Container(
height: 200,
width: double.infinity,
color: Colors.grey[300],
child: const Icon(Icons.person, size: 100, color: Colors.grey),
),
// Name section
if (contact.name != null) ...[
_buildSection('Name', [
if ((contact.name!.first ?? '').isNotEmpty)
_buildInfoRow('First', contact.name!.first ?? ''),
if ((contact.name!.last ?? '').isNotEmpty)
_buildInfoRow('Last', contact.name!.last ?? ''),
if ((contact.name!.middle ?? '').isNotEmpty)
_buildInfoRow('Middle', contact.name!.middle ?? ''),
if ((contact.name!.prefix ?? '').isNotEmpty)
_buildInfoRow('Prefix', contact.name!.prefix ?? ''),
if ((contact.name!.suffix ?? '').isNotEmpty)
_buildInfoRow('Suffix', contact.name!.suffix ?? ''),
]),
],
// Phones section
if (contact.phones.isNotEmpty)
_buildSection(
'Phones',
contact.phones
.map(
(p) => _buildInfoRow(
p.label.label.name,
p.number,
icon: Icons.phone,
),
)
.toList(),
),
// Emails section
if (contact.emails.isNotEmpty)
_buildSection(
'Emails',
contact.emails
.map(
(e) => _buildInfoRow(
e.label.label.name,
e.address,
icon: Icons.email,
),
)
.toList(),
),
// Addresses section
if (contact.addresses.isNotEmpty)
_buildSection(
'Addresses',
contact.addresses
.map(
(a) => _buildInfoRow(
a.label.label.name,
_formatAddress(a),
icon: Icons.location_on,
),
)
.toList(),
),
// Organization section
if (contact.organizations.isNotEmpty)
_buildSection(
'Organization',
contact.organizations
.map(
(o) => [
if ((o.name ?? '').isNotEmpty)
_buildInfoRow('Company', o.name ?? ''),
if ((o.jobTitle ?? '').isNotEmpty)
_buildInfoRow('Title', o.jobTitle ?? ''),
],
)
.expand((x) => x)
.toList(),
),
// Websites section
if (contact.websites.isNotEmpty)
_buildSection(
'Websites',
contact.websites
.map(
(w) => _buildInfoRow(
w.label.label.name,
w.url,
icon: Icons.link,
),
)
.toList(),
),
// Events section
if (contact.events.isNotEmpty)
_buildSection(
'Events',
contact.events
.map(
(e) => _buildInfoRow(
e.label.label.name,
_formatEvent(e),
icon: Icons.calendar_today,
),
)
.toList(),
),
// Notes section
if (contact.notes.isNotEmpty)
_buildSection(
'Notes',
contact.notes
.map((n) => _buildInfoRow('Note', n.note, icon: Icons.note))
.toList(),
),
// Metadata section
if (contact.id != null)
_buildSection('Metadata', [
_buildInfoRow('ID', contact.id!),
if (contact.displayName != null)
_buildInfoRow('Display Name', contact.displayName!),
]),
],
),
);
},
);
Widget _buildSection(String title, List<Widget> children) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 8),
child: Text(
title,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.blue,
),
),
),
...children,
const Divider(),
],
);
Widget _buildInfoRow(String label, String value, {IconData? icon}) =>
ListTile(
leading: icon != null ? Icon(icon, size: 20) : null,
title: Text(value),
subtitle: Text(label),
dense: true,
);
String _formatAddress(Address address) {
final parts = <String>[];
if (address.street?.isNotEmpty ?? false) parts.add(address.street!);
if (address.city?.isNotEmpty ?? false) parts.add(address.city!);
if (address.postalCode?.isNotEmpty ?? false) parts.add(address.postalCode!);
if (address.country?.isNotEmpty ?? false) parts.add(address.country!);
return parts.join(', ');
}
String _formatEvent(Event event) {
final parts = <String>[];
parts.add('${event.month}/${event.day}');
if (event.year != null) {
parts.add(event.year.toString());
}
return parts.join(' ');
}
}