☰
Flutter ScaffoldMessenger实战:SnackBar消息通知机制与避坑指南
2026/10/18 2:08:46 网站建设 项目流程

如果你最近在 Flutter 项目里维护消息通知,一定对 ScaffoldMessenger 不陌生。这个组件从 Flutter 2.0 开始就成为 SnackBar 的官方入口,但很多人只是照着文档写ScaffoldMessenger.of(context).showSnackBar(...),真到项目复杂起来——页面嵌套、Tab 切换、异步回调、跨路由提示——就开始出各种诡异问题。这篇文章把我实际维护项目时的踩坑经历整理出来,围绕 ScaffoldMessenger 的消息通知机制,讲清楚它解决的旧问题、核心用法、作用域陷阱、队列行为,以及怎么在真实项目里做一套稳定可靠的通知方案。适合已经被 SnackBar 逼疯过的 Flutter 开发者,也适合刚开始接触这个组件的初学者。

1. 为什么会有 ScaffoldMessenger:旧版 SnackBar 的两个历史痛点

1.1 Scaffold.of(context) 的时代

Flutter 早期版本里,想在页面上弹一条 SnackBar,标准写法是这样的:

Scaffold.of(context).showSnackBar( SnackBar(content: Text('保存成功')), );

那个年代的思路很直接:SnackBar 是 Scaffold 的一部分,所以要通过Scaffold.of(context)找到最近的 Scaffold,然后在它身上调用showSnackBar。

听起来合理,用起来却非常憋屈。最大的问题是Scaffold.of(context)依赖 context 在 widget 树上的位置。如果调用处的 context 离 Scaffold 太远,或者干脆不在 Scaffold 的子树里,就找不到。很多初学者第一次写,把点击事件放在一个按钮的onPressed里,而按钮的 context 可能经过了好几层 CustomPaint、GestureDetector、InkWell,这时候Scaffold.of(context)经常抛出一个“Scaffold.of() called with a context that does not contain a Scaffold”的错误。

为了解决这个问题,社区里流行过一种方案:在 widget 树里塞一个Builder,让新 context 成为 Scaffold 的直接子节点,再拿这个 context 去调用。代码写出来长这样:

return Scaffold( body: Builder( builder: (context) => ElevatedButton( onPressed: () { Scaffold.of(context).showSnackBar(...); }, ), ), );

这种写法能用,但很丑。而且它解决不了真正的痛点——跨页面通知。

1.2 Navigator 跳转之后,旧写法为什么必然翻车

假设这样一个非常常见的场景:用户提交订单,提交成功之后要返回上一页,并在上一页显示“订单提交成功”的提示。

用旧写法,最直接的尝试是这样的:

// 当前页面 A Scaffold.of(context).showSnackBar(...); // 在当前页弹 Navigator.of(context).pop(); // 返回上一页

但如果想先 pop 再 show,代码就会变成:

Navigator.of(context).pop(); Scaffold.of(context).showSnackBar(...); // 这句会崩溃

为什么崩溃?因为Navigator.pop()之后,当前路由的 Element 会被标记为失活,context 从 widget 树中被移除。这时候再通过这个 context 去找 Scaffold,理论上不应该找到任何东西。实际上 Flutter 的导航过程是异步的、带转场动画的,pop 之后页面不会立刻销毁,所以你会发现更隐蔽的表现:提示弹在了正在退出动画的旧页面上,旧页面一销毁,SnackBar 也跟着消失了。用户什么都没看到,代码也不报错,但功能是坏的。

这种“看着没毛病、实际永不出现”的隐性 bug,比直接报错更折磨人。当时我排查一个线上问题,用户反馈“操作之后没有提示”,本地反复测都不稳定,最后才发现是跳转和 SnackBar 的执行顺序问题。后来统一把提示放在跳转之前,又出现另一种情况:跳转到新页面后,新页面顶部偶尔还会残留上一个页面遗留的 SnackBar 动画残影。

1.3 ScaffoldMessenger 的引入和角色定位

Flutter 团队在 2.0 版本引入 ScaffoldMessenger,核心思路是:把 SnackBar 的“显示逻辑”从 Scaffold 身上剥离出来,交给一个独立的消息管理器。这个管理器由 MaterialApp 自动创建,挂在整棵 widget 树的最顶层,生命周期和应用一样长。SnackBar 被交给管理器之后,不再绑定某个具体页面的存活时间。

