refactor: convert ContactPage to StatelessWidget and optimize AlphabetScrollPage for better performance
All checks were successful
/ mirror (push) Successful in 4s
/ build (push) Successful in 10m17s
/ build-stealth (push) Successful in 10m13s

This commit is contained in:
AlexisDanlos 2025-04-05 02:24:56 +02:00
parent 1a020bbbd8
commit 00e7d850b0
7 changed files with 190 additions and 144 deletions

View File

@ -2,15 +2,9 @@ import 'package:flutter/material.dart';
import '../contacts/contact_state.dart'; import '../contacts/contact_state.dart';
import '../contacts/widgets/alphabet_scroll_page.dart'; import '../contacts/widgets/alphabet_scroll_page.dart';
class ContactPage extends StatefulWidget { class ContactPage extends StatelessWidget {
const ContactPage({super.key}); const ContactPage({super.key});
@override
_ContactPageState createState() => _ContactPageState();
}
class _ContactPageState extends State<ContactPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final contactState = ContactState.of(context); final contactState = ContactState.of(context);

View File

@ -24,12 +24,34 @@ class AlphabetScrollPage extends StatefulWidget {
class _AlphabetScrollPageState extends State<AlphabetScrollPage> { class _AlphabetScrollPageState extends State<AlphabetScrollPage> {
late ScrollController _scrollController; late ScrollController _scrollController;
final ObfuscateService _obfuscateService = ObfuscateService(); final ObfuscateService _obfuscateService = ObfuscateService();
late Map<String, List<Contact>> _alphabetizedContacts;
late List<String> _alphabetKeys;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_scrollController = ScrollController(initialScrollOffset: widget.scrollOffset); _scrollController = ScrollController(initialScrollOffset: widget.scrollOffset);
_scrollController.addListener(_onScroll); _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() { void _onScroll() {
@ -49,7 +71,7 @@ class _AlphabetScrollPageState extends State<AlphabetScrollPage> {
} }
} }
void _toggleFavorite(Contact contact) async { Future<void> _toggleFavorite(Contact contact) async {
try { try {
if (await FlutterContacts.requestPermission()) { if (await FlutterContacts.requestPermission()) {
Contact? fullContact = await FlutterContacts.getContact(contact.id, Contact? fullContact = await FlutterContacts.getContact(contact.id,
@ -62,36 +84,29 @@ class _AlphabetScrollPageState extends State<AlphabetScrollPage> {
fullContact.isStarred = !fullContact.isStarred; fullContact.isStarred = !fullContact.isStarred;
await FlutterContacts.updateContact(fullContact); await FlutterContacts.updateContact(fullContact);
} }
await _refreshContacts();
// Check if widget is still mounted before calling functions that use context
if (mounted) {
await _refreshContacts();
}
} else { } else {
debugPrint("Could not fetch contact details"); debugPrint("Could not fetch contact details");
} }
} catch (e) { } catch (e) {
debugPrint("Error updating favorite status: $e"); debugPrint("Error updating favorite status: $e");
ScaffoldMessenger.of(context).showSnackBar( // Only show snackbar if still mounted
SnackBar(content: Text('Failed to update contact favorite status')), if (mounted) {
); ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Failed to update contact favorite status')),
);
}
} }
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final contacts = widget.contacts;
final selfContact = ContactState.of(context).selfContact; final selfContact = ContactState.of(context).selfContact;
Map<String, List<Contact>> 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<String> alphabetKeys = alphabetizedContacts.keys.toList()..sort();
return Scaffold( return Scaffold(
backgroundColor: Colors.black, backgroundColor: Colors.black,
body: Column( body: Column(
@ -104,7 +119,7 @@ class _AlphabetScrollPageState extends State<AlphabetScrollPage> {
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
AddContactButton(), AddContactButton(),
QRCodeButton(contacts: contacts, selfContact: selfContact), QRCodeButton(contacts: widget.contacts, selfContact: selfContact),
], ],
), ),
), ),
@ -112,94 +127,11 @@ class _AlphabetScrollPageState extends State<AlphabetScrollPage> {
Expanded( Expanded(
child: ListView.builder( child: ListView.builder(
controller: _scrollController, controller: _scrollController,
itemCount: alphabetKeys.length, itemCount: _alphabetKeys.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
String letter = alphabetKeys[index]; String letter = _alphabetKeys[index];
List<Contact> contactsForLetter = alphabetizedContacts[letter]!; List<Contact> contactsForLetter = _alphabetizedContacts[letter]!;
return Column( return _buildLetterSection(letter, contactsForLetter);
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,
);
},
);
},
);
}),
],
);
}, },
), ),
), ),
@ -208,6 +140,90 @@ class _AlphabetScrollPageState extends State<AlphabetScrollPage> {
); );
} }
Widget _buildLetterSection(String letter, List<Contact> 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<void> _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 @override
void dispose() { void dispose() {
_scrollController.dispose(); _scrollController.dispose();

View File

@ -334,7 +334,9 @@ class _ContactModalState extends State<ContactModal> {
width: double.infinity, width: double.infinity,
child: ElevatedButton.icon( child: ElevatedButton.icon(
onPressed: () { onPressed: () {
// First close the modal to avoid unmounted widget issues
Navigator.of(context).pop(); Navigator.of(context).pop();
// Then toggle the favorite status
widget.onToggleFavorite(); widget.onToggleFavorite();
}, },
icon: Icon(widget.isFavorite icon: Icon(widget.isFavorite

View File

@ -20,7 +20,10 @@ class _CompositionPageState extends State<CompositionPage> {
final ContactService _contactService = ContactService(); final ContactService _contactService = ContactService();
final ObfuscateService _obfuscateService = ObfuscateService(); final ObfuscateService _obfuscateService = ObfuscateService();
final CallService _callService = CallService(); final CallService _callService = CallService();
// Cache for normalized phone numbers to avoid repeated processing
final Map<String, String> _normalizedPhoneCache = {};
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@ -33,15 +36,35 @@ class _CompositionPageState extends State<CompositionPage> {
setState(() {}); setState(() {});
} }
String _getNormalizedPhone(String phone) {
return _normalizedPhoneCache.putIfAbsent(
phone,
() => phone.replaceAll(RegExp(r'\D'), '')
);
}
void _filterContacts() { void _filterContacts() {
if (dialedNumber.isEmpty) {
setState(() {
_filteredContacts = _allContacts;
});
return;
}
final String normalizedDialed = dialedNumber.replaceAll(RegExp(r'\D'), '');
final String lowerDialed = dialedNumber.toLowerCase();
setState(() { setState(() {
_filteredContacts = _allContacts.where((contact) { _filteredContacts = _allContacts.where((contact) {
// Check phone numbers
final phoneMatch = contact.phones.any((phone) => final phoneMatch = contact.phones.any((phone) =>
phone.number.replaceAll(RegExp(r'\D'), '').contains(dialedNumber)); _getNormalizedPhone(phone.number).contains(normalizedDialed));
final nameMatch = contact.displayName
.toLowerCase() // Only check name if phone doesn't match (optimization)
.contains(dialedNumber.toLowerCase()); if (phoneMatch) return true;
return phoneMatch || nameMatch;
// Check display name
return contact.displayName.toLowerCase().contains(lowerDialed);
}).toList(); }).toList();
}); });
} }

View File

@ -10,27 +10,28 @@ class FavoritesPage extends StatelessWidget {
final contactState = ContactState.of(context); final contactState = ContactState.of(context);
if (contactState.loading) { if (contactState.loading) {
return const Center(child: CircularProgressIndicator()); return const Scaffold(
backgroundColor: Colors.black,
body: Center(child: CircularProgressIndicator()),
);
} }
final favorites = contactState.favoriteContacts; 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( return Scaffold(
backgroundColor: Colors.black, backgroundColor: Colors.black,
body: AlphabetScrollPage( body: favorites.isEmpty
scrollOffset: contactState.scrollOffset, ? const Center(
contacts: favorites, 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,
),
); );
} }
} }

View File

@ -79,14 +79,19 @@ class _HistoryPageState extends State<HistoryPage>
fullContact.isStarred = !fullContact.isStarred; fullContact.isStarred = !fullContact.isStarred;
await FlutterContacts.updateContact(fullContact); await FlutterContacts.updateContact(fullContact);
} }
await _refreshContacts(); // Check if still mounted before accessing context
if (mounted) {
await _refreshContacts();
}
} else { } else {
print("Could not fetch contact details"); debugPrint("Could not fetch contact details");
} }
} catch (e) { } catch (e) {
print("Error updating favorite status: $e"); debugPrint("Error updating favorite status: $e");
ScaffoldMessenger.of(context).showSnackBar( if (mounted) {
SnackBar(content: Text('Failed to update favorite status'))); ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Failed to update favorite status')));
}
} }
} }

View File

@ -107,14 +107,19 @@ class _MyHomePageState extends State<MyHomePage>
if (fullContact != null) { if (fullContact != null) {
fullContact.isStarred = !fullContact.isStarred; fullContact.isStarred = !fullContact.isStarred;
await FlutterContacts.updateContact(fullContact); await FlutterContacts.updateContact(fullContact);
_fetchContacts(); // Check if widget is still mounted before updating state
if (mounted) {
_fetchContacts();
}
} }
} }
} catch (e) { } catch (e) {
debugPrint("Error updating favorite status: $e"); debugPrint("Error updating favorite status: $e");
ScaffoldMessenger.of(context).showSnackBar( if (mounted) {
SnackBar(content: Text('Failed to update contact favorite status')), ScaffoldMessenger.of(context).showSnackBar(
); SnackBar(content: Text('Failed to update contact favorite status')),
);
}
} }
} }