perf: ajout d'un Debouncer 400ms sur toutes les barres de recherche
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
/// Utilitaire pour différer l'exécution d'une action après un délai.
|
||||
/// Utilisé principalement pour les champs de recherche afin d'éviter
|
||||
/// des requêtes à chaque frappe clavier.
|
||||
class Debouncer {
|
||||
final Duration delay;
|
||||
Timer? _timer;
|
||||
|
||||
Debouncer({this.delay = const Duration(milliseconds: 400)});
|
||||
|
||||
void call(VoidCallback action) {
|
||||
_timer?.cancel();
|
||||
_timer = Timer(delay, action);
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
_timer?.cancel();
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import 'package:em2rp/utils/debug_log.dart';
|
||||
import 'package:em2rp/utils/id_generator.dart';
|
||||
import 'package:em2rp/services/data_service.dart';
|
||||
import 'package:em2rp/services/api_service.dart';
|
||||
import 'package:em2rp/utils/debouncer.dart';
|
||||
|
||||
class ContainerFormPage extends StatefulWidget {
|
||||
final ContainerModel? container;
|
||||
@@ -658,6 +659,7 @@ class _EquipmentSelectorDialogState extends State<_EquipmentSelectorDialog> {
|
||||
EquipmentCategory? _filterCategory;
|
||||
String _searchQuery = '';
|
||||
late Set<String> _tempSelectedIds;
|
||||
final _searchDebouncer = Debouncer();
|
||||
|
||||
final List<EquipmentModel> _paginatedEquipments = [];
|
||||
bool _isLoadingMore = false;
|
||||
@@ -677,6 +679,7 @@ class _EquipmentSelectorDialogState extends State<_EquipmentSelectorDialog> {
|
||||
void dispose() {
|
||||
_searchController.dispose();
|
||||
_scrollController.dispose();
|
||||
_searchDebouncer.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -790,7 +793,7 @@ class _EquipmentSelectorDialogState extends State<_EquipmentSelectorDialog> {
|
||||
setState(() {
|
||||
_searchQuery = value;
|
||||
});
|
||||
_reloadData();
|
||||
_searchDebouncer(_reloadData);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
@@ -20,6 +20,7 @@ import 'package:em2rp/utils/equipment_delete_utils.dart';
|
||||
import 'package:em2rp/mixins/selection_mode_mixin.dart';
|
||||
import 'package:em2rp/views/widgets/common/search_actions_bar.dart';
|
||||
import 'package:em2rp/views/widgets/notification_badge.dart';
|
||||
import 'package:em2rp/utils/debouncer.dart';
|
||||
|
||||
class EquipmentManagementPage extends StatefulWidget {
|
||||
const EquipmentManagementPage({super.key});
|
||||
@@ -33,6 +34,7 @@ class _EquipmentManagementPageState extends State<EquipmentManagementPage>
|
||||
with SelectionModeMixin<EquipmentManagementPage> {
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
final ScrollController _scrollController = ScrollController();
|
||||
final _searchDebouncer = Debouncer();
|
||||
EquipmentCategory? _selectedCategory;
|
||||
List<EquipmentModel>? _cachedEquipment;
|
||||
bool _isLoadingMore = false; // Flag pour éviter les appels multiples
|
||||
@@ -87,6 +89,7 @@ class _EquipmentManagementPageState extends State<EquipmentManagementPage>
|
||||
_scrollController.removeListener(_onScroll);
|
||||
_scrollController.dispose();
|
||||
_searchController.dispose();
|
||||
_searchDebouncer.dispose();
|
||||
// Désactiver le mode pagination en quittant
|
||||
context.read<EquipmentProvider>().disablePagination();
|
||||
super.dispose();
|
||||
@@ -169,9 +172,7 @@ class _EquipmentManagementPageState extends State<EquipmentManagementPage>
|
||||
SearchActionsBar(
|
||||
controller: _searchController,
|
||||
hintText: 'Rechercher par nom, modèle ou ID...',
|
||||
onChanged: (value) {
|
||||
context.read<EquipmentProvider>().setSearchQuery(value);
|
||||
},
|
||||
onChanged: (value) => _searchDebouncer(() => context.read<EquipmentProvider>().setSearchQuery(value)),
|
||||
onClear: () {
|
||||
_searchController.clear();
|
||||
context.read<EquipmentProvider>().setSearchQuery('');
|
||||
@@ -342,9 +343,7 @@ class _EquipmentManagementPageState extends State<EquipmentManagementPage>
|
||||
SearchActionsBar(
|
||||
controller: _searchController,
|
||||
hintText: 'Rechercher par nom, modèle ou ID...',
|
||||
onChanged: (value) {
|
||||
context.read<EquipmentProvider>().setSearchQuery(value);
|
||||
},
|
||||
onChanged: (value) => _searchDebouncer(() => context.read<EquipmentProvider>().setSearchQuery(value)),
|
||||
onClear: () {
|
||||
_searchController.clear();
|
||||
context.read<EquipmentProvider>().setSearchQuery('');
|
||||
@@ -501,9 +500,11 @@ class _EquipmentManagementPageState extends State<EquipmentManagementPage>
|
||||
return ListView.builder(
|
||||
controller: _scrollController,
|
||||
itemCount: itemCount,
|
||||
// ✅ Ajouter une estimation de la hauteur pour améliorer le scroll
|
||||
// Note : À ajuster selon la hauteur réelle de vos cartes
|
||||
// itemExtent: 140, // Décommentez si toutes les cartes ont la même hauteur
|
||||
// ✅ prototypeItem utilisé car les cartes ont des hauteurs variables :
|
||||
// - Les équipements standards (ListTile + margin) font ~88px
|
||||
// - Les consommables/câbles affichent _buildQuantityDisplay en plus (~30px)
|
||||
// - prototypeItem permet à Flutter d'optimiser le scroll sans couper les items
|
||||
prototypeItem: const SizedBox(height: 88),
|
||||
// ✅ Augmenter le cache pour un scroll plus fluide
|
||||
cacheExtent: 500, // Précharger 500px en plus
|
||||
itemBuilder: (context, index) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import 'package:em2rp/utils/debug_log.dart';
|
||||
import 'package:em2rp/utils/debug_log.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:em2rp/models/equipment_model.dart';
|
||||
import 'package:em2rp/models/container_model.dart';
|
||||
@@ -7,6 +7,7 @@ import 'package:em2rp/services/event_availability_service.dart';
|
||||
import 'package:em2rp/services/data_service.dart';
|
||||
import 'package:em2rp/services/api_service.dart';
|
||||
import 'package:em2rp/utils/colors.dart';
|
||||
import 'package:em2rp/utils/debouncer.dart';
|
||||
|
||||
/// Type de s├®lection dans le dialog
|
||||
enum SelectionType { equipment, container }
|
||||
@@ -108,6 +109,7 @@ class _EquipmentSelectionDialogState extends State<EquipmentSelectionDialog> {
|
||||
|
||||
bool _isLoadingConflicts = false;
|
||||
String _searchQuery = '';
|
||||
final _searchDebouncer = Debouncer();
|
||||
|
||||
// Nouvelles options d'affichage
|
||||
bool _showConflictingItems = false; // Afficher les ├®quipements/bo├«tes en conflit
|
||||
@@ -435,6 +437,7 @@ class _EquipmentSelectionDialogState extends State<EquipmentSelectionDialog> {
|
||||
_searchController.dispose();
|
||||
_scrollController.dispose(); // Nettoyer le ScrollController
|
||||
_selectionChangeNotifier.dispose(); // Nettoyer le ValueNotifier
|
||||
_searchDebouncer.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -1209,7 +1212,7 @@ class _EquipmentSelectionDialogState extends State<EquipmentSelectionDialog> {
|
||||
onChanged: (value) {
|
||||
setState(() => _searchQuery = value.toLowerCase());
|
||||
// Recharger depuis le d├®but avec le nouveau filtre
|
||||
_reloadData();
|
||||
_searchDebouncer(_reloadData);
|
||||
},
|
||||
),
|
||||
|
||||
@@ -1379,12 +1382,12 @@ class _EquipmentSelectionDialogState extends State<EquipmentSelectionDialog> {
|
||||
// Containers
|
||||
final filteredContainers = _paginatedContainers.where((container) {
|
||||
if (!_showConflictingItems) {
|
||||
// V├®rifier si le container lui-m├¬me est en conflit
|
||||
// Vérifier si le container lui-même est en conflit
|
||||
if (_conflictingContainerIds.contains(container.id)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// V├®rifier si le container a des ├®quipements enfants en conflit
|
||||
// Vérifier si le container a des équipements enfants en conflit
|
||||
final hasConflictingChildren = container.equipmentIds.any(
|
||||
(eqId) => _conflictingEquipmentIds.contains(eqId),
|
||||
);
|
||||
@@ -1412,13 +1415,18 @@ class _EquipmentSelectionDialogState extends State<EquipmentSelectionDialog> {
|
||||
}
|
||||
return false;
|
||||
},
|
||||
child: ListView(
|
||||
controller: _scrollController,
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
// ✅ Construction de la liste complète avant le ListView.builder
|
||||
// pour permettre l'utilisation de ListView.builder (lazy rendering)
|
||||
// à la place de ListView(children:[]) qui construit tous les widgets
|
||||
// en mémoire d'un coup.
|
||||
// Pas d'itemExtent : les cards ont des hauteurs très variables
|
||||
// (80px sans conflit, jusqu'à 300px+ avec détails de conflits).
|
||||
final List<Widget> allChildren = [
|
||||
// Header
|
||||
_buildSectionHeader(
|
||||
_displayType == SelectionType.equipment ? 'Équipements' : 'Containers',
|
||||
_displayType == SelectionType.equipment ? 'Équipements' : 'Containers',
|
||||
_displayType == SelectionType.equipment ? Icons.inventory_2 : Icons.inventory,
|
||||
itemWidgets.length,
|
||||
),
|
||||
@@ -1448,7 +1456,7 @@ class _EquipmentSelectionDialogState extends State<EquipmentSelectionDialog> {
|
||||
),
|
||||
),
|
||||
|
||||
// Message si rien trouv├®
|
||||
// Message si rien trouvé
|
||||
if (itemWidgets.isEmpty && !_isLoadingMore)
|
||||
Center(
|
||||
child: Padding(
|
||||
@@ -1458,14 +1466,22 @@ class _EquipmentSelectionDialogState extends State<EquipmentSelectionDialog> {
|
||||
Icon(Icons.search_off, size: 64, color: Colors.grey.shade400),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Aucun r├®sultat trouv├®',
|
||||
'Aucun résultat trouvé',
|
||||
style: TextStyle(fontSize: 16, color: Colors.grey.shade600),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
];
|
||||
|
||||
return ListView.builder(
|
||||
controller: _scrollController,
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: allChildren.length,
|
||||
itemBuilder: (context, index) => allChildren[index],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user