Vue 开发者快速上手 Flutter(二)

第 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)

四、对照阅读建议

  1. 先看 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>
  1. 再看 flutter-todo/todo_page.dart,找出对应的 4 处:
    • State 字段、setState
    • TextEditingController
    • ListView.builder
    • add / 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()));
}
*/
  1. 两者逐行对照:你会发现结构几乎一一对应,只是写法不同。

相关推荐
用户11481867894841 小时前
Vue 开发者快速上手 Flutter(三)
前端
JavaAgent架构师1 小时前
前端AI工程化(六):Function Calling与RAG前端实践
前端·人工智能
用户11481867894841 小时前
Vue 开发者快速上手 Flutter(一)
前端
鹏多多2 小时前
Trae cn里使用Pencil来制作设计图的手把手教程
前端·ai编程·trae
客场消音器2 小时前
如何使用codex进行UI重构,让AI开发的前端页面不再千篇一律
前端·后端·微信小程序
大家的林语冰2 小时前
Canvas 文艺复兴,HTML-in-Canvas 炫酷特效摆拍走红,Canvas 中也能渲染交互式的 HTML 元素了
前端·javascript·html
WebGirl2 小时前
Visual Studio Code (VSCode) 中配置 MCP
前端
JarvanMo3 小时前
Fluwx 6.0 预览版本他来了
前端