这是系列的第一篇,目标很具体:把开发环境装好、跑通第一个应用,并且在这个过程中把 Flutter 最容易让人误解的两件事讲清楚——它的渲染方式,以及 Widget 到底是个什么东西。我自己是先把这两处理顺了,后面写起来才不拧巴。
Flutter 属于跨平台方案里的哪一类
移动端的跨平台方案大致分三类,Flutter 属于最后一类:
- WebView 套壳:UI 用 HTML/CSS 画,包在一个浏览器内核里。省事了,但滚动惯性、键盘行为、动画帧率都在告诉用户「这不是原生」。
- 原生控件映射:写一套 JS,运行时翻译成各平台的原生控件。样式能改的维度受限于原生控件本身暴露的属性。
- 自带渲染引擎:Flutter 就是这个。它不借用 Android 的
TextView、也不借用 iOS 的UILabel,而是自己把每一个像素画到一块画布上。你看到的按钮是 Flutter 画的,不是系统给的。
这么做的直接好处是两端表现完全一致——同一份代码在 Android 和 iOS 上字体渲染、动画曲线、滚动回弹一模一样,不存在「iOS 上正常,安卓某机型错位」这类问题。代价是包体积会大一些,因为引擎要打进产物里。
性能上还要再补一句:Flutter 引擎早期用 Skia 做后端,新版本换成了自研的 Impeller,专门解决着色器编译卡顿(shader jank)。而业务代码用的 Dart,在发布时是 AOT 编译成机器码的,不是解释执行。只在 debug 模式下才跑 JIT——因为热重载需要增量编译。开发期和发布期是两套机制,我一开始把它们混在一起过。
装环境:真正卡人的不是下载
# 解压 SDK 后加入 PATH,然后
flutter doctorflutter doctor 会把缺什么列得很清楚,但它「有叉」和「跑不起来」不是一回事。我自己卡过的是这几处:
Android licenses 没接受。装了 Android Studio 不等于授权完成,还要单独跑一次,我第一次就漏了:
flutter doctor --android-licenses一路
y。不跑这条,flutter run会挂在 Gradle 阶段,报错信息还看不出跟许可证有关。- iOS 需要 macOS。Windows 和 Linux 上无法编译 iOS 产物,只能写代码。真要出 iOS 包,绕不开一台 Mac 加 Xcode 和 CocoaPods。
国内网络拉不动依赖。
pub.dev和引擎产物下载经常超时,配一下镜像能省掉半小时折腾:export PUB_HOSTED_URL="https://pub.flutter-io.cn" export FLUTTER_STORAGE_BASE_URL="https://storage.flutter-io.cn"- Android Studio 里的 Flutter 插件。它不影响命令行构建,但断点调试和 UI 大纲视图这两样都得靠它。
环境算不算装好,我只看一条:flutter run 能在设备或模拟器上把默认计数器应用跑起来。这条过了,后面基本不会再有环境问题。
第一个项目,只改三行
flutter create hello_flutter
cd hello_flutter && flutter run生成的目录里,长期要打的交道只有两个:
lib/main.dart—— 全部 Dart 代码的入口pubspec.yaml—— 依赖声明,以及静态资源的注册表
android/、ios/、web/ 这些平台目录基本不用碰,除非要加原生权限、改包名、接第三方原生 SDK。
把默认模板换成下面这个最小结构,每一层各管一段:
import 'package:flutter/material.dart';
void main() => runApp(const HelloApp());
class HelloApp extends StatelessWidget {
const HelloApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Hello Flutter',
theme: ThemeData(useMaterial3: true),
home: Scaffold(
appBar: AppBar(title: const Text('第一个应用')),
body: const Center(child: Text('你好,Flutter')),
floatingActionButton: FloatingActionButton(
onPressed: () => debugPrint('按下了'),
child: const Icon(Icons.add),
),
),
);
}
}MaterialApp提供路由、主题、本地化的上下文,是应用的壳。Scaffold提供AppBar/body/floatingActionButton这套页面骨架。Center、Text、Icon是叶子节点。
改完保存,按 r 热重载,界面立刻变。
Widget 是一次性的描述
这里我自己踩空过。看到 StatelessWidget 里有 final 字段,很容易以为它是「一个长期存在的视图对象」。不是。
Flutter 内部有三种树:
| 角色 | 生命周期 | 职责 |
|---|---|---|
Widget | 极短,每次重建都换新的 | 一份配置清单,描述「这里应该长什么样」 |
Element | 长期存在 | 管理位置,负责比对新旧 Widget 决定复用还是重建 |
RenderObject | 长期存在 | 真正干布局、绘制、命中测试的 |
所以 build() 被反复调用是设计如此,不是性能问题。但它带来两个后果:
副作用放进 build() 就会每次重建都重复执行一遍。 网络请求、定时器、读文件都属于这一类,我后来把它们挪到 initState() 或者上层的服务里。
可变状态存在 StatelessWidget 的字段里留不住。 这个对象下一秒就被丢弃了。要留住状态得用 StatefulWidget,状态挂在配套的 State 对象上——只有 State 会被复用保留。
反过来,const 构造有实际意义:const Text('你好') 在编译期就固定下来,父级重建时 Flutter 可以直接认定这个子树没变、整棵跳过。所以上面的代码里 const 写得比较多,它在这里不是排版习惯,是省掉一整棵子树的重建。
热重载不是热重启
| 操作 | 快捷键 | 发生了什么 |
|---|---|---|
| 热重载 | r | 替换改动的代码,保留当前 State 和界面状态 |
| 热重启 | R | 整个应用从头跑,main() 重新执行,状态清空 |
热重载快,也因此有个坑:它不会重跑 initState() 和 main()。如果你改的是初始化逻辑、全局变量、或者某个 const 值,热重载看起来「没生效」,界面还是老样子。这时候按一下 R 就好了——我第一次遇到时以为是代码写错了,白找了半天。
另外枚举增删、类型的破坏性变更偶尔会让热重载直接报错,同样用热重启解决。
布局:先从四个最常用的容器说起
Flutter 的布局逻辑是「父级给约束,子级定大小」。方向由容器决定:
Column(
crossAxisAlignment: CrossAxisAlignment.start, // 交叉轴:水平方向对齐
children: [
const Text('标题'),
const SizedBox(height: 12), // 占位间距,比空 Container 直观
Row(
children: [
Expanded(
flex: 2, // 按权重分配剩余空间,2:1 分
child: Container(color: Colors.blue.shade100, height: 60),
),
Expanded(
child: Container(color: Colors.amber.shade100, height: 60),
),
],
),
],
)我自己的记法是:mainAxisAlignment 管主轴(Column 里是竖直,Row 里是水平),crossAxisAlignment 管另一头。Expanded 只在有剩余空间可分时起作用,所以它必须待在 Flex(Row/Column)里面——放到别处会直接报「RenderFlex child has non-zero flex but incoming width constraints are unbounded」。
学习时要特别注意的几处
- 图片显示了但线上没有:
pubspec.yaml里漏了assets:声明。Flutter 打包只收注册过的资源,import路径写对了也不行。 - 首次 iOS 构建慢到怀疑人生:正常,CocoaPods 装依赖加编译引擎,第一次十几分钟。之后有缓存。
- Android 报错关于 AndroidX:老模板或老插件残留,
gradle.properties里加android.useAndroidX=true和android.enableJetifier=true。 - 真机连不上:Android 要开 USB 调试并授权弹窗;iOS 要配签名证书。先用
flutter devices看设备列不列得出来,列不出来说明还没打通,跟代码无关。 - 改
pubspec.yaml加了依赖没反应:跑一次flutter pub get,或者让 IDE 自动执行。
评论
暂未收到