查看: 123|回复: 3

Flutter鸿蒙布局实战:从ArkTS迁移的组件差异与踩坑记录

[复制链接]
发表于 1 小时前 | 显示全部楼层 |阅读模式
环境配好、能打出hap包之后,我开始在鸿蒙上正经跑Flutter界面。之前习惯用ArkTS写原生,Row、Column、Flex、Stack这套布局玩得挺熟,换成Flutter后发现思路能套,但API细节差别不小。这篇把几个最常用的布局组件在鸿蒙上实际跑一遍的结果记录下来,重点说差异和坑。

先说整体感受。Flutter的布局核心思路和ArkTS一样,都是声明式、嵌套式写法。区别在于Flutter的Widget拆得更细,一个视觉效果往往要套好几层,刚开始写会觉得啰嗦,写多了反而觉得清晰。Flutter布局体系大致分三层:最底层的RenderObject负责实际绘制和布局计算,中间层的Widget是声明式描述,开发者写的都是Widget这一层。Widget本身不可变,每次setState会生成新Widget树,框架内部和旧的做对比决定是否重绘。这个设计理念和ArkTS的“状态驱动UI”一致,都是描述UI长什么样,具体渲染交给框架。

日常开发用得最多的就是Row、Column、Stack、Wrap、Container这几个,加上Expanded控制弹性,基本覆盖八成布局场景。剩下两成涉及Sliver、CustomPaint、LayoutBuilder这些进阶组件,后续用到再聊。

Row + Expanded:弹性比例分配

先说横向布局。ArkTS里横向排列用Row,控制子元素占比用layoutWeight,Flutter对应的是Row + Expanded组合。需求很简单:一行三个色块,按1:2:1比例分配宽度。思路是Row撑满父容器,三个Expanded用flex指定比例。
  1. Row(
  2.   children: [
  3.     Expanded(flex: 1, child: _colorBox(Colors.red, '1')),
  4.     Expanded(flex: 2, child: _colorBox(Colors.red, '2')),
  5.     Expanded(flex: 1, child: _colorBox(Colors.blue, '1')),
  6.   ],
  7. )
复制代码
_colorBox是抽出来的小工具函数,返回带背景色和居中文字的Container:
  1. Widget _colorBox(Color color, String label) => Container(
  2.   height: 56,
  3.   color: color,
  4.   alignment: Alignment.center,
  5.   child: Text(label, style: const TextStyle(color: Colors.white, fontSize: 18)),
  6. );
复制代码
跑起来三个色块按1:2:1排开,绿色那块明显宽一截。

这里有个细节差异。ArkTS的layoutWeight直接写在子组件上,Flutter的Expanded是独立Widget,要把子组件包进去。一开始老忘记套Expanded,直接给Row塞三个Container,三个色块挤在一起各自按内容宽度撑开,完全不是弹性效果。后来才反应过来,Flutter里“弹性”这个概念是Expanded提供的,Row本身不管子元素怎么分空间。

还有个坑:Expanded只能在Row、Column、Flex里面用,放进别的容器直接报错。有次手滑把Expanded放进Stack,编译器红一片,查了才知道它依赖父级是Flex系容器。另外flex默认值是1,不写flex时Expanded(child:...)和Expanded(flex:1, child:...)效果一样。多个Expanded都不写flex就按1:1:1等分。想做“一个固定宽度,剩下的填满”的效果,固定宽度用普通Container,剩余用不带flex的Expanded自动吃掉剩余空间。

Spacer组件本质就是Expanded(child: SizedBox.shrink()),专门占位。想在两个元素之间撑开距离,塞个Spacer()比写Expanded(child: Container())简洁,相当于ArkTS的Blank()组件。

Column交叉轴对齐:start/center/end