class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( home: HomePage(), ); } }

上面这段代码里,MaterialApp 内部就有一个默认的 ScaffoldMessenger。也就是说,只要你的应用用了 MaterialApp,任何地方都能通过ScaffoldMessenger.of(context)拿到这个管理器,然后调用它的showSnackBar。

它的角色更像一个“消息调度中心”,Scaffold 只是承载消息的舞台。舞台可以换,调度中心一直存在。这个架构调整,从根上解决了旧 API 的两大痛点:context 层级敏感、页面生命周期绑定过强。

2. 基础用法与参数解析:一个能跑的 SnackBar 需要哪些配置

2.1 最短可用代码与四要素

现在写一条 SnackBar,最短代码就是:

ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('已保存')), );

和旧写法对比,少了一层Scaffold.of(context)的前置查找。但这里有几个容易被忽略的要素,我拆开说:

第一,ScaffoldMessenger.of(context)必须在 MaterialApp 的子树里调用。第二,当前 widget 树上至少得注册一个 Scaffold,否则 SnackBar 没有承载舞台,运行时会直接报错。第三,SnackBar 必须带一个content,它就是显示给用户的核心文案。第四,showSnackBar返回一个ScaffoldFeatureController,你可以通过它监听 SnackBar 的关闭原因,后面专门讲。

很多人以为只要有 ScaffoldMessenger 就能弹,却忘了 SnackBar 的视觉表现依赖 Scaffold 的底部区域。你可以在一个没有 Scaffold 的 Overlay 里调用showSnackBar试试,大概率会看到类似“ScaffoldMessenger.showSnackBar was called, but there are currently no descendant Scaffolds”的断言失败。这是一个特别容易踩的隐性约束。

2.2 常用参数逐个说清楚

SnackBar 的构造参数不算多,但每个都有讲究。我按日常使用频率排个序:

参数作用备注
content提示内容,必填通常传 Text,可以包含图标、行内组件
action操作按钮经典用法是“撤销”操作
duration展示时长默认 4 秒,设置 action 后自动延长到 8 秒左右
behavior显示行为fixed 固定底部,floating 浮动显示
backgroundColor背景色用作全局主题时常用
shape形状配合 floating 做圆角卡片效果
margin外边距只有 floating 模式下生效
elevation阴影高度视觉层级控制
width宽度仅 floating 模式下,可以限制横向宽度

content不只是文本框,你可以塞 Row、Icon、多行文本都行。我在项目中用过一个组合:左侧小图标、右侧两行文字,一行标题一行详情,SnackBar 完全能承载这种复合布局,只要控制好高度。

action是 SnackBar 的精髓。Material 设计规范里,SnackBar 不需要用户关闭,它自动消失,所以一些不可逆操作必须提供“后悔药”。删除记录是最典型的场景:

ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('已删除这条记录'), action: SnackBarAction( label: '撤销', onPressed: () { // 恢复删除的数据 }, ), ), );

注意SnackBarAction的onPressed执行完,SnackBar 会立即关闭。如果你想在关闭之后再做点别的,比如上报埋点,需要通过closed回调监听,这个后面讲。

2.3 duration 和 action:源码里的8秒暗逻辑

duration是老手也经常搞混的参数。它控制 SnackBar 在屏幕上停留的时间。源码里的默认逻辑值得单独拎出来说:如果不显式传duration,SnackBar 默认使用 4 秒;但只要你传了action,且没有显式指定 duration,时长会自动翻倍到大约 8 秒。

原因很好理解:带 action 的 SnackBar 要留给用户足够的操作时间,4 秒太短,可能刚看清“撤销”按钮在哪就消失了。我在实际项目里从不手动设 duration,因为默认值已经过 Material 团队权衡,是有用户行为数据支撑的。只有一种情况需要改短:那种纯提示、不带操作、又经常弹出的场景,比如“正在刷新”,我会设成 2 秒,避免长时间占着底部位置。

还有一点和 Navigation 相关:SnackBar 的计时是从展示动画完成后开始的。如果用户正在操作键盘输入、切到后台,计时不会暂停。也就是说它显示 4 秒就是 4 秒,不会等人。特殊场景下——比如表单页用户正在专心填写,SnackBar 弹出来报“校验失败”——信息很容易被忽略。这种场景我建议改用 Dialog 或 MaterialBanner,而不是硬把 duration 调长。

2.4 全局 ScaffoldMessengerKey:从任何地方发起通知

前面说ScaffoldMessenger.of(context)需要一个 context,但有些场景你真拿不到合适的 context——比如全局网络拦截器、纯 Dart 工具类里。

MaterialApp 提供了scaffoldMessengerKey参数,绕开 context 也能弹消息:

final scaffoldMessengerKey = GlobalKey<ScaffoldMessengerState>(); class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( scaffoldMessengerKey: scaffoldMessengerKey, home: HomePage(), ); } } // 任意地方,包括纯 Dart 类 scaffoldMessengerKey.currentState?.showSnackBar( SnackBar(content: Text('网络错误,请稍后重试')), );

