4. Flutter Widgets 核心:StatelessWidget 与 StatefulWidget、基础布局组件、文本与图片组件
好,咱们今天来啃 Flutter 里最核心的一块——Widget。说白了,Flutter 里的一切都是 Widget。你看到的按钮、文字、图片、甚至整个页面,都是 Widget。我刚开始接触 Flutter 时,第一反应是:这也太“万物皆 Widget”了吧?但用久了你会发现,这个设计其实非常优雅。
4.1 StatelessWidget 与 StatefulWidget:静态与动态的分水岭
这两个家伙,是 Flutter 里最基础的两种 Widget 类型。你可以这么理解:StatelessWidget 是“死”的,StatefulWidget 是“活”的。
核心区别:StatelessWidget 一旦构建完成,就不会再变了。而 StatefulWidget 可以随着用户交互、数据变化而重新构建。
4.1.1 StatelessWidget:一次构建,终身不变
什么时候用 StatelessWidget?很简单——当你的 UI 只依赖于传入的配置信息,并且不会自己发生变化时。比如一个固定的图标、一段静态文字、一个纯展示用的卡片。
我记得在做一个嵌入式设备的状态显示界面时,设备名称、版本号这些信息,我就用了 StatelessWidget。因为它们从服务器拉下来后,基本不会变。没必要用 StatefulWidget 去“监听”它们的变化,反而浪费性能。
class DeviceInfoWidget extends StatelessWidget {
final String deviceName;
final String version;
const DeviceInfoWidget({Key? key, required this.deviceName, required this.version})
: super(key: key);
@override
Widget build(BuildContext context) {
return Column(
children: [
Text('设备名称:$deviceName'),
Text('固件版本:$version'),
],
);
}
}
你看,这个 Widget 接收两个参数,然后直接渲染。它不需要管理任何状态,也不需要响应任何变化。简洁、高效。
4.1.2 StatefulWidget:会“动”的界面
那什么时候用 StatefulWidget?当你的界面需要“动”起来的时候。比如按钮点击后改变颜色、计时器倒计时、传感器数据实时刷新……这些场景,都需要 StatefulWidget。
我曾经踩过一个坑:在做一个温控面板时,温度数据每隔 500ms 更新一次。我一开始图省事,把整个页面写成了 StatelessWidget,然后每次数据更新就重新创建整个页面。结果呢?界面疯狂闪烁,CPU 占用直接拉满。后来改成 StatefulWidget,只在 setState() 里更新温度数值那一小块,问题瞬间解决。
注意:setState() 会触发整个 Widget 的 build() 方法重新执行。所以尽量把变化的部分封装成小的 StatefulWidget,避免大范围重建。
class TemperatureDisplay extends StatefulWidget {
@override
_TemperatureDisplayState createState() => _TemperatureDisplayState();
}
class _TemperatureDisplayState extends State<TemperatureDisplay> {
double _currentTemp = 25.0;
void _updateTemp(double newTemp) {
setState(() {
_currentTemp = newTemp;
});
}
@override
Widget build(BuildContext context) {
return Text('当前温度:$_currentTemp°C');
}
}
4.2 基础布局组件:Container、Row、Column、Stack
布局组件,说白了就是“怎么把东西摆好”。Flutter 提供了几个非常基础但极其强大的布局组件。我个人习惯把它们分成两类:单子布局(Container)和多子布局(Row、Column、Stack)。
4.2.1 Container:万能的“盒子”
Container 是我用得最多的组件之一。它就像一个可以设置各种样式的盒子——你可以给它加 padding、margin、背景色、边框、圆角、甚至变换效果。
嗯,这里要注意:Container 其实是一个语法糖。它内部组合了 Padding、Align、DecoratedBox 等多个组件。所以如果你只需要一个简单的 padding,直接用 Padding 组件会更轻量。
Container(
margin: EdgeInsets.all(8.0),
padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
decoration: BoxDecoration(
color: Colors.blue.shade50,
borderRadius: BorderRadius.circular(8.0),
border: Border.all(color: Colors.blue),
),
child: Text('这是一个带样式的容器'),
)
小技巧:在嵌入式设备上,尽量避免在 Container 里嵌套太多层。因为 Container 本身就有一定的性能开销,嵌套多了会影响渲染帧率。
4.2.2 Row 与 Column:横向与纵向的“排排坐”
Row 和 Column 是 Flutter 里最常用的多子布局组件。Row 把子组件从左到右排列,Column 从上到下排列。它们都基于 Flex 布局模型,所以你可以用 mainAxisAlignment 和 crossAxisAlignment 来控制对齐方式。
我记得在做一个嵌入式设备的菜单界面时,需要把“设置”、“关于”、“退出”三个按钮横向排列。用 Row 配合 MainAxisAlignment.spaceEvenly,一行代码就搞定了均匀分布。
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
ElevatedButton(onPressed: () {}, child: Text('设置')),
ElevatedButton(onPressed: () {}, child: Text('关于')),
ElevatedButton(onPressed: () {}, child: Text('退出')),
],
)
Column 的用法完全一样,只是方向不同。你想想看,如果你需要做一个垂直排列的表单——用户名、密码、登录按钮——用 Column 再合适不过了。
4.2.3 Stack:层叠布局的“叠叠乐”
Stack 允许你把子组件一层层叠在一起。后面的子组件会覆盖在前面的上面。这在做“水印”、“遮罩层”、“悬浮按钮”时特别有用。
我曾经在做一个摄像头预览界面时,需要在视频画面上叠加一个“录制中”的红点和计时器。用 Stack 把视频组件放在底层,红点和文字放在顶层,完美解决。
Stack(
children: [
// 底层:摄像头画面
CameraPreview(),
// 顶层:录制指示器
Positioned(
top: 16.0,
right: 16.0,
child: Row(
children: [
Icon(Icons.circle, color: Colors.red, size: 12),
SizedBox(width: 4),
Text('录制中', style: TextStyle(color: Colors.white)),
],
),
),
],
)
关键点:Stack 配合 Positioned 组件,可以精确控制子组件在 Stack 中的位置。如果不使用 Positioned,子组件默认会从左上角开始排列。
4.3 文本与图片组件
文本和图片,是任何界面都绕不开的两个基本元素。Flutter 对它们的支持非常完善,但也有些细节需要注意。
4.3.1 Text:不只是“显示文字”
Text 组件看似简单,但它的能力远不止“显示文字”。你可以控制字体大小、颜色、粗细、行高、对齐方式、甚至文字溢出时的处理方式。
在嵌入式设备上,屏幕通常比较小,文字很容易溢出。我建议你养成一个好习惯:每次用 Text 时,都考虑一下溢出场景。用 overflow: TextOverflow.ellipsis 让超出的文字显示为省略号,或者用 softWrap: true 让文字自动换行。
Text(
'这是一段可能会很长的文字,在嵌入式设备的小屏幕上需要特别注意溢出处理',
style: TextStyle(fontSize: 14, color: Colors.black87),
maxLines: 2,
overflow: TextOverflow.ellipsis,
)
个人经验:在嵌入式设备上,尽量使用系统默认字体。不要加载自定义字体文件,因为会增加包体积和渲染开销。如果一定要用,记得做字体子集化。
4.3.2 Image:图片加载与缓存
Image 组件支持从多种来源加载图片:本地资源、网络、文件、甚至内存。在嵌入式设备上,我建议优先使用本地资源图片(Image.asset),因为网络加载会引入延迟和不确定性。
如果你必须加载网络图片,记得用 cached_network_image 插件做缓存。我曾经在一个设备上没做缓存,结果每次切换页面都重新下载图片,用户反馈“卡得要死”。加上缓存后,体验瞬间提升。
Image.asset(
'assets/images/device_icon.png',
width: 48,
height: 48,
fit: BoxFit.contain,
)
这里有个坑:图片的 fit 属性。默认是 BoxFit.scaleDown,意思是如果图片比容器大,就缩小到容器尺寸;如果比容器小,就保持原大小。但很多时候你需要 BoxFit.cover(裁剪填充)或 BoxFit.contain(完整显示)。选错了,图片就会变形或者留白。
性能警告:在嵌入式设备上,图片分辨率不要太高。一张 1920x1080 的图片在 320x240 的屏幕上显示,完全是浪费内存和 CPU。建议根据屏幕分辨率,提前压缩好图片。
4.4 小结
这一章的内容,说白了就是 Flutter 界面开发的“地基”。StatelessWidget 和 StatefulWidget 决定了你的组件会不会“动”;Container、Row、Column、Stack 决定了你的组件怎么“摆”;Text 和 Image 则是界面里最常用的“砖块”。
我个人建议你:先别急着写复杂界面。花点时间把这几个基础组件玩熟。比如用 Container 和 Row 做一个简单的卡片布局,用 Stack 做一个带角标的图标。等你对这些组件的“脾气”摸透了,后面学 ListView、GridView 这些高级组件时,就会觉得水到渠成。
下一章,我们会深入 Flutter 的布局原理,聊聊 Constraints 和 RenderObject。嗯,那才是真正考验内功的地方。