TECHNICAL TUTORIAL · FLUTTER LESSON 02
Flutter 速成第二课:核心概念与第一个自定义页面
面向 Java 背景 Android 工程师的 Dart 十分钟速览、Widget 心智模型与待办清单完整实战
技术栈 :Flutter 3.24.5 · Dart 3.5.4 | 前置:已完成第一课环境搭建并跑通计数器 Demo
阅读时长 :约 25 分钟 | 实操时长:约 30 分钟
2026 年 11 月 · 面向有 Android 原生开发经验的工程师
目录
- 一、本课目标
- 二、Dart 十分钟速成
- 2.1 变量与空安全
- 2.2 函数与命名参数
- 2.3 异步:Future、async/await 与 Stream
- 2.4 类与构造语法糖
- 三、一切皆 Widget
- 3.1 StatelessWidget 与 StatefulWidget
- 3.2 setState 与重建范围
- 3.3 Widget、Element 与 RenderObject 三棵树
- 四、Android 到 Flutter 概念映射
- 五、实战:把计数器改成待办清单
- 5.1 完整代码
- 5.2 逐段拆解
- 六、页面导航:Navigator
- 七、本课作业
- 八、常见问题 FAQ
- 九、元信息
一、本课目标
第一课已经把开发环境跑通、看到了计数器 Demo。本课的目标是:用 30 分钟建立 Flutter 的核心心智模型,并亲手把 Demo 改造成一个属于自己的页面(待办清单)。所有概念都按你已有的 Android 原生开发经验做映射,不从零讲起。
读完本课你应该能回答三个问题:Flutter 的 UI 为什么是「嵌套的代码」而不是 XML;数据变化后界面是怎么刷新的;一个页面跳到另一个页面该怎么写。
二、Dart 十分钟速成
一句话结论:有 Java 基础,读 Dart 几乎没有门槛。Dart 同样是强类型、面向对象、带垃圾回收的语言,类、接口、泛型、try/catch 都在,语法像 Java 与 TypeScript 的混合体,只是更紧凑。真正要适应的是三点:强制空安全、返回值类型写在最前并支持箭头简写、以及 Flutter 的 API 命名参数极多、Widget 嵌套极深------写起来更像「搭积木」而不是「写逻辑」。
2.1 变量与空安全
Dart 的变量声明能直接对上 Java,但有两处关键升级。第一,类型可以省略,编译器自动推断(写全类型也完全合法,Java 习惯可以保留);第二,Dart 强制空安全------变量默认不可为 null,想允许 null 必须显式写成 String?,编译期就能挡掉大部分空指针。下表按 Java 写法逐条对照。
| 概念 | Java 写法 | Dart 写法 | 备注 |
|---|---|---|---|
| 不可变变量 | final String name = "Tom"; |
final name = 'Tom'; |
语义同 Java 的 final,类型可省 |
| 可变变量 | int count = 0; |
var count = 0; |
写法几乎一致 |
| 编译期常量 | static final String TAG = "app"; |
const tag = 'app'; |
const 要求编译期可确定 |
| 可空类型 | String name = null;(无编译期检查) |
String? name; |
Dart 用问号显式声明可空 |
| 安全调用 | name != null ? name.length() : 0 |
name?.length |
省掉一层判空 |
| 空合并 | name != null ? name : "default" |
name ?? 'default' |
三目运算符的简写 |
| 非空断言 | 无(只有运行时 NPE) | name! |
显式声明「我保证不为空」 |
dart
// 命名参数 + 默认值 + required
void greet(String name, {required int times, String prefix = 'Hi'}) {
for (var i = 0; i < times; i++) {
print('$prefix, $name');
}
}
// 箭头函数:单表达式函数体
int square(int x) => x * x;
2.2 函数与命名参数
Dart 函数支持两种参数:位置参数和命名参数。Flutter 的组件 API 几乎全部使用命名参数,而且常用 required 修饰必填项。单表达式函数可以用箭头写法,事件回调里随处可见。
字符串插值用 $ 符号:'Hello, $name',表达式用 ${}。这就是 Java 里用加号拼字符串("Hello, " + name)的做法,Dart 把它做成了语法,省掉一堆加号和引号。
2.3 异步:Future、async/await 与 Stream
Dart 的异步建立在单线程事件循环之上,结构与你熟悉的主线程 Looper + MessageQueue 是同一套思路:任务排队、逐个执行,因此没有多线程加锁的负担。三个关键词对应到 Java 世界:Future 相当于 CompletableFuture,代表「将来会有结果」的容器;async/await 是把 then 回调链拉平成同步写法的语法糖;Stream 相当于 RxJava 的 Flowable,代表一串陆续到达的值。
dart
Future<List<Todo>> fetchTodos() async {
// async 函数返回 Future,await 后拿到列表
await Future.delayed(const Duration(seconds: 1)); // 模拟网络请求
return [Todo('写周报'), Todo('复习 Flutter')];
}
对应关系速记:Future ≈ CompletableFuture,async/await ≈ then 链的扁平写法,Stream ≈ RxJava Flowable,事件循环 ≈ 主线程 Looper + 消息队列。关键区别:await 只挂起当前函数,不阻塞线程。后续讲 Isolate 时再回到线程模型对比。
2.4 类与构造语法糖
Dart 的构造函数可以直接把参数赋给字段,省掉 Java 里一长串 this.title = title 的样板代码。Flutter 组件类还固定使用 const 构造 + super.key 的组合写法,看到照抄即可。
dart
class Todo {
final String title;
final bool done;
Todo(this.title, {this.done = false}); // 参数直接赋给字段
}
三、一切皆 Widget
在 Android 里,UI = XML 布局文件 + View 自定义类;在 Flutter 里,UI 就是 Widget 树的嵌套表达式,没有 XML、没有 findViewById。第一课计数器 App 的真实结构如下,从根到叶依次嵌套:
text
MyApp(根组件)
└─ MaterialApp(全局配置:主题、路由、首页)
└─ HomePage(StatefulWidget,持有计数状态)
└─ Scaffold(页面骨架:标题栏、正文、悬浮按钮)
├─ AppBar(顶部标题栏)
├─ Column(纵向布局:两行 Text + 一个按钮)
└─ FloatingActionButton(右下角加号按钮)
3.1 StatelessWidget 与 StatefulWidget
Widget 分两类。StatelessWidget 是纯函数式的:传入参数、build 出 UI,之后不再变化,适合静态内容。StatefulWidget 额外持有一个 State 对象存放可变数据,数据变化时调用 setState() 通知框架重画------心智模型上非常接近 RecyclerView 调 notifyDataSetChanged(),只是粒度更大:整个子树都会重新 build。
判断标准很简单:这个界面在生命周期内会不会变?会变就 StatefulWidget,不变就 StatelessWidget。拿不准时先写 StatelessWidget,编译器会提醒你。
3.2 setState 与重建范围
setState 做两件事:执行你传入的回调(修改数据),然后标记这个 State 关联的子树为「脏」,下一帧重新 build。因此有两条铁律:一,改数据必须包在 setState 里,直接改成员变量界面不会刷新;二,build() 里不能放耗时逻辑,因为它会被反复调用,必须是个快的纯描述函数。
初学者最常犯的错误:在回调里写 _items.add(text) 却忘了包 setState,界面纹丝不动。看到「数据明明改了但 UI 不更新」,第一个检查点永远是它。
3.3 Widget、Element 与 RenderObject 三棵树
「整棵树重建」听起来很贵,其实不然。Widget 只是轻量的配置描述(相当于「设计图纸」),可以随时丢弃重建;真正常驻、负责布局与绘制的是 Element 树和 RenderObject 树(相当于「施工结果」)。重建时框架会逐节点对比新旧 Widget,类型和配置没变的节点直接复用底层对象,只重绘真正变化的部分。理解了这一点,就能接受 Flutter「一切皆可重建」的激进设计。
四、Android 到 Flutter 概念映射
下表把 Android 日常开发中最常用的概念映射到 Flutter,建议打印或截图放在手边:
| Android | Flutter | 说明 |
|---|---|---|
| Activity / Fragment | 一个 StatefulWidget 页面 | 页面即组件 |
| XML 布局 / ConstraintLayout | Widget 嵌套(Row / Column / Stack / Expanded) | 布局就是代码 |
| findViewById / ViewBinding | 不需要 | 构造时直接持有引用 |
| RecyclerView + Adapter + ViewHolder | ListView.builder |
自带按需构建与回收 |
| Intent 跳转 | Navigator.push + MaterialPageRoute |
见第六节 |
| Handler / postDelayed | Future.delayed(Duration(...), () {...}) |
底层同为消息队列 |
| Bundle 传参 | 构造函数参数直接传 | 类型安全 |
| onBackPressed | PopScope |
拦截返回键 |
五、实战:把计数器改成待办清单
5.1 完整代码
把 lib/main.dart 的全部内容替换为下面这段,保存(Ctrl+S)触发热重载,立刻得到一个可以添加、删除待办的页面。这 60 行代码覆盖了 Flutter 日常开发 80% 的写法:
dart
import 'package:flutter/material.dart';
void main() => runApp(const TodoApp());
class TodoApp extends StatelessWidget {
const TodoApp({super.key});
@override
Widget build(BuildContext context) => MaterialApp(
theme: ThemeData(colorSchemeSeed: Colors.blue, useMaterial3: true),
home: const TodoPage(),
);
}
class TodoPage extends StatefulWidget {
const TodoPage({super.key});
@override
State<TodoPage> createState() => _TodoPageState();
}
class _TodoPageState extends State<TodoPage> {
final _items = <String>[];
final _controller = TextEditingController();
void _add() {
final text = _controller.text.trim();
if (text.isEmpty) return;
setState(() => _items.add(text)); // 修改数据 + 触发重建
_controller.clear();
}
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(title: const Text('我的待办')),
body: Column(children: [
Padding(
padding: const EdgeInsets.all(12),
child: Row(children: [
Expanded(
child: TextField(
controller: _controller,
decoration: const InputDecoration(hintText: '输入待办事项'),
),
),
IconButton(onPressed: _add, icon: const Icon(Icons.add)),
]),
),
Expanded(
child: ListView.builder(
itemCount: _items.length,
itemBuilder: (context, i) => ListTile(
leading: const Icon(Icons.check_circle_outline),
title: Text(_items[i]),
trailing: IconButton(
icon: const Icon(Icons.delete_outline),
onPressed: () => setState(() => _items.removeAt(i)),
),
),
),
),
]),
);
}
5.2 逐段拆解
- 状态持有 :
_TodoPageState里的_items与_controller,相当于 Activity 的成员变量。controller 持有输入框内容,避免重建后丢字。 - 数据驱动 UI :
setState(() => _items.add(text))把「改数据」和「通知刷新」合成一个调用,这是 Flutter 与命令式 UI 最本质的区别------你永远不手动操作控件,只改数据。 - 列表 :
ListView.builder的itemBuilder只构建可见项,滚动时按需创建,自带回收复用,不需要手写 Adapter 和 ViewHolder。 - 布局 :
Row/Column对应横向、纵向 LinearLayout;Expanded对应layout_weight=1,把剩余空间分给列表。
跑起来后试着加几条待办、删几条,全程热重载,不用重新编译。
六、页面导航:Navigator
页面跳转由 Navigator 管理,路由栈模型与 Android 的任务栈一致:push 压栈新页面,pop 出栈返回。传参走构造函数,类型安全,不需要 Intent 和 Bundle:
dart
// 跳转到详情页并传参
Navigator.push(
context,
MaterialPageRoute(builder: (context) => DetailPage(todo: _items[i])),
);
// 详情页内返回
Navigator.pop(context);
如果需要从详情页把编辑结果带回来,用 push 的返回值:await Navigator.push(...) 的结果就是 pop 时携带的对象,比 onActivityResult 简洁得多。
七、本课作业
三个梯度的作业,15 分钟起步,做完后第三课(状态管理与网络请求)就可以无痛跟进:
- 简单 :给
ListTile加一个Checkbox,勾选后文字显示删除线。提示:数据从List<String>改成List<Todo>小类,文字样式用TextDecoration.lineThrough。 - 中等 :点击某条待办,用
Navigator.push跳到详情页显示全文,练习页面导航与传参。 - 进阶 :用
Future.delayed模拟 1 秒网络请求拉取待办列表,加载期间显示CircularProgressIndicator,练async/await在 UI 层的标准用法。
八、常见问题 FAQ
Q1 setState 之后整棵树都重建吗?
只重建该 State 关联的子树。性能敏感时用 const 构造函数标注不变的子组件,或把大页面拆成小的 StatefulWidget,把 setState 的影响范围收窄。
Q2 TextField 输入到一半,列表一刷新内容就被清空?
TextField 必须通过 TextEditingController 持有状态,且 controller 要作为成员变量创建,不能放在 build 方法里每次新建。示例代码里 _controller 的写法就是标准答案。
Q3 Widget 嵌套太深看着头晕怎么办?
把一段完整 UI 抽成私有的 Widget 方法或独立类。Flutter 的官方答案永远是「拆组件」而不是「写 XML」,拆完每个 build 都短小清晰。
Q4 ListView.builder 和直接传 children 的 ListView 有什么区别?
builder 是懒构建,只创建屏幕内可见的列表项,适合长列表;children 写法会把所有子项一次性构建出来,适合项数固定且很少的场景。
Q5 Dart 的 Future 和 java.util.concurrent.Future 区别在哪?
Dart 的 Future 建立在单线程事件循环之上(类似主线程 Looper),await 不阻塞线程;Java 的 Future.get() 是阻塞调用,会把线程挂死直到结果就绪。它更像 JavaScript 的 Promise 或 Java 的 CompletableFuture,而不是会阻塞线程的 Future.get()。
九、元信息
- 目标读者:有 Android 原生开发经验、已完成第一课环境搭建并跑通计数器 Demo 的工程师。
- 前置知识:Java 基础、Activity 生命周期与 RecyclerView 使用经验。
- 技术栈版本:Flutter 3.24.5 stable / Dart 3.5.4。
- 阅读与实操:全文约 25 分钟读完;完整跟做约 30 分钟,作业另计 15 分钟。