这个技巧在项目里非常实用。比如统一封装 API 请求层,遇到 401 或者网络超时,不需要在每个调用点手动弹提示,直接在拦截器里通过 key 发起一个全局 SnackBar。这样业务代码干干净净,通知逻辑收拢到一处。

使用currentState时,务必加?.判空。因为 GlobalKey 只有挂载到 widget 树之后才会有 state,如果是应用启动早期触发的异步回调,可能拿到 null。我见过线上崩溃就是因为没判空,在scaffoldMessengerKey.currentState!上强解包。

3. 嵌套 Scaffold 的作用域陷阱:消息到底显示给谁

3.1 页面上多个 Scaffold 时,通知跑到哪儿去了

ScaffoldMessenger 解决了旧 API 的跨页面问题,但它自己也有一个经典陷阱:当屏幕上有多个 Scaffold 同时存在时,SnackBar 到底显示在哪个 Scaffold 上?

举一个实际场景:应用主页底部有 TabBar,每个 Tab 页面内部又自己套了一个 Scaffold,用于单独设置 AppBar 和底部按钮区。这时 widget 树里至少有两个 Scaffold:一个外层主页的,一个当前 Tab 的。你调用ScaffoldMessenger.of(context).showSnackBar(...),展示的可能是当前 Tab 内层那个,也可能是外层主页那个。如果你预期的位置和内层显示不一致,用户会看到 SnackBar 出现在奇怪的位置,甚至被 TabBar 遮挡一半。

更隐蔽的是 BottomSheet 和 Dialog 场景。打开 BottomSheet 之后,看起来整个屏幕是 BottomSheet 的天下,但底层 Scaffold 还活着。你响应 BottomSheet 里按钮点击,context 一路向上找,找到的还是底层页面的 ScaffoldMessenger,SnackBar 会出现在 BottomSheet 下方,被遮得严严实实,用户完全看不见。

我排查过一个 Bug:用户在表单底部弹了一个 BottomSheet 选择分类,选完之后触发校验失败,要提示“分类不能为空”。结果 SnackBar 显示在页面最底部,被 BottomSheet 挡住,测试人员反馈“没有任何提示”。找了一圈才发现是这个原因。

3.2 用子级 ScaffoldMessenger 圈定作用域

解决这类问题有一个标准套路:在需要控制范围的子树上,显式创建一个新的 ScaffoldMessenger。它的作用类似“圈地”,把自己子树内注册的 Scaffold 和外界隔离开来。

ScaffoldMessenger( key: _innerMessengerKey, child: Scaffold( body: BottomSheetContent(), ), )

这个内层 ScaffoldMessenger 显示 SnackBar 时,会优先在它子树内部的 Scaffold 上展示。即使底层页面还挂着全局的 ScaffoldMessenger,也不会串到那边去。

这个套路在实现一个页面内多个消息区域时特别有用。我做过一个后台管理应用,左侧是数据过滤面板,右侧是内容列表,两个区域各自需要独立的操作反馈。如果共享同一个 ScaffoldMessenger,左侧操作之后 SnackBar 会跑到右侧底部,体验非常割裂。给左右两侧各自包一层 ScaffoldMessenger,消息就各找各的承载区域了。

3.3 实践中如何确认当前显示的承载者

