歡迎區(qū)域跨端實戰(zhàn)解析)
把 Flutter 跑在 OpenHarmony 上做應(yīng)用很多人一聽就覺得“又要從零學(xué)一套新的 UI 框架了”。我接某高校宿舍管理系統(tǒng)的跨端需求時也是這個反應(yīng)移動端要跑 Android樓棟自助終端要跑 OpenHarmony最怕的就是同一套界面寫兩遍。結(jié)果做下來發(fā)現(xiàn)Flutter 在 OpenHarmony 上的工作方式比想象中順暢真正需要額外處理的是適配層的版本對齊和幾個平臺差異點。這篇我準(zhǔn)備用項目里最典型的“歡迎區(qū)域”作為例子把從跨端選型到組件樹規(guī)劃從核心源碼到實測適配的完整過程拆開講代碼可以直接抄走當(dāng)模板。適合正在折騰 Flutter OpenHarmony 的你也適合想看看一套 UI 代碼如何在不同系統(tǒng)上保持一致體驗的團(tuán)隊參考。1. 為什么是 Flutter OpenHarmony而不是重學(xué)一套 ArkUI先說清楚當(dāng)時面對的選擇。宿舍管理系統(tǒng)的使用對象有兩類一類是新生和樓管手里大概率是普通 Android 手機(jī)另一類是放在樓道里的自助終端屏幕大、系統(tǒng)定制跑的是 OpenHarmony。如果按傳統(tǒng)思路走手機(jī)端一套技術(shù)棧自助終端另一套技術(shù)棧UI 還原度、交互細(xì)節(jié)、驗收標(biāo)準(zhǔn)全是雙份工作。我當(dāng)時也認(rèn)真評估過 ArkTS/ArkUI。它作為 OpenHarmony 的原生方案離系統(tǒng)能力最近如果項目只面向 OpenHarmony 一個平臺我會毫不猶豫選它。但我們的現(xiàn)狀是存量代碼里已經(jīng)有 Flutter 業(yè)務(wù)組件團(tuán)隊對 Flutter 的掌握也更熟需要的是一條“從 Android 平滑覆蓋到 OpenHarmony”的路線而不是再養(yǎng)一個平行技術(shù)棧。三個維度的對比大概是這樣對比維度FlutterArkTS/ArkUI團(tuán)隊上手成本已有 Dart 基礎(chǔ)即可延續(xù)需要重新學(xué)習(xí)聲明式 UI 與狀態(tài)管理雙端覆蓋能力一套代碼可跑 Android / OpenHarmony原生能力只在 OpenHarmony 上有效UI 表現(xiàn)一致性自繪渲染跨端差異小平臺原生控件系統(tǒng)版本差異需適配OpenHarmony 適配程度已有社區(qū)適配工程能跑但需對齊版本原生最優(yōu)系統(tǒng)能力調(diào)用依賴插件適配部分能力有缺口直接調(diào)用鏈路短為什么“歡迎區(qū)域”這種界面特別適合用 Flutter 來做因為它本質(zhì)上是自繪型 UI。一個問候語、一張學(xué)期信息卡片、一列今日待辦、一組快捷入口這些內(nèi)容不依賴系統(tǒng)原生控件Flutter 渲染引擎在 OpenHarmony 上把 Skia 圖形棧跑起來之后呈現(xiàn)效果和 Android 端可以做到高度一致。越是這種“整頁都是自定義布局”的界面Flutter 越占優(yōu)勢。但選 Flutter 不是沒有代價。OpenHarmony 上的 Flutter 適配工程目前是跟著特定版本走的這意味著你不能無腦用最新版 Flutter SDK而是要先查適配工程對應(yīng)的 Flutter tag再基于那個版本創(chuàng)建工程。否則編譯產(chǎn)物進(jìn) OpenHarmony 設(shè)備后要么跑不起來要么運行一段時間后隨機(jī)閃退。我們最終是鎖定了一個經(jīng)過驗證的版本組合把 Flutter SDK 固定下來升級前先在測試機(jī)上完整回歸一遍才敢動版本。這一點是后面所有開發(fā)工作的前提也建議你做類似選型時第一個確認(rèn)。2. 歡迎區(qū)域拆解把視覺稿“翻譯”成一顆組件樹“歡迎區(qū)域”聽起來只是一張靜態(tài)歡迎頁實際上它承擔(dān)了好幾個信息層級先讓用戶看到自己的名字、確認(rèn)身份再獲取他今天需要知道的信息比如樓棟宿舍、輔導(dǎo)員、今日待辦最后給他可操作的動作入口比如報修、簽到、查詢。信息層級決定了 UI 層級所以我建議先把視覺稿拆成塊再動手寫代碼。我們在設(shè)計稿里把歡迎區(qū)域分成了五塊。最上面是漸變背景的問候頭部里面是姓名、日期和一句狀態(tài)文案下面是學(xué)期信息卡片展示樓棟、宿舍號、輔導(dǎo)員和報到狀態(tài)再往下是今日待辦列表底部是四個快捷入口還有個裝飾性的品牌落款放在最下方。請注意這里有一個重要決定歡迎區(qū)域本身不滾動。宿舍管理系統(tǒng)的首頁下方還有業(yè)務(wù)列表如果把所有東西都放進(jìn)一個滾動容器兩個滾動方向會打架用戶操作體驗會很怪。把視覺塊轉(zhuǎn)成組件樹的過程就是不斷問“這個模塊內(nèi)部是橫排還是豎排、需要不需要層疊”的過程。我們最后得到的結(jié)構(gòu)大致是WelcomeScreen └── SafeArea └── Column ├── GreetingHeader │ └── Stack │ ├── Container (漸變背景 半透明裝飾圓) │ └── Column (問候語) ├── SemesterCard │ └── Container (圓角 陰影) │ └── Row │ ├── Icon │ └── Column (樓棟/宿舍號/輔導(dǎo)員) ├── TodayNotice │ └── Expanded │ └── ListView / 空狀態(tài) └── QuickActions └── GridView.count這里每個選擇都有講究。外層用 Column 而不用 ListView是因為歡迎區(qū)域高度我們希望控制在單屏內(nèi)Column 足夠且更可控問候頭部用 Stack是為了能在漸變背景上疊一層半透明圓作為裝飾而不影響文字排版學(xué)期信息卡片內(nèi)部用 Row 是因為“圖標(biāo) 多行文本”是最經(jīng)典的組合比堆兩個 Widget 再手動對齊要穩(wěn)今日待辦必須用 Expanded 包住否則在不同屏幕尺寸下很容易出現(xiàn)底部溢出的問題。尺寸策略也值得單獨說。設(shè)計稿通?;谑謾C(jī)豎屏比例但樓棟自助終端的邏輯分辨率并不一定是標(biāo)準(zhǔn)手機(jī)比例可能接近 4:3 甚至更寬。如果在這些組件里寫死像素值小屏手機(jī)和大屏平板上總有一個要出問題。所以我們統(tǒng)一約定歡迎區(qū)域只使用相對布局和約束比例關(guān)鍵容器用 EdgeInsets 統(tǒng)一控制內(nèi)邊距列表區(qū)域用 Expanded 吸收剩余空間不寫死高度只有卡片圓角、圖標(biāo)大小這類視覺常量用固定值。這套約定在后續(xù)適配 OpenHarmony 大屏?xí)r幫了大忙。3. 核心源碼解析歡迎區(qū)域的每個 Widget 到底在干什么這一節(jié)我們直接進(jìn)源碼。我會按“骨架 → 問候頭部 → 信息卡片 → 待辦列表 → 快捷入口”的順序拆把每個 Widget 存在的理由和坑都交代清楚。3.1 頁面骨架為什么最外層是 SafeArea Column先看頁面的總體結(jié)構(gòu)class WelcomeScreen extends StatefulWidget { const WelcomeScreen({super.key, this.userName 新同學(xué)}); final String userName; override StateWelcomeScreen createState() _WelcomeScreenState(); } class _WelcomeScreenState extends StateWelcomeScreen { override Widget build(BuildContext context) { return Scaffold( body: SafeArea( child: Column( children: const [ GreetingHeader(), SemesterCard(), TodayNotice(), QuickActions(), ], ), ), ); } }最外層放 SafeArea 是必須的。OpenHarmony 設(shè)備以及部分 Android 定制終端的屏幕頂部有系統(tǒng)狀態(tài)欄或圓角區(qū)域不做 SafeArea問候文字會被裁切或者頂進(jìn)系統(tǒng)欄里。Column 作為主軸容器讓四個模塊天然保持自上而下的順序。這里沒有用 ListView默認(rèn)歡迎區(qū)域不滾動就避免了嵌套滾動問題。有人會問為什么 WelcomeScreen 是 StatefulWidget 但 build 里什么都沒 setState。這是因為頁面組件的 state 未來要接管數(shù)據(jù)加載和刷新當(dāng)前階段只是把靜態(tài)骨架搭好后面接真實數(shù)據(jù)時不用切換組件類型。這算是一個“為下一步留接口”的寫法不算過度設(shè)計。3.2 問候頭部Stack 疊加漸變與問候語問候頭部是整個頁面視覺的重心也是花心思最多的部分。直接看代碼class GreetingHeader extends StatelessWidget { const GreetingHeader({super.key, required this.userName}); final String userName; String _greetingByHour(int hour) { if (hour 12) return 早上好; if (hour 18) return 下午好; return 晚上好; } override Widget build(BuildContext context) { final hour DateTime.now().hour; final greeting _greetingByHour(hour); return Container( width: double.infinity, padding: const EdgeInsets.fromLTRB(24, 32, 24, 32), decoration: BoxDecoration( gradient: const LinearGradient( colors: [Color(0xFF2B6CB0), Color(0xFF38B2AC)], begin: Alignment.topLeft, end: Alignment.bottomRight, ), borderRadius: const BorderRadius.only( bottomRight: Radius.circular(40), ), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text($greeting$userName, style: const TextStyle( fontSize: 28, fontWeight: FontWeight.w600, color: Colors.white, )), const SizedBox(height: 8), Text(歡迎回到宿舍今天也要元氣滿滿, style: const TextStyle( fontSize: 14, color: Color(0xCCFFFFFF), )), ], ), ); } }這里有幾個細(xì)節(jié)值得講。顏色直接用 Color(0xFF2B6CB0) 而不是 Colors.blue是因為品牌色要全局統(tǒng)一后面主題章節(jié)會再講。問候語按小時分三種雖然邏輯很簡單但確實能提升“這個頁面有在認(rèn)真對待我”的感覺。bottomRight 單邊圓角 40 是有意為之讓頭部和下面的信息卡片之間形成一個自然過渡弧線比直上直下柔和很多。Stack 的運用我稍微提一下實際項目里我們還在這個 Container 里疊了一層帶半透明圓形的裝飾做法是在 Stack 中先畫一個大圓再在上層放文字。如果你也想做類似裝飾建議圓的顏色用 Colors.white 的 5%~10% 透明度的版本視覺效果最干凈不會搶正文信息。3.3 學(xué)期信息卡片Container 圓角陰影與 Row 信息布局接下來是學(xué)期信息卡片。它負(fù)責(zé)把樓棟、宿舍號、輔導(dǎo)員這些高頻信息放在用戶第一眼能看到的位置class SemesterCard extends StatelessWidget { const SemesterCard({super.key, required this.dormInfo}); final DormInfo dormInfo; override Widget build(BuildContext context) { return Container( margin: const EdgeInsets.fromLTRB(16, 16, 16, 0), padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(16), boxShadow: const [ BoxShadow( color: Color(0x1A000000), blurRadius: 12, offset: Offset(0, 4), ), ], ), child: Row( children: [ const Icon(Icons.apartment, color: Color(0xFF2B6CB0)), const SizedBox(width: 12), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(${dormInfo.building} ${dormInfo.room}), const SizedBox(height: 4), Text(輔導(dǎo)員${dormInfo.counselor}, style: const TextStyle( fontSize: 13, color: Color(0xFF666666), )), ], ), ], ), ); } }這里我想多說一句“為什么用 BoxShadow 而不是 elevation”。有些人圖省事直接給 Card 組件加 elevation確實能出陰影但陰影的模糊半徑、偏移量、顏色完全不受控不同系統(tǒng)上的渲染還略有差異。我們用 Container BoxShadow是因為大屏終端的像素密度和手機(jī)不一樣自定義陰影參數(shù)可以保證在兩種設(shè)備上呈現(xiàn)一致的距離感。DormInfo 只是一個極簡數(shù)據(jù)類承載 building、room、counselor 三個字段。注意這里沒有把網(wǎng)絡(luò)請求塞進(jìn)組件UI 組件只負(fù)責(zé)消費外部傳入的數(shù)據(jù)這個邊界在后面接真實接口時非常重要。3.4 今日待辦Expanded 接管剩余空間與空狀態(tài)設(shè)計今日待辦區(qū)域展示了“今天有什么需要處理的事”本質(zhì)上是一個可變高度列表。最穩(wěn)的寫法是讓它在頁面里獲得剩余高度Expanded( child: notices.isEmpty ? const _EmptyNotice() : ListView.separated( padding: const EdgeInsets.symmetric(horizontal: 16), itemCount: notices.length, separatorBuilder: (_, __) const SizedBox(height: 8), itemBuilder: (context, index) { final notice notices[index]; return _NoticeRow(notice: notice); }, ), )這里兩個關(guān)鍵點。第一Expanded 替代固定高度。如果給待辦區(qū)域一個固定高度小屏手機(jī)會溢出大屏平板上又浪費空間Expanded 讓列表自動吃掉卡片下面的剩余高度兩種屏幕都不會出問題。第二空狀態(tài)不是可選項。宿舍管理系統(tǒng)的場景里新生報到完成前可能沒有任何待辦如果直接顯示空白區(qū)域用戶會覺得頁面壞了所以必須有一個 _EmptyNotice 組件用一張溫和的插圖和一句“今天沒有待辦事項”來抵消空白感。_NoticeRow 內(nèi)部我用了行內(nèi)圓點作為時間標(biāo)記左側(cè)一個小圓點代表優(yōu)先級右側(cè)是通知標(biāo)題和發(fā)布時間。這些細(xì)節(jié)看起來瑣碎但正是歡迎區(qū)域“信息層級”的體現(xiàn)高優(yōu)先級任務(wù)通過顏色差異一眼就能識別。3.5 快捷入口GridView 與 ActionCard 的拆解底部四個快捷入口是“報修”“簽到”“查寢表”“繳費”在 UI 上用等寬網(wǎng)格排列GridView.count( crossAxisCount: 4, shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), padding: const EdgeInsets.symmetric(horizontal: 8), mainAxisSpacing: 8, crossAxisSpacing: 8, children: actions .map((action) ActionCard(action: action)) .toList(), )crossAxisCount 用 4是因為四入口的場景在手機(jī)寬度和樓棟終端寬度下都能保持合理的圖標(biāo)尺寸。shrinkWrap: true 是為了讓 GridView 在 Column 里按內(nèi)容高度收縮否則它默認(rèn)會試圖占滿無限高度直接導(dǎo)致布局報錯配合 NeverScrollableScrollPhysics徹底關(guān)閉網(wǎng)格自身的滾動能力避免和上層滾動容器沖突。每個入口單獨抽成 ActionCard 組件內(nèi)部就是一個 Icon Text 的縱向組合外加一個輕量水波紋反饋。抽組件的意義在于后續(xù)如果要給某個入口加“小紅點角標(biāo)”或“禁用態(tài)”只需要在 ActionCard 內(nèi)部做條件渲染不會影響頁面其他代碼。4. 讓歡迎區(qū)域“活”起來數(shù)據(jù)、狀態(tài)與刷新時機(jī)靜態(tài)界面跑通只是第一步。歡迎區(qū)域真正的挑戰(zhàn)是把用戶名、宿舍信息、今日待辦這些數(shù)據(jù)正確地填進(jìn)來并且在合適的時間刷新。這里我采用了“ViewModel Repository”的分層思路而不是直接在組件里寫網(wǎng)絡(luò)請求。4.1 數(shù)據(jù)邊界UI 組件不碰網(wǎng)絡(luò)在這個項目里歡迎區(qū)域涉及三類數(shù)據(jù)用戶資料、學(xué)期配置、今日待辦。它們分別來自本地緩存和遠(yuǎn)端接口。為了讓 UI 層保持干凈我定義了一個歡迎數(shù)據(jù)聚合接口由 Repository 層把三個數(shù)據(jù)源合并成一個 WelcomeData 返回。UI 組件只需要知道 WelcomeData 里有 userName、dormInfo、notices、actions 這幾個字段完全不關(guān)心它背后是緩存還是 HTTP。這樣設(shè)計最大的好處是后續(xù)接口調(diào)整時UI 代碼可以做到零改動。比如原來輔導(dǎo)員信息是從用戶資料接口返回的后來改成學(xué)期配置接口返回只需要改 Repository 的聚合邏輯頁面組件不用動。4.2 狀態(tài)管理為什么用一個簡單的 ChangeNotifier歡迎區(qū)域的狀態(tài)其實不復(fù)雜加載中、加載失敗、加載成功。用 setState 也能寫但一旦涉及“回到前臺刷新”“下拉重試”“跨頁面更新待辦數(shù)量”這些場景setState 會讓組件越來越臃腫。我最后選擇的是一個輕量 ViewModelclass WelcomeViewModel extends ChangeNotifier { WelcomeViewModel(this._repository); final WelcomeRepository _repository; WelcomeData? _data; bool _loading true; String? _error; Futurevoid load() async { _loading true; _error null; notifyListeners(); try { _data await _repository.fetchWelcomeData(); } catch (e) { _error 加載失敗; } finally { _loading false; notifyListeners(); } } }選擇 ChangeNotifier 而不是直接上重狀態(tài)管理框架是因為歡迎區(qū)域是頁面級局部狀態(tài)不值得引入全局 Store。真正進(jìn)入業(yè)務(wù)列表、跨頁面共享登錄態(tài)之后再考慮全局狀態(tài)管理方案也不遲。用 notifier 的好處是加載邏輯和 UI 渲染完全解耦后面加緩存、加重試機(jī)制都很自然。4.3 三態(tài) UI骨架屏而不是轉(zhuǎn)圈基于 ViewModel 的狀態(tài)頁面渲染邏輯分成三態(tài)if (viewModel.loading) { return const WelcomeSkeleton(); } if (viewModel.error ! null) { return ErrorRetry(onRetry: viewModel.load); } return WelcomeContent(data: viewModel.data!);這里有個容易踩坑的點加載中狀態(tài)不要用全屏轉(zhuǎn)圈。歡迎區(qū)域是頁面首屏如果每次進(jìn)入都先轉(zhuǎn)圈再出內(nèi)容視覺跳動感特別強(qiáng)。我們用了一塊和最終布局等價的灰色骨架屏用戶會覺得頁面在正常加載體驗平滑很多。骨架屏的做法很簡單就是幾塊淡灰色圓角矩形占位不需要額外引入組件庫。錯誤狀態(tài)也考慮了很多。自助終端如果網(wǎng)絡(luò)斷了不應(yīng)該白屏或者無限轉(zhuǎn)圈而是在頁面中央顯示“加載失敗”和重試按鈕。因為我們面向的是宿舍管理場景有時樓棟終端網(wǎng)絡(luò)不穩(wěn)定重試按鈕遠(yuǎn)比自動輪詢靠譜。4.4 刷新時機(jī)回到前臺和跨天切換歡迎區(qū)域有兩個刷新時機(jī)容易被忽略App 從后臺回到前臺時待辦狀態(tài)可能已經(jīng)變化需要重新拉取日期跨天后問候語應(yīng)從“晚上好”變成“早上好”。第二個場景如果只靠 build 里的 DateTime.now() 判斷不會自動刷新必須借助生命周期監(jiān)聽。我用了 WidgetsBindingObserver 監(jiān)聽 AppLifecycleState.resumed在每次回到前臺時對比一下上次訪問的日期如果日期變了或者遠(yuǎn)程數(shù)據(jù)標(biāo)記變了就重新調(diào)一次 load()。這套邏輯不復(fù)雜但能把“頁面看著像是活的”這個體驗做好。順帶說一句歡迎區(qū)域的問候語盡量不要用每分鐘定時器去刷新太消耗資源按“前臺恢復(fù) 日期變化”兩個觸發(fā)點刷新就夠了。5. OpenHarmony 平臺上碰到的幾個真實適配問題跑通跨端之后真正的難點開始在適配層浮出水面。這一節(jié)是實測過程中踩到的坑每個都值得記錄。5.1 編譯與運行版本對齊是第一道關(guān)OpenHarmony 上的 Flutter 不能直接用最新版 Flutter SDK 編譯。我們一開始圖省事本地 Flutter 版本比適配工程要求的版本新了一個小版本結(jié)果產(chǎn)物安裝到設(shè)備后界面能出但點擊事件偶爾失效。排查了一圈才發(fā)現(xiàn)是適配層和 Flutter 引擎版本不匹配。解決方式很樸實把 Flutter SDK 固定到適配工程文檔指定的版本重新創(chuàng)建項目再移植頁面代碼。另外OpenHarmony 設(shè)備的運行鏈路也比普通手機(jī)多幾步包括設(shè)備進(jìn)入開發(fā)者模式、配置調(diào)試證書、通過工具安裝產(chǎn)物。建議第一次跑通時最好準(zhǔn)備一臺固定的測試設(shè)備把所有環(huán)節(jié)完整走一遍之后每次迭代就順暢了。5.2 尺寸與 dpiMediaQuery 不是萬能的樓棟自助終端的屏幕比例和手機(jī)差很多有的是 4:3有的接近 16:10而且系統(tǒng) dpi 設(shè)置各不相同。直接用 MediaQuery.of(context).size 去算布局寬度在某些設(shè)備上會出現(xiàn)卡片被拉伸或者間距失真。我的建議是涉及整頁布局時用 LayoutBuilder 拿父容器約束而不是全局屏幕尺寸。比如快捷入口的網(wǎng)格間距用 maxWidth 的 2% 計算比固定 12 更穩(wěn)。另外歡迎區(qū)域的漸變背景在大屏上容易出現(xiàn)“豎屏漸變”的生硬感建議把漸變方向固定為對角線視覺上能掩蓋不少比例差異。5.3 中文字體打包字體遠(yuǎn)比依賴系統(tǒng)字體可靠部分 OpenHarmony 定制設(shè)備的中文字體不完整Text 會出現(xiàn)缺字、方框或者回退異常。尤其像“你”“好”這類常用字一般沒問題但一些生僻字或特定符號就可能掛掉。我們最后統(tǒng)一走的是“打包字體”路線把一款開源中文字體文件放進(jìn) assets/fonts在 MaterialApp 的 theme 里配置 fontFamily確保所有設(shè)備用同一套字體渲染。這里有一個性能提示全系統(tǒng)全局替換字體后首幀渲染會稍微變慢但歡迎區(qū)域本身就是首屏這個 trade-off 是值得的。如果你只在歡迎區(qū)域需要特殊字體也可以只在該區(qū)域單獨指定 TextStyle(fontFamily: ...)減小影響面。5.4 系統(tǒng)能力調(diào)用先確認(rèn)插件在 OpenHarmony 端可用歡迎區(qū)域的快捷入口里我們原本想做“一鍵報修”帶定位功能但在適配層檢查后發(fā)現(xiàn)對應(yīng)的定位插件在 OpenHarmony 端還沒有完整實現(xiàn)。強(qiáng)行調(diào)用會導(dǎo)致崩潰或靜默失敗。我的建議是凡是涉及相機(jī)、定位、藍(lán)牙這類系統(tǒng)能力的插件在排期階段就要逐個驗證 OpenHarmony 端的可用性。如果插件不支持就把入口設(shè)計成純前端跳轉(zhuǎn) 輔助表單模式先保證功能鏈路完整等適配插件到位后再替換成原生能力調(diào)用。寧可第一步樸素一點也不要在一個跑不通的坎上卡住整個頁面。5.5 性能與動畫大屏設(shè)備上的重繪控制歡迎區(qū)域雖然控件不多但漸變背景、卡片陰影、列表滾動這些元素疊加起來在低配置大屏設(shè)備上容易出現(xiàn)幀率波動。我做了三件事第一所有靜態(tài)組件盡可能標(biāo)注 const 構(gòu)造減少 build 階段的對象創(chuàng)建第二用 RepaintBoundary 把漸變頭部包一層避免列表滾動時整個頭部跟著重繪第三減少 setState 的粒度數(shù)據(jù)刷新只更新對應(yīng)的內(nèi)容區(qū)域而不是整頁重建。水波紋反饋在部分 OpenHarmony 設(shè)備上也有些不一致Material 默認(rèn)的 InkWell 在某些定制設(shè)備上幾乎看不到反饋。后來我在 ActionCard 里加了一層輕量的透明度動畫點擊時圖標(biāo)短暫變淡再恢復(fù)體驗就統(tǒng)一了。這種小細(xì)節(jié)往往是實際跑設(shè)備才能發(fā)現(xiàn)的問題。最后聊聊我的整體感受。這套歡迎區(qū)域真正花時間的不是寫 Text、Container、GridView 這些控件而是前期把組件邊界、數(shù)據(jù)邊界、平臺差異想清楚。我在項目里犯過的最大錯誤是把待辦列表數(shù)據(jù)和歡迎卡片狀態(tài)耦合在一個 State 里結(jié)果每次通知刷新都帶著整頁重繪拆開之后大屏設(shè)備上的幀率明顯穩(wěn)了。如果你也在做類似系統(tǒng)建議從第一天就把 ViewModel 和 Repository 的殼搭好即使現(xiàn)在往里面填的都是靜態(tài)數(shù)據(jù)。后面再接真實接口、加新入口時你就會發(fā)現(xiàn)這些前期約束幫你省下的時間遠(yuǎn)超想象。