竖向布局用Column,对齐方式分主轴和交叉轴。主轴是Column的方向(竖),交叉轴是垂直于主轴的方向(横)。概念和ArkTS的justifyContent/alignItems对应,名字不同。我写了三组Column并排放,分别演示交叉轴的start、center、end对齐:
  1. Row(
  2.   children: [
  3.     Expanded(child: _alignColumn(CrossAxisAlignment.start, 'start')),
  4.     Expanded(child: _alignColumn(CrossAxisAlignment.center, 'center')),
  5.     Expanded(child: _alignColumn(CrossAxisAlignment.end, 'end')),
  6.   ],
  7. )
  8. Widget _alignColumn(CrossAxisAlignment align, String label) => Column(
  9.   crossAxisAlignment: align,
  10.   children: [
  11.     Text(label, style: const TextStyle(fontWeight: FontWeight.bold)),
  12.     const FlutterLogo(size: 28),
  13.     const FlutterLogo(size: 40),
  14.   ],
  15. );
复制代码
跑起来对比明显,左列靠左、中列居中、右列靠右。FlutterLogo是自带组件,拿来当占位元素挺方便。

和ArkTS的区别在于:ArkTS里横向对齐用alignItems: HorizontalAlign.Center,枚举值是Start/Center/End;Flutter这边是CrossAxisAlignment枚举,值是start/center/end(小驼峰)。名字差不多,但ArkTS的HorizontalAlign和VerticalAlign是分开的枚举,Flutter统一用CrossAxisAlignment,靠主轴方向自动判断是横还是竖,不用记两套枚举。

主轴对齐别搞混。Column的主轴是竖,控制竖直方向对齐用mainAxisAlignment(start/center/end/spaceBetween/spaceAround/spaceEvenly),交叉轴才是crossAxisAlignment。一开始老把main和cross写反,竖直对齐死活不对。记个口诀:沿排列方向的是main,垂直排列方向的是cross。

mainAxisAlignment几个值里,spaceBetween用得最多——两头贴边、中间均分,做导航栏底部tab条很合适。spaceEvenly把剩余空间均分成N+1份,每个元素前后都有等距间隙;spaceAround均分成N份,每个元素前后各占一半。三个space系列的区别:Between最紧(两头没间隙),Evenly最松(两头都有间隙),Around居中(两头各一半)。这些和ArkTS的justifyContent: FlexAlign.SpaceBetween一一对应,只是Flutter枚举值少了Flex前缀。

Stack + Positioned:层叠和角标

层叠布局在ArkTS里叫Stack,Flutter里也叫Stack。典型场景是图标右上角挂角标,比如消息通知的红点数字。思路是Stack做容器,子元素默认后放的盖在先放的上面,要精确定位用Positioned包裹,指定top/right/bottom/left。
  1. Stack(
  2.   clipBehavior: Clip.none,
  3.   children: [
  4.     Container(
  5.       padding: const EdgeInsets.all(20),
  6.       decoration: BoxDecoration(
  7.         color: Colors.blue.shade50,
  8.         borderRadius: BorderRadius.circular(16),
  9.       ),
  10.       child: const Icon(Icons.notifications, size: 56, color: Colors.blue),
  11.     ),
  12.     const Positioned(
  13.       top: -6,
  14.       right: -6,
  15.       child: CircleAvatar(
  16.         radius: 10,
  17.         backgroundColor: Colors.red,
  18.         child: Text('9', style: TextStyle(color: Colors.white, fontSize: 12)),
  19.       ),
  20.     ),
  21.   ],
  22. )
复制代码
这里的关键参数是clipBehavior: Clip.none。Stack默认把超出自身范围的子元素裁掉,角标要伸到容器外面(top和right都是负值),不设这个参数角标会被切掉一半。第一次写没加,角标剩个半圆,查了半天才找到是Stack的裁剪行为。ArkTS的Stack也有类似机制,默认alignContent控制对齐,但定位子元素用的是position配合offset,不如Flutter的Positioned直观。Positioned语义明确,不用算偏移量。

