Flutter SearchBar: Um guia definitivo
Conteudo
Introdução
No âmbito do desenvolvimento de aplicativos móveis, um recurso de pesquisa bem projetado não é apenas uma conveniência – é um componente vital que aumenta significativamente o envolvimento e a acessibilidade do usuário. Isso é particularmente verdadeiro para aplicativos que lidam com dados extensos. O Flutter, com sua crescente popularidade entre os desenvolvedores de aplicativos móveis, destaca-se como uma excelente estrutura para a criação de aplicativos compilados nativamente. Neste artigo, embarcaremos em uma jornada para projetar um widget de pesquisa versátil no Flutter, culminando na criação de um widget genérico semelhante ao CustomSearchDelegate
. Usaremos o método showSearch
para implementar a pesquisa. Primeiro, criaremos uma barra de pesquisa básica e, depois, a personalizaremos para que possa ser usada em qualquer projeto.
showSearch
ShowSearch é um método na biblioteca de materiais do Flutter. Por isso, ele pode ser acessado de qualquer lugar na sua árvore de widgets.
Future<T?> showSearch<T>({ required BuildContext context, required SearchDelegate<T> delegate, String? query = '', bool useRootNavigator = false, })
Esse método requer um BuildContext
e um SearchDelegate
, que é uma classe abstrata que recebe um parâmetro do tipo T.
Exemplo.
showSearch(context: context, delegate: CustomSearchDelegate());
Portanto, para implementar uma funcionalidade de pesquisa, precisamos apenas criar uma classe que estenda um SearchDelegate
. Vamos ver como isso pode ser feito.
class CustomSearchDelegate extends SearchDelegate { @override List<Widget>? buildActions(BuildContext context) { // TODO: implement buildActions throw UnimplementedError(); } @override Widget? buildLeading(BuildContext context) { // TODO: implement buildLeading throw UnimplementedError(); } @override Widget buildResults(BuildContext context) { // TODO: implement buildResults throw UnimplementedError(); } @override Widget buildSuggestions(BuildContext context) { // TODO: implement buildSuggestions throw UnimplementedError(); } }
O SearchDelegate
exige a implementação de 4 métodos.
buildActions
: Gerencia ações como a limpeza da consulta de pesquisa. Assemelha-se ao parâmetro de ação do AppBar.buildLeading
: Normalmente, inclui um botão de voltar para navegação.buildResults
: Exibe os resultados da pesquisa.buildSuggestions
: Oferece sugestões à medida que os usuários digitam suas consultas.
Vamos implementar uma pesquisa básica usando esse delegado.
class CustomSearchDelegate extends SearchDelegate { List<String> searchables = List.generate(100, (index) => 'Item ${index + 1}'); @override List<Widget>? buildActions(BuildContext context) { return [ IconButton( onPressed: () { query = ''; }, icon: const Icon(Icons.clear), ), ]; } @override Widget? buildLeading(BuildContext context) { return IconButton( onPressed: () { close(context, []); }, icon: Icon( Platform.isAndroid ? Icons.arrow_back : Icons.arrow_back_ios, size: 22, ), ); } @override Widget buildResults(BuildContext context) { return SuggestionOrResultWidget(searchables: searchables, query: query); } @override Widget buildSuggestions(BuildContext context) { return SuggestionOrResultWidget(searchables: searchables, query: query); } }
class SuggestionOrResultWidget extends StatelessWidget { const SuggestionOrResultWidget({ super.key, required this.searchables, required this.query, }); final List<String> searchables; final String query; @override Widget build(BuildContext context) { final List<String> suggestions = query.isEmpty ? searchables : searchables.where((element) => element.toLowerCase().contains(query.toLowerCase())).toList(); if (suggestions.isEmpty) return const NoResultWidget(); return ListView.separated( itemBuilder: (context, index) => ListTile( title: Text(suggestions[index]), ), separatorBuilder: (context, index) => const Divider(), itemCount: suggestions.length, ); } }
class NoResultWidget extends StatelessWidget { const NoResultWidget({super.key}); @override Widget build(BuildContext context) { return const Center( child: Text('No Result Found'), ); } }
O resultado terá a seguinte aparência
Personalização do ThemeData de sua página de pesquisa AppBar
Uma coisa que você pode notar é que, quando a página de pesquisa é exibida, o tema não é o mesmo que o tema do aplicativo. Você pode ver isso especificamente nas cores da AppBar
. O código abaixo apenas passará o ThemeData
de seu aplicativo diretamente para a AppBar
na página de pesquisa.
class CustomSearchDelegate extends SearchDelegate { List<String> searchables = List.generate(100, (index) => 'Item ${index + 1}'); @override ThemeData appBarTheme(BuildContext context) { return ThemeData( // Customize appbar theme appBarTheme: const AppBarTheme( backgroundColor: Colors.blue, foregroundColor: Colors.white, ), // Customize input decoration theme inputDecorationTheme: const InputDecorationTheme( isDense: true, isCollapsed: true, contentPadding: EdgeInsets.only(left: 12, top: 6, bottom: 6), enabledBorder: OutlineInputBorder( borderSide: BorderSide( color: Colors.white, )), focusedBorder: OutlineInputBorder( borderSide: BorderSide( color: Colors.white, )), border: OutlineInputBorder( borderSide: BorderSide( color: Colors.white, ), ), ), ); }
Criando um widget de pesquisa genérico:
Passando para uma implementação mais avançada, vamos nos aprofundar no CustomSearchDelegate
. Esse widget genérico se destaca por sua reutilização e adaptabilidade em diferentes tipos de dados.
/// Generic Search Delegate which can be used anywhere class CustomSearchDelegate<T> extends SearchDelegate<List<T>> { CustomSearchDelegate({ required this.searchables, required this.suggestionOrResult, required this.itemMatcher, this.onTap, }); // Items to be searched final List<T> searchables; // Widget that needs to be displayed when getting suggestion or result. // If you want sepearate widget for suggestion and result, you can add // 2 arguments, suggestionWidget and resultWidget to build differently. final Widget Function(List<T>, String) suggestionOrResult; // Query against which matching will run final bool Function(T item, String query) itemMatcher; // Callback when item is tapped final ValueChanged<T>? onTap; @override List<Widget>? buildActions(BuildContext context) { return [ IconButton( onPressed: () { query = ''; }, icon: const Icon(Icons.clear), ), ]; } @override Widget buildLeading(BuildContext context) { return IconButton( onPressed: () { close(context, []); }, icon: Icon( Platform.isAndroid ? Icons.arrow_back : Icons.arrow_back_ios, size: 22, ), ); } @override Widget buildResults(BuildContext context) { return _buildSuggestionOrResult(); } @override Widget buildSuggestions(BuildContext context) { if (searchables.isEmpty) return const SizedBox(); return _buildSuggestionOrResult(); } Widget _buildSuggestionOrResult() { final List<T> suggestionList = query.isEmpty ? searchables : searchables.where((item) => itemMatcher(item, query)).toList(); if (suggestionList.isEmpty) { return NoResultFoundWidget(); } return suggestionOrResult(suggestionList, query); } }
Esse delegado genérico pode ser usado como:
showSearch( context: context, delegate: CustomSearchDelegate<List<String>>( searchables: searchables, suggestionOrResult: (List<String> suggestions,String query) => ListWidget( suggestionsList: searchables, query: query, ), itemMatcher: (String item,String query) => item.toLowerCase().contains(query.toLowerCase()), ), )
Bônus
Se você quiser destacar a letra digitada na lista de sugestões e rolar até o resultado, poderá usar o seguinte widget.
class ListWidget extends StatefulWidget { const ListWidget({ super.key, required this.suggestionsList, required this.query, }); final List<String> suggestionsList; final String query; @override State<ListWidget> createState() => _ListWidgetState(); } class _ListWidgetState extends State<ListWidget> { late List<String> filteredList; final ScrollController scrollController = ScrollController(); @override void initState() { super.initState(); filterList(); } @override void didUpdateWidget(ListWidget oldWidget) { super.didUpdateWidget(oldWidget); if (widget.query != oldWidget.query) { filterList(); } } void filterList() { filteredList = widget.suggestionsList.where((item) => item.toLowerCase().contains(widget.query.toLowerCase())).toList(); if (filteredList.isNotEmpty) { if (filteredList.isNotEmpty) { WidgetsBinding.instance.addPostFrameCallback((_) => scrollToFirstMatch()); } } } void scrollToFirstMatch() { int index = widget.suggestionsList.indexOf(filteredList.first); if (index != -1 && scrollController.hasClients) { scrollController.animateTo( index * 32.0, // Assuming each item has a height of 32 duration: const Duration(milliseconds: 100), curve: Curves.easeInOut, ); } } @override Widget build(BuildContext context) { return ListView.separated( controller: scrollController, itemCount: filteredList.length, itemBuilder: (context, index) { final String suggestionText = filteredList[index]; return ListTile( title: RichText( text: highlightMatch(suggestionText, widget.query), ), onTap: () { // Your onTap functionality here }, ); }, separatorBuilder: (context, index) => const Divider(), ); } TextSpan highlightMatch(String text, String query) { if (query.isEmpty || !text.toLowerCase().contains(query.toLowerCase())) { return TextSpan( text: text, style: const TextStyle(color: Colors.black), ); } List<TextSpan> spans = []; int start = 0; int indexOfHighlight = text.toLowerCase().indexOf(query.toLowerCase()); while (indexOfHighlight != -1) { spans.add(TextSpan( text: text.substring(start, indexOfHighlight), style: const TextStyle(color: Colors.black), )); spans.add(TextSpan( text: text.substring(indexOfHighlight, indexOfHighlight + query.length), style: const TextStyle(backgroundColor: Colors.yellow, color: Colors.black), )); start = indexOfHighlight + query.length; indexOfHighlight = text.toLowerCase().indexOf(query.toLowerCase(), start); } spans.add(TextSpan( text: text.substring(start), style: const TextStyle(color: Colors.black), )); return TextSpan(children: spans); } }
Conclusão
Um recurso de pesquisa eficaz é um elemento crítico no design de aplicativos móveis, impactando a satisfação e o envolvimento do usuário. Esse CustomSearchDelegate é uma prova disso, oferecendo um modelo para a criação de funcionalidades de pesquisa personalizáveis e versáteis em aplicativos Flutter.
Obrigado pela leitura. Boa programação 🙂