本章将详细介绍待办事项应用中的各个 UI 组件,帮助你理解 Flutter 界面是如何构建的。
7.1 主入口 main.dart
void main() {
WidgetsFlutterBinding.ensureInitialized();
// 设置首选方向(竖屏)
SystemChrome.setPreferredOrientations([
DeviceOrientation.portraitUp,
DeviceOrientation.portraitDown,
]);
runApp(const TodoApp());
}
class TodoApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => TaskProvider(),
child: MaterialApp(
title: '待办事项',
debugShowCheckedModeBanner: false,
theme: _buildLightTheme(),
darkTheme: _buildDarkTheme(),
themeMode: ThemeMode.system,
home: const HomeScreen(),
),
);
}
}MaterialApp 配置
| 属性 | 作用 |
|---|---|
title |
应用标题(显示在任务管理器中) |
debugShowCheckedModeBanner |
是否显示调试横幅 |
theme |
亮色主题 |
darkTheme |
暗色主题 |
themeMode |
主题模式(system/light/dark) |
home |
应用首页 |
7.2 主页面 HomeScreen
HomeScreen 是应用的主页面,包含:
- 可折叠的 AppBar
- 筛选芯片栏
- 任务列表
- 浮动操作按钮
页面结构
class HomeScreen extends StatefulWidget {
@override
State createState() => _HomeScreenState();
}
class _HomeScreenState extends State {
@override
void initState() {
super.initState();
// 页面初始化后加载任务
WidgetsBinding.instance.addPostFrameCallback((_) {
context.read().loadTasks();
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(
slivers: [
SliverAppBar(...), // 可折叠 AppBar
SliverToBoxAdapter(...), // 筛选芯片
SliverList(...), // 任务列表
],
),
floatingActionButton: FloatingActionButton(...),
);
}
}Sliver 系列组件
Sliver 是 Flutter 中用于实现可滚动效果的特殊组件。
普通滚动:ListView
┌─────────────────┐
│ AppBar │ ← 固定
├─────────────────┤
│ │
│ 可滚动内容 │ ← 一起滚动
│ │
└─────────────────┘
Sliver 滚动:CustomScrollView + Slivers
┌─────────────────┐
│ SliverAppBar │ ← 可折叠
│ (滚动时缩小) │
├─────────────────┤
│ │
│ SliverList │ ← 统一滚动
│ │
└─────────────────┘SliverAppBar
SliverAppBar(
expandedHeight: 120, // 展开高度
floating: true, // 滚动时是否浮动显示
pinned: true, // 是否固定
flexibleSpace: FlexibleSpaceBar(
title: Text('待办事项'),
centerTitle: true,
),
actions: [
IconButton(...), // 清除已完成按钮
],
)7.3 任务项 TaskItem
TaskItem 是显示单个任务的组件,支持:
- 点击切换完成状态
- 左滑删除
- 编辑和删除按钮
组件结构
class TaskItem extends StatelessWidget {
final Task task;
final VoidCallback onToggle;
final VoidCallback onDelete;
final Function(String) onEdit;
@override
Widget build(BuildContext context) {
return Dismissible( // 滑动删除
key: Key(task.id),
direction: DismissDirection.endToStart,
background: Container(color: Colors.red),
onDismissed: (_) => onDelete(),
child: Card( // 卡片容器
child: ListTile( // 列表项
leading: _buildCheckbox(),
title: _buildTitle(),
subtitle: _buildSubtitle(),
trailing: _buildActions(),
),
),
);
}
}Dismissible(滑动删除)
Dismissible(
key: Key(task.id), // 必须提供唯一 key
direction: DismissDirection.endToStart, // 从右向左滑动
background: Container(
color: Colors.red,
child: Icon(Icons.delete, color: Colors.white),
),
onDismissed: (_) {
// 滑动完成后删除
onDelete();
},
child: Card(...),
)自定义复选框
Widget _buildCheckbox(ColorScheme colorScheme) {
return InkWell(
onTap: onToggle,
child: Container(
width: 28,
height: 28,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
color: task.isCompleted
? colorScheme.primary
: colorScheme.outline,
),
color: task.isCompleted
? colorScheme.primary
: Colors.transparent,
),
child: task.isCompleted
? Icon(Icons.check, size: 16)
: null,
),
);
}效果:
未完成: 已完成:
┌────┐ ┌────┐
│ │ │ ✓ │
└────┘ └────┘
圆圈 填充圆圈带删除线的标题
Text(
task.title,
style: TextStyle(
decoration: task.isCompleted
? TextDecoration.lineThrough
: null,
color: task.isCompleted
? Colors.grey
: Colors.black,
),
)7.4 添加任务对话框 AddTaskDialog
对话框结构
class AddTaskDialog extends StatefulWidget {
@override
_AddTaskDialogState createState() => _AddTaskDialogState();
}
class _AddTaskDialogState extends State {
final _controller = TextEditingController();
DateTime? _selectedDate;
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text('添加新任务'),
content: Column(
children: [
TextField( // 任务输入框
controller: _controller,
decoration: InputDecoration(
labelText: '任务内容',
hintText: '输入你要完成的任务...',
),
),
_buildDateSelector(), // 日期选择器
],
),
actions: [
TextButton( // 取消按钮
onPressed: () => Navigator.pop(context),
child: Text('取消'),
),
FilledButton( // 添加按钮
onPressed: _submit,
child: Text('添加'),
),
],
);
}
}日期选择器
Future _pickDate() async {
final picked = await showDatePicker(
context: context,
initialDate: DateTime.now(),
firstDate: DateTime.now(),
lastDate: DateTime.now().add(Duration(days: 365 * 5)),
);
if (picked != null) {
setState(() {
_selectedDate = picked;
});
}
}返回数据
void _submit() {
final text = _controller.text.trim();
if (text.isEmpty) return;
// 返回数据并关闭对话框
Navigator.pop(context, {
'title': text,
'dueDate': _selectedDate,
});
}使用:
final result = await showDialog(
context: context,
builder: (context) => AddTaskDialog(),
);
if (result != null) {
provider.addTask(
result['title'],
dueDate: result['dueDate'],
);
}7.5 筛选栏 FilterChipBar
组件结构
class FilterChipBar extends StatelessWidget {
final TaskFilter currentFilter;
final Function(TaskFilter) onFilterChanged;
final int allCount;
final int activeCount;
final int completedCount;
@override
Widget build(BuildContext context) {
return Row(
children: [
_buildFilterChip(TaskFilter.all, allCount),
_buildFilterChip(TaskFilter.active, activeCount),
_buildFilterChip(TaskFilter.completed, completedCount),
],
);
}
}FilterChip 使用
Widget _buildFilterChip(TaskFilter filter, int count) {
final isSelected = currentFilter == filter;
return FilterChip(
selected: isSelected,
label: Row(
children: [
Text(filter.label),
Container(
padding: EdgeInsets.symmetric(horizontal: 6),
decoration: BoxDecoration(
color: isSelected ? Colors.blue : Colors.grey,
borderRadius: BorderRadius.circular(10),
),
child: Text(count.toString()),
),
],
),
onSelected: (_) => onFilterChanged(filter),
);
}效果:
[ 全部 3 ] [ 进行中 2 ] [ 已完成 1 ]
选中 未选中 未选中7.6 空状态 EmptyState
当没有任务时显示友好的提示:
class EmptyState extends StatelessWidget {
final bool isFiltered;
@override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
isFiltered ? Icons.filter_list_off : Icons.check_circle_outline,
size: 60,
color: Colors.grey,
),
SizedBox(height: 16),
Text(
isFiltered ? '没有符合条件的任务' : '还没有任务',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
SizedBox(height: 8),
Text(
isFiltered
? '尝试切换其他筛选条件'
: '点击右下角按钮添加第一个任务吧!',
style: TextStyle(color: Colors.grey),
),
],
),
);
}
}7.7 常用 Widget 速查
布局 Widget
| Widget | 作用 |
|---|---|
Container |
通用容器,可设置宽高、背景、边距 |
Row |
水平排列子组件 |
Column |
垂直排列子组件 |
Stack |
层叠排列子组件 |
Expanded |
在 Row/Column 中占据剩余空间 |
SizedBox |
固定尺寸的占位 |
Padding |
添加内边距 |
Center |
居中子组件 |
交互 Widget
| Widget | 作用 |
|---|---|
GestureDetector |
检测各种手势 |
InkWell |
带涟漪效果的点击区域 |
ElevatedButton |
凸起按钮 |
TextButton |
文本按钮 |
FilledButton |
填充按钮 |
IconButton |
图标按钮 |
FloatingActionButton |
浮动操作按钮 |
输入 Widget
| Widget | 作用 |
|---|---|
TextField |
文本输入框 |
TextFormField |
带验证的文本输入框 |
Checkbox |
复选框 |
Switch |
开关 |
Slider |
滑块 |
DropdownButton |
下拉选择 |
显示 Widget
| Widget | 作用 |
|---|---|
Text |
显示文本 |
Icon |
显示图标 |
Image |
显示图片 |
Card |
卡片容器 |
ListTile |
列表项 |
Divider |
分割线 |
CircularProgressIndicator |
圆形进度指示器 |
LinearProgressIndicator |
线性进度指示器 |
7.8 小结
本章介绍了应用的主要 UI 组件:
- MaterialApp - 应用根组件,配置主题和路由
- Scaffold - 页面基本结构
- SliverAppBar - 可折叠的顶部栏
- Dismissible - 滑动删除
- AlertDialog - 对话框
- FilterChip - 筛选芯片
- EmptyState - 空状态提示
下一步:第八章:关键技术深入