In this blog, we will explore the process of fetching recently added, updated, and deleted contacts from the native side in a Flutter application. Our goal is to provide a seamless way to access recent contacts, enhancing the user experience by allowing users to quickly reach their most important connections. By eliminating the need for comparing contacts with stored data, we can directly retrieve the most recently added or modified contacts. This approach ensures that users always have the latest contact information at their fingertips, fetched efficiently and effectively.
What Is Recent Contact Retrieval?
Recent contact retrieval refers to the process of identifying and retrieving contacts that have been recently created, updated, or deleted on Android and iOS devices by leveraging their respective native APIs. This process focuses on utilizing platform-specific capabilities to bypass manual data comparison and directly access recent contact changes.
- Android: By using the native ContactsContract API, the focus is on querying the device's contact database to retrieve contacts based on their creation, modification, or deletion timestamps.
- iOS: Utilizing the Contacts framework (e.g., CNContactStore, CNChangeHistoryFetchRequest), this approach involves accessing the contact change history, including recently added, updated, and deleted contacts, without storing previous states or comparing old data.
The goal is to streamline the process of fetching the latest contact data efficiently and effectively, allowing apps to access real-time contact updates directly from the native APIs.
Let's start the implementation
Prerequisites
Android: Add the required permissions in AndroidManifest.xml (e.g., READ_CONTACTS).
iOS: Add permissions in Info.plist (e.g., NSContactsUsageDescription).
Here’s the adjusted content with a proper title and formatted for clarity:
Setting Up Permissions and Method Channels for Contact Access
To successfully fetch contacts in a Flutter application, follow these steps:
- Add Required Permissions: Ensure that you add the necessary permissions in both Android and iOS to access the contacts.
- Create Method Channel: Set up a method channel for native communication in both Android and iOS. This channel will handle communication between Flutter and the native side to retrieve the data.
- Implement Native Code: Write the required native code for both Android and iOS to perform the necessary operations.
- Retrieve Contacts: Use the method channel to retrieve contacts from the native side.
- Display Contacts: Finally, display the retrieved contacts on the Flutter side.
Note: It’s essential to request contact permission from the user before processing any actions related to contacts.
iOS implementation
iOS does not directly provide timestamps for contacts due to privacy concerns. Instead, it offers a class called CNChangeHistoryFetchRequest, which retrieves a list of contacts based on a starting token from the change history. Initially, this starting token is set to , allowing the retrieval of all contacts. To optimize future retrievals, it’s essential to store the starting token locally, enabling access only to contacts added or modified after the token was last updated.
Additionally, iOS provides CNChangeHistoryEvent, which includes information about contacts that have been deleted, updated, or added during the last active session of the app.
Each available data field in contacts is associated with different keys, allowing for precise data extraction. The method changeHistoryFetchResult is used to obtain recently modified, added, or deleted contacts based on the starting token. However, this method is not directly supported in Swift. To access it, you need to create a bridge header to write the necessary code in Objective-C, which can then be called from Swift.
To access the change history of contacts in Swift, you can use the following Objective-C code. First, you need to import the required Objective-C class in your bridge header file. This allows you to leverage the functionality provided by Objective-C in your Swift project.
ContactStoreWrapper.h
1#ifndef ContactStoreWrapper_h2#define ContactStoreWrapper_h345#endif /* ContactStoreWrapper_h */678// ContactStoreWrapper.h9#import <Foundation/Foundation.h>1011NS_ASSUME_NONNULL_BEGIN1213@class CNContactStore;14@class CNChangeHistoryFetchRequest;15@class CNFetchResult;1617@interface ContactStoreWrapper : NSObject18- (instancetype)initWithStore:(CNContactStore *)store NS_DESIGNATED_INITIALIZER;1920- (CNFetchResult *)changeHistoryFetchResult:(CNChangeHistoryFetchRequest *)request21 error:(NSError *__autoreleasing _Nullable * _Nullable)error;2223@end2425NS_ASSUME_NONNULL_END
ContactStoreWrapper.m
1#import "ContactStoreWrapper.h"2@import Contacts;34@interface ContactStoreWrapper ()5@property (nonatomic, strong) CNContactStore *store;6@end7@implementation ContactStoreWrapper89- (instancetype)init {10 return [self initWithStore:[[CNContactStore alloc] init]];11}12- (instancetype)initWithStore:(CNContactStore *)store {13 if (self = [super init]) {14 _store = store;15 }16 return self;17}1819- (CNFetchResult *)changeHistoryFetchResult:(CNChangeHistoryFetchRequest *)request20 error:(NSError *__autoreleasing _Nullable * _Nullable)error API_AVAILABLE(ios(13.0)){21 CNFetchResult *fetchResult = [self.store enumeratorForChangeHistoryFetchRequest:request error:error];22 return fetchResult;23}24@end25
Method for fetching the change history
1 /// This method will fetched recently modified or created contact2 /// First time it will fetched all the contacts and save the3 ///startingToken and again if user open the app it will fetch the contact ///duration between last open application45 @available(iOS 13.0, *)6 func fetchChanges() async -> [[String: Any?]] {7 let fetchHistoryRequest = CNChangeHistoryFetchRequest()8 let store = CNContactStore()9 fetchHistoryRequest.startingToken = savedToken10 fetchHistoryRequest.additionalContactKeyDescriptors =11 [CNContactFormatter.descriptorForRequiredKeys(for: .fullName) CNContactPhoneNumbersKey as CNKeyDescriptor]12 let wrapper = ContactStoreWrapper(store: store)13 var contacts: [[String: Any?]] = []14 await withCheckedContinuation { continuation in15 DispatchQueue.global(qos: .userInitiated).async {16 let result =17 wrapper.changeHistoryFetchResult(fetchHistoryRequest, error: nil)18 // Saving the result's token as stated in CNContactStore documentation19 Task { @MainActor in20 self.savedToken = result.currentHistoryToken21 }22 guard let enumerator = result.value as? NSEnumerator else { return }23 enumerator24 .compactMap { $0 as? CNChangeHistoryEvent }25 .forEach { event in26 if let addEvent = event as?27 CNChangeHistoryAddContactEvent {28 // Provides added contact here..29 } else if let updateEvent = event as?30 CNChangeHistoryUpdateContactEvent {31 // Provides updated contact here..32 } else if let deletedEvent = event as?33 CNChangeHistoryDeleteContactEvent {34 // Provides deleted contact here..35 }36 }37 continuation.resume()38 }39 }40 return contacts41 }424344private var savedToken: Data? {45 get {46 UserDefaults.standard.data(forKey: savedTokenUserDefaultsKey)47 }48 set {49 UserDefaults.standard.set(newValue, forKey: savedTokenUserDefaultsKey)50 }51 }
By using the above code, you can fetch the latest changed, updated, or deleted contacts with ease, without any extra effort.
Android implementation
In Android, we can fetch contacts using ContactsContract and ContentResolver.query. This allows us to access all types of contacts (added, updated, or deleted). Additionally, we can retrieve contact history over any duration, which is not possible in iOS. In Android, we fetch contacts by comparing their last modified timestamps, something that iOS does not provide.
To achieve similar behavior in Android, we store the app's open timestamp in shared preferences, eliminating the need to pass a timestamp when fetching contacts. When the app is opened for the first time, it will fetch all contacts by default, using a base timestamp of January 1, 1970. After all contacts are fetched, the timestamp will be updated to the current time. The next time the app is opened, it will fetch contacts created between the last stored timestamp and the current time, so we need to pass this timestamp from Flutter or any other platform.
During the first launch, we may receive multiple deleted contacts from the device's contact database. This is something we cannot handle initially, but we can adjust for it in a real-time application since there will be no need for recent contacts right after the app is installed. From the second launch onward, the system works as expected.
12private fun getContacts(context: Context, contentResolver: ContentResolver): List<Map<String, Any?>> {3val contacts = mutableListOf<Map<String, Any?>>()4al sharedPreferences = context.getSharedPreferences("MyPrefs", Context.MODE_PRIVATE)5val savedTimestamp = sharedPreferences.getLong("lastFetchedTimestamp", 0L)6val currentTimestamp = System.currentTimeMillis()7val uri = ContactsContract.Contacts.CONTENT_URI8val selection = "${ContactsContract.Contacts.CONTACT_LAST_UPDATED_TIMESTAMP} > ?"9 val selectionArgs = arrayOf(savedTimestamp.toString())10 val cursor = contentResolver.query(uri, null, selection, selectionArgs, null)1112cursor?.use {13val contactIdIndex = it.getColumnIndex(ContactsContract.Contacts._ID)14 val displayNameIndex = it.getColumnIndex(ContactsContract.Contacts.DISPLAY_NAME)15 val lastUpdatedIndex = it.getColumnIndex(ContactsContract.Contacts.CONTACT_LAST_UPDATED_TIMESTAMP)1617if (contactIdIndex >= 0 && displayNameIndex >= 0 && lastUpdatedIndex >= 0) {18 while (it.moveToNext()) {19 val contactId = it.getString(contactIdIndex)20 val lastUpdated = it.getLong(lastUpdatedIndex)2122 if (lastUpdated > savedTimestamp) {23 /// You can use contactId here or store contacts object by using all keys24 }25 }26 }27 }2829 // Fetch deleted contacts30val deletedContactsUri = ContactsContract.DeletedContacts.CONTENT_URI31 val deletedCursor = contentResolver.query(deletedContactsUri, null, null, null, null)32deletedCursor?.use {33val contactIdIndex = it.getColumnIndex(ContactsContract.DeletedContacts.CONTACT_ID)34val timestampIndex = it.getColumnIndex(ContactsContract.DeletedContacts.CONTACT_DELETED_TIMESTAMP)35 if (contactIdIndex >= 0 && timestampIndex >= 0) {36 while (it.moveToNext()) {37 val contactId = it.getString(contactIdIndex) ?: "deleted"38 val lastUpdated = it.getLong(timestampIndex)3940 if (lastUpdated > savedTimestamp) {41 /// You can get deleted contactId here..42 }43 }44 }45 }4647 // Save the new timestamp48 with(sharedPreferences.edit()) {49 putLong("lastFetchedTimestamp", currentTimestamp)50 apply()51 }5253 return contacts54 }
You can call these methods through the respective method channels and return the contact list as a response to your Flutter app.
Here are some screenshots from a demo application. Initially, it provides all contacts, but after updating any contact and restarting the application, it will only display the updated contact. Additionally, various contact data can be accessed, as shown in the alert.




Conclusion
By using native APIs in Android and iOS, you can efficiently retrieve recently added, updated, or deleted contacts in your Flutter app. Implementing these solutions through method channels ensures smooth communication between the native and Flutter layers, providing real-time access to contact changes and enhancing the user experience.
Reference URL

TN3149: Fetching Contacts change history events | Apple Developer Documentation
Learn how to fetch and process the most recent changes to the Contacts database.

CNChangeHistoryFetchRequest | Apple Developer Documentation
An object that specifies the criteria for fetching change history.

Contacts Provider | Identity | Android Developers


