Реализация действия поиска в AppBar
В сегодняшнем рецепте я покажу вам, как действие поиска можно интегрировать в верхнюю часть страницы. Я буду использовать Flutter SearchDelegateкомпонент для достижения этой цели. Он поставляется с поддержкой заполнения предложений в строке поиска, добавления элементов действий в правую часть панели поиска. Я использую библиотеку дартс english_words
для заполнения списка слов.
Целевая аудитория: Новичок
Рецепт: Реализация действия поиска в AppBar с использованием мобильных приложений Flutter для Android и iOS.
Виджет фокуса: SearchDelegate
Цель: В этом рецепте я покажу вам: 1. Как заполнить отсортированный список английских слов в приложении и найти заданное слово с помощью панели поиска в верхней части приложения. 2. Как создать список предложений для панели поиска. 3. Как добавить такие действия, как «очистить», чтобы сбросить панель поиска, и фиктивное действие «микрофон» для голосового ввода.
Проверка в действии:
Поехали!
Шаг №0: Создайте проект Flutter Application в Android Studio.
Шаг 1. Получить зависимости. Первое, что нужно добавить english_words
зависимость вpubspec.yaml
чтобы иметь возможность заполнять английские слова в списке слов нашего приложения рецепта.
dependencies:
flutter:
sdk: flutter
# This is needed to populate English words in app's word list.
english_words: ^3.1.3
Шаг 2. Показать список слов Чтобы отобразить список английских слов в виде списка в приложении, первым делом нужно импортировать зависимость в main.dart
файл:
import 'package:english\_words/english\_words.dart' as words;
Шаг 3. Инициализировать список слов в SearchAppBarRecipeState
учебный класс. Теперь, в _SearchAppBarRecipeState
class, получите список слов, как показано ниже. Убедитесь, что у вас есть структура данных списка для хранения списка слов. я использую kWords
для этой цели.
class _SearchAppBarRecipeState extends State<SeachAppBarRecipe> {
//Data structure to hold word list
final List<String> kWords;
...
//Initializing kWords list with data fetched from english_words library
_SearchAppBarRecipeState()
: kWords = List.from(Set.from(words.all)),
super();
...
}
Шаг №4. Сортировка списка слов. Вы заметите, что в этот момент данные не отсортированы. Давайте сделаем это в алфавитном порядке, как показано ниже:
class _SearchAppBarRecipeState extends State<SeachAppBarRecipe> {
final List<String> kWords;
...
//Initializing with sorted list of english words
_SearchAppBarRecipeState()
: kWords = List.from(Set.from(words.all))
..sort(
(w1, w2) => w1.toLowerCase().compareTo(w2.toLowerCase()),
),
super();
...
}
Шаг № 5. Настраивать SearchDelegate
для реализации Search AppBar. Делегат поиска — это место, где происходит вся магия. Он содержит два списка слов. Один список представляет собой общий список слов, переданный во время инициализации. Другой список _history
содержит список слов истории. Вы можете предварительно заполнить _history
список, чтобы дать основу для элементов поиска в истории. Этот список будет показан, когда пользователь нажмет на строку поиска.
class _SearchAppBarDelegate extends SearchDelegate<String> {
//list holds the full word list
final List<String> _words;
//list holds history search words.
final List<String> _history;
//initialize delegate with full word list and history words
_SearchAppBarDelegate(List<String> words)
: _words = words,
//pre-populated history of words
_history = <String>['apple', 'orange', 'banana', 'watermelon'],
super();
...
}
Шаг №6. После настройки параметров списка слов в делегате пришло время выбрать значок, который будет размещен в левой части панели поиска. В основном это меню значков предназначено для возврата к предыдущему экрану.
class _SearchAppBarDelegate extends SearchDelegate<String> {
...
// Setting leading icon for the search bar.
//Clicking on back arrow will take control to main page
@override
Widget buildLeading(BuildContext context) {
return IconButton(
tooltip: 'Back',
icon: AnimatedIcon(
icon: AnimatedIcons.menu_arrow,
progress: transitionAnimation,
),
onPressed: () {
//Take control back to previous page
this.close(context, null);
},
);
}
...
}
Хорошо, мы получили значок для панели поиска! Что дальше ?
Шаг №7. Теперь пришло время реализовать buildResults
способ показать искомый элемент. Здесь поисковый запрос this.query
отобразит искомый элемент. Для простоты я добавлю только два виджета. Первый виджет Text
и отобразит сообщение «===Ваш выбор слова===». Второй виджетGestureDetector
у которого есть другой Text
виджет как его child
. Нажав на Text
виджет вернет управление на предыдущую страницу вместе с this.query
параметр для отображения искомого слова на главной странице.
class _SearchAppBarDelegate extends SearchDelegate<String> {
...
//Builds page to populate search results.
@override
Widget buildResults(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text('===Your Word Choice==='),
GestureDetector(
onTap: () {
//Define your action when clicking on result item.
//In this example, it simply closes the page
this.close(context, this.query);
},
child: Text(
this.query,
style: Theme.of(context)
.textTheme
.display2
.copyWith(fontWeight: FontWeight.normal),
),
),
],
),
),
);
}
...
}
Шаг №8. Далее пришло время для реализации buildSuggestions
метод класса SearchDelegate. Итак, что означают предложения по построению? Это список вариантов слов, которые вы увидите при вводе поискового запроса в строке поиска. Есть две части этого. Во-первых, мы должны передать список слов, которые могут отображаться в качестве предложений. Во-вторых, нам нужно реализовать виджет пользовательского интерфейса для отображения этого списка предложений.
Для части данных мы отображаем предварительно заполненные слова истории, когда термин запроса пуст. Когда пользователь начинает печатать, мы показываем все слова, которые начинаются с поисковых запросов, введенных в строку поиска. Вот и все !
class _SearchAppBarDelegate extends SearchDelegate<String> {
...
// Suggestions list while typing search query - this.query.
@override
Widget buildSuggestions(BuildContext context) {
final Iterable<String> suggestions = this.query.isEmpty
? _history
: _words.where((word) => word.startsWith(query));
...
}
...
}
Теперь очередь виджета пользовательского интерфейса. Как вы видете buildSuggestions
метод возвращает Widget
. Мы сделаем отдельный класс сказать _WordSuggestionList
чтобы заполнить виджет списка предложений и вернуть его из buildSuggestions
._WordSuggestionList
будет StatelessWidget
, и имеет три вещи: во-первых, список предложений. Во-вторых, поисковый запрос и последний обратный вызов говорят ValueChanged<String> onSelected
. Это будет ListView
от размера количества предоставленных предложений. Каждое предложение отображается в ListTile
виджет. Этот виджет покажетhistory
значок только для элементов, извлеченных из предварительно заполненных слов истории. Текст для виджета будет обновляться по мере того, как пользователь вводит поисковый запрос.
class _WordSuggestionList extends StatelessWidget {
const _WordSuggestionList({this.suggestions, this.query, this.onSelected});
final List<String> suggestions;
final String query;
final ValueChanged<String> onSelected;
@override
Widget build(BuildContext context) {
final textTheme = Theme.of(context).textTheme.subhead;
return ListView.builder(
itemCount: suggestions.length,
itemBuilder: (BuildContext context, int i) {
final String suggestion = suggestions[i];
return ListTile(
leading: query.isEmpty ? Icon(Icons.history) : Icon(null),
// Highlight the substring that matched the query.
title: RichText(
text: TextSpan(
text: suggestion.substring(0, query.length),
style: textTheme.copyWith(fontWeight: FontWeight.bold),
children: <TextSpan>[
TextSpan(
text: suggestion.substring(query.length),
style: textTheme,
),
],
),
),
onTap: () {
onSelected(suggestion);
},
);
},
);
}
}
Давайте подключим _WordSuggestionList
обратно в buildSuggestions
метод:
class _SearchAppBarDelegate extends SearchDelegate<String> {
...
// Suggestions list while typing search query - this.query.
@override
Widget buildSuggestions(BuildContext context) {
final Iterable<String> suggestions = this.query.isEmpty
? _history
: _words.where((word) => word.startsWith(query));
//calling wordsuggestion list
return _WordSuggestionList(
query: this.query,
suggestions: suggestions.toList(),
onSelected: (String suggestion) {
this.query = suggestion;
this._history.insert(0, suggestion);
showResults(context);
},
}
...
}
Шаг №9. Построение действий в SearchBar. Последний шаг — добавить меню действий в правую часть SearchBar. Я добавлю два действия меню. Одним из них является значок «Очистить», чтобы удалить поисковый запрос из панели поиска. Другой значок — это значок «микрофон», который можно улучшить, чтобы принимать голосовой ввод, что выходит за рамки этого рецепта. Однако, если вы хотите, чтобы я написал о другом рецепте приема голосового ввода, не стесняйтесь обращаться ко мне, как указано в конце этого поста.
class _SearchAppBarDelegate extends SearchDelegate<String> {
...
// Action buttons at the right of search bar.
@override
List<Widget> buildActions(BuildContext context) {
return <Widget>[
query.isNotEmpty ?
IconButton(
tooltip: 'Clear',
icon: const Icon(Icons.clear),
onPressed: () {
query = '';
showSuggestions(context);
},
) : IconButton(
icon: const Icon(Icons.mic),
tooltip: 'Voice input',
onPressed: () {
this.query = 'TBW: Get input from voice';
},
),
];
}
...
}
Шаг №10. Наконец, позвоните в Search Delegate из SearchAppBarRecipeState
. После создания SearchDelegate
класс, вызовите его из _SearchAppBarRecipeState
класс и инициализировать его в initState
.
class _SearchAppBarRecipeState extends State<SeachAppBarRecipe> {
final List<String> kWords;
//Calling search delegate class
_SearchAppBarDelegate _searchDelegate;
...
@override
void initState() {
super.initState();
//Initializing search delegate with sorted list of English words
_searchDelegate = _SearchAppBarDelegate(kWords);
}
...
}
Вот и все !
Полный пример кода
import 'package:flutter/material.dart';
import 'package:english_words/english_words.dart' as words;
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'SeachAppBarRecipe',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: SeachAppBarRecipe(title: 'SeachAppBarRecipe'),
);
}
}
class SeachAppBarRecipe extends StatefulWidget {
SeachAppBarRecipe({Key key, this.title}) : super(key: key);
final String title;
@override
_SearchAppBarRecipeState createState() => _SearchAppBarRecipeState();
}
class _SearchAppBarRecipeState extends State<SeachAppBarRecipe> {
final List<String> kWords;
_SearchAppBarDelegate _searchDelegate;
//Initializing with sorted list of english words
_SearchAppBarRecipeState()
: kWords = List.from(Set.from(words.all))
..sort(
(w1, w2) => w1.toLowerCase().compareTo(w2.toLowerCase()),
),
super();
@override
void initState() {
super.initState();
//Initializing search delegate with sorted list of English words
_searchDelegate = _SearchAppBarDelegate(kWords);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
automaticallyImplyLeading: false,
title: Text('Word List'),
actions: <Widget>[
//Adding the search widget in AppBar
IconButton(
tooltip: 'Search',
icon: const Icon(Icons.search),
//Don't block the main thread
onPressed: () {
showSearchPage(context, _searchDelegate);
},
),
],
),
body: Scrollbar(
//Displaying all English words in list in app's main page
child: ListView.builder(
itemCount: kWords.length,
itemBuilder: (context, idx) =>
ListTile(
title: Text(kWords[idx]),
onTap: () {
Scaffold.of(context).showSnackBar(
SnackBar(
content: Text("Click the Search action"),
action: SnackBarAction(
label: 'Search',
onPressed: (){
showSearchPage(context, _searchDelegate);
},
)
)
);
},
),
),
),
);
}
//Shows Search result
void showSearchPage(BuildContext context,
_SearchAppBarDelegate searchDelegate) async {
final String selected = await showSearch<String>(
context: context,
delegate: searchDelegate,
);
if (selected != null) {
Scaffold.of(context).showSnackBar(
SnackBar(
content: Text('Your Word Choice: $selected'),
),
);
}
}
}
//Search delegate
class _SearchAppBarDelegate extends SearchDelegate<String> {
final List<String> _words;
final List<String> _history;
_SearchAppBarDelegate(List<String> words)
: _words = words,
//pre-populated history of words
_history = <String>['apple', 'orange', 'banana', 'watermelon'],
super();
// Setting leading icon for the search bar.
//Clicking on back arrow will take control to main page
@override
Widget buildLeading(BuildContext context) {
return IconButton(
tooltip: 'Back',
icon: AnimatedIcon(
icon: AnimatedIcons.menu_arrow,
progress: transitionAnimation,
),
onPressed: () {
//Take control back to previous page
this.close(context, null);
},
);
}
// Builds page to populate search results.
@override
Widget buildResults(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text('===Your Word Choice==='),
GestureDetector(
onTap: () {
//Define your action when clicking on result item.
//In this example, it simply closes the page
this.close(context, this.query);
},
child: Text(
this.query,
style: Theme.of(context)
.textTheme
.display2
.copyWith(fontWeight: FontWeight.normal),
),
),
],
),
),
);
}
// Suggestions list while typing search query - this.query.
@override
Widget buildSuggestions(BuildContext context) {
final Iterable<String> suggestions = this.query.isEmpty
? _history
: _words.where((word) => word.startsWith(query));
return _WordSuggestionList(
query: this.query,
suggestions: suggestions.toList(),
onSelected: (String suggestion) {
this.query = suggestion;
this._history.insert(0, suggestion);
showResults(context);
},
);
}
// Action buttons at the right of search bar.
@override
List<Widget> buildActions(BuildContext context) {
return <Widget>[
query.isNotEmpty ?
IconButton(
tooltip: 'Clear',
icon: const Icon(Icons.clear),
onPressed: () {
query = '';
showSuggestions(context);
},
) : IconButton(
icon: const Icon(Icons.mic),
tooltip: 'Voice input',
onPressed: () {
this.query = 'TBW: Get input from voice';
},
),
];
}
}
// Suggestions list widget displayed in the search page.
class _WordSuggestionList extends StatelessWidget {
const _WordSuggestionList({this.suggestions, this.query, this.onSelected});
final List<String> suggestions;
final String query;
final ValueChanged<String> onSelected;
@override
Widget build(BuildContext context) {
final textTheme = Theme.of(context).textTheme.subhead;
return ListView.builder(
itemCount: suggestions.length,
itemBuilder: (BuildContext context, int i) {
final String suggestion = suggestions[i];
return ListTile(
leading: query.isEmpty ? Icon(Icons.history) : Icon(null),
// Highlight the substring that matched the query.
title: RichText(
text: TextSpan(
text: suggestion.substring(0, query.length),
style: textTheme.copyWith(fontWeight: FontWeight.bold),
children: <TextSpan>[
TextSpan(
text: suggestion.substring(query.length),
style: textTheme,
),
],
),
),
onTap: () {
onSelected(suggestion);
},
);
},
);
}
}
Репозиторий исходного кода: Исходный код рецепта доступен здесь
Использованная литература:
Удачной готовки с Flutter
Понравилась статья? Не нашли интересующую вас тему? Пожалуйста, оставляйте комментарии или пишите мне по электронной почте о темах, которые вы хотели бы, чтобы я написал!
Кстати, я люблю кексы и кофе