IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

    为 Flutter 应用添加搜索功能

    冷石发表于 2022-08-29 09:01:22
    love 0

    使用 SearchDelegate 给 Flutter 应用添加搜索功能

    前言

    SearchDelegate 是 Flutter 框架提供的一个实现搜索功能的类,使用它可以快速实现搜索功能,本文说明如何使用它来实现搜索功能。

    最终效果如下

    创建新项目,初始化代码如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    import 'package:flutter/material.dart';

    void main() {
    runApp(MyApp());
    }

    class MyApp extends StatelessWidget {
    @override
    Widget build(BuildContext context) {
    return MaterialApp(
    title: 'Search App',
    home: HomePage(),
    );
    }
    }

    class HomePage extends StatelessWidget {

    @override
    Widget build(BuildContext context) {
    return Scaffold(
    appBar: AppBar(
    title: Text('Search App'),
    ),
    );
    }
    }

    显示搜索页面

    showSearch 方法是 Flutter 里用来显示一个搜索页面的方法,这个页面由一个带有搜索框的 AppBar 和显示搜索建议或搜索结果的 body 组成。它有两个必要参数 context 和 delegate,context 即为当前的应用上下文,delegate 是一个实现了 SearchDelegate 抽象类自定义的部件,这个自定义部件定义了如何显示搜索页面,关闭搜索页面时返回用户选择的搜索结果。

    在 AppBar 的 actions 数组里面添加一个 IconButton,按下时调用 showSearch 方法,进入搜索页面。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    class HomePage extends StatelessWidget {
    @override
    Widget build(BuildContext context) {
    return Scaffold(
    appBar: AppBar(
    title: Text('Search App'),
    actions: <Widget>[
    IconButton(
    icon: Icon(Icons.search),
    onPressed: () {
    showSearch(context: context, delegate: CustomSearchDelegate());
    },
    )
    ],
    ),
    );
    }
    }

    初始化一个继承了 SearchDelegate 的 CustomSearchDelegate,类的名字是自定义的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    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();
    }
    }

    实现 CustomSearchDelegate

    自定义的 CustomSearchDelegate 需要实现四个方法

    • buildLeading 显示在输入框之前的部件,一般显示返回前一个页面箭头按钮
    • buildActions 显示在输入框之后的部件
    • buildResults 显示搜索结果
    • buildSuggestions 显示搜索建议

    先实现 buildActions 和 buildLeading,buildActions 显示一个清除按钮,可以把当前的 query 查询参数清空,并显示搜索建议。buildLeading 显示一个箭头的按钮,使用 close 方法关闭搜索页面,close 方法第二个参数是选定的搜索结果,如果使用系统后退按钮关闭搜索页面,则返回 null 值。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    List<Widget> buildActions(BuildContext context) {
    return [
    IconButton(
    tooltip: 'Clear',
    icon: const Icon(Icons.clear),
    onPressed: () {
    query = '';
    showSuggestions(context);
    },
    )
    ];
    }

    @override
    Widget buildLeading(BuildContext context) {
    return IconButton(
    tooltip: 'Back',
    icon: AnimatedIcon(
    icon: AnimatedIcons.menu_arrow,
    progress: transitionAnimation,
    ),
    onPressed: () {
    close(context, null);
    },
    );
    }

    @override
    Widget buildResults(BuildContext context) {
    return ListView();
    }

    @override
    Widget buildSuggestions(BuildContext context) {
    return ListView();
    }

    然后实现 buildResults 和 buildSuggestions,这两个方法用来展示搜索页面内容,可以使用不同的部显示,这里使用 ListView 部件。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    @override
    Widget buildResults(BuildContext context) {
    return ListView.builder(
    itemCount: Random().nextInt(10),
    itemBuilder: (context, index) {
    return ListTile(
    title: Text('result $index'),
    );
    },
    );
    }

    @override
    Widget buildSuggestions(BuildContext context) {
    return ListView(
    children: <Widget>[
    ListTile(title: Text('Suggest 01')),
    ListTile(title: Text('Suggest 02')),
    ListTile(title: Text('Suggest 03')),
    ListTile(title: Text('Suggest 04')),
    ListTile(title: Text('Suggest 05')),
    ],
    );
    }

    搜索结果

    搜索建议

    获取远程数据

    搜索功能一般需要请求后端的搜索接口来获取数据,此时可以使用 FutureBuilder 部件来请求数据然后渲染结果。首先需要定义一个请求接口的方法,返回一个 Future,然后在 buildResults 方法中使用 FutureBuilder 来展示结果。

    先添加 http 包,用来发送 http 请求,然后引入需要的依赖包

    1
    2
    dependencies:
    http: <latest_version>
    1
    2
    import 'dart:convert';
    import 'package:http/http.dart' as http;

    将键盘输入类型设置为数字,定义一个 _fetchPosts 方法用来获取远端数据,在 buildResults 方法里使用 FutureBuilder 渲染搜索结果。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    @override
    TextInputType get keyboardType => TextInputType.number;

    Future _fetchPosts() async {
    http.Response response =
    await http.get('https://jsonplaceholder.typicode.com/posts/$query');
    final data = await json.decode(response.body);
    return data;
    }

    @override
    Widget buildResults(BuildContext context) {
    if (int.tryParse(query) >= 100) {
    return Center(child: Text('请输入小于 100 的数字'));
    }

    return FutureBuilder(
    future: _fetchPosts(),
    builder: (context, AsyncSnapshot snapshot) {
    if (snapshot.hasData) {
    final post = snapshot.data;

    return ListTile(
    title: Text(post['title'], maxLines: 1),
    subtitle: Text(post['body'], maxLines: 3),
    );
    }
    return Center(child: CircularProgressIndicator());
    },
    );
    }

    使用 FutureBuilder 部件获取了远程的数据,但是遇到一个问题,搜索结果可能是分页显示的,一开始只获取了第一页的数据,想追加下一页数据时需要像 stateFullWidget 那样使用 setState 方法更新页面,但是在 SearchDelegate 里无法使用…暂时没想到解决方法。

    完整代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    import 'dart:convert';

    import 'package:flutter/material.dart';
    import 'package:http/http.dart' as http;

    void main() {
    runApp(MyApp());
    }

    class MyApp extends StatelessWidget {
    @override
    Widget build(BuildContext context) {
    return MaterialApp(
    title: 'Search App',
    home: HomePage(),
    );
    }
    }

    class HomePage extends StatelessWidget {
    @override
    Widget build(BuildContext context) {
    return Scaffold(
    appBar: AppBar(
    title: Text('Search App'),
    actions: <Widget>[
    IconButton(
    icon: Icon(Icons.search),
    onPressed: () {
    showSearch(context: context, delegate: CustomSearchDelegate());
    },
    )
    ],
    ),
    );
    }
    }

    class CustomSearchDelegate extends SearchDelegate {
    @override
    List<Widget> buildActions(BuildContext context) {
    return [
    IconButton(
    tooltip: 'Clear',
    icon: const Icon(Icons.clear),
    onPressed: () {
    query = '';
    showSuggestions(context);
    },
    )
    ];
    }

    @override
    Widget buildLeading(BuildContext context) {
    return IconButton(
    tooltip: 'Back',
    icon: AnimatedIcon(
    icon: AnimatedIcons.menu_arrow,
    progress: transitionAnimation,
    ),
    onPressed: () {
    this.close(context, null);
    },
    );
    }

    @override
    TextInputType get keyboardType => TextInputType.number;

    Future _fetchPosts() async {
    http.Response response =
    await http.get('https://jsonplaceholder.typicode.com/posts/$query');
    final data = await json.decode(response.body);

    return data;
    }

    @override
    Widget buildResults(BuildContext context) {
    if (int.parse(query) >= 100) {
    return Center(child: Text('请输入小于 100 的数字'));
    }

    return FutureBuilder(
    future: _fetchPosts(),
    builder: (context, AsyncSnapshot snapshot) {
    if (snapshot.hasData) {
    final post = snapshot.data;

    return ListTile(
    title: Text(post['title'], maxLines: 1),
    subtitle: Text(post['body'], maxLines: 3),
    );
    }

    return Center(child: CircularProgressIndicator());
    },
    );
    }

    @override
    Widget buildSuggestions(BuildContext context) {
    return ListView(
    children: <Widget>[
    ListTile(title: Text('Suggest 01')),
    ListTile(title: Text('Suggest 02')),
    ListTile(title: Text('Suggest 03')),
    ListTile(title: Text('Suggest 04')),
    ListTile(title: Text('Suggest 05')),
    ],
    );
    }
    }


沪ICP备19023445号-2号
友情链接