要点
从 flutter create 到跑通一个应用,中间只有三步:创建工程、选设备运行、改代码保存。第一周真正要建立的概念只有两个——热重载(Hot Reload)让改代码的成本低到可以随手试错;”一切皆 Widget(组件)”让界面、样式、布局、甚至应用本身都用同一种东西搭出来。写过 React 或 React Native 的话,这两个概念都有对应物,迁移成本主要在忘掉 JSX 和 CSS。
创建工程
flutter create todo_app
cd todo_app
flutter devices # 看本机可用设备:模拟器、真机、Chrome、桌面
flutter run # 只有一个设备直接跑;多个设备会弹出选择列表
1. 也可以显式指定:flutter run -d chrome 或 -d windows
flutter create 一次生成全部平台的壳工程(android、ios、web、windows、macos、linux 目录)与一份默认计数器应用。命令行工具不依赖任何 IDE,Android Studio 与 VS Code 里的”新建 Flutter 项目”只是同一条命令的图形界面。跑起来后你会看到右下角加号计数的示例页——它就是官方给你的第一个热重载教材:把 _counter 递增那行改成 _counter += 2,保存,屏幕上的数字没变,再点一次加号,步长变成了 2。状态还在,代码已换,这就是热重载。
启动链路:flutter run 在做什么
和 RN 的”Metro 起服务、设备来拉 bundle”不同,Flutter 开发模式的链路是:flutter run 把 Dart 代码 JIT 编译后注入目标设备上的 Dart VM 运行,同时保持一个连接,热重载的增量代码修改就从这个连接注入;发布构建(flutter build)则改为 AOT 编译成机器码打进安装包。渲染层面,Flutter 不桥接平台原生控件,所有像素由自带引擎(iOS 端当前默认 Impeller,Android 端 Impeller 可选开启)直接绘制——这是它和 React Native 最根本的差异:没有”JS 组件到原生视图”的翻译层,Scaffold 在 Android 和 iOS 上长得一样,因为根本就是同一套自绘结果。运行时终端会提示 r 热重载、R 热重启、q 退出;DevTools 调试面板也会在运行时自动提示打开。
第一个界面:待办清单
把 lib/main.dart 整个替换为下面的代码。默认模板的计数器只演示了按钮改数字,这里加上输入框与列表,覆盖日常开发的三个最小闭环:输入、更新状态、按状态渲染列表。
import 'package:flutter/material.dart';
void main() => runApp(const TodoApp());
class TodoApp extends StatelessWidget {
const TodoApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
home: TodoHome(),
);
}
}
class TodoHome extends StatefulWidget {
const TodoHome({super.key});
@override
State<TodoHome> createState() => _TodoHomeState();
}
class _TodoHomeState extends State<TodoHome> {
final _input = TextEditingController();
final _todos = <String>[];
void add() {
final text = _input.text.trim();
if (text.isEmpty) return; // 拒绝空条目:边界在入口处挡,而不是渲染时兜底
setState(() { // 只有在 setState 里改,框架才会重跑 build
_todos.add(text);
});
_input.clear();
}
@override
void dispose() {
_input.dispose(); // controller 持有底层资源,State 销毁时必须释放
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('待办')),
body: Column(
children: [
Padding(
padding: const EdgeInsets.all(12),
child: Row(
children: [
Expanded(
child: TextField(
controller: _input,
// 键盘的完成键也能提交,不只靠按钮
onSubmitted: (_) => add(),
),
),
const SizedBox(width: 8),
FilledButton(onPressed: add, child: const Text('添加')),
],
),
),
// Expanded 吃掉剩余空间,列表才知道自己有多高可滚
Expanded(
child: ListView.builder(
itemCount: _todos.length,
itemBuilder: (context, i) => ListTile(
title: Text(_todos[i]),
trailing: IconButton(
icon: const Icon(Icons.delete_outline),
onPressed: () => setState(() => _todos.removeAt(i)),
),
),
),
),
],
),
);
}
}
可观察的行为:输入文字点”添加”(或按回车)条目出现在列表;删除按钮移除对应行;输入为空时点添加没有任何反应。改一处代码保存——比如把 AppBar 标题换成”今天要做”——不到一秒界面就变,列表内容不受影响。这段代码里有两个新手必懂的机制:setState 标记当前 State 为脏、触发下一次 build;dispose 里释放 controller,否则会资源泄漏(Dart 的 GC 不会替你调用 dispose)。
热重载的第一惊喜与边界
| 操作 | 快捷键 / 命令 | 保留什么 | 什么时候必须用它 |
|---|---|---|---|
| 热重载 | 保存文件,或终端按 r | State 状态、导航栈位置 | 日常 95% 的改动:改 build 里的界面与逻辑 |
| 热重启 | 终端按 R | 丢弃 State 与导航,回首页;编译缓存仍在 | 改了 State 字段的初始值、main() 入口、全局变量 |
| 完整停止重跑 | q 后重新 flutter run | 全部重来 | 改了原生代码、pubspec 依赖、平台配置 |
“改了初始值热重载不生效”是最常见的困惑:热重载只替换可执行代码,不会重新执行已经跑过的初始化。把 _todos 的初始值从空列表改成三个条目,热重载后屏幕没变,按 R 才变——这不是 bug,理解这条边界比背快捷键重要。React Native 的 Fast Refresh 在概念上同源,但 Flutter 的 reload 不需要你理解哪个模块会被整页替换,粒度问题少得多。
Widget 是什么:一切皆 Widget
Flutter 没有”元素/组件/指令”之分:Scaffold 是 Widget(页面骨架),Padding 是 Widget(样式),Expanded 是 Widget(布局约束),MaterialApp 也是 Widget(应用本身)。Widget 两个关键性质:其一,不可变——Widget 是轻量配置对象,状态放在 StatefulWidget 的 State 里,重建 Widget 不等于重建界面,框架内部用 Element 树做协调复用(角色类似 React 的 reconciliation);其二,组合优于配置——Flutter 没有属性继承链,Padding、Alignment、背景色都是一层层 Widget 包出来的, Material 组件库本身就大量用这种组合实现。StatelessWidget 与 StatefulWidget 的选择标准只有一条:build 过程依赖会变的数据吗?不依赖(如 TodoApp 的壳)就 Stateless,依赖(如 _todos)就 Stateful。
与 React / RN 的心智对照
从 React 系过来的开发者按这张表对号入座,能省掉大半”为什么这里不一样”的卡顿:
| 概念 | React / RN | Flutter |
|---|---|---|
| UI 描述 | JSX,标签语法 | 嵌套的构造函数调用,具名参数代替 props |
| 状态更新 | useState 返回独立 setter | 一个 setState 包住字段修改,标记整个 State 重建 |
| 重渲染 | setter 触发组件函数重跑 | setState 触发 build 重跑 |
| 树协调 | Virtual DOM diff、reconciliation | Widget/Element 两层树按类型与 key 匹配复用 |
| 样式 | CSS / StyleSheet 对象 | 没有 CSS,样式即 Widget(Padding、TextStyle、DecoratedBox) |
| 渲染目标 | RN 映射原生控件;Web 是 DOM | 全部自绘,Android/iOS/桌面/Web 同一套绘制结果 |
最容易带过来的坏习惯是 CSS 思维:在 Flutter 里找”margin: 0 auto”的等价物会一无所获,间距是 Padding,居中是 Center 或 MainAxisAlignment,盒子样式是 Container 的装饰参数,它们都是 Widget 或构造参数而不是样式表。setState 的快照语义与”什么时候会重建”的困惑,读《React Hooks 心智模型》里对渲染输入输出的拆解后回头对照,会发现两边共享同一条铁律:状态是渲染的输入,渲染不回头改状态。
目录速读
todo_app/
├── lib/ # 95% 的代码在这里,入口是 main.dart
├── android/ # 平台壳工程:接原生 SDK、改权限时才动
├── ios/ # 同上,且仅 macOS 上可构建
├── web/ windows/ macos/ linux/
├── test/ # widget test 放这里,flutter test 运行
└── pubspec.yaml # 依赖与资源清单,角色等同 package.json
依赖管理是 pub 而非 npm:加包改 pubspec.yaml(或 flutter pub add 包名),锁文件是 pubspec.lock,团队提交它以保证构建一致。
最佳实践与反模式
反模式一:把副作用写进 build。build 在一帧内可能被调用多次,网络请求、事件订阅、创建 controller 放在这里会成倍执行;初始化归 initState,清理归 dispose,渲染归 build,三者不要混。
反模式二:处处 StatefulWidget。每个 Stateful 都多出一份 State 生命周期要管,而无状态组件加 const 构造后框架可以直接跳过它的重建。判断标准就一句:build 依赖会变的数据吗?不依赖就是 Stateless。
节奏上信热重载:先让应用跑起来再改,改一小块保存看一次效果,比”攒一大坨代码再运行找错”快一个数量级——这是 Flutter 给开发体验的最大红利,值得从第一个应用就养成习惯。
常见问题
Q1: A RenderFlex overflowed by N pixels
Row/Column 内容超出可用空间,屏幕上出现黄黑条纹。新手几乎必撞。解法是让某个子项弹性伸缩:包一层 Expanded 或 Flexible,代码示例里输入框正是这么处理的。这不是样式警告,是布局约束的真实冲突。
Q2: 热重载没反应或行为怪异
先确认 flutter run 的终端还连着(没按过 q);再确认改的是 lib/ 下的 Dart 代码而不是平台目录;改了初始化代码就按 R 热重启。仍无效再完整重跑。
Q3: setState 之后界面没变
检查修改是不是真的包在 setState 的回调里。只写 _todos.add(x) 再单独调 setState() 不带修改,列表数据其实变了但界面不会刷新——setState 的参数是”修改动作本身”,不是通知信号。
练习与实践
练习一:给条目加”完成”状态。把 <String>[] 换成自定义类或 record(如 list of (String, bool)),点击行切换布尔值并用删除线样式呈现。做这个练习会逼你经历一次”数据结构变了,build 怎么跟着变”的完整闭环。
练习二:验证热重载边界。先改 AppBar 颜色保存(热重载即时生效);再把 _todos 初始值改成两条假数据保存(热重载无效),按 R 生效。把两个结果记下来,这条边界之后你不会再踩。
下一步
这一篇的产出是一个能增删条目的应用,以及对”状态驱动界面”的第一手感。界面长出来了,接下来该补语言层:下一篇《Dart 语法要点(写给 JS/TS 背景的你)》把 null safety、async 与 isolate 这些写任何正经功能都绕不开的机制一次讲清;环境准备环节有遗漏的回头翻《Flutter 环境搭建与工具链》;写过 React Native 的读者推荐对照《第一个 React Native 应用》,两篇刻意做成同构,逐节对照能看清”桥接原生”与”自绘引擎”两条路线在开发体验上的真实差别。
版本核实(撰写时检索,2024-04-22):Flutter 当前稳定版为 3.19.x(配套 Dart 3.3);iOS 端默认渲染引擎为 Impeller(自 3.10 起默认),Android 端 Impeller 仍为可选开启、默认渲染引擎仍是 Skia;material/cupertino 组件库仍内置在 SDK 中,本篇 import 'package:flutter/material.dart' 写法可直接运行。flutter create / run 等命令为长期稳定接口。