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/utils/id_generator.dart';
|
||||||
import 'package:em2rp/services/data_service.dart';
|
import 'package:em2rp/services/data_service.dart';
|
||||||
import 'package:em2rp/services/api_service.dart';
|
import 'package:em2rp/services/api_service.dart';
|
||||||
|
import 'package:em2rp/utils/debouncer.dart';
|
||||||
|
|
||||||
class ContainerFormPage extends StatefulWidget {
|
class ContainerFormPage extends StatefulWidget {
|
||||||
final ContainerModel? container;
|
final ContainerModel? container;
|
||||||
@@ -658,6 +659,7 @@ class _EquipmentSelectorDialogState extends State<_EquipmentSelectorDialog> {
|
|||||||
EquipmentCategory? _filterCategory;
|
EquipmentCategory? _filterCategory;
|
||||||
String _searchQuery = '';
|
String _searchQuery = '';
|
||||||
late Set<String> _tempSelectedIds;
|
late Set<String> _tempSelectedIds;
|
||||||
|
final _searchDebouncer = Debouncer();
|
||||||
|
|
||||||
final List<EquipmentModel> _paginatedEquipments = [];
|
final List<EquipmentModel> _paginatedEquipments = [];
|
||||||
bool _isLoadingMore = false;
|
bool _isLoadingMore = false;
|
||||||
@@ -677,6 +679,7 @@ class _EquipmentSelectorDialogState extends State<_EquipmentSelectorDialog> {
|
|||||||
void dispose() {
|
void dispose() {
|
||||||
_searchController.dispose();
|
_searchController.dispose();
|
||||||
_scrollController.dispose();
|
_scrollController.dispose();
|
||||||
|
_searchDebouncer.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -790,7 +793,7 @@ class _EquipmentSelectorDialogState extends State<_EquipmentSelectorDialog> {
|
|||||||
setState(() {
|
setState(() {
|
||||||
_searchQuery = value;
|
_searchQuery = value;
|
||||||
});
|
});
|
||||||
_reloadData();
|
_searchDebouncer(_reloadData);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
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/mixins/selection_mode_mixin.dart';
|
||||||
import 'package:em2rp/views/widgets/common/search_actions_bar.dart';
|
import 'package:em2rp/views/widgets/common/search_actions_bar.dart';
|
||||||
import 'package:em2rp/views/widgets/notification_badge.dart';
|
import 'package:em2rp/views/widgets/notification_badge.dart';
|
||||||
|
import 'package:em2rp/utils/debouncer.dart';
|
||||||
|
|
||||||
class EquipmentManagementPage extends StatefulWidget {
|
class EquipmentManagementPage extends StatefulWidget {
|
||||||
const EquipmentManagementPage({super.key});
|
const EquipmentManagementPage({super.key});
|
||||||
@@ -33,6 +34,7 @@ class _EquipmentManagementPageState extends State<EquipmentManagementPage>
|
|||||||
with SelectionModeMixin<EquipmentManagementPage> {
|
with SelectionModeMixin<EquipmentManagementPage> {
|
||||||
final TextEditingController _searchController = TextEditingController();
|
final TextEditingController _searchController = TextEditingController();
|
||||||
final ScrollController _scrollController = ScrollController();
|
final ScrollController _scrollController = ScrollController();
|
||||||
|
final _searchDebouncer = Debouncer();
|
||||||
EquipmentCategory? _selectedCategory;
|
EquipmentCategory? _selectedCategory;
|
||||||
List<EquipmentModel>? _cachedEquipment;
|
List<EquipmentModel>? _cachedEquipment;
|
||||||
bool _isLoadingMore = false; // Flag pour éviter les appels multiples
|
bool _isLoadingMore = false; // Flag pour éviter les appels multiples
|
||||||
@@ -87,6 +89,7 @@ class _EquipmentManagementPageState extends State<EquipmentManagementPage>
|
|||||||
_scrollController.removeListener(_onScroll);
|
_scrollController.removeListener(_onScroll);
|
||||||
_scrollController.dispose();
|
_scrollController.dispose();
|
||||||
_searchController.dispose();
|
_searchController.dispose();
|
||||||
|
_searchDebouncer.dispose();
|
||||||
// Désactiver le mode pagination en quittant
|
// Désactiver le mode pagination en quittant
|
||||||
context.read<EquipmentProvider>().disablePagination();
|
context.read<EquipmentProvider>().disablePagination();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
@@ -169,9 +172,7 @@ class _EquipmentManagementPageState extends State<EquipmentManagementPage>
|
|||||||
SearchActionsBar(
|
SearchActionsBar(
|
||||||
controller: _searchController,
|
controller: _searchController,
|
||||||
hintText: 'Rechercher par nom, modèle ou ID...',
|
hintText: 'Rechercher par nom, modèle ou ID...',
|
||||||
onChanged: (value) {
|
onChanged: (value) => _searchDebouncer(() => context.read<EquipmentProvider>().setSearchQuery(value)),
|
||||||
context.read<EquipmentProvider>().setSearchQuery(value);
|
|
||||||
},
|
|
||||||
onClear: () {
|
onClear: () {
|
||||||
_searchController.clear();
|
_searchController.clear();
|
||||||
context.read<EquipmentProvider>().setSearchQuery('');
|
context.read<EquipmentProvider>().setSearchQuery('');
|
||||||
@@ -342,9 +343,7 @@ class _EquipmentManagementPageState extends State<EquipmentManagementPage>
|
|||||||
SearchActionsBar(
|
SearchActionsBar(
|
||||||
controller: _searchController,
|
controller: _searchController,
|
||||||
hintText: 'Rechercher par nom, modèle ou ID...',
|
hintText: 'Rechercher par nom, modèle ou ID...',
|
||||||
onChanged: (value) {
|
onChanged: (value) => _searchDebouncer(() => context.read<EquipmentProvider>().setSearchQuery(value)),
|
||||||
context.read<EquipmentProvider>().setSearchQuery(value);
|
|
||||||
},
|
|
||||||
onClear: () {
|
onClear: () {
|
||||||
_searchController.clear();
|
_searchController.clear();
|
||||||
context.read<EquipmentProvider>().setSearchQuery('');
|
context.read<EquipmentProvider>().setSearchQuery('');
|
||||||
@@ -501,9 +500,11 @@ class _EquipmentManagementPageState extends State<EquipmentManagementPage>
|
|||||||
return ListView.builder(
|
return ListView.builder(
|
||||||
controller: _scrollController,
|
controller: _scrollController,
|
||||||
itemCount: itemCount,
|
itemCount: itemCount,
|
||||||
// ✅ Ajouter une estimation de la hauteur pour améliorer le scroll
|
// ✅ prototypeItem utilisé car les cartes ont des hauteurs variables :
|
||||||
// Note : À ajuster selon la hauteur réelle de vos cartes
|
// - Les équipements standards (ListTile + margin) font ~88px
|
||||||
// itemExtent: 140, // Décommentez si toutes les cartes ont la même hauteur
|
// - 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
|
// ✅ Augmenter le cache pour un scroll plus fluide
|
||||||
cacheExtent: 500, // Précharger 500px en plus
|
cacheExtent: 500, // Précharger 500px en plus
|
||||||
itemBuilder: (context, index) {
|
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:flutter/material.dart';
|
||||||
import 'package:em2rp/models/equipment_model.dart';
|
import 'package:em2rp/models/equipment_model.dart';
|
||||||
import 'package:em2rp/models/container_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/data_service.dart';
|
||||||
import 'package:em2rp/services/api_service.dart';
|
import 'package:em2rp/services/api_service.dart';
|
||||||
import 'package:em2rp/utils/colors.dart';
|
import 'package:em2rp/utils/colors.dart';
|
||||||
|
import 'package:em2rp/utils/debouncer.dart';
|
||||||
|
|
||||||
/// Type de s├®lection dans le dialog
|
/// Type de s├®lection dans le dialog
|
||||||
enum SelectionType { equipment, container }
|
enum SelectionType { equipment, container }
|
||||||
@@ -108,6 +109,7 @@ class _EquipmentSelectionDialogState extends State<EquipmentSelectionDialog> {
|
|||||||
|
|
||||||
bool _isLoadingConflicts = false;
|
bool _isLoadingConflicts = false;
|
||||||
String _searchQuery = '';
|
String _searchQuery = '';
|
||||||
|
final _searchDebouncer = Debouncer();
|
||||||
|
|
||||||
// Nouvelles options d'affichage
|
// Nouvelles options d'affichage
|
||||||
bool _showConflictingItems = false; // Afficher les ├®quipements/bo├«tes en conflit
|
bool _showConflictingItems = false; // Afficher les ├®quipements/bo├«tes en conflit
|
||||||
@@ -435,6 +437,7 @@ class _EquipmentSelectionDialogState extends State<EquipmentSelectionDialog> {
|
|||||||
_searchController.dispose();
|
_searchController.dispose();
|
||||||
_scrollController.dispose(); // Nettoyer le ScrollController
|
_scrollController.dispose(); // Nettoyer le ScrollController
|
||||||
_selectionChangeNotifier.dispose(); // Nettoyer le ValueNotifier
|
_selectionChangeNotifier.dispose(); // Nettoyer le ValueNotifier
|
||||||
|
_searchDebouncer.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1209,7 +1212,7 @@ class _EquipmentSelectionDialogState extends State<EquipmentSelectionDialog> {
|
|||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
setState(() => _searchQuery = value.toLowerCase());
|
setState(() => _searchQuery = value.toLowerCase());
|
||||||
// Recharger depuis le d├®but avec le nouveau filtre
|
// Recharger depuis le d├®but avec le nouveau filtre
|
||||||
_reloadData();
|
_searchDebouncer(_reloadData);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
||||||
@@ -1379,12 +1382,12 @@ class _EquipmentSelectionDialogState extends State<EquipmentSelectionDialog> {
|
|||||||
// Containers
|
// Containers
|
||||||
final filteredContainers = _paginatedContainers.where((container) {
|
final filteredContainers = _paginatedContainers.where((container) {
|
||||||
if (!_showConflictingItems) {
|
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)) {
|
if (_conflictingContainerIds.contains(container.id)) {
|
||||||
return false;
|
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(
|
final hasConflictingChildren = container.equipmentIds.any(
|
||||||
(eqId) => _conflictingEquipmentIds.contains(eqId),
|
(eqId) => _conflictingEquipmentIds.contains(eqId),
|
||||||
);
|
);
|
||||||
@@ -1412,13 +1415,18 @@ class _EquipmentSelectionDialogState extends State<EquipmentSelectionDialog> {
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
child: ListView(
|
child: Builder(
|
||||||
controller: _scrollController,
|
builder: (context) {
|
||||||
padding: const EdgeInsets.all(16),
|
// ✅ Construction de la liste complète avant le ListView.builder
|
||||||
children: [
|
// 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
|
// Header
|
||||||
_buildSectionHeader(
|
_buildSectionHeader(
|
||||||
_displayType == SelectionType.equipment ? 'Équipements' : 'Containers',
|
_displayType == SelectionType.equipment ? 'Équipements' : 'Containers',
|
||||||
_displayType == SelectionType.equipment ? Icons.inventory_2 : Icons.inventory,
|
_displayType == SelectionType.equipment ? Icons.inventory_2 : Icons.inventory,
|
||||||
itemWidgets.length,
|
itemWidgets.length,
|
||||||
),
|
),
|
||||||
@@ -1448,7 +1456,7 @@ class _EquipmentSelectionDialogState extends State<EquipmentSelectionDialog> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
// Message si rien trouv├®
|
// Message si rien trouvé
|
||||||
if (itemWidgets.isEmpty && !_isLoadingMore)
|
if (itemWidgets.isEmpty && !_isLoadingMore)
|
||||||
Center(
|
Center(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
@@ -1458,14 +1466,22 @@ class _EquipmentSelectionDialogState extends State<EquipmentSelectionDialog> {
|
|||||||
Icon(Icons.search_off, size: 64, color: Colors.grey.shade400),
|
Icon(Icons.search_off, size: 64, color: Colors.grey.shade400),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Text(
|
Text(
|
||||||
'Aucun r├®sultat trouv├®',
|
'Aucun résultat trouvé',
|
||||||
style: TextStyle(fontSize: 16, color: Colors.grey.shade600),
|
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