Files
flutter_wisetronic/lib/widgets/general/search_bar.dart
peima 1a578c925d feat: Phase 2 — upgrade deps to null-safe, replace deprecated packages
- pubspec: bump all maintained deps to null-safe latest; flutter pub get resolves
- Remove 12 packages (0-ref or no null-safe): splashscreen, flappy_search_bar,
  searchable_dropdown, share, platform_detect, flutter_inappwebview, countdown,
  gender_selection, flutter_dash, smooth_star_rating, hovering, ffi
- Add flutter_rating_bar
- Keep discontinued-but-null-safe for now (Phase 5): pull_to_refresh, hive, catcher
- New null-safe custom components (API-compatible, callers change only imports):
  utils/countdown.dart, widgets/general/{hover_widget,gender_selection,
  smooth_star_rating,search_bar}.dart
- main.dart: drop dead SplashScreen _buildBody + _to field
- 23 consuming files: swap deprecated imports to local components

Null-safety migration + upgraded-API adaptation = Phase 3.
2026-07-24 03:10:12 +08:00

186 lines
5.2 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'dart:async';
import 'package:flutter/material.dart';
/// 轻量级异步搜索控制器,配合 [SearchBar] 使用。
///
/// 替代已停维的 `flappy_search_bar` 的 `SearchBarController`。
/// 保留泛型 `<T>` 以兼容调用方 `SearchBarController<Product>()` 写法。
/// [replayLastSearch] 用于“加载更多 / 分页”:外部 page++ 后触发重跑上一次搜索。
class SearchBarController<T> {
String lastQuery = '';
VoidCallback? _replay;
void _register(VoidCallback replay) {
_replay = replay;
}
/// 重新执行上一次搜索(分页加载更多时调用)。
void replayLastSearch() => _replay?.call();
}
/// 轻量级 Web 异步搜索栏,替代已停维的 `flappy_search_bar` 的 `SearchBar`。
///
/// 基本流程:输入达到 [minimumChars] 个字符后,调用 [onSearch] 取结果,
/// 用 [onItemFound] 渲染每一条;无结果时显示 [emptyWidget];出错显示 [onError]。
///
/// 注意:`searchBarController` 参数刻意使用裸类型(`SearchBarController?`
/// 以兼容调用方 `SearchBarController _controller = SearchBarController<Product>();`
/// 的写法Dart 3 sound 类型下避免泛型不变性报错)。
class SearchBar<T> extends StatefulWidget {
final SearchBarController? searchBarController;
final int minimumChars;
final String? hintText;
final Widget? cancellationWidget;
final Future<List<T>> Function(String text) onSearch;
final Widget Function(T item, int index) onItemFound;
final Widget Function(dynamic error)? onError;
final Widget? emptyWidget;
final Duration debounceDuration;
const SearchBar({
super.key,
this.searchBarController,
this.minimumChars = 2,
this.hintText,
this.cancellationWidget,
required this.onSearch,
required this.onItemFound,
this.onError,
this.emptyWidget,
this.debounceDuration = const Duration(milliseconds: 350),
});
@override
State<SearchBar<T>> createState() => _SearchBarState<T>();
}
class _SearchBarState<T> extends State<SearchBar<T>> {
final TextEditingController _textController = TextEditingController();
final FocusNode _focusNode = FocusNode();
Timer? _debounce;
List<T> _results = <T>[];
bool _loading = false;
dynamic _error;
bool _hasSearched = false;
@override
void initState() {
super.initState();
widget.searchBarController?._register(_runSearch);
_textController.addListener(_onTextChanged);
}
@override
void dispose() {
_debounce?.cancel();
_textController.dispose();
_focusNode.dispose();
super.dispose();
}
void _onTextChanged() {
final text = _textController.text;
_debounce?.cancel();
if (text.length >= widget.minimumChars) {
_debounce = Timer(widget.debounceDuration, _runSearch);
} else if (text.isEmpty) {
setState(() {
_results = <T>[];
_hasSearched = false;
_error = null;
});
}
}
Future<void> _runSearch() async {
final query = _textController.text;
if (query.length < widget.minimumChars) return;
if (widget.searchBarController != null) {
widget.searchBarController!.lastQuery = query;
}
setState(() {
_loading = true;
_error = null;
_hasSearched = true;
});
try {
final results = await widget.onSearch(query);
if (mounted) {
setState(() {
_results = results;
_loading = false;
});
}
} catch (e) {
if (mounted) {
setState(() {
_error = e;
_loading = false;
});
}
}
}
void _cancel() {
_textController.clear();
_focusNode.unfocus();
setState(() {
_results = <T>[];
_hasSearched = false;
_error = null;
});
}
@override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
TextField(
controller: _textController,
focusNode: _focusNode,
decoration: InputDecoration(
hintText: widget.hintText,
prefixIcon: const Icon(Icons.search),
suffixIcon: _textController.text.isNotEmpty
? GestureDetector(
onTap: _cancel,
child: widget.cancellationWidget ??
const Icon(Icons.close, size: 20),
)
: null,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
contentPadding: const EdgeInsets.symmetric(horizontal: 12),
),
),
const SizedBox(height: 8),
Expanded(child: _buildBody()),
],
);
}
Widget _buildBody() {
if (_loading) {
return const Center(child: CircularProgressIndicator());
}
if (_error != null) {
return widget.onError != null
? widget.onError!(_error)
: Center(child: Text('$_error'));
}
if (!_hasSearched) {
return const SizedBox.shrink();
}
if (_results.isEmpty) {
return widget.emptyWidget ?? const SizedBox.shrink();
}
return ListView.builder(
itemCount: _results.length,
itemBuilder: (BuildContext context, int index) =>
widget.onItemFound(_results[index], index),
);
}
}