
在移动应用开发中,工具类应用(如字典、计算器、备忘录)是检验开发者对 UI/UX、状态管理和数据流理解的绝佳练兵场。本文将深入剖析一个基于 Flutter 构建的 中文词语字典查询 App 的完整代码,全面讲解其 搜索交互、历史记录、结果展示和详情页设计 四大核心模块的实现原理与最佳实践。
完整效果展示


该 App 采用经典的 单页面 + 路由跳转 架构:
DictionaryHomeScreen: 主页,负责搜索、结果显示和历史记录管理。DictionaryDetailScreen: 详情页,展示单个词语的完整信息。通过 Navigator.push 实现页面间的平滑过渡,符合 Material Design 规范。
// 跳转到详情页的核心代码
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => DictionaryDetailScreen(entry: entry),
),
);
这种分离关注点的设计,使得主页专注于 “发现”,详情页专注于 “理解”,职责清晰,易于维护。
DictionaryHomeScreen):智能搜索中枢主页是用户与字典交互的第一入口,其核心功能围绕 搜索 展开。
主页使用 StatefulWidget 管理四个关键状态:
状态变量 | 类型 | 作用 |
|---|---|---|
_searchController | TextEditingController | 绑定搜索框的输入内容 |
_searchResults | List<DictionaryEntry> | 存储当前搜索匹配的结果 |
_isSearching | bool | 标记是否处于搜索加载中 |
_searchHistory | List<String> | 记录用户的搜索历史 |
这些状态共同决定了 UI 的四种主要形态:
CircularProgressIndicator。_performSearch 方法这是整个 App 的 业务逻辑核心。
void _performSearch(String query) {
if (query.trim().isEmpty) {
setState(() { _searchResults = []; });
return;
}
// ... 设置 _isSearching 为 true
Future.delayed(const Duration(milliseconds: 300), () {
final results = _dictionaryData.where((entry) {
return entry.word.contains(query) || // 匹配词语
entry.pinyin.toLowerCase().contains(query.toLowerCase()) || // 匹配拼音
entry.definition.contains(query); // 匹配释义
}).toList();
// ... 更新搜索历史
setState(() {
_searchResults = results;
_isSearching = false;
});
});
}
关键亮点:
Future.delayed 模拟真实 API 请求的延迟,让 UI 反馈更真实。onChanged 直接触发搜索,在简单场景下可接受。对于复杂场景,建议加入防抖以避免频繁请求。搜索历史功能让用户能快速回溯之前的查询,是优秀 UX 的体现。
// 添加到历史
if (query.isNotEmpty && !_searchHistory.contains(query)) {
_searchHistory.insert(0, query); // 最新查询放在最前面
if (_searchHistory.length > 10) {
_searchHistory.removeLast(); // 限制最多10条
}
}
// 清除历史
_searchHistory.clear();
// 删除单条历史
_searchHistory.remove(term);
UI 实现:
Wrap 布局展示历史词条,自动换行,适应不同屏幕。Chip 组件呈现,并带有删除按钮 (onDeleted),操作直观。💡 条件渲染:历史记录区域仅在
_searchController.text.isEmpty时显示,避免与搜索结果冲突。
borderRadius: 12),内填充 (filled: true),视觉上更柔和。suffixIcon),方便用户一键清空。BoxShadow) 提升层次感。ListTile):chevron_right 明确指示可点击进入详情。Card 组件,带圆角和阴影,形成独立的信息块。DictionaryDetailScreen):沉浸式学习体验当用户点击某个词语后,会进入一个精心设计的详情页,提供全方位的语言学习支持。
详情页通过构造函数接收一个完整的 DictionaryEntry 对象。
class DictionaryDetailScreen extends StatelessWidget {
final DictionaryEntry entry; // 接收的数据
const DictionaryDetailScreen({super.key, required this.entry});
// ...
}required 关键字:确保调用方必须传入 entry,避免空指针异常。final 修饰:保证数据在页面生命周期内不可变,符合 Flutter 的声明式编程思想。_buildSection 的复用艺术详情页内容被清晰地划分为 词语信息、释义、例句、相关词语 四个模块。为了减少重复代码,作者巧妙地封装了一个通用的 _buildSection 方法。
Widget _buildSection(String title, IconData icon, Widget content) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(children: [Icon(icon), Text(title)]), // 统一的标题栏
const SizedBox(height: 12),
content, // 各模块自定义的内容
],
);
}优势:
fontSize: 48) 突出显示词语,营造视觉焦点。partOfSpeech) 用彩色标签 (Container) 高亮,信息一目了然。definition 字段。") 和斜体 (fontStyle: FontStyle.italic) 模拟真实引用。Colors.amber[50]) 背景,与普通文本区分,提升可读性。Chip 带有圆形头像 (CircleAvatar),头像内显示词语首字,设计新颖且节省空间。Colors.blue[50]) 背景,与 App 主色调 (0xFF4A90E2) 呼应。收藏 和 分享 是字典类 App 的核心操作。收藏 用红色系强调,“分享”用主蓝色系,符合用户心智模型。SnackBar,告知用户操作成功。DictionaryEntry):结构化的基石一个清晰的数据模型是构建健壮应用的前提。
class DictionaryEntry {
final String word; // 词语
final String pinyin; // 拼音
final String partOfSpeech; // 词性
final String definition; // 释义
final String example; // 例句
final List<String> relatedWords; // 相关词语
}final 字段:保证对象创建后不可变,线程安全,也便于 Flutter 的 const 构造和性能优化。这个字典 App 虽小,却五脏俱全,完美展示了 Flutter 开发的核心思想:
Card, Chip, _buildSection 等可复用组件。StatefulWidget 管理局部状态,逻辑清晰。required、final 等关键字,保证了代码的健壮性和可读性。欢迎加入 开源鸿蒙跨平台开发者社区,获取最新资源与技术支持: 👉 开源鸿蒙跨平台开发者社区
技术因分享而进步,生态因共建而繁荣。 —— 晚霞的不甘 · 与您共赴鸿蒙跨平台开发之旅
import 'package:flutter/material.dart';
void main() {
runApp(const DictionaryApp());
}
class DictionaryApp extends StatelessWidget {
const DictionaryApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: '字典查询',
theme: ThemeData(
primaryColor: const Color(0xFF4A90E2),
useMaterial3: true,
),
home: const DictionaryHomeScreen(),
);
}
}
class DictionaryHomeScreen extends StatefulWidget {
const DictionaryHomeScreen({super.key});
@override
State<DictionaryHomeScreen> createState() => _DictionaryHomeScreenState();
}
class _DictionaryHomeScreenState extends State<DictionaryHomeScreen> {
final TextEditingController _searchController = TextEditingController();
List<DictionaryEntry> _searchResults = [];
bool _isSearching = false;
List<String> _searchHistory = [];
// 模拟字典数据
final List<DictionaryEntry> _dictionaryData = [
DictionaryEntry(
word: '学习',
pinyin: 'xué xí',
partOfSpeech: '动词',
definition: '通过阅读、听讲、研究、实践等途径获得知识或技能。',
example: '我们要努力学习科学文化知识。',
relatedWords: ['自学', '研习', '进修'],
),
DictionaryEntry(
word: '努力',
pinyin: 'nǔ lì',
partOfSpeech: '形容词',
definition: '尽最大力量;尽一切可能。',
example: '他工作非常努力。',
relatedWords: ['勤奋', '刻苦', '尽力'],
),
DictionaryEntry(
word: '知识',
pinyin: 'zhī shi',
partOfSpeech: '名词',
definition: '人们在认识世界、改造世界的过程中积累起来的经验。',
example: '知识就是力量。',
relatedWords: ['常识', '学识', '见识'],
),
DictionaryEntry(
word: '文化',
pinyin: 'wén huà',
partOfSpeech: '名词',
definition: '人类在社会实践中所创造的物质财富和精神财富的总和。',
example: '中华文明历史悠久,文化灿烂。',
relatedWords: ['文明', '艺术', '传统'],
),
DictionaryEntry(
word: '创新',
pinyin: 'chuàng xīn',
partOfSpeech: '动词',
definition: '抛开旧的,创造新的。',
example: '科技创新推动了社会进步。',
relatedWords: ['改革', '创造', '发明'],
),
DictionaryEntry(
word: '发展',
pinyin: 'fā zhǎn',
partOfSpeech: '动词',
definition: '事物由小到大、由简到繁、由低级到高级的变化。',
example: '经济持续健康发展。',
relatedWords: ['进步', '增长', '扩展'],
),
DictionaryEntry(
word: '理想',
pinyin: 'lǐ xiǎng',
partOfSpeech: '名词',
definition: '对未来事物的想象或希望。',
example: '每个人都有自己的理想。',
relatedWords: ['梦想', '抱负', '目标'],
),
DictionaryEntry(
word: '坚持',
pinyin: 'jiān chí',
partOfSpeech: '动词',
definition: '坚决保持、维持或进行。',
example: '坚持就是胜利。',
relatedWords: ['坚守', '执着', '持续'],
),
DictionaryEntry(
word: '成功',
pinyin: 'chéng gōng',
partOfSpeech: '名词',
definition: '达到预期的目的或结果。',
example: '经过不懈努力,他终于成功了。',
relatedWords: ['胜利', '成就', '收获'],
),
DictionaryEntry(
word: '友谊',
pinyin: 'yǒu yì',
partOfSpeech: '名词',
definition: '朋友之间的交情。',
example: '友谊是人生最宝贵的财富之一。',
relatedWords: ['友情', '交情', '伙伴'],
),
];
void _performSearch(String query) {
if (query.trim().isEmpty) {
setState(() {
_searchResults = [];
});
return;
}
setState(() {
_isSearching = true;
});
// 模拟搜索延迟
Future.delayed(const Duration(milliseconds: 300), () {
final results = _dictionaryData.where((entry) {
return entry.word.contains(query) ||
entry.pinyin.toLowerCase().contains(query.toLowerCase()) ||
entry.definition.contains(query);
}).toList();
// 添加到搜索历史
if (query.isNotEmpty && !_searchHistory.contains(query)) {
setState(() {
_searchHistory.insert(0, query);
if (_searchHistory.length > 10) {
_searchHistory.removeLast();
}
});
}
setState(() {
_searchResults = results;
_isSearching = false;
});
});
}
void _showDetailScreen(DictionaryEntry entry) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => DictionaryDetailScreen(entry: entry)),
);
}
@override
void dispose() {
_searchController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('字典查询'),
backgroundColor: const Color(0xFF4A90E2),
elevation: 0,
),
body: Column(
children: [
// 搜索栏
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.1),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: TextField(
controller: _searchController,
decoration: InputDecoration(
hintText: '输入词语或文章进行查询...',
prefixIcon: const Icon(Icons.search),
suffixIcon: _searchController.text.isNotEmpty
? IconButton(
icon: const Icon(Icons.clear),
onPressed: () {
_searchController.clear();
setState(() {
_searchResults = [];
});
},
)
: null,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide.none,
),
filled: true,
fillColor: Colors.grey[100],
),
onChanged: (value) {
_performSearch(value);
},
onSubmitted: (value) {
_performSearch(value);
},
),
),
// 搜索结果
Expanded(
child: _isSearching
? const Center(
child: CircularProgressIndicator(),
)
: _searchResults.isEmpty
? Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.book_outlined,
size: 100,
color: Colors.grey[300],
),
const SizedBox(height: 20),
Text(
_searchController.text.isEmpty
? '输入词语开始查询'
: '未找到相关词语',
style: TextStyle(
fontSize: 18,
color: Colors.grey[600],
),
),
],
)
: ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: _searchResults.length,
itemBuilder: (context, index) {
final entry = _searchResults[index];
return Card(
elevation: 2,
margin: const EdgeInsets.only(bottom: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: ListTile(
contentPadding: const EdgeInsets.all(16),
title: Text(
entry.word,
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 4),
Text(
entry.pinyin,
style: TextStyle(
fontSize: 14,
color: Colors.grey[600],
),
),
const SizedBox(height: 8),
Text(
entry.definition,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 14,
color: Colors.grey[800],
),
),
],
),
trailing: const Icon(
Icons.chevron_right,
color: Colors.grey,
),
onTap: () => _showDetailScreen(entry),
),
);
},
),
),
// 搜索历史
if (_searchHistory.isNotEmpty && _searchController.text.isEmpty)
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
border: Border(
top: BorderSide(color: Colors.grey[200]!),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'搜索历史',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
TextButton(
onPressed: () {
setState(() {
_searchHistory.clear();
});
},
child: const Text('清除'),
),
],
),
const SizedBox(height: 8),
Wrap(
spacing: 8,
runSpacing: 8,
children: _searchHistory.map((term) {
return Chip(
label: Text(term),
onDeleted: () {
setState(() {
_searchHistory.remove(term);
});
},
deleteIcon: const Icon(Icons.close, size: 16),
backgroundColor: Colors.grey[100],
padding: const EdgeInsets.symmetric(horizontal: 8),
);
}).toList(),
),
],
),
),
],
),
);
}
}
// 词语详情页面
class DictionaryDetailScreen extends StatelessWidget {
final DictionaryEntry entry;
const DictionaryDetailScreen({super.key, required this.entry});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(entry.word),
backgroundColor: const Color(0xFF4A90E2),
elevation: 0,
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 词语和拼音
Card(
elevation: 4,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
children: [
Text(
entry.word,
style: const TextStyle(
fontSize: 48,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 12),
Text(
entry.pinyin,
style: TextStyle(
fontSize: 24,
color: Colors.grey[600],
),
),
const SizedBox(height: 12),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
decoration: BoxDecoration(
color: const Color(0xFF4A90E2).withOpacity(0.1),
borderRadius: BorderRadius.circular(20),
),
child: Text(
entry.partOfSpeech,
style: const TextStyle(
color: Color(0xFF4A90E2),
fontWeight: FontWeight.bold,
),
),
),
],
),
),
),
const SizedBox(height: 24),
// 释义
_buildSection(
'释义',
Icons.info_outline,
Text(
entry.definition,
style: const TextStyle(fontSize: 16, height: 1.6),
),
),
const SizedBox(height: 24),
// 例句
_buildSection(
'例句',
Icons.format_quote,
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.amber[50],
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.amber[200]!),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'"',
style: TextStyle(
fontSize: 32,
color: Colors.amber,
fontWeight: FontWeight.bold,
),
),
Expanded(
child: Text(
entry.example,
style: const TextStyle(
fontSize: 16,
fontStyle: FontStyle.italic,
height: 1.6,
),
),
),
],
),
),
),
const SizedBox(height: 24),
// 相关词语
_buildSection(
'相关词语',
Icons.link,
Wrap(
spacing: 12,
runSpacing: 12,
children: entry.relatedWords.map((word) {
return Chip(
label: Text(word),
avatar: CircleAvatar(
backgroundColor: const Color(0xFF4A90E2),
child: Text(
word[0],
style: const TextStyle(
color: Colors.white,
fontSize: 12,
),
),
),
backgroundColor: Colors.blue[50],
padding:
const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
);
}).toList(),
),
),
const SizedBox(height: 32),
// 操作按钮
Row(
children: [
Expanded(
child: ElevatedButton.icon(
onPressed: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('已收藏'),
duration: Duration(seconds: 1),
),
);
},
icon: const Icon(Icons.favorite_border),
label: const Text('收藏'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red[50],
foregroundColor: Colors.red,
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
),
),
const SizedBox(width: 16),
Expanded(
child: ElevatedButton.icon(
onPressed: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('已分享'),
duration: Duration(seconds: 1),
),
);
},
icon: const Icon(Icons.share),
label: const Text('分享'),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF4A90E2),
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
),
),
],
),
],
),
),
);
}
Widget _buildSection(String title, IconData icon, Widget content) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(icon, color: const Color(0xFF4A90E2)),
const SizedBox(width: 8),
Text(
title,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
],
),
const SizedBox(height: 12),
content,
],
);
}
}
// 字典条目数据模型
class DictionaryEntry {
final String word;
final String pinyin;
final String partOfSpeech;
final String definition;
final String example;
final List<String> relatedWords;
DictionaryEntry({
required this.word,
required this.pinyin,
required this.partOfSpeech,
required this.definition,
required this.example,
required this.relatedWords,
});
}