contacts_getter 1.0.0 copy "contacts_getter: ^1.0.0" to clipboard
contacts_getter: ^1.0.0 copied to clipboard

powerful Flutter plugin for contacts, call logs, SMS, and device information on Android and iOS. Auto-requests permissions with clear error messages.

example/lib/main.dart

// Import required packages for Flutter, contacts plugin, permissions, and date formatting
import 'package:contacts_getter/contacts_getter.dart';
import 'package:contacts_getter/models/call_logs_model.dart';
import 'package:contacts_getter/models/contacts_model.dart';
import 'package:contacts_getter/models/messages_model.dart';
import 'package:contacts_getter/models/account_model.dart';
import 'package:flutter/material.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:intl/intl.dart';

// Main entry point of the application
void main() {
  runApp(const FileManagerDemoApp());
}

// Main application widget, sets up the MaterialApp with a modern theme
class FileManagerDemoApp extends StatelessWidget {
  const FileManagerDemoApp({super.key});

  @override
  Widget build(BuildContext context) {
    // Configure MaterialApp with Material 3 theme and custom styling
    return MaterialApp(
      title: 'Contacts Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(
          seedColor: Colors.indigo,
          primary: Colors.indigo,
          secondary: Colors.indigoAccent,
          brightness: Brightness.light,
        ),
        useMaterial3: true,
        appBarTheme: const AppBarTheme(
          elevation: 2,
          shadowColor: Colors.black26,
          backgroundColor: Colors.indigo,
          foregroundColor: Colors.white,
        ),

        textTheme: const TextTheme(
          bodyMedium: TextStyle(fontSize: 16, color: Colors.black87),
          titleLarge: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
          labelLarge: TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
        ),
        elevatedButtonTheme: ElevatedButtonThemeData(
          style: ElevatedButton.styleFrom(
            padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
            shape: RoundedRectangleBorder(
              borderRadius: BorderRadius.circular(8),
            ),
          ),
        ),
        floatingActionButtonTheme: const FloatingActionButtonThemeData(
          backgroundColor: Colors.indigo,
          foregroundColor: Colors.white,
        ),
        inputDecorationTheme: const InputDecorationTheme(
          border: OutlineInputBorder(
            borderRadius: BorderRadius.all(Radius.circular(8)),
          ),
          contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 12),
        ),
      ),
      home: const HomePage(),
    );
  }
}

// Stateful widget for the home page, manages state for data fetching and UI
class HomePage extends StatefulWidget {
  const HomePage({super.key});

  @override
  State<HomePage> createState() => _HomePageState();
}