想确认 SnackBar 最终跑到哪个 Scaffold 上,最笨也最有效的方法:给不同的 Scaffold 设置不同的backgroundColor或者 AppBar 颜色,然后触发 SnackBar 看它出现在哪一块背景上。开发调试时这个办法非常直观,不用加日志。

还有一种代码层面的排查方式:在当前页面所有的 Scaffold 处打断点,看_register相关调用链。不过这个比较底层,日常开发不建议深入。更实用的建议是——在架构上就规避多 Scaffold 嵌套。一个页面能只有一个 Scaffold 就只保留一个。AppBar 和底部按钮不一定非要内层 Scaffold 才能实现,使用 BottomNavigationBar、Column + SafeArea 都能替代。把页面结构简化之后,作用域问题会减少一半以上。

如果确实无法避免多 Scaffold,就按上面说的,给消息区域包独立的 ScaffoldMessenger,并明确管理者。不要让消息“自然流动”到不确定的承载者上。

4. 队列与覆盖策略:快速连续触发时的正确姿势

4.1 连续 show 三次会发生什么

很多开发者在快速连续触发 SnackBar 时发现一个现象:只显示了最后一条,前面的好像被“顶”掉了。其实不是被顶掉,而是默认有一个队列机制。

连续调用三次showSnackBar,ScaffoldMessenger 会把三条 SnackBar 依次排队。第一条先展示 4 秒,消失后第二条接着展示,再消失后第三条展示。所以你看到的最终效果是:屏幕上每 4 秒换一条,持续 12 秒。

听起来合理,但真实用户场景往往不是“排队展示”,而是“只关心最后一条”。比如用户连续点了三次“刷新”按钮,每次都触发一个“刷新中”的 SnackBar,你的代码可能已经优化成只发最后一次请求了,但三条提示还在排队,足足转 12 秒。用户看着烦,产品也恼火。

更严重的场景是轮询类通知:新消息不断进来,每条都showSnackBar,SnackBar 永远显示不完,底部一直被占用。这种情形必须干预。

4.2 hide、remove、clear 的区别与适用场景

ScaffoldMessengerState 提供了三个和队列相关的方法,很多新手分不清,其实逻辑链路是这样的:

  • hideCurrentSnackBar():把当前显示的 SnackBar 隐藏掉,带退出动画。队列里如果还有下一条,会立刻继续展示。
  • removeCurrentSnackBar():把当前显示的 SnackBar 直接移除,无动画。队列里的下一条同样继续。
  • clearSnackBars():把所有正在显示的、以及队列里排队的全部清空,一锅端。

所以如果你想“新消息来了,立刻顶掉旧消息”,标准做法是:

final messenger = ScaffoldMessenger.of(context); messenger ..removeCurrentSnackBar() ..showSnackBar(SnackBar(content: Text('新的提示')));

这里用removeCurrentSnackBar而不是hideCurrentSnackBar,是因为隐藏动画期间新 SnackBar 可能已经开始入场,两条会交叠闪烁,视觉上很脏。无动画移除再展示新的是最干净的切换。

但要小心一个细节:removeCurrentSnackBar只移除当前那条,队列里排着的旧消息还在。如果旧消息队列里还有两条,你会发现“新的提示”之后,旧的排队消息又接上来了。这会让人摸不着头脑。所以如果场景是“丢弃所有旧消息”,应该先clearSnackBars()再 show:

messenger ..clearSnackBars() ..showSnackBar(SnackBar(content: Text('最新提示')));

我自己的经验法则是:页面操作触发的提示,用removeCurrentSnackBar+ show;全局性的、可能积压大量历史提示的,用clearSnackBars+ show。优先保证用户看到最新一条,而不是排队轰炸。

4.3 监听关闭原因:close 之后才知道用户做了什么

showSnackBar返回的 controller 带一个closed属性,它是一个Future<SnackBarClosedReason>。这个 future 会在 SnackBar 关闭时完成,带着关闭原因:

