二.Flutter 速成第二课:核心概念与第一个自定义页面

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 分钟起步,做完后第三课(状态管理与网络请求)就可以无痛跟进:

  1. 简单 :给 ListTile 加一个 Checkbox,勾选后文字显示删除线。提示:数据从 List<String> 改成 List<Todo> 小类,文字样式用 TextDecoration.lineThrough。
  2. 中等 :点击某条待办,用 Navigator.push 跳到详情页显示全文,练习页面导航与传参。
  3. 进阶 :用 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 分钟。
相关推荐
好的佩奇10 小时前
蓝牙之通信架构
android·flutter·蓝牙
yujunlong391921 小时前
[特殊字符] Serverpod 4.1.0 全栈项目开发与 Docker 部署规范文档
flutter·docker·serverpod
FungLeo1 天前
成为全栈·Go 后端篇·换后端不改前端:四个客户端如何做兼容回归
flutter·golang·go·成为全栈·兼容测试·taro 小程序
恋猫de小郭1 天前
Flutter Android 性能修复:8 个大核的手机,Raster 居然只跑在 2 个核上?
android·flutter
恋猫de小郭2 天前
感人,Flutter 居然有了一个超实用提案,期待落地
android·前端·flutter
好的佩奇3 天前
Dart 之增强枚举
android·flutter·dart
好的佩奇3 天前
蓝牙之无线电与通信距离
android·flutter·蓝牙
程序员老刘·4 天前
从短剧App的bug看响应式编程在AI时代的优势
flutter·ai编程·跨平台开发
传奇开心果编程4 天前
【Flutter踩坑排雷】第7课:巨型 build 函数坑|全部 UI 写在同一个 build,维护难、隐性异常
android·flutter·ios·移动开发·跨平台·dart·声明式ui