From 00e7d850b03f8433f328b011adde4888d25e8cf8 Mon Sep 17 00:00:00 2001 From: AlexisDanlos <91090088+AlexisDanlos@users.noreply.github.com> Date: Sat, 5 Apr 2025 02:24:56 +0200 Subject: [PATCH] refactor: convert ContactPage to StatelessWidget and optimize AlphabetScrollPage for better performance --- .../features/contacts/contact_page.dart | 8 +- .../widgets/alphabet_scroll_page.dart | 230 ++++++++++-------- .../contacts/widgets/contact_modal.dart | 2 + .../features/dialer/composition_page.dart | 35 ++- .../features/favorites/favorites_page.dart | 31 +-- .../features/history/history_page.dart | 15 +- .../presentation/features/home/home_page.dart | 13 +- 7 files changed, 190 insertions(+), 144 deletions(-) diff --git a/dialer/lib/presentation/features/contacts/contact_page.dart b/dialer/lib/presentation/features/contacts/contact_page.dart index bbbe935..bebaad3 100644 --- a/dialer/lib/presentation/features/contacts/contact_page.dart +++ b/dialer/lib/presentation/features/contacts/contact_page.dart @@ -2,15 +2,9 @@ import 'package:flutter/material.dart'; import '../contacts/contact_state.dart'; import '../contacts/widgets/alphabet_scroll_page.dart'; -class ContactPage extends StatefulWidget { +class ContactPage extends StatelessWidget { const ContactPage({super.key}); - @override - _ContactPageState createState() => _ContactPageState(); -} - -class _ContactPageState extends State { - @override Widget build(BuildContext context) { final contactState = ContactState.of(context); diff --git a/dialer/lib/presentation/features/contacts/widgets/alphabet_scroll_page.dart b/dialer/lib/presentation/features/contacts/widgets/alphabet_scroll_page.dart index 63ba9f4..c0af877 100644 --- a/dialer/lib/presentation/features/contacts/widgets/alphabet_scroll_page.dart +++ b/dialer/lib/presentation/features/contacts/widgets/alphabet_scroll_page.dart @@ -24,12 +24,34 @@ class AlphabetScrollPage extends StatefulWidget { class _AlphabetScrollPageState extends State { late ScrollController _scrollController; final ObfuscateService _obfuscateService = ObfuscateService(); + late Map> _alphabetizedContacts; + late List _alphabetKeys; @override void initState() { super.initState(); _scrollController = ScrollController(initialScrollOffset: widget.scrollOffset); _scrollController.addListener(_onScroll); + _organizeContacts(); + } + + @override + void didUpdateWidget(AlphabetScrollPage oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.contacts != widget.contacts) { + _organizeContacts(); + } + } + + void _organizeContacts() { + _alphabetizedContacts = {}; + for (var contact in widget.contacts) { + String firstLetter = contact.displayName.isNotEmpty + ? contact.displayName[0].toUpperCase() + : '#'; + (_alphabetizedContacts[firstLetter] ??= []).add(contact); + } + _alphabetKeys = _alphabetizedContacts.keys.toList()..sort(); } void _onScroll() { @@ -49,7 +71,7 @@ class _AlphabetScrollPageState extends State { } } - void _toggleFavorite(Contact contact) async { + Future _toggleFavorite(Contact contact) async { try { if (await FlutterContacts.requestPermission()) { Contact? fullContact = await FlutterContacts.getContact(contact.id, @@ -62,36 +84,29 @@ class _AlphabetScrollPageState extends State { fullContact.isStarred = !fullContact.isStarred; await FlutterContacts.updateContact(fullContact); } - await _refreshContacts(); + + // Check if widget is still mounted before calling functions that use context + if (mounted) { + await _refreshContacts(); + } } else { debugPrint("Could not fetch contact details"); } } catch (e) { debugPrint("Error updating favorite status: $e"); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Failed to update contact favorite status')), - ); + // Only show snackbar if still mounted + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Failed to update contact favorite status')), + ); + } } } @override Widget build(BuildContext context) { - final contacts = widget.contacts; final selfContact = ContactState.of(context).selfContact; - Map> alphabetizedContacts = {}; - for (var contact in contacts) { - String firstLetter = contact.displayName.isNotEmpty - ? contact.displayName[0].toUpperCase() - : '#'; - if (!alphabetizedContacts.containsKey(firstLetter)) { - alphabetizedContacts[firstLetter] = []; - } - alphabetizedContacts[firstLetter]!.add(contact); - } - - List alphabetKeys = alphabetizedContacts.keys.toList()..sort(); - return Scaffold( backgroundColor: Colors.black, body: Column( @@ -104,7 +119,7 @@ class _AlphabetScrollPageState extends State { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ AddContactButton(), - QRCodeButton(contacts: contacts, selfContact: selfContact), + QRCodeButton(contacts: widget.contacts, selfContact: selfContact), ], ), ), @@ -112,94 +127,11 @@ class _AlphabetScrollPageState extends State { Expanded( child: ListView.builder( controller: _scrollController, - itemCount: alphabetKeys.length, + itemCount: _alphabetKeys.length, itemBuilder: (context, index) { - String letter = alphabetKeys[index]; - List contactsForLetter = alphabetizedContacts[letter]!; - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Alphabet Letter Header - Padding( - padding: const EdgeInsets.symmetric( - vertical: 8.0, horizontal: 16.0), - child: Text( - letter, - style: const TextStyle( - fontSize: 28, - fontWeight: FontWeight.bold, - color: Colors.white, - ), - ), - ), - // Contact Entries - ...contactsForLetter.map((contact) { - String phoneNumber = contact.phones.isNotEmpty - ? _obfuscateService.obfuscateData(contact.phones.first.number) - : 'No phone number'; - Color avatarColor = - generateColorFromName(contact.displayName); - return ListTile( - leading: ObfuscatedAvatar( - imageBytes: contact.thumbnail, - radius: 25, - backgroundColor: avatarColor, - fallbackInitial: contact.displayName, - ), - title: Text( - _obfuscateService.obfuscateData(contact.displayName), - style: const TextStyle(color: Colors.white), - ), - subtitle: Text( - phoneNumber, - style: const TextStyle(color: Colors.white70), - ), - onTap: () { - showModalBottomSheet( - context: context, - isScrollControlled: true, - backgroundColor: Colors.transparent, - builder: (context) { - return ContactModal( - contact: contact, - onEdit: () async { - if (await FlutterContacts.requestPermission()) { - final updatedContact = - await FlutterContacts.openExternalEdit( - contact.id); - if (updatedContact != null) { - await _refreshContacts(); - Navigator.of(context).pop(); - ScaffoldMessenger.of(context) - .showSnackBar( - SnackBar( - content: Text( - '${contact.displayName} updated successfully!'), - ), - ); - } else { - ScaffoldMessenger.of(context) - .showSnackBar( - SnackBar( - content: - Text('Edit canceled or failed.'), - ), - ); - } - } - }, - onToggleFavorite: () { - _toggleFavorite(contact); - }, - isFavorite: contact.isStarred, - ); - }, - ); - }, - ); - }), - ], - ); + String letter = _alphabetKeys[index]; + List contactsForLetter = _alphabetizedContacts[letter]!; + return _buildLetterSection(letter, contactsForLetter); }, ), ), @@ -208,6 +140,90 @@ class _AlphabetScrollPageState extends State { ); } + Widget _buildLetterSection(String letter, List contactsForLetter) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Alphabet Letter Header + Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0), + child: Text( + letter, + style: const TextStyle( + fontSize: 28, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + ), + // Contact Entries + ...contactsForLetter.map((contact) => _buildContactTile(contact)), + ], + ); + } + + Widget _buildContactTile(Contact contact) { + String phoneNumber = contact.phones.isNotEmpty + ? _obfuscateService.obfuscateData(contact.phones.first.number) + : 'No phone number'; + Color avatarColor = generateColorFromName(contact.displayName); + + return ListTile( + leading: ObfuscatedAvatar( + imageBytes: contact.thumbnail, + radius: 25, + backgroundColor: avatarColor, + fallbackInitial: contact.displayName, + ), + title: Text( + _obfuscateService.obfuscateData(contact.displayName), + style: const TextStyle(color: Colors.white), + ), + subtitle: Text( + phoneNumber, + style: const TextStyle(color: Colors.white70), + ), + onTap: () => _showContactModal(contact), + ); + } + + void _showContactModal(Contact contact) { + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (context) { + return ContactModal( + contact: contact, + onEdit: () => _onEditContact(contact), + onToggleFavorite: () => _toggleFavorite(contact), + isFavorite: contact.isStarred, + ); + }, + ); + } + + Future _onEditContact(Contact contact) async { + if (await FlutterContacts.requestPermission()) { + final updatedContact = await FlutterContacts.openExternalEdit(contact.id); + if (updatedContact != null) { + await _refreshContacts(); + Navigator.of(context).pop(); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('${contact.displayName} updated successfully!'), + ), + ); + } else { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Edit canceled or failed.'), + ), + ); + } + } + } + @override void dispose() { _scrollController.dispose(); diff --git a/dialer/lib/presentation/features/contacts/widgets/contact_modal.dart b/dialer/lib/presentation/features/contacts/widgets/contact_modal.dart index 21109ec..a0a1dce 100644 --- a/dialer/lib/presentation/features/contacts/widgets/contact_modal.dart +++ b/dialer/lib/presentation/features/contacts/widgets/contact_modal.dart @@ -334,7 +334,9 @@ class _ContactModalState extends State { width: double.infinity, child: ElevatedButton.icon( onPressed: () { + // First close the modal to avoid unmounted widget issues Navigator.of(context).pop(); + // Then toggle the favorite status widget.onToggleFavorite(); }, icon: Icon(widget.isFavorite diff --git a/dialer/lib/presentation/features/dialer/composition_page.dart b/dialer/lib/presentation/features/dialer/composition_page.dart index b6c8b30..5f186c7 100644 --- a/dialer/lib/presentation/features/dialer/composition_page.dart +++ b/dialer/lib/presentation/features/dialer/composition_page.dart @@ -20,7 +20,10 @@ class _CompositionPageState extends State { final ContactService _contactService = ContactService(); final ObfuscateService _obfuscateService = ObfuscateService(); final CallService _callService = CallService(); - + + // Cache for normalized phone numbers to avoid repeated processing + final Map _normalizedPhoneCache = {}; + @override void initState() { super.initState(); @@ -33,15 +36,35 @@ class _CompositionPageState extends State { setState(() {}); } + String _getNormalizedPhone(String phone) { + return _normalizedPhoneCache.putIfAbsent( + phone, + () => phone.replaceAll(RegExp(r'\D'), '') + ); + } + void _filterContacts() { + if (dialedNumber.isEmpty) { + setState(() { + _filteredContacts = _allContacts; + }); + return; + } + + final String normalizedDialed = dialedNumber.replaceAll(RegExp(r'\D'), ''); + final String lowerDialed = dialedNumber.toLowerCase(); + setState(() { _filteredContacts = _allContacts.where((contact) { + // Check phone numbers final phoneMatch = contact.phones.any((phone) => - phone.number.replaceAll(RegExp(r'\D'), '').contains(dialedNumber)); - final nameMatch = contact.displayName - .toLowerCase() - .contains(dialedNumber.toLowerCase()); - return phoneMatch || nameMatch; + _getNormalizedPhone(phone.number).contains(normalizedDialed)); + + // Only check name if phone doesn't match (optimization) + if (phoneMatch) return true; + + // Check display name + return contact.displayName.toLowerCase().contains(lowerDialed); }).toList(); }); } diff --git a/dialer/lib/presentation/features/favorites/favorites_page.dart b/dialer/lib/presentation/features/favorites/favorites_page.dart index ca8d39f..712ece6 100644 --- a/dialer/lib/presentation/features/favorites/favorites_page.dart +++ b/dialer/lib/presentation/features/favorites/favorites_page.dart @@ -10,27 +10,28 @@ class FavoritesPage extends StatelessWidget { final contactState = ContactState.of(context); if (contactState.loading) { - return const Center(child: CircularProgressIndicator()); + return const Scaffold( + backgroundColor: Colors.black, + body: Center(child: CircularProgressIndicator()), + ); } final favorites = contactState.favoriteContacts; - if (favorites.isEmpty) { - return const Center( - child: Text( - 'No favorites yet.\nStar your contacts to add them here.', - style: TextStyle(color: Colors.white60), - textAlign: TextAlign.center, - ), - ); - } - return Scaffold( backgroundColor: Colors.black, - body: AlphabetScrollPage( - scrollOffset: contactState.scrollOffset, - contacts: favorites, - ), + body: favorites.isEmpty + ? const Center( + child: Text( + 'No favorites yet.\nStar your contacts to add them here.', + style: TextStyle(color: Colors.white60), + textAlign: TextAlign.center, + ), + ) + : AlphabetScrollPage( + scrollOffset: contactState.scrollOffset, + contacts: favorites, + ), ); } } diff --git a/dialer/lib/presentation/features/history/history_page.dart b/dialer/lib/presentation/features/history/history_page.dart index 317f0e2..e193119 100644 --- a/dialer/lib/presentation/features/history/history_page.dart +++ b/dialer/lib/presentation/features/history/history_page.dart @@ -79,14 +79,19 @@ class _HistoryPageState extends State fullContact.isStarred = !fullContact.isStarred; await FlutterContacts.updateContact(fullContact); } - await _refreshContacts(); + // Check if still mounted before accessing context + if (mounted) { + await _refreshContacts(); + } } else { - print("Could not fetch contact details"); + debugPrint("Could not fetch contact details"); } } catch (e) { - print("Error updating favorite status: $e"); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Failed to update favorite status'))); + debugPrint("Error updating favorite status: $e"); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Failed to update favorite status'))); + } } } diff --git a/dialer/lib/presentation/features/home/home_page.dart b/dialer/lib/presentation/features/home/home_page.dart index fb43fc6..77babf9 100644 --- a/dialer/lib/presentation/features/home/home_page.dart +++ b/dialer/lib/presentation/features/home/home_page.dart @@ -107,14 +107,19 @@ class _MyHomePageState extends State if (fullContact != null) { fullContact.isStarred = !fullContact.isStarred; await FlutterContacts.updateContact(fullContact); - _fetchContacts(); + // Check if widget is still mounted before updating state + if (mounted) { + _fetchContacts(); + } } } } catch (e) { debugPrint("Error updating favorite status: $e"); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Failed to update contact favorite status')), - ); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Failed to update contact favorite status')), + ); + } } }