final controller = ScaffoldMessenger.of(context).showSnackBar(snackBar); controller.closed.then((reason) { if (reason == SnackBarClosedReason.action) { // 用户点击了 action 按钮 } else if (reason == SnackBarClosedReason.timeout) { // 自动超时消失 } else if (reason == SnackBarClosedReason.swipe) { // 用户手动滑掉了 } });

这个监听非常有用。比如做数据埋点时,想知道用户是看到提示后主动点了撤销,还是压根没管让提示自己消失,这两种行为背后反映的是不同的功能设计效果。通过closed回调就能拿到准确数据。

另一个用途是联动操作。SnackBar 展示期间如果用户滑掉了“已删除”的提示,那么“撤销”入口就没了,你可以在这里补一个兜底逻辑:比如重新询问用户是否要执行删除、或者把删除操作记录到日志。总之,不要只把 SnackBar 当成一个无状态展示组件,它是有完整生命周期的。

4.4 时长选择与用户体验折中

关于 duration,除了默认值,我还想补充一个经验:不要因为觉得“用户可能没看清”就把时长调成 10 秒。SnackBar 的本质是瞬态提示,它不适合展示需要长时间阅读的内容。如果一条消息需要用户看 10 秒以上,那它应该改用 Dialog 或者模态面板。

反向的坑也存在:duration 过短,比如 1 秒,用户还没看清就消失了,反而比不弹更差。SnackBar 里如果带了 action,最短建议 4 秒起步。不带 action 的纯提示,也建议不少于 2 秒。

如果你担心 SnackBar 频繁触发打扰用户,正确的做法不是缩时长,而是加防抖逻辑。比如搜索场景的“正在加载更多”提示,做一个简单的节流:同一类型提示在 2 秒内只允许弹一次。这样既保证用户能感知状态,又不会连续弹屏。实现防抖可以自己写一个简单的 map 记录,也可以引入现成的节流工具库,核心是对提示的唯一标识做时间窗口判断。

5. 跨页面通知与全局封装:从“局部提示”到“消息中心”

5.1 先保存引用再跳转:跨页面提示的最稳姿势

ScaffoldMessenger 的生命周期和应用一样长,所以理论上它能做到“页面跳转之后再弹提示”。但实际操作里,很多人跳转之后拿不到原来的 context,又或者拿到的 context 已经失活。

最稳妥的跨页面提示写法,是在跳转之前先把 messenger 保存下来:

final messenger = ScaffoldMessenger.of(context); Navigator.of(context).push( MaterialPageRoute(builder: (_) => NextPage()), ); // 页面跳转后依旧可以显示 messenger.showSnackBar( SnackBar(content: Text('操作成功,已返回列表')), );

关键是ScaffoldMessenger.of(context)要在Navigator.push之前调用。它拿到的是一整个 State 引用,只要应用不退出,这个引用始终有效,和当前页面是否销毁无关。页面跳转动画结束、新页面完全展示之后,再调用这个 messenger,SnackBar 就会出现在新的页面上。

我有个项目里做了类似“提交后自动返回列表页并提示”的流程。最初直接在新页面初始化时读取一个全局状态再加 SnackBar,处理时机非常难把握。后来改成上面这种写法:源页面保存 messenger,Navigator.pop成功之后直接展示。逻辑清晰,也不需要跨组件传状态,代码量少了一大半。

不过要注意:如果源页面本身已经不在 widget 树上了——比如你从某个全局弹窗里发起跳转,弹窗关掉了——那保存的 messenger 引用依然有效,只是 SnackBar 会显示到当前屏幕上注册的 Scaffold 上。这时要确认新页面确实有 Scaffold,否则依然会断言报错。

5.2 MaterialBanner:顶部横幅的配套玩法

ScaffoldMessenger 不只管 SnackBar,它还负责 MaterialBanner。MaterialBanner 是显示在页面顶部的一条横幅,和 SnackBar 的底部位置正好互补。

ScaffoldMessenger.of(context).showMaterialBanner( MaterialBanner( content: Text('当前网络不稳定,部分功能可能受限'), leading: Icon(Icons.wifi_off), actions: [ TextButton( onPressed: () { ScaffoldMessenger.of(context).hideCurrentMaterialBanner(); }, child: Text('知道了'), ), ], ), );

MaterialBanner 适合放“需要持续存在,但又不至于打断操作”的提示。比如弱网警告、数据同步状态、权限提示。它的存在感比 SnackBar 强,但比 Dialog 温和。用户不处理就一直停在页面顶部,处理了就能手动关闭。

实际项目中我通常这样分工:SnackBar 负责操作反馈,MaterialBanner 负责状态提醒。操作反馈是短瞬的,状态提醒是需要持续可见的。两者都用 ScaffoldMessenger 管理,但不会同时弹,因为同时弹会抢占上下两块空间,太吵了。设计页面时最好先想清楚这条提示属于哪一类,再决定用哪种组件。

MaterialBanner 也有隐藏和移除方法,命名和 SnackBar 高度统一:hideCurrentMaterialBanner、removeCurrentMaterialBanner、clearMaterialBanners。规则完全一致,队列和覆盖策略参考 SnackBar 那套就行。

5.3 主题统一:SnackBarThemeData 一处改全局

项目里的 SnackBar 如果每个页面各写各的样式,迟早会变成灾难。Flutter 提供了SnackBarThemeData,可以在全局 Theme 里统一消息通知的观感:

ThemeData( snackBarTheme: SnackBarThemeData( behavior: SnackBarBehavior.floating, backgroundColor: Colors.grey[900], contentTextStyle: const TextStyle( color: Colors.white, fontSize: 14, ), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), actionTextColor: Colors.amber, elevation: 4, ), )

配置一次,全项目所有 SnackBar 的底色、字体、圆角、行为模式全部统一。后续产品要调风格,只改这一处,不用每个页面逐个找。

我强烈建议团队项目里把 SnackBar 的behavior也统一到主题里。fixed 和 floating 的观感差异很大:fixed 贴底、宽度撑满,floating 浮起来、带圆角。混用会让用户觉得页面风格不统一。如果产品设计稿没明确,我一般默认用 floating,它在视觉上更轻,配合圆角更好看,也不会完全遮住底部导航内容。

主题里统一了shape和elevation之后,SnackBar 的卡片质感会明显增强。有一点要注意:如果主题里设了behavior,但单条 SnackBar 又显式传了behavior,单条优先。这符合 Flutter 一贯的样式覆盖规则,所以页面想个别定制是可以的,但建议控制数量,别让例外泛滥。

5.4 项目里的通知封装思路

把 ScaffoldMessenger 用顺之后,可以做一个轻量的通知工具,把常用逻辑固化下来。这是我在实际项目里整理的封装思路,不是让你照抄,而是给一个方向:

class AppNotify { static void show(BuildContext context, String message, {SnackBarAction? action}) { ScaffoldMessenger.of(context) ..clearSnackBars() ..showSnackBar( SnackBar( content: Text(message), action: action, ), ); } static void showGlobal(String message) { scaffoldMessengerKey.currentState ?..clearSnackBars() ..showSnackBar(SnackBar(content: Text(message))); } static void dismiss() { scaffoldMessengerKey.currentState?.clearSnackBars(); } }

封装的核心价值有三个。

第一是统一“新提示取代旧提示”的策略。每个调用者不用记clearSnackBars、removeCurrentSnackBar这些细节,工具类内部处理。

第二是提供全局和局部两套入口。业务代码里有 context 的用show,网络层等无 context 场景用showGlobal。

第三是统一了清空时机。比如页面切换时,在主页面路由切换事件里调用一次dismiss(),避免上一个页面的 SnackBar 残留到新页面。

封装时要注意一点:不要在工具类内部判断 mounted。工具类不持有页面 context 的引用,它只接收 context 或者使用 GlobalKey,所以不存在生命周期泄漏。真正需要小心的是,调用方要在页面销毁后不要再调用toast(context: context, ...)这类方法,否则 context 已失活,行为依然不稳定。

还有个经验:不要在build方法里直接调用showSnackBar,会引发 build 期间 setState 的异常。SnackBar 的触发源应该是按钮点击、异步回调、网络监听这些用户或事件的驱动点,而不是 build 流程里。这个规则看似基础,实际项目里真的见过有人把 SnackBar 逻辑误写到 build 里,导致无限重建弹提示,页面一打开就弹个不停。

最后再分享一个小技巧。如果你在做登录页,登录失败的提示不要用 SnackBar,用 SnackBar 很容易被键盘顶来顶去,位置不稳定。登录接口错误反馈建议直接放在表单区域下方,用一行红色文字内联展示——既稳定又明显。SnackBar 最适合的永远是“轻量、瞬时、可被忽略”的通知。想清楚这条边界,你的消息通知设计基本就立住了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询