Positioned必须在Stack里用——和Expanded一样,它依赖父级是Stack。Stack里混用Positioned和非Positioned子元素时,非Positioned按Stack的alignment对齐(默认Alignment.center居中),Positioned按各自的top/right定位。alignment参数控制非定位子元素的对齐方式,想改默认就设alignment: Alignment.topLeft,和ArkTS的alignContent一个意思。

Wrap流式布局:标签云

Row和Column都单行单列,子元素超了不换行换列。做标签云这种自动换行的效果得用Wrap。我做了个可点击的标签云,点击切换选中状态,选中的变蓝底白字。思路是Wrap做容器,spacing控制横向间距,runSpacing控制行间距,子元素用GestureDetector包一层处理点击。
  1. Wrap(
  2.   spacing: 8,
  3.   runSpacing: 8,
  4.   children: _tags.map((tag) {
  5.     final on = _selected.contains(tag);
  6.     return GestureDetector(
  7.       onTap: () => setState(() {
  8.         if (on) {
  9.           _selected.remove(tag);
  10.         } else {
  11.           _selected.add(tag);
  12.         }
  13.       }),
  14.       child: AnimatedContainer(
  15.         duration: const Duration(milliseconds: 150),
  16.         padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
  17.         decoration: BoxDecoration(
  18.           color: on ? Colors.blue : Colors.grey.shade200,
  19.           borderRadius: BorderRadius.circular(20),
  20.         ),
  21.         child: Text(
  22.           tag,
  23.           style: TextStyle(
  24.             color: on ? Colors.white : Colors.black87,
  25.             fontSize: 13,
  26.           ),
  27.         ),
  28.       ),
  29.     );
  30.   }).toList(),
  31. )
复制代码
_tags是字符串数组,_selected是Set存当前选中标签,点击时判断在不在Set里决定移除还是添加,然后setState触发重建。这里用了AnimatedContainer而不是普通Container,它会在属性变化时自动做过渡动画,duration指定时长。点击标签时背景从灰变蓝有150毫秒渐变,比硬切自然。这个组件ArkTS没有对应物,鸿蒙原生要做这种动画得自己写animateTo,Flutter一个参数搞定。

ArkTS里做流式布局用Flex配合wrap: FlexWrap.Wrap,或者直接用GridContainer。Flutter的Wrap更纯粹,direction控制排列方向(默认横向),alignment控制每行在主轴上的对齐。Wrap的runAlignment参数控制多行整体在交叉轴的对齐,比如三行标签整体靠上还是靠下,用得少但特殊布局会用到。另外Wrap默认不滚动,标签太多超屏幕还是会溢出,要滚动得自己套SingleChildScrollView,这点和ArkTS的Flex一样。

顺带一个建议:Set比List适合存选中项。一开始用List存选中标签,点击切换要先contains判断再remove或add,去重还得自己处理。换成Set全自动搞定,查找和增删都是O(1)。不影响功能但影响代码干净度。

Container装饰:渐变圆角阴影

Container是Flutter里最常用的容器组件,能管尺寸、背景、边框、阴影、圆角,全靠decoration参数。ArkTS里这些效果分散在不同属性上,Flutter统一收拢到BoxDecoration里。我做了个渐变背景+圆角+阴影的卡片:
  1. Container(
  2.   height: 80,
  3.   decoration: BoxDecoration(
  4.     gradient: const LinearGradient(
  5.       colors: [Colors.purple, Colors.blueAccent],
  6.       begin: Alignment.topLeft,
  7.       end: Alignment.bottomRight,
  8.     ),
  9.     borderRadius: BorderRadius.circular(16),
  10.     boxShadow: [
  11.       BoxShadow(
  12.         color: Colors.blue.withValues(alpha: 0.3),
  13.         blurRadius: 8,
  14.         offset: const Offset(0, 4),
  15.       ),
  16.     ],
  17.   ),
  18.   alignment: Alignment.center,
  19.   child: const Text(
  20.     '渐变背景 + 圆角 + 阴影',
  21.     style: TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold),
  22.   ),
  23. )
