第一个 Flutter 应用

要点

从 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 等命令为长期稳定接口。

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