在这里插入图片描述

在海量的菜谱中找到想要的那一道,搜索是最直接的方式。今天我们要实现菜谱搜索功能,让用户能够通过关键词快速找到目标菜谱。

搜索功能的设计思路

搜索功能要解决的核心问题是:如何让用户快速准确地找到想要的菜谱?我选择了实时搜索的方式,用户输入关键词时立即显示结果,不需要点击搜索按钮。

搜索页面分为两个状态:未输入和已输入。未输入时显示热门搜索标签,帮助用户快速开始搜索。已输入时显示搜索结果,按相关度排序。

热门搜索标签使用 Chip 组件,点击标签就自动填入搜索框。这种设计在电商应用中很常见,能有效引导用户搜索。

创建有状态组件

搜索功能需要管理输入状态,所以要使用 StatefulWidget。

import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';

class RecipeSearchPage extends StatefulWidget {
  const RecipeSearchPage({super.key});

  
  State<RecipeSearchPage> createState() => _RecipeSearchPageState();
}

class _RecipeSearchPageState extends State<RecipeSearchPage> {
  final TextEditingController _controller = TextEditingController();

TextEditingController 用于控制搜索框的输入。我们可以通过它获取输入内容,也可以程序化地设置输入内容。

创建 controller 后要记得在 dispose 方法中释放,避免内存泄漏:


void dispose() {
  _controller.dispose();
  super.dispose();
}

构建搜索界面

搜索框放在 AppBar 的 title 位置,这是搜索页面的标准设计。

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: TextField(
          controller: _controller,
          decoration: const InputDecoration(
            hintText: '搜索菜谱...',
            border: InputBorder.none,
            hintStyle: TextStyle(color: Colors.white70),
          ),
          style: const TextStyle(color: Colors.white),
        ),

TextField 使用白色文字,提示文字使用半透明的白色。border 设置为 none,去掉默认的下划线,让搜索框和 AppBar 融为一体。

autofocus 可以设置为 true,让页面打开时自动聚焦搜索框,用户可以直接输入。但这会自动弹出键盘,可能不是所有用户都喜欢。

        actions: [
          IconButton(
            icon: const Icon(Icons.search),
            onPressed: () {},
          ),
        ],
      ),

AppBar 右侧放了一个搜索图标,虽然我们使用实时搜索,但这个图标能让用户知道这是搜索页面。点击图标可以触发搜索,或者关闭键盘。

      body: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Padding(
            padding: EdgeInsets.all(16.w),
            child: Text('热门搜索', style: TextStyle(fontSize: 16.sp, fontWeight: FontWeight.bold)),
          ),
          Wrap(
            spacing: 8.w,
            runSpacing: 8.h,
            children: ['宫保鸡丁', '红烧肉', '糖醋排骨', '麻婆豆腐', '鱼香肉丝']
                .map((tag) => Chip(label: Text(tag)))
                .toList(),
          ),
        ],
      ),
    );
  }
}

body 显示热门搜索标签。标题使用粗体,字号 16.sp。标签使用 Wrap 组件排列,会自动换行。

spacing 和 runSpacing 都设置为 8,让标签之间有适当的间距。每个标签使用 Chip 组件,这是 Flutter 提供的标签组件。

实现标签点击

点击热门搜索标签应该自动填入搜索框并触发搜索:

Chip(
  label: Text(tag),
  onPressed: () {
    _controller.text = tag;
    _performSearch(tag);
  },
)

但 Chip 组件没有 onPressed 属性,需要用 ActionChip 或 GestureDetector 包裹:

GestureDetector(
  onTap: () {
    _controller.text = tag;
    _performSearch(tag);
  },
  child: Chip(label: Text(tag)),
)

点击标签时,设置 controller 的 text 属性,搜索框会自动显示这个文字。然后调用 _performSearch 方法执行搜索。

实现实时搜索

用户输入时应该实时显示搜索结果:

TextField(
  controller: _controller,
  onChanged: (value) {
    setState(() {
      _performSearch(value);
    });
  },
  // ...
)

onChanged 回调在输入内容改变时触发。我们调用 setState 触发重建,并执行搜索。

但这样会导致每输入一个字符就搜索一次,如果搜索涉及网络请求,会造成大量无效请求。可以使用防抖技术,延迟一段时间再搜索:

Timer? _debounce;

void _onSearchChanged(String value) {
  if (_debounce?.isActive ?? false) _debounce!.cancel();
  _debounce = Timer(const Duration(milliseconds: 500), () {
    _performSearch(value);
  });
}


void dispose() {
  _debounce?.cancel();
  _controller.dispose();
  super.dispose();
}

使用 Timer 延迟 500 毫秒执行搜索。如果在这期间用户又输入了新的字符,就取消之前的 Timer,重新计时。

这样只有用户停止输入 500 毫秒后才会执行搜索,大大减少了搜索次数。

显示搜索结果

搜索结果使用列表展示:

body: _controller.text.isEmpty
    ? _buildHotSearches()
    : _buildSearchResults(),

根据搜索框是否为空,显示不同的内容。为空时显示热门搜索,不为空时显示搜索结果。

Widget _buildSearchResults() {
  return ListView.builder(
    padding: EdgeInsets.all(16.w),
    itemCount: searchResults.length,
    itemBuilder: (context, index) {
      return _buildResultItem(searchResults[index]);
    },
  );
}

搜索结果使用 ListView.builder 展示。实际开发中,searchResults 应该根据搜索关键词从数据库或网络获取。