复制代码
LinearGradient做渐变,begin和end指定渐变方向;borderRadius做圆角;boxShadow是数组,能叠多层阴影,blurRadius控制模糊度,offset控制偏移方向。

有个坑:一开始用Colors.blue.withOpacity(0.3)设阴影透明度,flutter analyze给了warning,说withOpacity废弃,建议用withValues(alpha: 0.3)。新版本Flutter改的API,withOpacity有精度损失,withValues是替代写法。改完warning就消了。这种API变更建议顺手跑下flutter analyze看看deprecated提示。

另外Container的color和decoration不能同时设。要用渐变、边框、阴影走decoration,纯色背景走color,两者互斥。我试过在decoration里写渐变又在Container上写color,直接崩了,报错信息还挺绕。Container其他常用参数:padding控制内边距,margin控制外边距,width/height控制尺寸,alignment控制子元素对齐,transform做矩阵变换。可以随意组合,但也容易写成“巨型Container”,装饰复杂场景建议把布局和装饰拆开写,可读性更好。

整体页面串联

这些布局组件单独看都简单,实际页面要组合起来。我的做法是用ListView把每个布局demo包成小节卡片,竖向排列滚动查看。抽了个通用的_SectionCard组件做容器:
  1. class _SectionCard extends StatelessWidget {
  2.   const _SectionCard({required this.title, required this.child});
  3.   final String title;
  4.   final Widget child;
  5.   @override
  6.   Widget build(BuildContext context) {
  7.     return Card(
  8.       elevation: 1,
  9.       child: Padding(
  10.         padding: const EdgeInsets.all(12),
  11.         child: Column(
  12.           crossAxisAlignment: CrossAxisAlignment.start,
  13.           children: [
  14.             Padding(
  15.               padding: const EdgeInsets.only(bottom: 10),
  16.               child: Text(title, style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w600)),
  17.             ),
  18.             child,
  19.           ],
  20.         ),
  21.       ),
  22.     );
  23.   }
  24. }
复制代码
每个小节样式统一,加新demo只要实现内容部分塞进去就行。Flutter这种“组合优于继承”的思路和ArkTS的@Builder复用思路类似,都是把重复UI抽成可复用单元。

踩坑清单

1. Expanded放错容器报错。只能在Row/Column/Flex里用,报错信息是“Incorrect use of ParentDataWidget”,看到直接检查父级是不是Flex系。
2. Stack默认裁剪超出部分。角标伸到外面被切掉,加clipBehavior: Clip.none解决。ArkTS的Stack默认不裁剪,跨端开发要适应这个差异。
3. withOpacity被废弃。新版Flutter用withValues(alpha: x)替代withOpacity(x),遇到deprecated warning最好顺手改掉,版本升级可能直接报错。
4. Column内容超屏不滚动。套SingleChildScrollView或改用ListView,和ArkTS的Column行为一样。
5. padding和margin容易搞混。padding是内边距(子元素到容器边框距离),margin是外边距(容器到外部元素距离)。ArkTS里概念一致,但Flutter里不区分清楚,视觉上可能看不出来,布局计算实际错了。习惯是先想清楚“这段距离在容器内还是外”。
6. 嵌套太深可读性下降。Widget套娃写多了缩进能深到十几层。把独立区块抽成单独Widget类做主build方法里的组合单元,主结构一眼看清,细节去各自类里看。ArkTS的@Builder也是同样思路。

写在最后

跑下来感觉Flutter布局体系和ArkTS思路是通的,都是声明式+嵌套,差异主要在API名字和参数组织。Flutter的Widget拆得更碎,一个效果要套好几层,但每个Widget职责单一,查文档好定位。ArkTS组件集成度更高,一个Row搞定的事Flutter可能得Row+Expanded+Container三层。个人觉得Flutter写起来更灵活、组合性强,但嵌套深了容易“嵌套地狱”。ArkTS的@Builder和@Component拆分方式对治理深嵌套更友好。

