refactor: convert ContactPage to StatelessWidget and optimize AlphabetScrollPage for better performance
This commit is contained in:
parent
1a020bbbd8
commit
00e7d850b0
@ -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);
|
||||||
|
@ -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();
|
||||||
|
@ -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
|
||||||
|
@ -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();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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')));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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')),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user