第 2 部分 · Todo 列表刻意练习(Vue → Flutter)
学习目标(1 天):把 Vue 写过的最简单 Todo 用 Flutter 重写一遍,亲自感受概念差异。 功能要求:列表展示 + 添加 + 删除 + 勾选完成。
一、为什么是 Todo?
Todo List 是检验框架基本功的"原子级"练习------只要你能独立写出来,说明你已经掌握了:
- 状态(list 数据)
- 输入(文本框双向绑定 /
TextEditingController) - 列表渲染(
v-for/ListView.builder) - 列表增删(
push/splice/setState+add/removeAt) - 简单的样式与布局
二、Vue ↔ Flutter 关键差异(在 Todo 里能直接感受到的)
| 场景 | Vue 3 | Flutter |
|---|---|---|
| 维护数组 | const list = ref([]) |
List<TodoItem> list = [] 在 State 类里 |
| 数组改了刷新 UI | 自动 | 必须 setState(() { list.add(...) }) |
| 输入框双向绑定 | v-model="text" |
TextField(controller: ctrl, onSubmitted: ...) |
| 删除某项 | list.value.splice(i, 1) |
setState(() => list.removeAt(i)) |
| 勾选状态切换 | item.done = !item.done |
setState(() => item.done = !item.done) |
| 释放资源 | 不需要 | dispose() 里 controller.dispose()(重要!) |
三、目录
arduino
02-Todo练习/
├── 练习说明.md ← 本文件
├── vue-todo/
│ └── TodoApp.vue ← Vue 3 setup 版本
└── flutter-todo/
└── todo_page.dart ← Flutter 版本(StatefulWidget)
四、对照阅读建议
- 先看 vue-todo/TodoApp.vue,找出 4 个关键点:
ref在哪v-model怎么用v-for怎么写- 增删怎么实现
vue
<!--
Vue 3 setup 版 Todo
功能:增、删、勾选完成、过滤显示
-->
<template>
<div class="todo-app">
<h2>📝 Todo List (Vue)</h2>
<!-- 输入框 -->
<div class="input-row">
<input
v-model="text"
@keyup.enter="addTodo"
placeholder="输入待办事项,回车添加"
/>
<button @click="addTodo">添加</button>
</div>
<!-- 列表 -->
<ul>
<li v-for="(item, i) in list" :key="item.id" :class="{ done: item.done }">
<input type="checkbox" v-model="item.done" />
<span>{{ item.text }}</span>
<button @click="removeTodo(i)">删除</button>
</li>
</ul>
<p v-if="list.length === 0" class="empty">暂无任务</p>
<p v-else>共 {{ list.length }} 项,已完成 {{ doneCount }}</p>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const text = ref('')
const list = ref([])
const doneCount = computed(() => list.value.filter(i => i.done).length)
function addTodo() {
const v = text.value.trim()
if (!v) return
list.value.push({ id: Date.now(), text: v, done: false })
text.value = ''
}
function removeTodo(i) {
list.value.splice(i, 1)
}
</script>
<style scoped>
.todo-app { max-width: 480px; margin: 24px auto; font-family: sans-serif; }
.input-row { display: flex; gap: 8px; }
.input-row input { flex: 1; padding: 6px 8px; }
ul { list-style: none; padding: 0; }
li { display: flex; align-items: center; gap: 8px; padding: 6px 0; border-bottom: 1px solid #eee; }
li.done span { text-decoration: line-through; color: #999; }
.empty { color: #999; text-align: center; }
</style>
- 再看 flutter-todo/todo_page.dart,找出对应的 4 处:
State字段、setStateTextEditingControllerListView.builderadd/removeAt的位置
dart
import 'package:flutter/material.dart';
/// Flutter 版 Todo(对照 vue-todo/TodoApp.vue)
///
/// 关键对照点:
/// - Vue 的 ref([]) → State 字段 List<TodoItem>
/// - Vue 的 v-model → TextEditingController + onChanged/onSubmitted
/// - Vue 的 v-for → ListView.builder
/// - Vue 的 push/splice → setState(() => list.add / removeAt)
/// - Vue 的 computed → Dart 里直接写 getter
class TodoItem {
final int id;
String text;
bool done;
TodoItem({required this.id, required this.text, this.done = false});
}
class TodoPage extends StatefulWidget {
const TodoPage({super.key});
@override
State<TodoPage> createState() => _TodoPageState();
}
class _TodoPageState extends State<TodoPage> {
final TextEditingController _ctrl = TextEditingController();
final List<TodoItem> _list = [];
// 对应 Vue 的 computed
int get doneCount => _list.where((e) => e.done).length;
@override
void dispose() {
// 重要:和 Vue 不一样,必须手动释放 controller
_ctrl.dispose();
super.dispose();
}
void _addTodo() {
final v = _ctrl.text.trim();
if (v.isEmpty) return;
setState(() {
_list.add(TodoItem(
id: DateTime.now().millisecondsSinceEpoch,
text: v,
));
_ctrl.clear();
});
}
void _removeTodo(int i) {
setState(() => _list.removeAt(i));
}
void _toggle(int i, bool? v) {
setState(() => _list[i].done = v ?? false);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('📝 Todo List (Flutter)')),
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// 输入区(对应 v-model + 添加按钮)
Row(
children: [
Expanded(
child: TextField(
controller: _ctrl,
decoration: const InputDecoration(
hintText: '输入待办事项,回车添加',
border: OutlineInputBorder(),
isDense: true,
),
onSubmitted: (_) => _addTodo(),
),
),
const SizedBox(width: 8),
ElevatedButton(onPressed: _addTodo, child: const Text('添加')),
],
),
const SizedBox(height: 12),
// 列表区(对应 v-for)
Expanded(
child: _list.isEmpty
? const Center(child: Text('暂无任务', style: TextStyle(color: Colors.grey)))
: ListView.builder(
itemCount: _list.length,
itemBuilder: (ctx, i) {
final item = _list[i];
return ListTile(
leading: Checkbox(
value: item.done,
onChanged: (v) => _toggle(i, v),
),
title: Text(
item.text,
style: TextStyle(
decoration: item.done ? TextDecoration.lineThrough : null,
color: item.done ? Colors.grey : null,
),
),
trailing: IconButton(
icon: const Icon(Icons.delete_outline),
onPressed: () => _removeTodo(i),
),
);
},
),
),
// 底部统计
if (_list.isNotEmpty)
Padding(
padding: const EdgeInsets.only(top: 8),
child: Text('共 ${_list.length} 项,已完成 $doneCount'),
),
],
),
),
);
}
}
/* main 入口示例:
void main() {
runApp(const MaterialApp(home: TodoPage()));
}
*/
- 两者逐行对照:你会发现结构几乎一一对应,只是写法不同。