perf(ui): debounce species/name lookups in the edit sheet

Typing in the edit sheet ran heavy DB work on every keystroke: a SQL
LIKE search on the species field, and a full species-catalog load plus
O(n^2) label matching on the name field. That made typing laggy.

Debounce both lookups (250ms) so they only run once the grower pauses,
and drop stale results with a per-field query counter so an earlier
response can't flicker an outdated suggestion back in.
This commit is contained in:
vjrj 2026-07-10 02:32:02 +02:00
parent b840b83c42
commit 913e058122

View file

@ -1,3 +1,4 @@
import 'dart:async';
import 'dart:typed_data';
import 'package:drift/drift.dart' show Value;
@ -619,8 +620,20 @@ class _EditVarietySheetState extends State<_EditVarietySheet> {
late int? _seedHarvestMonths = widget.detail.seedHarvestMonths;
late bool _calendarOpen = widget.detail.hasCropCalendar;
// Typing stays responsive by not touching the DB on every keystroke: each
// field waits until the grower pauses before running its (heavy) lookup, and
// a per-field query counter drops any result that a later keystroke has
// already superseded, so stale suggestions never flicker back in.
static const _debounce = Duration(milliseconds: 250);
Timer? _speciesDebounce;
Timer? _nameDebounce;
int _speciesQueryId = 0;
int _nameQueryId = 0;
@override
void dispose() {
_speciesDebounce?.cancel();
_nameDebounce?.cancel();
_name.dispose();
_category.dispose();
_notes.dispose();
@ -628,21 +641,38 @@ class _EditVarietySheetState extends State<_EditVarietySheet> {
super.dispose();
}
Future<void> _onSpeciesQuery(String query) async {
void _onSpeciesQuery(String query) {
_speciesDebounce?.cancel();
_speciesDebounce = Timer(_debounce, () => _runSpeciesQuery(query));
}
Future<void> _runSpeciesQuery(String query) async {
final queryId = ++_speciesQueryId;
final lang = Localizations.localeOf(context).languageCode;
final results = await widget.species.search(query, languageCode: lang);
// A manual species search takes over from the name-based suggestion.
if (mounted) setState(() => _suggestions = results);
if (mounted && queryId == _speciesQueryId) {
setState(() => _suggestions = results);
}
}
void _onNameChanged(String name) {
if (widget.detail.speciesId != null || _pickedSpeciesId != null) return;
_nameDebounce?.cancel();
_nameDebounce = Timer(_debounce, () => _runNameClassify(name));
}
/// As the name is typed, offer the species it names but only when the
/// variety has no species yet and the grower hasn't picked one, so an
/// existing link is never second-guessed.
Future<void> _onNameChanged(String name) async {
Future<void> _runNameClassify(String name) async {
if (widget.detail.speciesId != null || _pickedSpeciesId != null) return;
final queryId = ++_nameQueryId;
final lang = Localizations.localeOf(context).languageCode;
final match = await widget.species.classifyLabel(name, languageCode: lang);
if (mounted) setState(() => _suggested = match);
if (mounted && queryId == _nameQueryId) {
setState(() => _suggested = match);
}
}
void _pick(SpeciesMatch match) {