Flutter技术与实战(4)
Flutter基礎
文章目錄
- Flutter基礎
- Widget,構建Flutter界面的基石。
- Widget渲染過程
- Widget
- Element
- RenderObject
- RenderObjectWidget 介紹
- 案例展示
- Widget中的State到底是什么
- UI編程范式
- StateLessWidget
- StatefulWidget
- StatefulWidget 不是萬金油,要慎用
- 生命周期
- State生命周期
- 創建
- 更新
- 銷毀
- App生命周期
- 生命周期回調
- 幀繪制回調
- 經典控件(一):文本、圖片和按鈕
- 文本控件
- 圖片
- 按鈕
- 經典控件(二):ListView與CustomScrollView
- ListView
- CustomScrollView
- ScrollController與ScrollNotification
- ScrollController
- ScrollNotification
- 問題
- 經典布局:如何定義子控件在父容器中的排版位置
- 單子Widget布局:Container、Padding與Center
- 多子Widget布局:Row、Column與Expanded
- 層疊Widget布局:Stack與Positioned
- 組合與自繪,何種方式定義Widget
- 組裝
- 自繪
- 從夜間模式說起,定制不同的App主題
- 主題定制
- 全局統一的視覺風格定制
- 局部獨立的視覺風格定制
- 分平臺主題定制
- 依賴管理(一):圖片、配置和字體
- 資源管理
- 原生平臺的資源設置
- 更換App圖標
- 更換啟動圖
- 依賴管理(二):第三方組件庫在FLutter如何管理
- Pub
- 舉例
- 問題
- 用戶交互事件如何響應
- 指針事件
- 手勢識別
- 手勢競技場實現
- 跨組件傳遞數據
- InheritedWidget
- Notification
- EventBus
- 路由與導航實現頁面切換
- 路由管理
- 基本路由
- 命名路由
- 頁面參數
- 補充
Widget,構建Flutter界面的基石。
- Widget 是 Flutter 功能的抽象描述,是視圖的配置信息,同樣也是數據的映射,是 Flutter 開發框架中最基本的概念。前端框架中常見的名詞,比如視圖(View)、視圖控制器(View Controller)、活動(Activity)、應用(Application)、布局(Layout)等,在 Flutter 中都是 Widget。
Widget渲染過程
- 通常情況下,不同的 UI 框架中會以不同的方式去處理這一問題,但無一例外地都會用到視圖樹(View Tree)的概念。而 Flutter 將視圖樹的概念進行了擴展,把視圖數據的組織和渲染抽象為三部分,即 Widget,Element 和 RenderObject。
Widget
- Widget 是 Flutter 世界里對視圖的一種結構化描述,你可以把它看作是前端中的“控件”或“組件”。Widget 是控件實現的基本邏輯單位,里面存儲的是有關視圖渲染的配置信息,包括布局、渲染屬性、事件響應信息等。
- Flutter 將 Widget 設計成不可變的,所以當視圖渲染的配置信息發生變化時,Flutter 會選擇重建 Widget 樹的方式進行數據更新,以數據驅動 UI 構建的方式簡單高效。
- 但,這樣做的缺點是,因為涉及到大量對象的銷毀和重建,所以會對垃圾回收造成壓力。不過,Widget 本身并不涉及實際渲染位圖,所以它只是一份輕量級的數據結構,重建的成本很低。
- 另外,由于 Widget 的不可變性,可以以較低成本進行渲染節點復用,因此在一個真實的渲染樹中可能存在不同的 Widget 對應同一個渲染節點的情況,這無疑又降低了重建 UI 的成本。
Element
- Element 是 Widget 的一個實例化對象,它承載了視圖構建的上下文數據,是連接結構化的配置信息到完成最終渲染的橋梁。
- Flutter 渲染過程,可以分為這么三步:
- 首先,通過 Widget 樹生成對應的 Element 樹;
- 然后,創建相應的 RenderObject 并關聯到 Element.renderObject 屬性上;
- 最后,構建成 RenderObject 樹,以完成最終的渲染。
- Element 同時持有 Widget 和 RenderObject。而無論是 Widget 還是 Element,其實都不負責最后的渲染,只負責發號施令,真正去干活兒的只有 RenderObject。
- 增加中間的這層 Element 樹,不直接由 Widget 命令 RenderObject,這樣做會極大地減少渲染帶來的性能損耗。
- Element 樹存在的意義。因為 Widget 具有不可變性,但 Element 卻是可變的。實際上,Element 樹這一層將 Widget 樹的變化(類似 React 虛擬 DOM diff)做了抽象,可以只將真正需要修改的部分同步到真實的 RenderObject 樹中,最大程度降低對真實渲染視圖的修改,提高渲染效率,而不是銷毀整個渲染視圖樹重建。
RenderObject
- RenderObject 是主要負責實現視圖渲染的對象。
- Flutter 通過控件樹(Widget 樹)中的每個控件(Widget)創建不同類型的渲染對象,組成渲染對象樹。
- 而渲染對象樹在 Flutter 的展示過程分為四個階段,即布局、繪制、合成和渲染。 其中,布局和繪制在 RenderObject 中完成,Flutter 采用深度優先機制遍歷渲染對象樹,確定樹中各個對象的位置和尺寸,并把它們繪制到不同的圖層上。繪制完畢后,合成和渲染的工作則交給 Skia 搞定。
- Flutter 通過引入 Widget、Element 與 RenderObject 這三個概念,把原本從視圖數據到視圖渲染的復雜構建過程拆分得更簡單、直接,在易于集中治理的同時,保證了較高的渲染效率。
RenderObjectWidget 介紹
- StatelessWidget 和 StatefulWidget 只是用來組裝控件的容器,并不負責組件最后的布局和繪制。在 Flutter 中,布局和繪制工作實際上是在 Widget 的另一個子類 RenderObjectWidget 內完成的。
- 實際上,RenderObjectWidget 本身并不負責這些對象的創建與更新。
- 對于 Element 的創建,Flutter 會在遍歷 Widget 樹時,調用 createElement 去同步 Widget 自身配置,從而生成對應節點的 Element 對象。而對于 RenderObject 的創建與更新,其實是在 RenderObjectElement 類中完成的。
- 在 Element 創建完畢后,Flutter 會調用 Element 的 mount 方法。在這個方法里,會完成與之關聯的 RenderObject 對象的創建,以及與渲染樹的插入工作,插入到渲染樹后的 Element 就可以顯示到屏幕中了。
- 如果 Widget 的配置數據發生了改變,那么持有該 Widget 的 Element 節點也會被標記為 dirty。在下一個周期的繪制時,Flutter 就會觸發 Element 樹的更新,并使用最新的 Widget 數據更新自身以及關聯的 RenderObject 對象,接下來便會進入 Layout 和 Paint 的流程。而真正的繪制和布局過程,則完全交由 RenderObject 完成。
- 布局和繪制完成后,接下來的事情就交給 Skia 了。在 VSync 信號同步時直接從渲染樹合成 Bitmap,然后提交給 GPU。
案例展示
- 在 Flutter 遍歷完 Widget 樹,創建了各個子 Widget 對應的 Element 的同時,也創建了與之關聯的、負責實際布局和繪制的 RenderObject。
Widget中的State到底是什么
- StatefulWidget 應對有交互、需要動態變化視覺效果的場景,而 StatelessWidget 則用于處理靜態的、無狀態的視圖展示。
UI編程范式
- 要想理解 StatelessWidget 與 StatefulWidget 的使用場景,我們首先需要了解,在 Flutter 中,如何調整一個控件(Widget)的展示樣式,即 UI 編程范式。
- 對于Android、IOS或原生JavaScript開發者來說,視圖開發是命令式的,需要精確地告訴操作系統或瀏覽器用何種方式去做事情。比如,如果我們想要變更界面的某個文案,則需要找到具體的文本控件并調用它的控件方法命令,才能完成文字變更。
- Flutter 的視圖開發是聲明式的,其核心設計思想就是將視圖和數據分離,這與 React 的設計思路完全一致。
- 總結來說,命令式編程強調精確控制過程細節;而聲明式編程強調通過意圖輸出結果整體。
- 對應到 Flutter 中,意圖是綁定了組件狀態的 State,結果則是重新渲染后的組件。在 Widget 的生命周期內,應用到 State 中的任何更改都將強制 Widget 重新構建。
- 當你所要構建的用戶界面不隨任何狀態信息的變化而變化時,需要選擇使用 StatelessWidget,反之則選用 StatefulWidget。前者一般用于靜態內容的展示,而后者則用于存在交互反饋的內容呈現中。
StateLessWidget
-
在 Flutter 中,Widget 采用由父到子、自頂向下的方式進行構建,父 Widget 控制著子 Widget 的顯示樣式,其樣式配置由父 Widget 在構建時提供。
-
用這種方式構建出的 Widget,有些(比如 Text、Container、Row、Column 等)在創建時,除了這些配置參數之外不依賴于任何其他信息,換句話說,它們一旦創建成功就不再關心、也不響應任何數據變化進行重繪。在 Flutter 中,這樣的 Widget 被稱為 StatelessWidget(無狀態組件)。
-
以 Text 的部分源碼為例,說明 StatelessWidget 的構建過程。
- 什么場景下應該使用 StatelessWidget ?父 Widget 是否能通過初始化參數完全控制其 UI 展示效果?如果能,那么我們就可以使用 StatelessWidget 來設計構造函數接口了。
StatefulWidget
- 與 StatelessWidget 相對應的,有一些 Widget(比如 Image、Checkbox)的展示,除了父 Widget 初始化時傳入的靜態配置之外,還需要處理用戶的交互(比如,用戶點擊按鈕)或其內部數據的變化(比如,網絡數據回包),并體現在 UI 上。
- 換句話說,這些 Widget 創建完成后,還需要關心和響應數據變化來進行重繪。在 Flutter 中,這一類 Widget 被稱為 StatefulWidget(有狀態組件)。
- StatefulWidget 是以 State 類代理 Widget 構建的設計方式實現的。接下來,以 Image 的部分源碼為例,說明 StatefulWidget 的構建過程。
- Image 以一種動態的方式運行:監聽變化,更新視圖。與 StatelessWidget 通過父 Widget 完全控制 UI 展示不同,StatefulWidget 的父 Widget 僅定義了它的初始化狀態,而其自身視圖運行的狀態則需要自己處理,并根據處理情況即時更新 UI 展示。
StatefulWidget 不是萬金油,要慎用
- 對于 UI 框架而言,同樣的展示效果一般可以通過多種控件實現。從定義來看,StatefulWidget 仿佛是萬能的,替代 StatelessWidget 看起來合情合理。于是 StatefulWidget 的濫用,也容易因此變得順理成章,難以避免。但事實是,StatefulWidget 的濫用會直接影響 Flutter 應用的渲染性能。
Widget 是不可變的,更新則意味著銷毀 + 重建(build)。StatelessWidget 是靜態的,一旦創建則無需更新;而對于 StatefulWidget 來說,在 State 類中調用 setState 方法更新數據,會觸發視圖的銷毀和重建,也將間接地觸發其每個子 Widget 的銷毀和重建。
- 如果我們的根布局是一個 StatefulWidget,在其 State 中每調用一次更新 UI,都將是一整個頁面所有 Widget 的銷毀和重建。
- 正確評估你的視圖展示需求,避免無謂的 StatefulWidget 使用,是提高 Flutter 應用渲染性能最簡單也是最直接的手段。
需要注意的是,除了我們主動地通過 State 刷新 UI 之外,在一些特殊場景下,Widget 的 build 方法有可能會執行多次。因此,我們不應該在這個方法內部,放置太多有耗時的操作。
反思:build執行多次,通過接口獲取表單數據,不要在build里寫耗時方法,外部處理傳入一個變量即可!
生命周期
- 從 Widget(的 State)和 App 這兩個維度,介紹它們的生命周期。
State生命周期
- State 的生命周期,指的是在用戶參與的情況下,其關聯的 Widget 所經歷的,從創建到顯示再到更新最后到停止,直至銷毀等各個過程階段。
- 這些不同的階段涉及到特定的任務處理,因此為了寫出一個體驗和性能良好的控件,正確理解 State 的生命周期至關重要。
- State 的生命周期可以分為 3 個階段:創建(插入視圖樹)、更新(在視圖樹中存在)、銷毀(從視圖樹中移除)。接下來,我們一起看看每一個階段的具體流程。
創建
- State 初始化時會依次執行 :構造方法 -> initState -> didChangeDependencies -> build,隨后完成頁面渲染??匆幌鲁跏蓟^程中每個方法的意義。
- 構造方法是 State 生命周期的起點,Flutter 會通過調用 StatefulWidget.createState() 來創建一個 State。我們可以通過構造方法,來接收父 Widget 傳遞的初始化 UI 配置數據。這些配置數據,決定了 Widget 最初的呈現效果。
- initState,會在 State 對象被插入視圖樹的時候調用。這個函數在 State 的生命周期中只會被調用一次,所以我們可以在這里做一些初始化工作,比如為狀態變量設定默認值。
- didChangeDependencies 則用來專門處理 State 對象依賴關系變化,會在 initState() 調用結束后,被 Flutter 調用。
- build,作用是構建視圖。經過以上步驟,Framework 認為 State 已經準備好了,于是調用 build。我們需要在這個函數中,根據父 Widget 傳遞過來的初始化配置數據,以及 State 的當前狀態,創建一個 Widget 然后返回。
更新
- Widget 的狀態更新,主要由 3 個方法觸發:setState、didchangeDependencies 與 didUpdateWidget。這三個方法分別會在什么場景下調用。
- setState:我們最熟悉的方法之一。當狀態數據發生變化時,我們總是通過調用這個方法告訴 Flutter:“我這兒的數據變啦,請使用更新后的數據重建 UI!”
- didChangeDependencies:State 對象的依賴關系發生變化后,Flutter 會回調這個方法,隨后觸發組件構建。哪些情況下 State 對象的依賴關系會發生變化呢?典型的場景是,系統語言 Locale 或應用主題改變時,系統會通知 State 執行 didChangeDependencies 回調方法。
- didUpdateWidget:當 Widget 的配置發生變化時,比如,父 Widget 觸發重建(即父 Widget 的狀態發生變化時),熱重載時,系統會調用這個函數。
- 一旦這三個方法被調用,Flutter 隨后就會銷毀老 Widget,并調用 build 方法重建 Widget。
銷毀
-
組件銷毀相對比較簡單。比如組件被移除,或是頁面銷毀的時候,系統會調用 deactivate 和 dispose 這兩個方法,來移除或銷毀組件。
- 當組件的可見狀態發生變化時,deactivate 函數會被調用,這時 State 會被暫時從視圖樹中移除。值得注意的是,頁面切換時,由于 State 對象在視圖樹中的位置發生了變化,需要先暫時移除后再重新添加,重新觸發組件構建,因此這個函數也會被調用。
- 當 State 被永久地從視圖樹中移除時,Flutter 會調用 dispose 函數。而一旦到這個階段,組件就要被銷毀了,所以我們可以在這里進行最終的資源釋放、移除監聽、清理環境,等等。
-
左邊部分展示了當父 Widget 狀態發生變化時,父子雙方共同的生命周期;而中間和右邊部分則描述了頁面切換時,兩個關聯的 Widget 的生命周期函數是如何響應的。State生命周期中的方法調用對比如圖。
App生命周期
- 視圖的生命周期,定義了視圖的加載到構建的全過程,其回調機制能夠確保我們可以根據視圖的狀態選擇合適的時機做恰當的事情。而 App 的生命周期,則定義了 App 從啟動到退出的全過程。
- 在原生 Android、iOS 開發中,有時我們需要在對應的 App 生命周期事件中做相應處理,比如 App 從后臺進入前臺、從前臺退到后臺,或是在 UI 繪制完成后做一些處理。在 Flutter 中,我們可以利用 WidgetsBindingObserver 類,來實現同樣的需求。
- App 生命周期的回調 didChangeAppLifecycleState,和幀繪制回調 addPostFrameCallback 與 addPersistentFrameCallback。
生命周期回調
-
didChangeAppLifecycleState 回調函數中,有一個參數類型為 AppLifecycleState 的枚舉類,這個枚舉類是 Flutter 對 App 生命周期狀態的封裝。它的常用狀態包括 resumed、inactive、paused 這三個。
- resumed:可見的,并能響應用戶的輸入。
- inactive:處在不活動狀態,無法處理用戶響應。
- paused:不可見并不能響應用戶的輸入,但是在后臺繼續活動中。
-
試著切換一下前、后臺,觀察控制臺輸出的 App 狀態,可以發現:
- 從后臺切入前臺,控制臺打印的 App 生命周期變化如下: AppLifecycleState.paused->AppLifecycleState.inactive->AppLifecycleState.resumed;
- 從前臺退回后臺,控制臺打印的 App 生命周期變化則變成了:AppLifecycleState.resumed->AppLifecycleState.inactive->AppLifecycleState.paused。
-
可以看到,App 前后臺切換過程中打印出的狀態是完全符合預期的。
幀繪制回調
-
除了需要監聽 App 的生命周期回調做相應的處理之外,有時候我們還需要在組件渲染之后做一些與顯示安全相關的操作。在 Android 開發中,我們可以通過 View.post() 插入消息隊列,來保證在組件渲染后進行相關操作。
-
在 Flutter 中實現同樣的需求會更簡單:依然使用萬能的 WidgetsBinding 來實現。
-
WidgetsBinding 提供了單次 Frame 繪制回調,以及實時 Frame 繪制回調兩種機制,來分別滿足不同的需求。
- 單次 Frame 繪制回調,通過 addPostFrameCallback 實現。它會在當前 Frame 繪制完成后進行進行回調,并且只會回調一次,如果要再次監聽則需要再設置一次。
- 實時 Frame 繪制回調,則通過 addPersistentFrameCallback 實現。這個函數會在每次繪制 Frame 結束后進行回調,可以用做 FPS 監測。
經典控件(一):文本、圖片和按鈕
文本控件
- 文本是視圖系統中的常見控件,用來顯示一段特定樣式的字符串,就比如 Android 里的 TextView、iOS 中的 UILabel。而在 Flutter 中,文本展示是通過 Text 控件實現的。
- Text 支持兩種類型的文本展示,一個是默認的展示單一樣式的文本 Text,另一個是支持多種混合樣式的富文本 Text.rich。
- 單一樣式文本 Text 的初始化,是要傳入需要展示的字符串。而這個字符串的具體展示效果,受構造函數中的其他參數控制。這些參數大致可以分為兩類:
- 控制整體文本布局的參數,如文本對齊方式 textAlign、文本排版方向 textDirection,文本顯示最大行數 maxLines、文本截斷規則 overflow 等等,這些都是構造函數中的參數;
- 控制文本展示樣式的參數,如字體名稱 fontFamily、字體大小 fontSize、文本顏色 color、文本陰影 shadows 等等,這些參數被統一封裝到了構造函數中的參數 style 中。
- 混合展示樣式與單一樣式的關鍵區別在于分片,即如何把一段字符串分為幾個片段來管理,給每個片段單獨設置樣式。面對這樣的需求,在 Android 中,我們使用 SpannableString 來實現;在 iOS 中,我們使用 NSAttributedString 來實現;而在 Flutter 中也有類似的概念,即 TextSpan。
- TextSpan 定義了一個字符串片段該如何控制其展示樣式,而將這些有著獨立展示樣式的字符串組裝在一起,則可以支持混合樣式的富文本展示。
圖片
- 使用 Image,可以讓我們向用戶展示一張圖片。圖片的顯示方式有很多,比如資源圖片、網絡圖片、文件圖片等,圖片格式也各不相同,因此在 Flutter 中也有多種方式,用來加載不同形式、支持不同格式的圖片。
- 加載本地資源圖片,如 Image.asset(‘images/logo.png’);
- 加載本地(File 文件)圖片,如 Image.file(new File(’/storage/xxx/xxx/test.jpg’));
- 加載網絡圖片,如 Image.network(‘http://xxx/xxx/test.gif’) 。
- 除了可以根據圖片的顯示方式設置不同的圖片源之外,圖片的構造方法還提供了填充模式 fit、拉伸模式 centerSlice、重復模式 repeat 等屬性,可以針對圖片與目標區域的寬高比差異制定排版模式。
- Flutter 中的 FadeInImage 控件。在加載網絡圖片的時候,為了提升用戶的等待體驗,我們往往會加入占位圖、加載動畫等元素,但是默認的 Image.network 構造方法并不支持這些高級功能,這時候 FadeInImage 控件就派上用場了。
- FadeInImage 控件提供了圖片占位的功能,并且支持在圖片加載完成時淡入淡出的視覺效果。此外,由于 Image 支持 gif 格式,我們甚至還可以將一些炫酷的加載動畫作為占位圖。
- Image 控件需要根據圖片資源異步加載的情況,決定自身的顯示效果,因此是一個 StatefulWidget。圖片加載過程由 ImageProvider 觸發,而 ImageProvider 表示異步獲取圖片數據的操作,可以從資源、文件和網絡等不同的渠道獲取圖片。
- 首先,ImageProvider 根據 _ImageState 中傳遞的圖片配置生成對應的圖片緩存 key;然后,去 ImageCache 中查找是否有對應的圖片緩存,如果有,則通知 _ImageState 刷新 UI;如果沒有,則啟動 ImageStream 開始異步加載,加載完畢后,更新緩存;最后,通知 _ImageState 刷新 UI。
-
ImageCache 使用 LRU(Least Recently Used,最近最少使用)算法進行緩存更新策略,并且默認最多存儲 1000 張圖片,最大緩存限制為 100MB,當限定的空間已存滿數據時,把最久沒有被訪問到的圖片清除。圖片緩存只會在運行期間生效,也就是只緩存在內存中。如果想要支持緩存到文件系統,可以使用第三方的CachedNetworkImage控件。
-
CachedNetworkImage 的使用方法與 Image 類似,除了支持圖片緩存外,還提供了比 FadeInImage 更為強大的加載過程占位與加載錯誤占位,可以支持比用圖片占位更靈活的自定義控件占位。
按鈕
- 通過按鈕,我們可以響應用戶的交互事件。Flutter 提供了三個基本的按鈕控件,即 FloatingActionButton、FlatButton 和 RaisedButton。
- FloatingActionButton:一個圓形的按鈕,一般出現在屏幕內容的前面,用來處理界面中最常用、最基礎的用戶動作。
- RaisedButton:凸起的按鈕,默認帶有灰色背景,被點擊后灰色背景會加深。
- FlatButton:扁平化的按鈕,默認透明背景,被點擊后會呈現灰色背景。
- 這三個按鈕控件的使用方法類似,唯一的區別只是默認樣式不同而已。
- 既然是按鈕,因此除了控制基本樣式之外,還需要響應用戶點擊行為。這就對應著按鈕控件中的兩個最重要的參數了:
- onPressed 參數用于設置點擊回調,告訴 Flutter 在按鈕被點擊時通知我們。如果 onPressed 參數為空,則按鈕會處于禁用狀態,不響應用戶點擊。
- child 參數用于設置按鈕的內容,告訴 Flutter 控件應該長成什么樣,也就是控制著按鈕控件的基本樣式。child 可以接收任意的 Widget,比如我們在上面的例子中傳入的 Text,除此之外我們還可以傳入 Image 等控件。
- 通常情況下,我們還是會進行控件樣式定制。與 Text 控件類似,按鈕控件也提供了豐富的樣式定制功能,比如背景顏色 color、按鈕形狀 shape、主題顏色 colorBrightness,等等。
經典控件(二):ListView與CustomScrollView
- 當元素的排列布局超過屏幕顯示尺寸(即超過一屏)時,我們就需要引入列表控件來展示視圖的完整內容,并根據元素的多少進行自適應滾動展示。
- 在 Android 中是由 ListView 或 RecyclerView 實現的,在 iOS 中是用 UITableView 實現的;而在 Flutter 中,實現這種需求的則是列表控件 ListView。
ListView
- 在 Flutter 中,ListView 可以沿一個方向(垂直或水平方向)來排列其所有子 Widget,因此常被用于需要展示一組連續視圖元素的場景,比如通信錄、優惠券、商家列表等。
- ListView 提供了一個默認構造函數 ListView,我們可以通過設置它的 children 參數,很方便地將所有的子 Widget 包含到 ListView 中。
- 不過,這種創建方式要求提前將所有子 Widget 一次性創建好,而不是等到它們真正在屏幕上需要顯示時才創建,所以有一個很明顯的缺點,就是性能不好。因此,這種方式僅適用于列表中含有少量元素的場景。
備注:ListTile 是 Flutter 提供的用于快速構建列表項元素的一個小組件單元,用于 1~3 行(leading、title、subtitle)展示文本、圖標等視圖元素的場景,通常與 ListView 配合使用。上面這段代碼中用到 ListTile,是為了演示 ListView 的能力。
- 除了默認的垂直方向布局外,ListView 還可以通過設置 scrollDirection 參數支持水平方向布局。如下所示,我定義了一組不同顏色背景的組件,將它們的寬度設置為 140,并包在了水平布局的 ListView 中,讓它們可以橫向滾動。
-
考慮到創建子 Widget 產生的性能問題,更好的方法是抽象出創建子 Widget 的方法,交由 ListView 統一管理,在真正需要展示該子 Widget 時再去創建。
-
ListView 的另一個構造函數 ListView.builder,則適用于子 Widget 比較多的場景。這個構造函數有兩個關鍵參數:
- itemBuilder,是列表項的創建方法。當列表滾動到相應位置時,ListView 會調用該方法創建對應的子 Widget。
- itemCount,表示列表項的數量,如果為空,則表示 ListView 為無限列表。
- itemExtent 并不是一個必填參數。但對于定高的列表項元素,建議提前設置好這個參數的值。
- 但如果提前設置好 itemExtent,ListView 則可以提前計算好每一個列表項元素的相對位置,以及自身的視圖高度,省去了無謂的計算。
- 因此,在 ListView 中,指定 itemExtent 比讓子 Widget 自己決定自身高度會更高效。
- 在 ListView 中,有兩種方式支持分割線:
- 一種是,在 itemBuilder 中,根據 index 的值動態創建分割線,也就是將分割線視為列表項的一部分;
- 另一種是,使用 ListView 的另一個構造方法 ListView.separated,單獨設置分割線的樣式。
- ListView常見的構造方法及其適用場景。
CustomScrollView
- 對于某些特殊交互場景,比如多個效果聯動、嵌套滾動、精細滑動、視圖跟隨手勢操作等,還需要嵌套多個 ListView 來實現。這時,各自視圖的滾動和布局模型就是相互獨立、分離的,就很難保證整個頁面統一一致的滑動效果。
- Flutter 是如何解決多 ListView 嵌套時,頁面滑動效果不一致的問題的呢?在 Flutter 中有一個專門的控件 CustomScrollView,用來處理多個需要自定義滾動效果的 Widget。在 CustomScrollView 中,這些彼此獨立的、可滾動的 Widget 被統稱為 Sliver。
- 視差滾動是指讓多層背景以不同的速度移動,在形成立體滾動效果的同時,還能保證良好的視覺體驗。 作為移動應用交互設計的熱點趨勢,越來越多的移動應用使用了這項技術。
- 實現效果類似于可折疊式標題欄。例如QQ好友動態頭部效果!
ScrollController與ScrollNotification
ScrollController
- 在某些情況下,我們希望獲取視圖的滾動信息,并進行相應的控制。比如,列表是否已經滑到底(頂)了?如何快速回到列表頂部?列表滾動是否已經開始,或者是否已經停下來了?對于前兩個問題,我們可以使用 ScrollController 進行滾動信息的監聽,以及相應的滾動控制;而最后一個問題,則需要接收 ScrollNotification 通知進行滾動事件的獲取。
- 在 Flutter 中,因為 Widget 并不是渲染到屏幕的最終視覺元素(RenderObject 才是),所以我們無法像原生的 Android 或 iOS 系統那樣,向持有的 Widget 對象獲取或設置最終渲染相關的視覺信息,而必須通過對應的組件控制器才能實現。
- ListView 的組件控制器則是 ScrollControler,我們可以通過它來獲取視圖的滾動信息,更新視圖的滾動位置。一般而言,獲取視圖的滾動信息往往是為了進行界面的狀態控制,因此 ScrollController 的初始化、監聽及銷毀需要與 StatefulWidget 的狀態保持同步。
ScrollNotification
- 在 Flutter 中,ScrollNotification 通知的獲取是通過 NotificationListener 來實現的。與 ScrollController 不同的是,NotificationListener 是一個 Widget,為了監聽滾動類型的事件,我們需要將 NotificationListener 添加為 ListView 的父容器,從而捕獲 ListView 中的通知。而這些通知,需要通過 onNotification 回調函數實現監聽邏輯:
- 相比于 ScrollController 只能和具體的 ListView 關聯后才可以監聽到滾動信息;通過 NotificationListener 則可以監聽其子 Widget 中的任意 ListView,不僅可以得到這些 ListView 的當前滾動位置信息,還可以獲取當前的滾動事件信息 。
問題
在ListView中,如何提前緩存子元素?
答:ListView構造函數中有一個cacheExtent參數,即預渲染區域長度,ListView會在其可視化區域的兩邊留一個cacheExtent長度的區域作為預渲染區域,相當于提前緩存些元素,這樣當滑動時迅速呈現。
經典布局:如何定義子控件在父容器中的排版位置
- Flutter 提供了 31 種布局 Widget,對布局控件的劃分非常詳細,一些相同(或相似)的視覺效果可以通過多種布局控件實現。
單子Widget布局:Container、Padding與Center
- 單子 Widget 布局類容器比較簡單,一般用來對其唯一的子 Widget 進行樣式包裝,比如限制大小、添加背景色樣式、內間距、旋轉變換等。
- 在 Flutter 中,Container 本身可以單獨作為控件存在(比如單獨設置背景色、寬高),也可以作為其他控件的父級存在:Container 可以定義布局過程中子 Widget 如何擺放,以及如何展示。與其他框架不同的是,Flutter 的 Container 僅能包含一個子 Widget。
- 如果我們只需要將子 Widget 設定間距,則可以使用另一個單子容器控件 Padding 進行內容填充。
- 在需要設置內容間距時,我們可以通過 EdgeInsets 的不同構造函數,分別制定四個方向的不同補白方式,如均使用同樣數值留白、只設置左留白或對稱方向留白等。
- Center 會將其子 Widget 居中排列。
- 需要注意的是,為了實現居中布局,Center 所占據的空間一定要比其子 Widget 要大才行,這也是顯而易見的:如果 Center 和其子 Widget 一樣大,自然就不需要居中,也沒空間居中了。因此 Center 通常會結合 Container 一起使用。
- 我們通過 Center 容器實現了 Container 容器中 alignment: Alignment.center 的效果。
多子Widget布局:Row、Column與Expanded
- 對于擁有多個子 Widget 的布局類容器而言,其布局行為無非就是兩種規則的抽象:水平方向上應該如何布局、垂直方向上應該如何布局。
- Row 與 Column 的使用方法很簡單,我們只需要將各個子 Widget 按序加入到 children 數組即可。
- 單純使用 Row 和 Column 控件,在子 Widget 的尺寸較小時,無法將容器填滿,視覺樣式比較難看。對于這樣的場景,我們可以通過 Expanded 控件,來制定分配規則填滿容器的剩余空間。
- 我們希望 Row 組件(或 Column 組件)中的綠色容器與黃色容器均分剩下的空間,于是就可以設置它們的彈性系數參數 flex 都為 1,這兩個 Expanded 會按照其 flex 的比例(即 1:1)來分割剩余的 Row 橫向(Column 縱向)空間。
- 于 Row 與 Column 而言,Flutter 提供了依據坐標軸的布局對齊行為,即根據布局方向劃分出主軸和縱軸:主軸,表示容器依次擺放子 Widget 的方向;縱軸,則是與主軸垂直的另一個方向。
- 我們可以根據主軸與縱軸,設置子 Widget 在這兩個方向上的對齊規則 mainAxisAlignment 與 crossAxisAlignment。比如,主軸方向 start 表示靠左對齊、center 表示橫向居中對齊、end 表示靠右對齊、spaceEvenly 表示按固定間距對齊;而縱軸方向 start 則表示靠上對齊、center 表示縱向居中對齊、end 表示靠下對齊。
- 需要注意的是,對于主軸而言,Flutter 默認是讓父容器決定其長度,即盡可能大,類似 Android 中的 match_parent。
- Row 的寬度為屏幕寬度,Column 的高度為屏幕高度。主軸長度大于所有子 Widget 的總長度,意味著容器在主軸方向的空間比子 Widget 要大,這也是我們能通過主軸對齊方式設置子 Widget 布局效果的原因。
- Row 與 Column 自身的大小由父widget的大小、子widget的大小、以及mainSize設置共同決定(mainAxisSize和crossAxisSize)
- 主軸(縱軸)值為max:主軸(縱軸)大小等于屏幕主軸(縱軸)方向大小或者父widget主軸(縱軸)方向大小。
- 主軸(縱軸)值為min: 所有子widget組合在一起的主軸(縱軸)大小。
- 如果想讓容器與子 Widget 在主軸上完全匹配,我們可以通過設置 Row 的 mainAxisSize 參數為 MainAxisSize.min,由所有子 Widget 來決定主軸方向的容器長度,即主軸方向的長度盡可能小,類似 Android 中的 wrap_content。
層疊Widget布局:Stack與Positioned
- Stack 容器與前端中的絕對定位、Android 中的 Frame 布局非常類似,子 Widget 之間允許疊加,還可以根據父容器上、下、左、右四個角的位置來確定自己的位置。
- Stack 提供了層疊布局的容器,而 Positioned 則提供了設置子 Widget 位置的能力。
- Stack 控件允許其子 Widget 按照創建的先后順序進行層疊擺放,而 Positioned 控件則用來控制這些子 Widget 的擺放位置。需要注意的是,Positioned 控件只能在 Stack 中使用,在其他容器中使用會報錯。
組合與自繪,何種方式定義Widget
- 在 Flutter 中,自定義 Widget 與其他平臺類似:可以使用基本 Widget 組裝成一個高級別的 Widget,也可以自己在畫板上根據特殊需求來畫界面。
組裝
- 使用組合的方式自定義 Widget,即通過我們之前介紹的布局方式,擺放項目所需要的基礎 Widget,并在控件內部設置這些基礎 Widget 的樣式,從而組合成一個更高級的控件。
- 在 Flutter 中,組合的思想始終貫穿在框架設計之中,這也是 Flutter 提供了如此豐富的控件庫的原因之一。比如,在新聞類應用中。
- 將上下兩部分控件通過 Column 包裝起來,這次升級項 UI 定制就完成了。
- 按照從上到下、從左到右去拆解 UI 的布局結構,把復雜的 UI 分解成各個小 UI 元素,在以組裝的方式去自定義 UI 中非常有用,請一定記住這樣的拆解方法。
自繪
- Flutter 提供了非常豐富的控件和布局方式,使得我們可以通過組合去構建一個新的視圖。但對于一些不規則的視圖,用 SDK 提供的現有 Widget 組合可能無法實現,比如餅圖,k 線圖等,這個時候我們就需要自己用畫筆去繪制了。
- 在原生 iOS 和 Android 開發中,我們可以繼承 UIView/View,在 drawRect/onDraw 方法里進行繪制操作。其實,在 Flutter 中也有類似的方案,那就是 CustomPaint。
- CustomPaint 是用以承接自繪控件的容器,并不負責真正的繪制。既然是繪制,那就需要用到畫布與畫筆。
- 在 Flutter 中,畫布是 Canvas,畫筆則是 Paint,而畫成什么樣子,則由定義了繪制邏輯的 CustomPainter 來控制。將 CustomPainter 設置給容器 CustomPaint 的 painter 屬性,我們就完成了一個自繪控件的封裝。
- 對于畫筆 Paint,我們可以配置它的各種屬性,比如顏色、樣式、粗細等;而畫布 Canvas,則提供了各種常見的繪制方法,比如畫線 drawLine、畫矩形 drawRect、畫點 DrawPoint、畫路徑 drawPath、畫圓 drawCircle、畫圓弧 drawArc 等。
- 在實現視覺需求上,自繪需要自己親自處理繪制邏輯,而組合則是通過子 Widget 的拼接來實現繪制意圖。因此從渲染邏輯處理上,自繪方案可以進行深度的渲染定制,從而實現少數通過組合很難實現的需求(比如餅圖、k 線圖)。不過,當視覺效果需要調整時,采用自繪的方案可能需要大量修改繪制代碼,而組合方案則相對簡單:只要布局拆分設計合理,可以通過更換子 Widget 類型來輕松搞定。
從夜間模式說起,定制不同的App主題
主題定制
- 主題,又叫皮膚、配色,一般由顏色、圖片、字號、字體等組成,我們可以把它看做是視覺效果在不同場景下的可視資源,以及相應的配置集合。比如,App 的按鈕,無論在什么場景下都需要背景圖片資源、字體顏色、字號大小等,而所謂的主題切換只是在不同主題之間更新這些資源及配置集合而已。
- 因此在 App 開發中,我們通常不關心資源和配置的視覺效果好不好看,只要關心資源提供的視覺功能能不能用。比如,對于圖片類資源,我們并不需要關心它渲染出來的實際效果,只需要確定它渲染出來是一張固定寬高尺寸的區域,不影響頁面布局,能把業務流程跑通即可。
- 視覺效果是易變的,我們將這些變化的部分抽離出來,把提供不同視覺效果的資源和配置按照主題進行歸類,整合到一個統一的中間層去管理,這樣我們就能實現主題的管理和切換了。
- 在 iOS 中,我們通常會將主題的配置信息預先寫到 plist 文件中,通過一個單例來控制 App 應該使用哪種配置;而 Android 的配置信息則寫入各個 style 屬性值的 xml 中,通過 activity 的 setTheme 進行切換;前端的處理方式也類似,簡單更換 css 就可以實現多套主題 / 配色之間的切換。
- Flutter 也提供了類似的能力,由 ThemeData 來統一管理主題的配置信息。
- ThemeData 涵蓋了 Material Design 規范的可自定義部分樣式,比如應用明暗模式 brightness、應用主色調 primaryColor、應用次級色調 accentColor、文本字體 fontFamily、輸入框光標顏色 cursorColor 等。
- 通過 ThemeData 來自定義應用主題,我們可以實現 App 全局范圍,或是 Widget 局部范圍的樣式切換。
全局統一的視覺風格定制
- 在 Flutter 中,應用程序類 MaterialApp 的初始化方法,為我們提供了設置主題的能力。我們可以通過參數 theme,選擇改變 App 的主題色、字體等,設置界面在 MaterialApp 下的展示樣式。
- 雖然我們只修改了主色調和明暗模式兩個參數,但按鈕、文字顏色都隨之調整了。這是因為默認情況下,ThemeData 中很多其他次級視覺屬性,都會受到主色調與明暗模式的影響。再細化一下主題配置。
局部獨立的視覺風格定制
- 為整個 App 提供統一的視覺呈現效果固然很有必要,但有時我們希望為某個頁面、或是某個區塊設置不同于 App 風格的展現樣式。以主題切換功能為例,我們希望為不同的主題提供不同的展示預覽。
- 在 Flutter 中,我們可以使用 Theme 來對 App 的主題進行局部覆蓋。Theme 是一個單子 Widget 容器,與 MaterialApp 類似的,我們可以通過設置其 data 屬性,對其子 Widget 進行樣式定制:
- 如果我們不想繼承任何 App 全局的顏色或字體樣式,可以直接新建一個 ThemeData 實例,依次設置對應的樣式;
- 而如果我們不想在局部重寫所有的樣式,則可以繼承 App 的主題,使用 copyWith 方法,只更新部分樣式。
- 除了定義 Material Design 規范中那些可自定義部分樣式外,主題的另一個重要用途是樣式復用。
- 比如,如果我們想為一段文字復用 Materia Design 規范中的 title 樣式,或是為某個子 Widget 的背景色復用 App 的主題色,我們就可以通過 Theme.of(context) 方法,取出對應的屬性,應用到這段文字的樣式中。
- Theme.of(context) 方法將向上查找 Widget 樹,并返回 Widget 樹中最近的主題 Theme。如果 Widget 的父 Widget 們有一個單獨的主題定義,則使用該主題。如果不是,那就使用 App 全局主題。
分平臺主題定制
- 有時候,為了滿足不同平臺的用戶需求,我們希望針對特定的平臺設置不同的樣式。比如,在 iOS 平臺上設置淺色主題,在 Android 平臺上設置深色主題。面對這樣的需求,我們可以根據 defaultTargetPlatform 來判斷當前應用所運行的平臺,從而根據系統類型來設置對應的主題。
- 當然,除了主題之外,你也可以用 defaultTargetPlatform 這個變量去實現一些其他需要判斷平臺的邏輯,比如在界面上使用更符合 Android 或 iOS 設計風格的組件。
依賴管理(一):圖片、配置和字體
- 一個應用程序主要由兩部分內容組成:代碼和資源。代碼關注邏輯功能,而如圖片、字符串、字體、配置文件等資源則關注視覺功能。
- 資源外部化,即把代碼與資源分離,是現代 UI 框架的主流設計理念。因為這樣不僅有利于單獨維護資源,還可以對特定設備提供更準確的兼容性支持,使得我們的應用程序可以自動根據實際運行環境來組織視覺功能,適應不同的屏幕大小和密度等。
資源管理
- 在移動開發中,常見的資源類型包括 JSON 文件、配置文件、圖標、圖片以及字體文件等。它們都會被打包到 App 安裝包中,而 App 中的代碼可以在運行時訪問這些資源。
- 在 Android、iOS 平臺中,為了區分不同分辨率的手機設備,圖片和其他原始資源是區別對待的:
- iOS 使用 Images.xcassets 來管理圖片,其他的資源直接拖進工程項目即可;
- Android 的資源管理粒度則更為細致,使用以 drawable+ 分辨率命名的文件夾來分別存放不同分辨率的圖片,其他類型的資源也都有各自的存放方式,比如布局文件放在 res/layout 目錄下,資源描述文件放在 res/values 目錄下,原始文件放在 assets 目錄下等。
- 而在 Flutter 中,資源管理則簡單得多:資源(assets)可以是任意類型的文件,比如 JSON 配置文件或是字體文件等,而不僅僅是圖片。
- 而關于資源的存放位置,Flutter 并沒有像 Android 那樣預先定義資源的目錄結構,所以我們可以把資源存放在項目中的任意目錄下,只需要使用根目錄下的 pubspec.yaml 文件,對這些資源的所在位置進行顯式聲明就可以了,以幫助 Flutter 識別出這些資源。
- 需要注意的是,目錄批量指定并不遞歸,只有在該目錄下的文件才可以被包括,如果下面還有子目錄的話,需要單獨聲明子目錄下的文件。
- 完成資源的聲明后,我們就可以在代碼中訪問它們了。在 Flutter 中,對不同類型的資源文件處理方式略有差異。
- 對于圖片類資源的訪問,我們可以使用 Image.asset 構造方法完成圖片資源的加載及顯示。
- 而對于其他資源文件的加載,我們可以通過 Flutter 應用的主資源 Bundle 對象 rootBundle,來直接訪問。
- 對于字符串文件資源,我們使用 loadString 方法;而對于二進制文件資源,則通過 load 方法。
- 與 Android、iOS 開發類似,Flutter 也遵循了基于像素密度的管理方式,如 1.0x、2.0x、3.0x 或其他任意倍數,Flutter 可以根據當前設備分辨率加載最接近設備像素比例的圖片資源。而為了讓 Flutter 更好地識別,我們的資源目錄應該將 1.0x、2.0x 與 3.0x 的圖片資源分開管理。
- 而在 pubspec.yaml 文件聲明這個圖片資源時,僅聲明 1.0x 圖資源即可。
- 1.0x 分辨率的圖片是資源標識符,而 Flutter 則會根據實際屏幕像素比例加載相應分辨率的圖片。這時,如果主資源缺少某個分辨率資源,Flutter 會在剩余的分辨率資源中選擇最接近的分辨率資源去加載。
- 字體則是另外一類較為常用的資源。手機操作系統一般只有默認的幾種字體,在大部分情況下可以滿足我們的正常需求。但是,在一些特殊的情況下,我們可能需要使用自定義字體來提升視覺體驗。
- 在 Flutter 中,使用自定義字體同樣需要在 pubspec.yaml 文件中提前聲明。需要注意的是,字體實際上是字符圖形的映射。所以,除了正常字體文件外,如果你的應用需要支持粗體和斜體,同樣也需要有對應的粗體和斜體字體文件。
- 在將 RobotoCondensed 字體擺放至 assets 目錄下的 fonts 子目錄后,下面的代碼演示了如何將支持斜體與粗體的 RobotoCondensed 字體加到我們的應用中。
原生平臺的資源設置
- Flutter 需要原生環境才能運行,但是有些資源我們需要在 Flutter 框架運行之前提前使用,比如要給應用添加圖標,或是希望在等待 Flutter 框架啟動時添加啟動圖,我們就需要在對應的原生工程中完成相應的配置,所以下面介紹的操作步驟都是在原生系統中完成的。
更換App圖標
- 對于 Android 平臺,啟動圖標位于根目錄 android/app/src/main/res/mipmap 下。我們只需要遵守對應的像素密度標準,保留原始圖標名稱,將圖標更換為目標資源即可。
- 對于 iOS 平臺,啟動圖位于根目錄 ios/Runner/Assets.xcassets/AppIcon.appiconset 下。同樣地,我們只需要遵守對應的像素密度標準,將其替換為目標資源并保留原始圖標名稱即可。
更換啟動圖
- 對于 Android 平臺,啟動圖位于根目錄 android/app/src/main/res/drawable 下,是一個名為 launch_background 的 XML 界面描述文件。
- 而對于 iOS 平臺,啟動圖位于根目錄 ios/Runner/Assets.xcassets/LaunchImage.imageset 下。我們保留原始啟動圖名稱,將圖片依次按照對應像素密度標準,更換為目標啟動圖即可。
依賴管理(二):第三方組件庫在FLutter如何管理
- pubspec.yaml 更為重要的作用是管理 Flutter 工程代碼的依賴,比如第三方庫、Dart 運行環境、Flutter SDK 版本都可以通過它來進行統一管理。所以,pubspec.yaml 與 iOS 中的 Podfile、Android 中的 build.gradle、前端的 package.json 在功能上是類似的。
Pub
- Dart 提供了包管理工具 Pub,用來管理代碼和資源。從本質上說,包(package)實際上就是一個包含了 pubspec.yaml 文件的目錄,其內部可以包含代碼、資源、腳本、測試和文檔等文件。包中包含了需要被外部依賴的功能抽象,也可以依賴其他包。
- 與 Android 中的 JCenter/Maven、iOS 中的 CocoaPods、前端中的 npm 庫類似,Dart 提供了官方的包倉庫 Pub。通過 Pub,我們可以很方便地查找到有用的第三方包
- 。當然,這并不意味著我們可以簡單地拿別人的庫來拼湊成一個應用程序。Dart 提供包管理工具 Pub 的真正目的是,讓你能夠找到真正好用的、經過線上大量驗證的庫,復用他人的成果來縮短開發周期,提升軟件質量。
- 在 Dart 中,庫和應用都屬于包。pubspec.yaml 是包的配置文件,包含了包的元數據(比如,包的名稱和版本)、運行環境(也就是 Dart SDK 與 Fluter SDK 版本)、外部依賴、內部配置(比如,資源管理)。
- 下面的例子中,我們聲明了一個 flutter_app_example 的應用配置文件,其版本為 1.0,Dart 運行環境支持 2.1 至 3.0 之間,依賴 flutter 和 cupertino_icon。
- 運行環境和依賴庫 cupertino_icons 冒號后面的部分是版本約束信息,由一組空格分隔的版本描述組成,可以支持指定版本、版本號區間,以及任意版本這三種版本約束方式。比如上面的例子中,cupertino_icons 引用了大于 0.1.1 的版本。
- 需要注意的是,由于元數據與名稱使用空格分隔,因此版本號中不能出現空格;同時又由于大于符號“>”也是 YAML 語法中的折疊換行符號,因此在指定版本范圍的時候,必須使用引號, 比如">=2.1.0 < 3.0.0"。
- 對于包,我們通常是指定版本區間,而很少直接指定特定版本,因為包升級變化很頻繁,如果有其他的包直接或間接依賴這個包的其他版本時,就會經常發生沖突。
- 而對于運行環境,如果是團隊多人協作的工程,建議將 Dart 與 Flutter 的 SDK 環境寫死,統一團隊的開發環境,避免因為跨 SDK 版本出現的 API 差異進而導致工程問題。
- 基于版本的方式引用第三方包,需要在其 Pub 上進行公開發布,我們可以訪問https://pub.dev/來獲取可用的第三方包。
- 而對于不對外公開發布,或者目前處于開發調試階段的包,我們需要設置數據源,使用本地路徑或 Git 地址的方式進行包聲明。
- 在開發應用時,我們可以不寫明具體的版本號,而是以區間的方式聲明包的依賴;但對于一個程序而言,其運行時具體引用哪個版本的依賴包必須要確定下來。因此,除了管理第三方依賴,包管理工具 Pub 的另一個職責是,找出一組同時滿足每個包版本約束的包版本。包版本一旦確定,接下來就是下載對應版本的包了。
- 對于 dependencies 中的不同數據源,Dart 會使用不同的方式進行管理,最終會將遠端的包全部下載到本地。比如,對于 Git 聲明依賴的方式,Pub 會 clone Git 倉庫;對于版本號的方式,Pub 則會從 pub.dartlang.org 下載包。如果包還有其他的依賴包,比如 package1 包還依賴 package3 包,Pub 也會一并下載。
- 然后,在完成了所有依賴包的下載后,Pub 會在應用的根目錄下創建.packages 文件,將依賴的包名與系統緩存中的包文件路徑進行映射,方便后續維護。
- 最后,Pub 會自動創建 pubspec.lock 文件。pubspec.lock 文件的作用類似 iOS 的 Podfile.lock 或前端的 package-lock.json 文件,用于記錄當前狀態下實際安裝的各個直接依賴、間接依賴的包的具體來源和版本號。
- 除了提供功能和代碼維度的依賴之外,包還可以提供資源的依賴。在依賴包中的 pubspec.yaml 文件已經聲明了同樣資源的情況下,為節省應用程序安裝包大小,我們需要復用依賴包中的資源。
舉例
- 在 Flutter 中,提供了表達日期的數據結構DateTime,這個類擁有極大的表示范圍,可以表達 1970-01-01 UTC 時間后 100,000,000 天內的任意時刻。不過,如果我們想要格式化顯示日期和時間,DateTime 并沒有提供非常方便的方法,我們不得不自己取出年、月、日、時、分、秒,來定制顯示方式。
- 值得慶幸的是,我們可以通過 date_format 這個第三方包來實現我們的訴求:date_format 提供了若干常用的日期格式化方法,可以很方便地實現格式化日期的功能。
- 首先,我們在 Pub 上找到 date_format 這個包,確定其使用說明。
- date_format 包最新的版本是 1.0.6,于是接下來我們把 date_format 添加到 pubspec.yaml 中。
- 隨后,IDE(Android Studio)監測到了配置文件的改動,提醒我們進行安裝包依賴更新。于是,我們點擊 Get dependencies,下載 date_format 。
- 下載完成后,我們就可以在工程中使用 date_format 來進行日期的格式化了。
- 現代編程語言大都自帶第依賴管理機制,其核心功能是為工程中所有直接或間接依賴的代碼庫找到合適的版本,但這并不容易。就比如前端的依賴管理器 npm 的早期版本,就曾因為不太合理的算法設計,導致計算依賴耗時過長,依賴文件夾也高速膨脹,一度被開發者們戲稱為“黑洞”。而 Dart 使用的 Pub 依賴管理機制所采用的PubGrub 算法則解決了這些問題,因此被稱為下一代版本依賴解決算法,在 2018 年底被蘋果公司吸納,成為 Swift 所采用的依賴管理器算法。
問題
- .packages 與 pubspec.lock 是否需要做代碼版本管理呢?為什么?
答:pubspec.lock需要做版本管理,因為lock文件把版本鎖定,統一工程環境
.packages不需要版本管理,因為跟本地環境有關,無法做到統一。
用戶交互事件如何響應
- 手勢操作在 Flutter 中分為兩類:
- 第一類是原始的指針事件(Pointer Event),即原生開發中常見的觸摸事件,表示屏幕上觸摸(或鼠標、手寫筆)行為觸發的位移行為;
- 第二類則是手勢識別(Gesture Detector),表示多個原始指針事件的組合操作,如點擊、雙擊、長按等,是指針事件的語義化封裝。
指針事件
- 指針事件表示用戶交互的原始觸摸數據,如手指接觸屏幕 PointerDownEvent、手指在屏幕上移動 PointerMoveEvent、手指抬起 PointerUpEvent,以及觸摸取消 PointerCancelEvent,這與原生系統的底層觸摸事件抽象是一致的。
- 在手指接觸屏幕,觸摸事件發起時,Flutter 會確定手指與屏幕發生接觸的位置上究竟有哪些組件,并將觸摸事件交給最內層的組件去響應。與瀏覽器中的事件冒泡機制類似,事件會從這個最內層的組件開始,沿著組件樹向根節點向上冒泡分發。
- 不過 Flutter 無法像瀏覽器冒泡那樣取消或者停止事件進一步分發,我們只能通過 hitTestBehavior 去調整組件在命中測試期內應該如何表現,比如把觸摸事件交給子組件,或者交給其視圖層級之下的組件去響應。
- 關于組件層面的原始指針事件的監聽,Flutter 提供了 Listener Widget,可以監聽其子 Widget 的原始指針事件。
手勢識別
- 通常情況下,響應用戶交互行為的話,我們會使用封裝了手勢語義操作的 Gesture,如點擊 onTap、雙擊 onDoubleTap、長按 onLongPress、拖拽 onPanUpdate、縮放 onScaleUpdate 等。另外,Gesture 可以支持同時分發多個手勢交互行為,意味著我們可以通過 Gesture 同時監聽多個事件。
- Gesture 是手勢語義的抽象,而如果我們想從組件層監聽手勢,則需要使用 GestureDetector。GestureDetector 是一個處理各種高級用戶觸摸行為的 Widget,與 Listener 一樣,也是一個功能性組件。
- 我們對一個 Widget 同時監聽了多個手勢事件,但最終只會有一個手勢能夠得到本次事件的處理權。對于多個手勢的識別,Flutter 引入了手勢競技場(Arena)的概念,用來識別究竟哪個手勢可以響應用戶事件。手勢競技場會考慮用戶觸摸屏幕的時長、位移以及拖動方向,來確定最終手勢。
手勢競技場實現
- 實際上,GestureDetector 內部對每一個手勢都建立了一個工廠類(Gesture Factory)。而工廠類的內部會使用手勢識別類(GestureRecognizer),來確定當前處理的手勢。
- 而所有手勢的工廠類都會被交給 RawGestureDetector 類,以完成監測手勢的大量工作:使用 Listener 監聽原始指針事件,并在狀態改變時把信息同步給所有的手勢識別器,然后這些手勢會在競技場決定最后由誰來響應用戶事件。
- 有些時候我們可能會在應用中給多個視圖注冊同類型的手勢監聽器,比如微博的信息流列表中的微博,點擊不同區域會有不同的響應:點擊頭像會進入用戶個人主頁,點擊圖片會進入查看大圖頁面,點擊其他部分會進入微博詳情頁等。
- 像這樣的手勢識別發生在多個存在父子關系的視圖時,手勢競技場會一并檢查父視圖和子視圖的手勢,并且通常最終會確認由子視圖來響應事件。而這也是合乎常理的:從視覺效果上看,子視圖的視圖層級位于父視圖之上,相當于對其進行了遮擋,因此從事件處理上看,子視圖自然是事件響應的第一責任人。
- 運行這段代碼,然后在藍色區域進行點擊,可以發現:盡管父容器也監聽了點擊事件,但 Flutter 只響應了子容器的點擊事件。
- 為了讓父容器也能接收到手勢,我們需要同時使用 RawGestureDetector 和 GestureFactory,來改變競技場決定由誰來響應用戶事件的結果。
- 在此之前,我們還需要自定義一個手勢識別器,讓這個識別器在競技場被 PK 失敗時,能夠再把自己重新添加回來,以便接下來還能繼續去響應用戶事件。
- 接下來,我們需要將手勢識別器和其工廠類傳遞給 RawGestureDetector,以便用戶產生手勢交互事件時能夠立刻找到對應的識別方法。事實上,RawGestureDetector 的初始化函數所做的配置工作,就是定義不同手勢識別器和其工廠類的映射關系。
- 這里,由于我們只需要處理點擊事件,所以只配置一個識別器即可。工廠類的初始化采用 GestureRecognizerFactoryWithHandlers 函數完成,這個函數提供了手勢識別對象創建,以及對應的初始化入口。
- 運行一下這段代碼,我們可以看到,當點擊藍色容器時,其父容器也收到了 Tap 事件。
跨組件傳遞數據
-
通過組合嵌套的方式,利用數據對基礎 Widget 的樣式進行視覺屬性定制,我們已經實現了多種界面布局,在 Flutter 中實現跨組件數據傳遞的標準方式是通過屬性傳值。
-
對于稍微復雜一點的、尤其視圖層級比較深的 UI 樣式,一個屬性可能需要跨越很多層才能傳遞給子組件,這種傳遞方式就會導致中間很多并不需要這個屬性的組件也需要接收其子 Widget 的數據,不僅繁瑣而且冗余。
-
所以,對于數據的跨層傳遞,Flutter 還提供了三種方案:InheritedWidget、Notification 和 EventBus。接下來,我將依次為你講解這三種方案。
InheritedWidget
- InheritedWidget 是 Flutter 中的一個功能型 Widget,適用于在 Widget 樹中共享數據的場景。通過它,我們可以高效地將數據在 Widget 樹中進行跨層傳遞。
- 之前通過 Theme 去訪問當前界面的樣式風格,從而進行樣式復用的例子,比如 Theme.of(context).primaryColor。
- Theme 類是通過 InheritedWidget 實現的典型案例。在子 Widget 中通過 Theme.of 方法找到上層 Theme 的 Widget,獲取到其屬性的同時,建立子 Widget 和上層父 Widget 的觀察者關系,當上層父 Widget 屬性修改的時候,子 Widget 也會觸發更新。
- 以 Flutter 工程模板中的計數器為例,說明 InheritedWidget 的使用方法。
- 首先,為了使用 InheritedWidget,我們定義了一個繼承自它的新類 CountContainer。
- 然后,我們將計數器狀態 count 屬性放到 CountContainer 中,并提供了一個 of 方法方便其子 Widget 在 Widget 樹中找到它。最后,我們重寫了 updateShouldNotify 方法,這個方法會在 Flutter 判斷 InheritedWidget 是否需要重建,從而通知下層觀察者組件更新數據時被調用到。在這里,我們直接判斷 count 是否相等即可。
- 然后,我們使用 CountContainer 作為根節點,并用 0 初始化 count。隨后在其子 Widget Counter 中,我們通過 InheritedCountContainer.of 方法找到它,獲取計數狀態 count 并展示。
- 可以看到 InheritedWidget 的使用方法還是比較簡單的,無論 Counter 在 CountContainer 下層什么位置,都能獲取到其父 Widget 的計數屬性 count,再也不用手動傳遞屬性了。
- 不過,InheritedWidget 僅提供了數據讀的能力,如果我們想要修改它的數據,則需要把它和 StatefulWidget 中的 State 配套使用。我們需要把 InheritedWidget 中的數據和相關的數據修改方法,全部移到 StatefulWidget 中的 State 上,而 InheritedWidget 只需要保留對它們的引用。
- 然后,我們將 count 數據和其對應的修改方法放在了 State 中,仍然使用 CountContainer 作為根節點,完成了數據和修改方法的初始化。
- 在其子 Widget Counter 中,我們還是通過 InheritedCountContainer.of 方法找到它,將計數狀態 count 與 UI 展示同步,將按鈕的點擊事件與數據修改同步。
Notification
- Notification 是 Flutter 中進行跨層數據共享的另一個重要的機制。如果說 InheritedWidget 的數據流動方式是從父 Widget 到子 Widget 逐層傳遞,那 Notificaiton 則恰恰相反,數據流動方式是從子 Widget 向上傳遞至父 Widget。這樣的數據傳遞機制適用于子 Widget 狀態變更,發送通知上報的場景。
- 在之前的ListView學習中,介紹了 ScrollNotification 的使用方法:ListView 在滾動時會分發通知,我們可以在上層使用 NotificationListener 監聽 ScrollNotification,根據其狀態做出相應的處理。
- 自定義通知的監聽與 ScrollNotification 并無不同,而如果想要實現自定義通知,我們首先需要繼承 Notification 類。Notification 類提供了 dispatch 方法,可以讓我們沿著 context 對應的 Element 節點樹向上逐層發送通知。
- 自定義了一個通知和子 Widget。子 Widget 是一個按鈕,在點擊時會發送通知。
- 在子 Widget 的父 Widget 中,我們監聽了這個通知,一旦收到通知,就會觸發界面刷新,展示收到的通知信息。
EventBus
- 無論是 InheritedWidget 還是 Notificaiton,它們的使用場景都需要依靠 Widget 樹,也就意味著只能在有父子關系的 Widget 之間進行數據共享。但是,組件間數據傳遞還有一種常見場景:這些組件間不存在父子關系。這時,事件總線 EventBus 就登場了。
- 事件總線是在 Flutter 中實現跨組件通信的機制。它遵循發布 / 訂閱模式,允許訂閱者訂閱事件,當發布者觸發事件時,訂閱者和發布者之間可以通過事件進行交互。發布者和訂閱者之間無需有父子關系,甚至非 Widget 對象也可以發布 / 訂閱。這些特點與其他平臺的事件總線機制是類似的。
- EventBus 是一個第三方插件,因此我們需要在 pubspec.yaml 文件中聲明它。
- EventBus 的使用方式靈活,可以支持任意對象的傳遞。所以在這里,我們傳輸數據的載體就選擇了一個有字符串屬性的自定義事件類 CustomEvent。
- 然后定義了一個全局的 eventBus 對象,并在第一個頁面監聽了 CustomEvent 事件,一旦收到事件,就會刷新 UI。需要注意的是,千萬別忘了在 State 被銷毀時清理掉事件注冊,否則你會發現 State 永遠被 EventBus 持有著,無法釋放,從而造成內存泄漏。
- 最后,我們在第二個頁面以按鈕點擊回調的方式,觸發了 CustomEvent 事件。
- 屬性傳值、InheritedWidget、Notification 與 EventBus 數據傳遞方式對比。
路由與導航實現頁面切換
- 如果說 UI 框架的視圖元素的基本單位是組件,那應用程序的基本單位就是頁面了。對于擁有多個頁面的應用程序而言,如何從一個頁面平滑地過渡到另一個頁面,我們需要有一個統一的機制來管理頁面之間的跳轉,通常被稱為路由管理或導航管理。
- 我們首先需要知道目標頁面對象,在完成目標頁面初始化后,用框架提供的方式打開它。比如,在 Android/iOS 中我們通常會初始化一個 Intent 或 ViewController,通過 startActivity 或 pushViewController 來打開一個新的頁面;而在 React 中,我們使用 navigation 來管理所有頁面,只要知道頁面的名稱,就可以立即導航到這個頁面。
- 其實,Flutter 的路由管理也借鑒了這兩種設計思路。
路由管理
- 在 Flutter 中,頁面之間的跳轉是通過 Route 和 Navigator 來管理的。
- Route 是頁面的抽象,主要負責創建對應的界面,接收參數,響應 Navigator 打開和關閉;
- 而 Navigator 則會維護一個路由棧管理 Route,Route 打開即入棧,Route 關閉即出棧,還可以直接替換棧內的某一個 Route。
- 而根據是否需要提前注冊頁面標識符,Flutter 中的路由管理可以分為兩種方式。
- 基本路由。無需提前注冊,在頁面切換時需要自己構造頁面實例。
- 命名路由。需要提前注冊頁面標識符,在頁面切換時通過標識符直接打開新的路由。
基本路由
- 在 Flutter 中,基本路由的使用方法和 Android/iOS 打開新頁面的方式非常相似。要導航到一個新的頁面,我們需要創建一個 MaterialPageRoute 的實例,調用 Navigator.push 方法將新頁面壓到堆棧的頂部。
- 其中,MaterialPageRoute 是一種路由模板,定義了路由創建及切換過渡動畫的相關配置,可以針對不同平臺,實現與平臺頁面切換動畫風格一致的路由切換動畫。
- 而如果我們想返回上一個頁面,則需要調用 Navigator.pop 方法從堆棧中刪除這個頁面。
命名路由
- 基本路由使用方式相對簡單靈活,適用于應用中頁面不多的場景。而在應用中頁面比較多的情況下,再使用基本路由方式,那么每次跳轉到一個新的頁面,我們都要手動創建 MaterialPageRoute 實例,初始化頁面,然后調用 push 方法打開它,還是比較麻煩的。
- 所以,Flutter 提供了另外一種方式來簡化路由管理,即命名路由。我們給頁面起一個名字,然后就可以直接通過頁面名字打開它了。這種方式簡單直觀,與 React 中的 navigation 使用方式類似。
- 要想通過名字來指定頁面切換,我們必須先給應用程序 MaterialApp 提供一個頁面名稱映射規則,即路由表 routes,這樣 Flutter 才知道名字與頁面 Widget 的對應關系。
- 路由表實際上是一個 Map,其中 key 值對應頁面名字,而 value 值則是一個 WidgetBuilder 回調函數,我們需要在這個函數中創建對應的頁面。而一旦在路由表中定義好了頁面名字,我們就可以使用 Navigator.pushNamed 來打開頁面了。
- 不過由于路由的注冊和使用都采用字符串來標識,這就會帶來一個隱患:如果我們打開了一個不存在的路由會怎么辦?
- 我們可以約定使用字符串常量去定義、使用路由,但我們無法避免通過接口數據下發的錯誤路由標識符場景。面對這種情況,無論是直接報錯或是不響應錯誤路由,都不是一個用戶體驗良好的解決辦法。
- 更好的辦法是,對用戶進行友好的錯誤提示,比如跳轉到一個統一的 NotFoundScreen 頁面,也方便我們對這類錯誤進行統一收集、上報。
- 在注冊路由表時,Flutter 提供了 UnknownRoute 屬性,我們可以對未知的路由標識符進行統一的頁面跳轉處理。
頁面參數
- 與基本路由能夠精確地控制目標頁面初始化方式不同,命名路由只能通過字符串名字來初始化固定目標頁面。為了解決不同場景下目標頁面的初始化需求,Flutter 提供了路由參數的機制,可以在打開路由時傳遞相關參數,在目標頁面通過 RouteSettings 來獲取頁面參數。
- 除了頁面打開時需要傳遞參數,對于特定的頁面,在其關閉時,也需要傳遞參數告知頁面處理結果。
- 比如在電商場景下,我們會在用戶把商品加入購物車時,打開登錄頁面讓用戶登錄,而在登錄操作完成之后,關閉登錄頁面返回到當前頁面時,登錄頁面會告訴當前頁面新的用戶身份,當前頁面則會用新的用戶身份刷新頁面。
- 與 Android 提供的 startActivityForResult 方法可以監聽目標頁面的處理結果類似,Flutter 也提供了返回參數的機制。在 push 目標頁面時,可以設置目標頁面關閉時監聽函數,以獲取返回參數;而目標頁面可以在關閉路由時傳遞相關參數。
- 在中大型應用中,我們通常會使用命名路由來管理頁面間的切換。命名路由的最重要作用,就是建立了字符串標識符與各個頁面之間的映射關系,使得各個頁面之間完全解耦,應用內頁面的切換只需要通過一個字符串標識符就可以搞定,為后期模塊化打好基礎。
補充
問題:Navigator.pushA->B->C->D,請問如何 D頁面 pop 到 B 呢?
答:Navigator.popUntil(context,ModalRoute.withName(‘B’));
總結
以上是生活随笔為你收集整理的Flutter技术与实战(4)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 恢复云数据库MySQL的备份文件到自建数
- 下一篇: android qq skype,Sky