// State class for HomePage, handles permissions, data fetching, and tabbed UI
class _HomePageState extends State<HomePage>
    with SingleTickerProviderStateMixin {
  // Lists to store fetched data
  List<Contact> contacts = [];
  List<CallLog> callLogs = [];
  List<Message> messages = [];

  // Flags for loading and error states per tab
  bool isContactsLoading = false;
  bool isCallLogsLoading = false;
  bool isMessagesLoading = false;
  bool isOthersLoading = false;
  String? contactsError;
  String? callLogsError;
  String? messagesError;
  String? othersError;

  List<Account> accounts = [];
  List<Map<String, dynamic>> simCards = [];

  // Tab controller for managing Contacts, Call Logs, and Messages tabs
  late TabController _tabController;

  // Keys for AnimatedList
  final GlobalKey<AnimatedListState> _contactsListKey =
      GlobalKey<AnimatedListState>();
  final GlobalKey<AnimatedListState> _callLogsListKey =
      GlobalKey<AnimatedListState>();
  final GlobalKey<AnimatedListState> _messagesListKey =
      GlobalKey<AnimatedListState>();

  @override
  void initState() {
    super.initState();
    // Initialize TabController with 4 tabs
    _tabController = TabController(length: 4, vsync: this);
    // Fetch data on app startup
    WidgetsBinding.instance.addPostFrameCallback((_) => _fetchData());
  }

  @override
  void dispose() {
    _tabController.dispose();
    super.dispose();
  }

  // Requests required permissions (contacts, phone, SMS)
  Future<bool> _requestPermissions() async {
    // Request multiple permissions at once
    Map<Permission, PermissionStatus> statuses = await [
      Permission.contacts,
      Permission.phone,
      Permission.sms,
    ].request();

    // Check if each permission is granted
    bool contactsAccess = statuses[Permission.contacts]?.isGranted ?? false;
    bool callLogAccess = statuses[Permission.phone]?.isGranted ?? false;
    bool smsAccess = statuses[Permission.sms]?.isGranted ?? false;

    // Log permission denials for debugging
    if (!contactsAccess) debugPrint("👉🏻 Contacts permission denied");
    if (!callLogAccess) debugPrint("👉🏻 Phone permission denied");
    if (!smsAccess) debugPrint("👉🏻 SMS permission denied");

    // Return true only if all permissions are granted
    return contactsAccess && callLogAccess && smsAccess;
  }

  // Fetches contacts, call logs, and messages, and updates the UI
  Future<void> _fetchData() async {
    // Check permissions
    bool hasPermissions = await _requestPermissions();
    if (!mounted) return;
    if (!hasPermissions) {
      setState(() {
        contactsError = callLogsError = messagesError =
            "Please grant all required permissions";
      });
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text("Please grant all required permissions")),
      );
      return;
    }

    // Fetch contacts
    setState(() {
      isContactsLoading = true;
      contactsError = null;
    });
    try {
      debugPrint("👉🏻 Calling getContacts");
      var fetchedContacts = await ContactsGetter().getContacts(
        limit: 10,
        orderByDesc: true,
      );
      debugPrint("👉🏻 Contacts: $fetchedContacts");
      setState(() {
        contacts = fetchedContacts;
      });
      for (var contact in fetchedContacts) {
        debugPrint(
          "👉🏻 Contact: ${contact.displayName}, ${contact.phoneNumber}",
        );
      }
    } catch (e) {
      debugPrint("👉🏻 Error getting contacts: $e");
      setState(() {
        contactsError = "Error getting contacts: $e";
      });
      ScaffoldMessenger.of(
        context,
      ).showSnackBar(SnackBar(content: Text("Error getting contacts: $e")));
    }
    setState(() {
      isContactsLoading = false;
    });

    // Fetch call logs
    setState(() {
      isCallLogsLoading = true;
      callLogsError = null;
    });
    try {
      debugPrint("👉🏻 Calling getCallLogs");
      var fetchedCallLogs = await ContactsGetter().getCallLogs(
        limit: 10,
        orderByDesc: true,
      );
      debugPrint("👉🏻 Call Logs: $fetchedCallLogs");
      setState(() {
        callLogs = fetchedCallLogs;
      });
      for (var call in fetchedCallLogs) {
        debugPrint(
          "👉🏻 Call: ${call.number}, ${call.type}, ${call.date}, ${call.duration}s",
        );
      }
    } catch (e) {
      debugPrint("👉🏻 Error getting call logs: $e");
      setState(() {
        callLogsError = "Error getting call logs: $e";
      });
      ScaffoldMessenger.of(
        context,
      ).showSnackBar(SnackBar(content: Text("Error getting call logs: $e")));
    }
    setState(() {
      isCallLogsLoading = false;
    });

    // Fetch messages
    setState(() {
      isMessagesLoading = true;
      messagesError = null;
    });
    try {
      debugPrint("👉🏻 Calling getMessages");
      var fetchedMessages = await ContactsGetter().getMessages(
        limit: 10,
        orderByDesc: true,
      );
      debugPrint("👉🏻 Messages: $fetchedMessages");
      setState(() {
        messages = fetchedMessages;
      });
      for (var message in fetchedMessages) {
        debugPrint(
          "👉🏻 Message: ${message.address}, ${message.type}, ${message.body}, ${message.date}",
        );
      }
    } catch (e) {
      debugPrint("👉🏻 Error getting messages: $e");
      setState(() {
        messagesError = "Error getting messages: $e";
      });
      ScaffoldMessenger.of(
        context,
      ).showSnackBar(SnackBar(content: Text("Error getting messages: $e")));
    }
    setState(() {
      isMessagesLoading = false;
    });

    // Fetch accounts and SIM cards
    setState(() {
      isOthersLoading = true;
      othersError = null;
    });
    try {
      debugPrint("👉🏻 Calling getDeviceAccounts");
      var fetchedAccounts = await ContactsGetter().getDeviceAccounts();
      debugPrint("👉🏻 Accounts: $fetchedAccounts");

      debugPrint("👉🏻 Calling getSimCards");
      var fetchedSimCards = await ContactsGetter().getSimCards();
      debugPrint("👉🏻 SIM Cards: $fetchedSimCards");

      setState(() {
        accounts = fetchedAccounts;
        simCards = fetchedSimCards;
      });
    } catch (e) {
      debugPrint("👉🏻 Error getting accounts/SIMs: $e");
      setState(() {
        othersError = "Error getting accounts/SIMs: $e";
      });
    }
    setState(() {
      isOthersLoading = false;
    });
  }

  // Adds a new contact using name and phone number from a dialog
  Future<void> _addContact() async {
    // Check if WRITE_CONTACTS permission is granted
    bool hasPermissions = await _requestPermissions();
    if (!mounted) return;
    if (!hasPermissions) {
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text("Please grant all required permissions")),
      );
      return;
    }

    // Controllers for the dialog input fields
    final nameController = TextEditingController();
    final phoneController = TextEditingController();
    bool isNameValid = true;
    bool isPhoneValid = true;

    // Show dialog to input contact details
    await showDialog(
      context: context,
      builder: (context) => StatefulBuilder(
        builder: (context, setDialogState) => AlertDialog(
          shape: RoundedRectangleBorder(
            borderRadius: BorderRadius.circular(12),
          ),
          title: const Text("Add New Contact"),
          content: Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              TextField(
                controller: nameController,
                decoration: InputDecoration(
                  labelText: "Name",
                  prefixIcon: const Icon(Icons.person, color: Colors.indigo),
                  errorText: isNameValid ? null : "Name is required",
                ),
                onChanged: (value) {
                  setDialogState(() {
                    isNameValid = value.trim().isNotEmpty;
                  });
                },
              ),
              const SizedBox(height: 12),
              TextField(
                controller: phoneController,
                decoration: InputDecoration(
                  labelText: "Phone Number",
                  prefixIcon: const Icon(Icons.phone, color: Colors.indigo),
                  errorText: isPhoneValid ? null : "Phone number is required",
                ),
                keyboardType: TextInputType.phone,
                onChanged: (value) {
                  setDialogState(() {
                    isPhoneValid = value.trim().isNotEmpty;
                  });
                },
              ),
            ],
          ),
          actions: [
            TextButton(
              onPressed: () => Navigator.pop(context),
              child: const Text("Cancel"),
            ),
            FilledButton(
              onPressed: () {
                setDialogState(() {
                  isNameValid = nameController.text.trim().isNotEmpty;
                  isPhoneValid = phoneController.text.trim().isNotEmpty;
                });
                if (isNameValid && isPhoneValid) {
                  Navigator.pop(context, true);
                }
              },
              child: const Text("Add"),
            ),
          ],
        ),
      ),
    );

    // If user confirms, add the contact
    if (nameController.text.trim().isNotEmpty &&
        phoneController.text.trim().isNotEmpty) {
      setState(() {
        isContactsLoading = true;
      });
      try {
        final success = await ContactsGetter().addContact(
          name: nameController.text.trim(),
          phoneNumber: phoneController.text.trim(),
        );
        debugPrint("👉🏻 Add contact result: $success");
        if (success) {
          if (!mounted) return;
          await _fetchData();
          if (!mounted) return;
          ScaffoldMessenger.of(context).showSnackBar(
            const SnackBar(content: Text("Contact added successfully")),
          );
        } else {
          ScaffoldMessenger.of(context).showSnackBar(
            const SnackBar(content: Text("Failed to add contact")),
          );
        }
      } catch (e) {
        debugPrint("👉🏻 Error adding contact: $e");
        if (!mounted) return;
        ScaffoldMessenger.of(
          context,
        ).showSnackBar(SnackBar(content: Text("Error adding contact: $e")));
      }
      setState(() {
        isContactsLoading = false;
      });
    }

    // Dispose controllers to prevent memory leaks
    nameController.dispose();
    phoneController.dispose();
  }

  // Deletes a contact by ID and refreshes the UI
  Future<void> _deleteContact(String contactId) async {
    // Check if WRITE_CONTACTS permission is granted
    bool hasPermissions = await _requestPermissions();
    if (!mounted) return;
    if (!hasPermissions) {
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text("Please grant all required permissions")),
      );
      return;
    }

    setState(() {
      isContactsLoading = true;
    });
    try {
      final success = await ContactsGetter().deleteContact(
        contactId: contactId,
      );
      debugPrint("👉🏻 Delete contact result: $success");
      if (success) {
        await _fetchData();
        ScaffoldMessenger.of(context).showSnackBar(
          const SnackBar(content: Text("Contact deleted successfully")),
        );
      } else {
        ScaffoldMessenger.of(context).showSnackBar(
          const SnackBar(content: Text("Failed to delete contact")),
        );
      }
    } catch (e) {
      debugPrint("👉🏻 Error deleting contact: $e");
      if (!mounted) return;
      ScaffoldMessenger.of(
        context,
      ).showSnackBar(SnackBar(content: Text("Error deleting contact: $e")));
    }
    setState(() {
      isContactsLoading = false;
    });
  }

  // Clears all call logs and refreshes the UI
  Future<void> _clearCallLogs() async {
    // Check if WRITE_CALL_LOG permission is granted
    bool hasPermissions = await _requestPermissions();
    if (!mounted) return;
    if (!hasPermissions) {
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text("Please grant all required permissions")),
      );
      return;
    }

    setState(() {
      isCallLogsLoading = true;
    });
    try {
      final success = await ContactsGetter().clearCallLogs();
      debugPrint("👉🏻 Clear call logs result: $success");
      if (success) {
        await _fetchData();
        ScaffoldMessenger.of(context).showSnackBar(
          const SnackBar(content: Text("Call logs cleared successfully")),
        );
      } else {
        ScaffoldMessenger.of(context).showSnackBar(
          const SnackBar(content: Text("Failed to clear call logs")),
        );
      }
    } catch (e) {
      debugPrint("👉🏻 Error clearing call logs: $e");
      if (!mounted) return;
      ScaffoldMessenger.of(
        context,
      ).showSnackBar(SnackBar(content: Text("Error clearing call logs: $e")));
    }
    setState(() {
      isCallLogsLoading = false;
    });
  }

  // Builds a card for each contact with animation
  Widget _buildContactCard(Contact contact, int index) {
    return Card(
      child: ListTile(
        leading: CircleAvatar(
          backgroundColor: Colors.indigo.withValues(alpha: 0.1),
          child: const Icon(Icons.person, color: Colors.indigo),
        ),
        title: Text(
          contact.displayName.isEmpty ? 'Unknown' : contact.displayName,
          style: const TextStyle(fontWeight: FontWeight.w600),
        ),
        subtitle: Text(
          contact.phoneNumber.isEmpty ? 'No phone' : contact.phoneNumber,
        ),
        trailing: IconButton(
          icon: const Icon(Icons.delete, color: Colors.redAccent),
          onPressed: () => _deleteContact(contact.id),
        ),
      ),
    );
  }

  // Builds a card for each call log with animation
  Widget _buildCallLogCard(CallLog call, int index) {
    final date = DateTime.parse(call.date);
    final formattedDate = DateFormat('MMM dd, yyyy HH:mm').format(date);
    IconData icon;
    Color iconColor;
    switch (call.type) {
      case 'incoming':
        icon = Icons.call_received;
        iconColor = Colors.green;
        break;
      case 'outgoing':
        icon = Icons.call_made;
        iconColor = Colors.blue;
        break;
      case 'missed':
        icon = Icons.call_missed;
        iconColor = Colors.red;
        break;
      default:
        icon = Icons.call;
        iconColor = Colors.grey;
    }
    return Card(
      child: ListTile(
        leading: CircleAvatar(
          backgroundColor: iconColor.withValues(alpha: 0.1),
          child: Icon(icon, color: iconColor),
        ),
        title: Text(
          call.number.isEmpty ? 'Unknown' : call.number,
          style: const TextStyle(fontWeight: FontWeight.w600),
        ),
        subtitle: Text('$formattedDate • ${call.duration}s'),
      ),
    );
  }

  // Builds a card for each message with animation
  Widget _buildMessageCard(Message message, int index) {
    final date = DateTime.parse(message.date);
    final formattedDate = DateFormat('MMM dd, yyyy HH:mm').format(date);
    final body = message.body.length > 50
        ? '${message.body.substring(0, 50)}...'
        : message.body;
    return Card(
      child: ListTile(
        leading: CircleAvatar(
          backgroundColor: (message.type == 'sent' ? Colors.blue : Colors.green)
              .withValues(alpha: 0.1),
          child: Icon(
            message.type == 'sent' ? Icons.send : Icons.message,
            color: message.type == 'sent' ? Colors.blue : Colors.green,
          ),
        ),
        title: Text(
          message.address.isEmpty ? 'Unknown' : message.address,
          style: const TextStyle(fontWeight: FontWeight.w600),
        ),
        subtitle: Text('$body\n$formattedDate'),
      ),
    );
  }

  // Builds an empty state widget with an icon and message
  Widget _buildEmptyState(String message, IconData icon) {
    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Icon(icon, size: 64, color: Colors.grey.withValues(alpha: 0.5)),
          const SizedBox(height: 16),
          Text(
            message,
            style: TextStyle(fontSize: 18, color: Colors.grey[600]),
            textAlign: TextAlign.center,
          ),
        ],
      ),
    );
  }

  // Builds an error state widget with a retry button
  Widget _buildErrorState(String error, VoidCallback onRetry) {
    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          const Icon(Icons.error_outline, size: 64, color: Colors.redAccent),
          const SizedBox(height: 16),
          Text(
            error,
            style: TextStyle(fontSize: 18, color: Colors.grey[600]),
            textAlign: TextAlign.center,
          ),
          const SizedBox(height: 16),
          ElevatedButton.icon(
            onPressed: onRetry,
            icon: const Icon(Icons.refresh),
            label: const Text("Retry"),
          ),
        ],
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("Contacts Demo"),
        bottom: TabBar(
          controller: _tabController,
          indicatorColor: Colors.white,
          labelColor: Colors.white,
          unselectedLabelColor: Colors.white70,
          tabs: const [
            Tab(text: "Contacts", icon: Icon(Icons.contacts)),
            Tab(text: "Call Logs", icon: Icon(Icons.call)),
            Tab(text: "Messages", icon: Icon(Icons.message)),
            Tab(text: "Accounts & SIM", icon: Icon(Icons.account_box)),
          ],
        ),
      ),
      floatingActionButton: _tabController.index == 0
          ? FloatingActionButton(
              onPressed: _addContact,
              tooltip: "Add Contact",
              child: const Icon(Icons.person_add),
            )
          : null,
      body: Stack(
        children: [
          TabBarView(
            controller: _tabController,
            children: [
              // Contacts tab
              Stack(
                children: [
                  if (contactsError != null)
                    _buildErrorState(contactsError!, _fetchData)
                  else if (contacts.isEmpty && !isContactsLoading)
                    _buildEmptyState("No contacts found", Icons.contacts)
                  else
                    AnimatedList(
                      key: _contactsListKey,
                      initialItemCount: contacts.length,
                      padding: const EdgeInsets.only(top: 8, bottom: 80),
                      itemBuilder: (context, index, animation) =>
                          SlideTransition(
                            position: Tween<Offset>(
                              begin: const Offset(1, 0),
                              end: Offset.zero,
                            ).animate(animation),
                            child: _buildContactCard(contacts[index], index),
                          ),
                    ),
                  if (isContactsLoading)
                    Container(
                      color: Colors.black.withValues(alpha: 0.3),
                      child: const Center(child: CircularProgressIndicator()),
                    ),
                ],
              ),
              // Call Logs tab
              Stack(
                children: [
                  Column(
                    children: [
                      Padding(
                        padding: const EdgeInsets.all(12.0),
                        child: ElevatedButton.icon(
                          onPressed: _clearCallLogs,
                          icon: const Icon(Icons.delete_sweep),
                          label: const Text("Clear Call Logs"),
                          style: ElevatedButton.styleFrom(
                            backgroundColor: Colors.redAccent,
                            foregroundColor: Colors.white,
                            minimumSize: const Size(double.infinity, 48),
                          ),
                        ),
                      ),
                      Expanded(
                        child: callLogsError != null
                            ? _buildErrorState(callLogsError!, _fetchData)
                            : callLogs.isEmpty && !isCallLogsLoading
                            ? _buildEmptyState("No call logs found", Icons.call)
                            : AnimatedList(
                                key: _callLogsListKey,
                                initialItemCount: callLogs.length,
                                padding: const EdgeInsets.only(bottom: 80),
                                itemBuilder: (context, index, animation) =>
                                    SlideTransition(
                                      position: Tween<Offset>(
                                        begin: const Offset(1, 0),
                                        end: Offset.zero,
                                      ).animate(animation),
                                      child: _buildCallLogCard(
                                        callLogs[index],
                                        index,
                                      ),
                                    ),
                              ),
                      ),
                    ],
                  ),
                  if (isCallLogsLoading)
                    Container(
                      color: Colors.black.withValues(alpha: 0.3),
                      child: const Center(child: CircularProgressIndicator()),
                    ),
                ],
              ),
              // Messages tab
              Stack(
                children: [
                  messagesError != null
                      ? _buildErrorState(messagesError!, _fetchData)
                      : messages.isEmpty && !isMessagesLoading
                      ? _buildEmptyState("No messages found", Icons.message)
                      : AnimatedList(
                          key: _messagesListKey,
                          initialItemCount: messages.length,
                          padding: const EdgeInsets.only(top: 8, bottom: 80),
                          itemBuilder: (context, index, animation) =>
                              SlideTransition(
                                position: Tween<Offset>(
                                  begin: const Offset(1, 0),
                                  end: Offset.zero,
                                ).animate(animation),
                                child: _buildMessageCard(
                                  messages[index],
                                  index,
                                ),
                              ),
                        ),
                  if (isMessagesLoading)
                    Container(
                      color: Colors.black.withValues(alpha: 0.3),
                      child: const Center(child: CircularProgressIndicator()),
                    ),
                ],
              ),
              // Accounts & SIM tab
              Stack(
                children: [
                  if (othersError != null)
                    _buildErrorState(othersError!, _fetchData)
                  else
                    SingleChildScrollView(
                      padding: const EdgeInsets.all(16),
                      child: Column(
                        crossAxisAlignment: CrossAxisAlignment.start,
                        children: [
                          const Text(
                            "Google Accounts",
                            style: TextStyle(
                              fontSize: 20,
                              fontWeight: FontWeight.bold,
                            ),
                          ),
                          const SizedBox(height: 8),
                          if (accounts.isEmpty)
                            const Text("No Google accounts found")
                          else
                            ...accounts.map(
                              (acc) => Card(
                                child: ListTile(
                                  leading: const Icon(Icons.email),
                                  title: Text(acc.name),
                                  subtitle: Text(acc.type),
                                ),
                              ),
                            ),
                          const SizedBox(height: 24),
                          const Text(
                            "SIM Card Information",
                            style: TextStyle(
                              fontSize: 20,
                              fontWeight: FontWeight.bold,
                            ),
                          ),
                          const SizedBox(height: 8),
                          if (simCards.isEmpty)
                            const Text("No SIM card info found")
                          else
                            ...simCards.map(
                              (sim) => Card(
                                child: ListTile(
                                  leading: const Icon(Icons.sim_card),
                                  title: Text(
                                    sim['number']?.isEmpty ?? true
                                        ? "Unknown Number"
                                        : sim['number'],
                                  ),
                                  subtitle: Text(
                                    "${sim['carrierName']} (Slot ${sim['slotIndex']})",
                                  ),
                                  trailing: Text(sim['countryIso'] ?? ""),
                                ),
                              ),
                            ),
                        ],
                      ),
                    ),
                  if (isOthersLoading)
                    Container(
                      color: Colors.black.withValues(alpha: 0.3),
                      child: const Center(child: CircularProgressIndicator()),
                    ),
                ],
              ),
            ],
          ),
        ],
      ),
    );
  }
}
0
likes
130
points
162
downloads
screenshot

Publisher

unverified uploader

Weekly Downloads

powerful Flutter plugin for contacts, call logs, SMS, and device information on Android and iOS. Auto-requests permissions with clear error messages.

Repository (GitHub)
View/report issues

Topics

#contacts #sms #call #device-info #whatsapp

Documentation

Documentation
API reference

License

MIT (license)

Dependencies

flutter, permission_handler, plugin_platform_interface

More

Packages that depend on contacts_getter

Packages that implement contacts_getter