资讯动态

Android转Flutter核心认知:Text、ListView与触摸事件的底层重构

发布时间:2026/9/10 12:41:20 来源:尧图企业网站定制
这个系列写到第三节我想先跟同样从Android转过来的朋友说一句Flutter 里的 Text、List、触摸事件跟你脑子里的 TextView、RecyclerView、OnTouchListener 只是名字上沾边底层逻辑几乎全换了。你带着 Android 的肌肉记忆去写 Flutter前两周会非常痛苦但一旦把认知框架切换过来后面学布局、学动画、学状态管理都会顺很多。这一节我打算换个讲法不罗列 API而是以 Android 开发者的认知基础为参照物逐个拆解 Flutter 的触摸事件、List、Text 这三块。每个主题都会讲清楚“Android 里你习惯怎么做”“Flutter 里实际是什么机制”“从 A 到 F 的思维到底卡在哪”。如果你正好是 Android 转 Flutter这篇笔记应该能帮你少走不少弯路。1. 把心态调对为什么说 Android 老手学 Flutter 反而容易踩空1.1 你熟悉的三个老朋友到了 Flutter 全变了样先看一张我当初整理的映射表表面上看一一对应Android 里的习惯Flutter 里看起来对应的东西TextView XML 属性设置Text TextStyleRecyclerView Adapter ViewHolderListView.builder itemBuildersetOnClickListener / setOnTouchListenerGestureDetector / Listener刚接触 Flutter 时我一度以为就是把 XML 属性改成构造函数参数、把 Adapter 改成 itemBuilder 而已。实际上手三天就发现完全不是这么回事Text 没有背景、不能点击、连选中文本都要换组件ListView 没有 LayoutManager 的概念item 滚出屏幕后状态真的会丢触摸事件更是从“责任链分发”变成了“竞技场比赛”。这种落差不是语法层面的而是架构理念层面的。Android 的 UI 是命令式的你拿到一个 View设置属性、添加监听、调 notifyDataSetChanged 去刷新一切都在一个可变的 View 对象上操作。Flutter 是声明式的给定数据 → 构建 Widget → 渲染数据变了重新 build组件树里很多节点会被销毁重建你没法“拿到一个 TextView 然后改它的文字”。1.2 为什么“认知映射”比“API 对照”更值得做市面上很多 Flutter 教程喜欢做一份对照表Android 的 LinearLayout 对应 Flutter 的 Row/ColumnAndroid 的 FrameLayout 对应 StackAndroid 的 TextView 对应 Text。表是好表但如果你只停留在“对应”这个层面就会忽略掉每个组件背后完全不同的工作方式。举个最典型的例子在 Android 里TextView 本身就是一个 View有宽高、有背景、可以 setOnClickListener在 Flutter 里Text 只是一个“绘制文本的组件”它没有背景、没有 padding、没有点击事件。你想让一段文字可点击Android 里直接给 TextView 加监听就行Flutter 里必须把 Text 包进 GestureDetector 或 InkWell。为什么因为 Flutter 的 Text 底层是 RenderParagraph它只管排版和绘制不参与命中测试的“消费”逻辑也没有“自己作为 View 去响应点击”的概念。所以我的建议是学 Flutter 不要背“对应关系”而是要把每个组件当成“一个不认识的工具箱”先搞清楚它内部是怎么工作的再动手写。这一节后面三个主题我都按这个思路展开。2. Text 不是 TextView一个没有背景、不能点击、选起来还费劲的文本绘制器2.1 第一印象从 XML 属性到构造参数的迁移先看最基础的写法迁移。Android 里我们这样写一个文本TextView android:layout_widthwrap_content android:layout_heightwrap_content android:textHello Flutter android:textSize16sp android:textColor#333333 android:textStylebold android:maxLines2 android:ellipsizeend /Flutter 里对应的写法是这样Text( Hello Flutter, style: TextStyle( fontSize: 16, color: Color(0xFF333333), fontWeight: FontWeight.bold, ), maxLines: 2, overflow: TextOverflow.ellipsis, )看着挺像对吧但有几个细节是 Android 开发者最容易忽略的第一Flutter 里没有 wrap_content 和 match_parent。文本组件的尺寸由内容撑开如果你想让 Text 撑满一行并且居中需要把它放到一个 SizedBox(width: double.infinity) 或 Center 里而不是给 Text 本身设置宽度。第二TextStyle 里没有 lineSpacingExtra 这种“追加行距”的写法。Flutter 用height属性表示行高倍率它是一个相对值比如 height: 1.5 表示行高是字号fontSize的 1.5 倍而不是 Android 里“额外加多少像素”。如果你在 Android 里习惯了 lineSpacingExtra 来微调行距到 Flutter 里要改成调整 height 倍率。这个差异在处理中文排版时尤其明显因为中文字体的默认 metrics 和英文不太一样height 给 1.2 可能比 1.4 看起来还紧凑。第三Text 没有 background。Android 的 TextView 可以直接设 android:backgroundFlutter 里要给文本加背景色必须用 Container 包一层然后给 Container 设置 decorationContainer( padding: EdgeInsets.all(4), decoration: BoxDecoration( color: Colors.amber, borderRadius: BorderRadius.circular(4), ), child: Text(带背景的文本), )这个操作本来很直觉但很多从 Android 转过来的朋友一开始会下意识找 Text 的 background 参数找不到就懵了。其实不是 API 缺失而是 Flutter 的设计哲学里“装饰”和“内容”是分离的。2.2 dp、sp 与 Flutter 逻辑像素系统字体缩放怎么处理Android 团队对 sp 是有执念的文本尺寸必须用 sp这样系统字体缩放时应用内文本会跟着调整照顾视障用户。Flutter 里没有 sp所有尺寸都是逻辑像素那系统字体缩放怎么办实际上 Flutter 是通过 MediaQuery 的 textScaler 来做的。MaterialApp 默认会把系统的 textScaleFactorAndroid 上的字体缩放比例传给整个组件树Text 在构建时会读取这个值把你在 TextStyle 里写的 fontSize 乘以缩放系数。所以你不需要像写 Android 那样手动区分 dp 和 sp只要你在 MaterialApp 里正常构建应用Text 默认就是“跟着系统字体缩放”的。但这里有一个新坑如果你在某个页面为了布局稳定强制关闭了文本缩放千万记得在用户可访问性设置里提供一个入口。Android 开发者比较容易踩到的误区是给某个 Text 套了一个固定高度的 Container系统字体调到超大之后文本溢出被裁掉。Flutter 里遇到这个问题不要直接改 fontSize先看 MediaQuery.of(context).textScaler 的值或者用 maxLines overflow 做兜底。另外Flutter 里还有一种常见的“文本不跟系统缩放”的写法就是自己构造一个 MediaQuery 覆盖 textScaler。这个操作要慎重因为它会影响到整个子树内所有文本的可访问性。我自己在项目里的做法是全局保持默认缩放只对极少数必须固定尺寸的 Tab 标签做局部覆盖并且加注释说明原因。2.3 RichText 与 TextSpan比 SpannableString 更“递归”的文本拼装Android 里做富文本你需要一个 SpannableStringBuilder然后各种 setSpanval spannable SpannableStringBuilder(这是蓝色加粗这是红色) spannable.setSpan( ForegroundColorSpan(Color.BLUE), 0, 6, Spannable.SPAN_INCLUSIVE_EXCLUSIVE ) spannable.setSpan( StyleSpan(Typeface.BOLD), 0, 6, Spannable.SPAN_INCLUSIVE_EXCLUSIVE ) spannable.setSpan( ForegroundColorSpan(Color.RED), 7, spannable.length, Spannable.SPAN_INCLUSIVE_EXCLUSIVE ) textView.text spannableFlutter 里你不需要“给一段文本设置多个区间样式”而是直接把文本拆成嵌套的 TextSpanText.rich( TextSpan( children: [ TextSpan( text: 这是蓝色加粗, style: TextStyle(color: Colors.blue, fontWeight: FontWeight.bold), ), TextSpan( text: 这是红色, style: TextStyle(color: Colors.red), ), ], ), )Android 的思维是“同一段字符串打上多组区间标记”Flutter 的思维是“把文本拆成树状结构每个节点有自己的样式”。后者和前端 HTML 里 span 嵌套的思路更像。对习惯 Spannable 的 Android 开发者来说最难改的一点是Flutter 里 TextSpan 是可以无限嵌套的父级 style 会被子级继承子级只需要声明自己“不同于父级”的部分。这个继承逻辑在复杂富文本场景下比 setSpan 清晰很多但如果你硬要在 TextSpan 里找 Span 的 start 和 end你会发现根本找不到因为它的模型里压根没有“区间”这个概念。我曾经在做一个“聊天消息支持某人和链接高亮”的需求时初始代码用 Text.rich 拼 TextSpan一开始觉得不如 Spannable 灵活但后来发现Flutter 这种方式在“样式动态变化”时反而更好管理——你只需要重新 build 一棵文本树不需要像 Android 那样小心维护 Span 的区间索引。2.4 文本选中的坑Text 不能选中SelectableText 才勉强能Android 的 TextView 设置 android:textIsSelectabletrue 之后用户就可以长按选择、复制文本。Flutter 的 Text 默认是“完全不可选中”的你长按它不会有光标、不会有选择手柄、不会弹出复制菜单。如果你需要文本可复制必须改用 SelectableText。但 SelectableText 也不是完美的替代品。它的样式控制比 Text 弱一些有些 TextStyle 的细节效果在 SelectableText 上表现不太一样而且它没有 textAlign 里那种“justify”之类的精细化排版。还有一个老坑SelectableText 放在一些滚动容器里时长按选择区域和滚动的手势偶尔会打架具体表现是“选择手柄拉不动”。我后来在项目里遇到这个问题最简单的临时方案是给 SelectableText 外层包一层 GestureDetector把 onLongPress 的竞技场行为调一调但坦白说这个方案并不优雅更好的做法是改用 SelectableArea 之类的第三方组件或者调整滚动容器的 physics。所以我的建议是产品需求里明确需要“长按复制文本”的直接从需求阶段就用 SelectableText如果只是展示型文本别为了“万一用户想复制”而全局加 SelectableText性能和体验都不划算。3. ListView 不等于 RecyclerView没有 Adapter 和 ViewHolder但要面对新的性能问题3.1 写一个列表的三种姿势和它们各自的适用场景Android 写列表是固定套路RecyclerView Adapter ViewHolder LayoutManager还得自己处理空布局、加载更多、点击事件。Flutter 里一个 ListView.builder 就搞定大部分场景ListView.builder( itemCount: items.length, itemBuilder: (context, index) { return ListTile( title: Text(items[index].title), onTap: () { // 点击跳转 }, ); }, )第一次写这个代码的 Android 开发者大概率会一脸问号我的 ViewHolder 呢我的 getItemViewType 呢我的 LayoutManager 呢其实 Flutter 把这些东西全部隐掉了换来的是你需要理解三件事第一ListView(children: [...]) 和 ListView.builder(itemBuilder: ...) 有本质区别。前者会一次性构建所有子项适合列表项数量很少比如小于 20的场景后者是懒加载只会构建滚入视口附近的子项适合长列表。如果你直接用 ListView(children: ...) 加载 1000 条数据性能和首帧都会很难看。这跟 Android 里“不要用 ScrollView 包 LinearLayout 去做长列表”是同一个道理。第二ListView.separated 可以方便地在每两项之间插入分割线。Android 里你要么在 item 布局里加一个 View要么用 DividerItemDecorationFlutter 里直接把分隔线的构建逻辑塞给separatorBuilder就行。第三列表项的动态类型不用通过 viewType 区分。Android 的 Adapter 里有 getItemViewType 来返回不同布局类型Flutter 只需要在 itemBuilder 里根据数据写 if/else 返回不同 Widget 即可itemBuilder: (context, index) { final item items[index]; if (item.type banner) { return BannerView(item); } else if (item.type article) { return ArticleCard(item); } return Divider(); }背后的 SliverChildBuilderDelegate 会自动按位置管理子元素你不需要为“缓存哪一种 item 类型”操任何心。3.2 告别 ViewHolder 之后的 item 状态问题Android 的 RecyclerView 会复用 ViewHolder滚动时 item 滚出屏幕ViewHolder 进入回收池滚回来时重新绑定数据。开发者在处理“item 是否被选中”“某个开关是否打开”这类状态时其实一直在跟复用机制博弈稍不注意就会状态残留。Flutter 里没有 ViewHolder 复用这个概念它的模型更简单列表项滚出视口且超出 cacheExtent 之后对应的 Element 可能被销毁state 也会丢失。等它重新滚回来Flutter 会重建这个 Widget。所以 Flutter 里最典型的问题是一个列表项里有 CheckBox你勾选了它往下滚再滚回来勾选没了。Android 开发者容易下意识去找“缓存复用”的开关但 Flutter 的正确做法是“状态提升”把每个 item 的选中状态放到 ListView 外面或者放到 item 对应的数据模型里通过回调改变数据再用 setState 触发重建。如果你真的需要 item 在滚出屏幕后保持 Widget 树里的状态比如一个正在播放的视频可以用 AutomaticKeepAliveClientMixin让这个 item 在 Sliver 里保持存活。这个 mixin 对应 Android 里“让某个 ViewHolder 不被回收”的思路但它是按 item 维度控制的粒度更细也更消耗资源不要轻易全局使用。我自己早期在 Flutter 里写“购物车列表”时就踩过这个坑每个 item 里有个数量加减按钮我用局部 StatefulWidget 存数量结果滚动几次之后数量就乱了。后来把所有数量的 Map 提升到页面级的 State 里通过 itemBuilder 传值和回调问题才彻底解决。3.3 itemExtent 和 prototypeItemFlutter 版“固定高度优化”Android 开发者都知道RecyclerView 如果所有 item 高度一致性能会好很多因为 LayoutManager 可以提前计算滑动范围。Flutter 的 ListView 也有对应的优化手段而且它做得更直白通过 itemExtent 告诉列表“每一项的高度都是这个值”。ListView.builder( itemExtent: 80, // 每一项高度固定为 80 逻辑像素 itemCount: items.length, itemBuilder: (context, index) { return ListTile(title: Text(items[index].title)); }, )设置 itemExtent 之后Sliver 在估算滚动总长时不需要真正 layout 每个 item性能提升非常明显。缺点是一旦某个 item 的内容超过 80 高度你需要在外部就保证内容裁剪或自适应否则会出现重叠或溢出报错。如果不确定所有 item 高度完全一致但希望 ListView 用一个“原型 item”来估算可以用 prototypeItemListView.builder( prototypeItem: ListTile(title: Text(估个高度)), itemCount: items.length, itemBuilder: (context, index) { return _buildItemByIndex(index); }, )这个参数的含义是“假设所有 item 高度都跟这个原型差不多”它不要求严格相等但越接近滚动条估算越准。这比 itemExtent 灵活也比全部让 Sliver 测量省性能。对刚转过来的 Android 开发者来说简单记一句话就行列表越长越要告诉列表“你的孩子大概多高”别让它频繁去量。3.4 嵌套滚动的经典翻车shrinkWrap 和 NeverScrollableScrollPhysicsAndroid 里经常用 NestedScrollView 套 RecyclerView实现“整体页面滑动 内部列表滑动”。Flutter 里如果你直接在 Column 里放一个 ListView大概率会遇到这个经典报错RenderBox was not laid out: RenderViewport#... needs a layout vertical viewport was given unbounded height原因很简单ListView 是一个视口组件它要求父级给它一个有界的高度而 Column 会给子组件“无限高度”的约束。Android 里 ScrollView 嵌套 RecyclerView 时RecyclerView 会被测量出实际内容高度再滚动Flutter 的 ListView 不会这样工作。有两种常见解法第一种给 ListView 设置 shrinkWrap: true。这会让 ListView 放弃“视口”行为像普通组件一样去测量自己的全部内容高度。代价是所有 item 都会被构建懒加载失效列表特别长时会卡。shrinkWrap 只适合“不会太长、且必须嵌在滚动容器里”的场景。第二种在 Column 里用 Expanded 包裹 ListView把它限制在有界高度内让它自己滚动Column( children: [ Expanded( child: ListView.builder( itemCount: items.length, itemBuilder: (context, index) Text(items[index]), ), ), BottomBar(), ], )这是更接近“Android 里 RecyclerView 占剩余高度”的解法。嵌套滚动的坑是 Flutter 社区里问得最多的问题之一核心在于理解“视口组件需要固定高度”这个思路想通了后续遇到底部弹窗里放列表、TabBarView 里放列表等问题都会好解决。4. 触摸事件从责任链分发到手势竞技场的认知重构4.1 Android 的 dispatchTouchEvent 和 Flutter 的 hitTest 有什么本质区别Android 触摸事件的核心是 dispatchTouchEvent 这条责任链事件从 Activity 流向 ViewGroup父 View 通过 onInterceptTouchEvent 决定是否拦截子 View 通过 onTouchEvent 决定是否消费子 View 还可以用 requestDisallowInterceptTouchEvent 跟父 View“抢事件”。这套机制里“拦截”和“消费”是两个明确的行为。Flutter 里没有 View 树的 dispatchTouchEvent也没有 onInterceptTouchEvent。事件到来时Flutter 会先做一次命中测试hit test从根节点出发沿着渲染树找到指针位置命中的一系列 RenderObject形成一个 HitTestResult 路径。然后事件会按照这条路径分发路径上的每一个目标都有机会处理事件。这里有一个关键差异路径的顺序是从最深层的子节点开始一路向外层父节点扩散。也就是说如果父组件和子组件都监听了同一个手势事件子组件会先收到父组件后收到。Android 里父 View 可以在事件到达子 View 之前就截住它拦截Flutter 里父级没有“提前拦截”的接口父级能做的只是“在这个手势的竞技场里让我的手势识别器跟你的手势识别器比赛谁先判断出这是自己要的手势谁就赢”。这个区别直接决定了你排查触摸问题的方式Android 里你重点看 onInterceptTouchEvent 里 return true 了没有Flutter 里你重点看 GestureRecognizer 们在竞技场里谁先 resolve 了。4.2 Listener 与 GestureDetector原始指针事件和语义手势是两套体系Flutter 触摸相关组件有两个层级很多新手一上来就混淆。第一层级是 Listener它直接监听原始指针事件Listener( onPointerDown: (event) { print(按下); }, onPointerMove: (event) { print(移动); }, onPointerUp: (event) { print(抬起); }, child: Container(width: 100, height: 100, color: Colors.red), )这相当于 Android 里的 setOnTouchListener它给你的就是最原始的 down/move/up 事件流不帮你判断这是单击、双击、长按还是拖拽。第二层级是 GestureDetector它内部封装了各种 GestureRecognizer帮你做语义识别GestureDetector( onTap: () {}, onDoubleTap: () {}, onLongPress: () {}, onHorizontalDragUpdate: (details) {}, child: Container(width: 100, height: 100, color: Colors.red), )这相当于 Android 里 GestureDetector GestureListener 组合是“带语义的手势识别”。用 Listener 收到的原始事件状态管理成本很高用 GestureDetector 则直接拿到业务语义。Android 转过来的朋友要注意Listener 和 GestureDetector 是可以共存的而且 GestureDetector 内部也依赖 Listener 来接收原始事件再交给 recognizer 去识别。还有一点非常容易踩坑HitTestBehavior。默认情况下GestureDetector 只会在“自身或其 child 被命中”时才参与命中测试。如果你给 GestureDetector 包了一个没有颜色的空 Container它可能因为没有任何渲染内容而点不中。这时候就要显式设置 behaviorGestureDetector( behavior: HitTestBehavior.opaque, onTap: () print(点击生效), child: Container(width: 100, height: 100), // 透明Container )HitTestBehavior.deferToChild仅当 child 命中时自己才参与命中默认。HitTestBehavior.opaque只要点击位置在自己布局范围内自己就参与命中即使 child 没被命中。HitTestBehavior.translucent自己参与命中但不影响其他更底层目标也命中。Android 里有一个很接近的概念是 View 的 clickable 属性clickabletrue 时即使 View 没有背景也能接收点击。HitTestBehavior.opaque 的作用就有点像它让一个“视觉上透明但占着位置的组件”也能响应点击。4.3 手势竞技场怎么决定谁赢一个 ListView item 点击的完整过程Flutter 的手势识别竞速机制叫 GestureArena手势竞技场。一句话解释就是当一个指针按下时所有可能对这个手势感兴趣的识别器都进入竞技场后续 move/up 事件会让竞技场不断淘汰“不符合特征”的识别器直到最后剩下一个胜出者。拿最典型的场景举例ListView 里每一项都用 GestureDetector 包了 onTap。手指按下去时竞技场里至少有两个成员GestureDetector 的 TapGestureRecognizer它觉得这可能是单击。ListView 的 VerticalDragGestureRecognizer它觉得这可能是垂直滚动。接下来手指原地不动过一小段时间后抬起 → 移动量没有超过 touchSlop手势判定阈值VerticalDragGestureRecognizer 认为“这不是滑动”直接放弃。TapGestureRecognizer 在抬手时胜出onTap 触发。手指向下拖了一段距离超过 touchSlop → VerticalDragGestureRecognizer 宣布“这是滚动”竞技场立刻判定它胜出。TapGestureRecognizer 被淘汰onTap 不触发。这个机制解释了 Android 开发者最常见的困惑为什么在列表项上加了 onTap滚动列表时不会误触发点击。因为在 Android 里你需要在手指滑动距离超过 touchSlop 时手动取消 click 事件在 Flutter 里竞技场帮你做了这件事。它也能解释另一个现象如果一个组件同时设置了 onTap 和 onDoubleTap单击的响应会有一个“延迟感”——因为 TapGestureRecognizer 需要等待一小段时间确认这不是双击前面的那一次。这个延迟是竞技场机制的固有代价Android 里也需要通过 GestureDetector 的 setOnDoubleTapListener 处理类似的逻辑。4.4 我在实战中遇到的三个“事件怪癖”及定位方法先说我遇到最多的坑内层 GestureDetector 和外层 GestureDetector 都设置 onTap怎么让内层按下时不触发外层Android 里的直觉是“事件从外到内分发内层消费了外层就不该触发”但 Flutter 的命中路径是从内向外冒泡的两者都会收到事件竞技场也允许两个 TapGestureRecognizer 同时存活最后结果是两个 onTap 都会被触发。解决办法是如果你希望内层 GestureDetector 吃掉点击不让外层收到可以在外层 GestureDetector 的 onTap 里做排除判断或者给内层包一个 AbsorbPointer拒绝命中测试继续向外传递。用 AbsorbPointer 时要注意它会阻隔整个子树外的事件传播如果你只想临时关闭点击它是一个简单的开关但别在需要手势组合的场景里用。第二个坑滚动列表项里加 Dismissible左滑删除跟 ListView 的垂直滚动手势冲突。实战场上 Dismissible 的横向拖拽识别器和 ListView 的垂直拖动识别器会同时进入竞技场Flutter 会根据用户实际滑动方向决定谁赢——你水平滑就删除垂直滑就滚动。这个体验其实很好不用你写任何协调代码。但如果你的列表项里还想支持水平方向的自定义拖拽比如拖一个滑块那就要小心了两个水平方向的识别器会抢占同一方向必须设置竞争的优先级。第三个坑某个区域点击无响应但样式上明明有 Container 和颜色。这时大概率是命中测试的问题。排查路径很固定先看这个区域有没有被其他组件盖住再看 Container 的尺寸是否为 0最后看 GestureDetector 的 behavior 是不是 deferToChild。我用这个排查流程解决了很多“明明写了 onTap 但点了没反应”的问题比直接翻代码看逻辑高效得多。5. 第三节留给 Android 开发者的课后迁移练习5.1 用“组件三问”去拆解每一个新组件学完这一节你会发现 Android 经验迁移到 Flutter 时最大的阻力不是语法而是思维惯性。我给自己的要求是每遇到一个新组件先回答三个问题再写代码它在渲染树里扮演什么角色是只负责绘制还是参与了布局、命中测试它的状态存在哪里组件销毁重建后状态会丢吗它跟手势系统怎么交互它会不会参与手势竞技场的竞争拿 Text 举例回答完这三个问题后你就不会纠结“为什么 Text 没有背景、不能点击”了。拿 ListView 举例回答完“状态存在哪里”之后你自然就理解了为什么要状态提升。这套方法对新手特别有用强烈建议你用笔写下来而不是只在脑子里过一遍。5.2 坚持做“同屏对照”左边 Android 代码右边 Flutter 代码我的练习方法是拿一个以前写过的 Android 页面左边打开 Java/Kotlin 代码右边新建一个 Flutter 文件逼自己用 Flutter 重写一遍。不用管 UI 像素级还原重点是“同一个业务需求两种技术栈各自的写法”。比如“一个文本列表点击每一项弹出提示”Android 里你写 RecyclerView Adapter ViewHolder OnClickFlutter 里你只用 ListView.builder ListTile GestureDetector/ListTile.onTap。写完之后对照你会很直观地感受到Android 的代码是“面向 View 的流程式组装”Flutter 的代码是“面向数据的声明式描述”。我在做这个练习时最大的收获不是“少写了好多代码”而是理解了 Flutter 为什么推荐“数据变化就重建整个 Widget 树”——因为组件树本身是轻量的真正的渲染由 RenderObject 层管理重建 Widget 并不等于重新绘制页面。5.3 小练习用 Flutter 重写一个“带列表的搜索页”最后留一个小练习如果你能顺利完成这一节的核心内容基本就过关了页面顶部有一个搜索框TextField。输入文字后列表实时过滤并展示匹配结果ListView.builder。列表项用 Text 展示标题点击后跳转新页面GestureDetector 或 ListTile 的 onTap。列表为空时显示空状态提示。注意列表项滚出屏幕后回到视口输入状态不能丢这其实是 Flutter 自动保持的但你可以试试看。这个练习把 Text、List、触摸事件全部串了起来。你先用 Android 的方式想一遍怎么实现再用 Flutter 的方式写一遍写得过程中注意我上面讲的几个关键点Text 的样式和溢出处理、ListView.builder 的懒加载、item 点击与滚动手势的竞技场关系。写完之后再回来看这篇文章你会有新的体会。我个人的经验是从 Android 转 Flutter前两周是最难熬的后面会越来越顺因为你已经养成了新的“直觉”——看到一个 UI 需求不再去想“我要创建哪个 View”而是想“我要构建一棵什么样的 Widget 树”。有这个转变就算入门了。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价