Widget _buildResultItem(Recipe recipe) {
  return Container(
    margin: EdgeInsets.only(bottom: 12.h),
    padding: EdgeInsets.all(12.w),
    decoration: BoxDecoration(
      color: Colors.white,
      borderRadius: BorderRadius.circular(12.r),
    ),
    child: Row(
      children: [
        Container(
          width: 60.w,
          height: 60.h,
          decoration: BoxDecoration(
            color: Colors.orange.shade100,
            borderRadius: BorderRadius.circular(8.r),
          ),
          child: Icon(Icons.restaurant, size: 30.sp, color: Colors.orange),
        ),
        SizedBox(width: 12.w),
        Expanded(
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Text(
                recipe.name,
                style: TextStyle(fontSize: 14.sp, fontWeight: FontWeight.bold),
              ),
              SizedBox(height: 4.h),
              Text(
                recipe.description,
                style: TextStyle(fontSize: 11.sp, color: Colors.grey),
                maxLines: 1,
                overflow: TextOverflow.ellipsis,
              ),
            ],
          ),
        ),
      ],
    ),
  );
}

搜索结果项和其他列表项类似,包含图片、名称和描述。描述使用单行显示,超出部分用省略号表示。

高亮搜索关键词

为了让用户更容易找到匹配的部分,可以高亮显示搜索关键词:

Text.rich(
  TextSpan(
    children: _highlightKeyword(recipe.name, _controller.text),
  ),
)

_highlightKeyword 方法将文本分割成多个部分,匹配的部分使用不同的样式:

List<TextSpan> _highlightKeyword(String text, String keyword) {
  if (keyword.isEmpty) {
    return [TextSpan(text: text)];
  }
  
  final List<TextSpan> spans = [];
  final lowerText = text.toLowerCase();
  final lowerKeyword = keyword.toLowerCase();
  
  int start = 0;
  int index = lowerText.indexOf(lowerKeyword);
  
  while (index != -1) {
    if (index > start) {
      spans.add(TextSpan(text: text.substring(start, index)));
    }
    spans.add(TextSpan(
      text: text.substring(index, index + keyword.length),
      style: TextStyle(color: Colors.orange, fontWeight: FontWeight.bold),
    ));
    start = index + keyword.length;
    index = lowerText.indexOf(lowerKeyword, start);
  }
  
  if (start < text.length) {
    spans.add(TextSpan(text: text.substring(start)));
  }
  
  return spans;
}

这个方法找到所有匹配的位置,将匹配的部分用橙色粗体显示,其他部分用默认样式。

添加搜索历史

用户可能会重复搜索相同的关键词,可以保存搜索历史:

List<String> searchHistory = [];

void _performSearch(String keyword) {
  if (keyword.isNotEmpty && !searchHistory.contains(keyword)) {
    setState(() {
      searchHistory.insert(0, keyword);
      if (searchHistory.length > 10) {
        searchHistory.removeLast();
      }
    });
  }
  // 执行搜索
}

每次搜索时,将关键词添加到历史记录。使用 insert(0, keyword) 将新记录添加到开头,保持最近的记录在前面。

限制历史记录数量为 10,超过时删除最旧的记录。这样可以避免历史记录无限增长。

搜索历史可以显示在热门搜索下方:

if (searchHistory.isNotEmpty) {
  Column(
    crossAxisAlignment: CrossAxisAlignment.start,
    children: [
      Padding(
        padding: EdgeInsets.all(16.w),
        child: Row(
          mainAxisAlignment: MainAxisAlignment.spaceBetween,
          children: [
            Text('搜索历史', style: TextStyle(fontSize: 16.sp, fontWeight: FontWeight.bold)),
            TextButton(
              onPressed: () {
                setState(() {
                  searchHistory.clear();
                });
              },
              child: Text('清空'),
            ),
          ],
        ),
      ),
      ...searchHistory.map((keyword) => ListTile(
        leading: Icon(Icons.history),
        title: Text(keyword),
        onTap: () {
          _controller.text = keyword;
          _performSearch(keyword);
        },
      )),
    ],
  ),
}

搜索历史使用 ListTile 展示,每个历史记录可以点击重新搜索。右上角有一个"清空"按钮,可以清空所有历史记录。

添加空状态

如果搜索没有结果,需要显示空状态:

if (searchResults.isEmpty) {
  return Center(
    child: Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        Icon(Icons.search_off, size: 64.sp, color: Colors.grey),
        SizedBox(height: 16.h),
        Text('没有找到相关菜谱', style: TextStyle(color: Colors.grey)),
      ],
    ),
  );
}

空状态使用搜索图标和文字说明,让用户知道这是正常的情况,不是出错了。

优化搜索性能

如果菜谱数量很多,搜索可能会比较慢。可以使用全文搜索引擎,或者在数据库中建立索引:

Future<List<Recipe>> searchRecipes(String keyword) async {
  final db = await database;
  return await db.query(
    'recipes',
    where: 'name LIKE ? OR description LIKE ?',
    whereArgs: ['%$keyword%', '%$keyword%'],
  );
}

使用 LIKE 查询可以匹配包含关键词的菜谱。但 LIKE 查询性能不好,如果数据量大,应该使用全文搜索。

总结

菜谱搜索功能使用实时搜索的方式,用户输入时立即显示结果。热门搜索标签和搜索历史能帮助用户快速开始搜索。

通过合理的交互设计和性能优化,我们让搜索功能既快速又准确。用户可以轻松找到想要的菜谱,不用在海量数据中慢慢翻找。

下一篇文章我们将实现菜谱详情展示功能,让用户能够查看完整的菜谱信息。


欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