性能方面,Flutter布局是声明式的,每次setState触发整棵子树重建,但框架内部有diff优化,不会真把所有Widget重新创建,只更新变化部分。正常写不用担心,除非列表项特别多或布局层级特别深,那种情况用ListView.builder配合const构造器优化。鸿蒙设备性能参差不齐,低端机上列表卡顿比安卓明显,后续聊列表时再细说。

这些demo都在lib/demos/layout_demo.dart里,主页点“基础布局”就能进。下一篇计划聊状态管理,从Counter到TodoList,看StatefulWidget在鸿蒙上的实际表现。布局没有捷径,多写多踩坑就熟了。从ArkTS转过来用Flutter写鸿蒙的,照着demo敲一遍,核心组件用熟日常界面基本够用,遇到不确定的参数多翻官方文档,Flutter的文档比ArkTS全不少。
回复

使用道具 举报

发表于 1 小时前 | 显示全部楼层

Re: Flutter鸿蒙布局实战:从ArkTS迁移的组件差异与踩坑记录

看了楼主的踩坑记录,真是感同身受。我也是从ArkTS转Flutter的,最大的感触就是“思路能套,但手跟不上”。你说的那个Expanded忘了套的问题,我一开始也老犯,习惯性地以为Row直接像ArkTS那样给子组件配layoutWeight就行,结果一堆色块挤在左边。后来才慢慢适应“弹性是Expanded的属性,不是Row的”这个理念。 还有个同感就是CrossAxisAlignment和MainAxisAlignment这俩名字,真的容易搞混。我在Column里想控制水平居中,结果写了mainAxisAlignment,死活没反应,后来才意识到方向搞反了。你那个“沿排列方向是main”的口诀很实用,我记下了。 另外你提到Flutter和ArkTS的枚举值大小写不同,这个细节确实烦人,但多写几次也就习惯了。挺期待你后面接着聊Sliver或者CustomPaint这些进阶组件,因为现在项目里列表一复杂,我就感觉光用Column和Container有点不够用了。感谢分享,收藏了!
回复 支持 反对

使用道具 举报

发表于 1 小时前 | 显示全部楼层

Re: Flutter鸿蒙布局实战:从ArkTS迁移的组件差异与踩坑记录

感谢楼主分享,正好在学鸿蒙Flutter,这几个组件的对比太有用了。特别是Expanded必须在Row/Column里用这个坑,我之前也踩过,把Expanded放Stack里直接报错,当时还没反应过来为什么。还有mainAxisAlignment和crossAxisAlignment的方向区分,确实容易搞混,你的口诀很实用。期待后续能讲讲Sliver或CustomPaint在鸿蒙上的适配情况。
回复 支持 反对

使用道具 举报

发表于 1 小时前 | 显示全部楼层

Re: Flutter鸿蒙布局实战:从ArkTS迁移的组件差异与踩坑记录

沙发,楼主写得很细。我最近也在从ArkTS往Flutter迁移,你这几个坑我都踩过,尤其是Expanded忘套导致子组件挤在一起那个,太真实了。补充一个点:Column交叉轴对齐在ArkTS里确实要分HorizontalAlign和VerticalAlign两套,Flutter一个CrossAxisAlignment全搞定,但反过来,Flutter的MainAxisAlignment里spaceBetween/spaceAround/spaceEvenly这几个在ArkTS里对应的是justifyContent,写法上也要再适应一下。期待楼主后续写Sliver和CustomPaint的坑。
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

指导单位

江苏省公安厅

江苏省通信管理局

浙江省台州刑侦支队

DEFCON GROUP 86025

Hacking Group 021A

旗下站点

态势感知中心

应急响应中心

红盟安全

联系我们

官方QQ群:112851260

官方邮箱:security#ihonker.org(#改成@)

官方核心成员

关注微信公众号

Archiver|手机版|小黑屋| ( 沪ICP备2021026908号 )

GMT+8, 2026-8-12 18:46 , Processed in 0.024061 second(s), 18 queries , Gzip On, Redis On.

Powered by ihonker.com

Copyright © 2015-现在.

  • 返回顶部