Last active
September 2, 2025 07:32
-
-
Save chornthorn/61eeccb23eaa6ee69f2391b665cee05e to your computer and use it in GitHub Desktop.
DebouncedSearchField
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import 'dart:async'; | |
| import 'package:flutter/material.dart'; | |
| void main() { | |
| runApp(const MyApp()); | |
| } | |
| class MyApp extends StatelessWidget { | |
| const MyApp({Key? key}) : super(key: key); | |
| @override | |
| Widget build(BuildContext context) { | |
| return MaterialApp( | |
| title: 'Debounced Search Demo', | |
| theme: ThemeData( | |
| colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue), | |
| useMaterial3: true, | |
| ), | |
| home: const SearchPage(), | |
| ); | |
| } | |
| } | |
| class DebouncedSearchField extends StatefulWidget { | |
| final Function(String) onSearch; | |
| final String? hintText; | |
| final Duration debounceDuration; | |
| const DebouncedSearchField({ | |
| Key? key, | |
| required this.onSearch, | |
| this.hintText = 'Search...', | |
| this.debounceDuration = const Duration(milliseconds: 600), | |
| }) : super(key: key); | |
| @override | |
| State<DebouncedSearchField> createState() => _DebouncedSearchFieldState(); | |
| } | |
| class _DebouncedSearchFieldState extends State<DebouncedSearchField> { | |
| final TextEditingController _searchController = TextEditingController(); | |
| Timer? _debounceTimer; | |
| @override | |
| void dispose() { | |
| _debounceTimer?.cancel(); | |
| _searchController.dispose(); | |
| super.dispose(); | |
| } | |
| void _onSearchChanged(String query) { | |
| // Cancel the previous timer | |
| _debounceTimer?.cancel(); | |
| // Start a new timer | |
| _debounceTimer = Timer(widget.debounceDuration, () { | |
| widget.onSearch(query); | |
| }); | |
| } | |
| @override | |
| Widget build(BuildContext context) { | |
| return TextField( | |
| controller: _searchController, | |
| onChanged: _onSearchChanged, | |
| decoration: InputDecoration( | |
| hintText: widget.hintText, | |
| hintStyle: TextStyle( | |
| color: Theme.of(context).colorScheme.onSurfaceVariant, | |
| ), | |
| prefixIcon: Icon( | |
| Icons.search, | |
| color: Theme.of(context).colorScheme.onSurfaceVariant, | |
| ), | |
| suffixIcon: _searchController.text.isNotEmpty | |
| ? IconButton( | |
| icon: Icon( | |
| Icons.clear, | |
| color: Theme.of(context).colorScheme.onSurfaceVariant, | |
| ), | |
| onPressed: () { | |
| _searchController.clear(); | |
| _debounceTimer?.cancel(); | |
| widget.onSearch(''); | |
| setState(() {}); | |
| }, | |
| ) | |
| : null, | |
| border: OutlineInputBorder( | |
| borderRadius: BorderRadius.circular(12), | |
| borderSide: BorderSide(color: Theme.of(context).colorScheme.outline), | |
| ), | |
| enabledBorder: OutlineInputBorder( | |
| borderRadius: BorderRadius.circular(12), | |
| borderSide: BorderSide(color: Theme.of(context).colorScheme.outline), | |
| ), | |
| focusedBorder: OutlineInputBorder( | |
| borderRadius: BorderRadius.circular(12), | |
| borderSide: BorderSide( | |
| color: Theme.of(context).colorScheme.primary, | |
| width: 2, | |
| ), | |
| ), | |
| filled: true, | |
| fillColor: Theme.of(context).colorScheme.surface, | |
| ), | |
| style: Theme.of(context).textTheme.bodyLarge, | |
| ); | |
| } | |
| } | |
| class SearchPage extends StatefulWidget { | |
| const SearchPage({Key? key}) : super(key: key); | |
| @override | |
| State<SearchPage> createState() => _SearchPageState(); | |
| } | |
| class _SearchPageState extends State<SearchPage> { | |
| List<String> _allItems = [ | |
| 'Apple iPhone 15', | |
| 'Samsung Galaxy S24', | |
| 'Google Pixel 8', | |
| 'OnePlus 12', | |
| 'Xiaomi 14', | |
| 'Sony Xperia 1 V', | |
| 'Nothing Phone 2', | |
| 'Motorola Edge 40', | |
| 'Realme GT 5', | |
| 'Oppo Find X6', | |
| 'Vivo X90', | |
| 'Honor Magic 5', | |
| 'Asus ROG Phone 7', | |
| 'Red Magic 8', | |
| 'Black Shark 5', | |
| ]; | |
| List<String> _searchResults = []; | |
| bool _isLoading = false; | |
| String _currentQuery = ''; | |
| void _performSearch(String query) async { | |
| setState(() { | |
| _currentQuery = query; | |
| _isLoading = true; | |
| }); | |
| // Simulate network delay | |
| await Future.delayed(const Duration(milliseconds: 300)); | |
| if (query.trim().isEmpty) { | |
| setState(() { | |
| _searchResults = []; | |
| _isLoading = false; | |
| }); | |
| return; | |
| } | |
| // Filter items based on query | |
| final results = _allItems | |
| .where((item) => item.toLowerCase().contains(query.toLowerCase())) | |
| .toList(); | |
| setState(() { | |
| _searchResults = results; | |
| _isLoading = false; | |
| }); | |
| } | |
| @override | |
| Widget build(BuildContext context) { | |
| return Scaffold( | |
| backgroundColor: Theme.of(context).colorScheme.background, | |
| appBar: AppBar( | |
| title: Text( | |
| 'Search Demo', | |
| style: Theme.of(context).textTheme.titleLarge, | |
| ), | |
| backgroundColor: Theme.of(context).colorScheme.surface, | |
| elevation: 0, | |
| ), | |
| body: Padding( | |
| padding: const EdgeInsets.all(16.0), | |
| child: Column( | |
| children: [ | |
| DebouncedSearchField( | |
| onSearch: _performSearch, | |
| hintText: 'Search phones...', | |
| ), | |
| const SizedBox(height: 16), | |
| if (_currentQuery.isNotEmpty) | |
| Padding( | |
| padding: const EdgeInsets.only(bottom: 8), | |
| child: Align( | |
| alignment: Alignment.centerLeft, | |
| child: Text( | |
| 'Results for "$_currentQuery"', | |
| style: Theme.of(context).textTheme.bodyMedium?.copyWith( | |
| color: Theme.of(context).colorScheme.onSurfaceVariant, | |
| ), | |
| ), | |
| ), | |
| ), | |
| Expanded( | |
| child: _isLoading | |
| ? Center( | |
| child: CircularProgressIndicator( | |
| color: Theme.of(context).colorScheme.primary, | |
| ), | |
| ) | |
| : _currentQuery.isEmpty | |
| ? Center( | |
| child: Column( | |
| mainAxisAlignment: MainAxisAlignment.center, | |
| children: [ | |
| Icon( | |
| Icons.search, | |
| size: 64, | |
| color: Theme.of( | |
| context, | |
| ).colorScheme.onSurfaceVariant, | |
| ), | |
| const SizedBox(height: 16), | |
| Text( | |
| 'Start typing to search', | |
| style: Theme.of(context).textTheme.titleMedium | |
| ?.copyWith( | |
| color: Theme.of( | |
| context, | |
| ).colorScheme.onSurfaceVariant, | |
| ), | |
| ), | |
| ], | |
| ), | |
| ) | |
| : _searchResults.isEmpty | |
| ? Center( | |
| child: Column( | |
| mainAxisAlignment: MainAxisAlignment.center, | |
| children: [ | |
| Icon( | |
| Icons.search_off, | |
| size: 64, | |
| color: Theme.of( | |
| context, | |
| ).colorScheme.onSurfaceVariant, | |
| ), | |
| const SizedBox(height: 16), | |
| Text( | |
| 'No results found', | |
| style: Theme.of(context).textTheme.titleMedium | |
| ?.copyWith( | |
| color: Theme.of( | |
| context, | |
| ).colorScheme.onSurfaceVariant, | |
| ), | |
| ), | |
| const SizedBox(height: 8), | |
| Text( | |
| 'Try searching for something else', | |
| style: Theme.of(context).textTheme.bodyMedium | |
| ?.copyWith( | |
| color: Theme.of( | |
| context, | |
| ).colorScheme.onSurfaceVariant, | |
| ), | |
| ), | |
| ], | |
| ), | |
| ) | |
| : ListView.builder( | |
| itemCount: _searchResults.length, | |
| itemBuilder: (context, index) { | |
| return Card( | |
| margin: const EdgeInsets.only(bottom: 8), | |
| elevation: 2, | |
| color: Theme.of(context).colorScheme.surface, | |
| child: ListTile( | |
| title: Text( | |
| _searchResults[index], | |
| style: Theme.of(context).textTheme.bodyLarge, | |
| ), | |
| leading: CircleAvatar( | |
| backgroundColor: Theme.of( | |
| context, | |
| ).colorScheme.primaryContainer, | |
| child: Icon( | |
| Icons.phone_android, | |
| color: Theme.of( | |
| context, | |
| ).colorScheme.onPrimaryContainer, | |
| ), | |
| ), | |
| trailing: Icon( | |
| Icons.arrow_forward_ios, | |
| size: 16, | |
| color: Theme.of( | |
| context, | |
| ).colorScheme.onSurfaceVariant, | |
| ), | |
| onTap: () { | |
| ScaffoldMessenger.of(context).showSnackBar( | |
| SnackBar( | |
| content: Text( | |
| 'Selected: ${_searchResults[index]}', | |
| ), | |
| backgroundColor: Theme.of( | |
| context, | |
| ).colorScheme.primary, | |
| ), | |
| ); | |
| }, | |
| ), | |
| ); | |
| }, | |
| ), | |
| ), | |
| ], | |
| ), | |
| ), | |
| ); | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment