畫面莫名其妙地重 build 了

閱讀時間約 13 分鐘


Flutter 自帶各式各樣的 Widget,能透過改變 Widget 的參數,讓畫面符合開發者想要的設計。在大部分的時間裏,能有效減低開發者的開發時間。但是如果開發者使用方式不正確的話,往往會造成不預期的結果,今天就來分享一個問題。

舉個例子

假設我們有一個簡單的應用,總共有兩個頁面:第一個頁面會顯示一組隨機繁體中文數字,然後使用者需要記下該數字,並且在第二頁輸入結果。Submit 之後,會在第一個頁面的底部顯示答案是否正確。

raw-image

第一個頁面的程式碼

class FirstPage extends StatelessWidget {

@override
Widget build(BuildContext context) {
String randomNumber = _getRandomNumber();
return Scaffold(
body: Column(
children: [
Container(
alignment: Alignment.center,
height: MediaQuery.of(context).size.height * 0.6,
child: Text(
"What's the number: ${NumberConvertor.toText(randomNumber)}",
),
),
OutlinedButton(
child: const Text("Go to answer"),
onPressed: () async {
var result = await Navigator.of(context).pushNamed("/second");
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text("Answer is ${result == randomNumber ? "correct" : "wrong"}"),
));
},
),
],
),
);
}

String _getRandomNumber() {
return Random().nextInt(100).toString();
}
}

第二個頁面的程式碼

class SecondPage extends StatelessWidget {

@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Container(
alignment: Alignment.center,
width: 300,
child: TextFormField(
keyboardType: TextInputType.number,
onFieldSubmitted: (text) => Navigator.of(context).pop(text),
decoration: const InputDecoration(
border: OutlineInputBorder(),
label: Text("Answer"),
),
),
),
),
);
}
}

發生了不預期狀況

當使用者在第二個頁面填完答案回到第一頁面時,會發現雖然訊息顯示答案正確,但是原本的題目卻已經變成另外一組了

raw-image

發生了什麼事

如果我們 debug 了一下程式,就會發現一個神奇的狀況:當使用者在第二的頁面點開鍵盤時,第一個頁面就會重新 build 了一次,導致畫面又重新取了一次亂數,新的數字就出現在畫面上。

raw-image

為什麼麼第二個頁面的動作會影響到第一個頁面呢?讓我們回到第一個頁面的程式碼,仔細觀察與實驗就會發現,是 MediaQuery.of(context) 在搞的鬼。

Container(
alignment: Alignment.center,
height: MediaQuery.of(context).size.height * 0.6,
child: Text(
"What's the number: ${NumberConvertor.toText(randomNumber)}",
),
),

如果我們把 MediaQuery.of(context).size.height * 0.6 置換成固定值 500。

Container(
alignment: Alignment.center,
height: 500,
child: Text(
"What's the number: ${NumberConvertor.toText(randomNumber)}",
),
),

當輸入答案回來之後,題目還是維持的原來的題目。

raw-image

MediaQuery.of(context) 做了什麼?

如果我們查看 MediaQuery.of(context) 的原始碼,會發現其中有段 context.dependOnInheritedWidgetOfExtractType,如果再往裡面查,會發現這不過是 BuildContext 這個介面上的一個方法。

static MediaQueryData of(BuildContext context) {
assert(context != null);
assert(debugCheckHasMediaQuery(context));
return context.dependOnInheritedWidgetOfExactType<MediaQuery>()!.data;
}

實作則需要找到 Element 這個類別,在 Element 中的實作如下。

@override
T? dependOnInheritedWidgetOfExactType<T extends InheritedWidget>({Object? aspect}) {
assert(_debugCheckStateIsActiveForAncestorLookup());
final InheritedElement? ancestor = _inheritedWidgets == null ? null : _inheritedWidgets![T];
if (ancestor != null) {
return dependOnInheritedElement(ancestor, aspect: aspect) as T;
}
_hadUnsatisfiedDependencies = true;
return null;
}

@override
InheritedWidget dependOnInheritedElement(InheritedElement ancestor, { Object? aspect }) {
assert(ancestor != null);
_dependencies ??= HashSet<InheritedElement>();
_dependencies!.add(ancestor);
ancestor.updateDependencies(this, aspect);
return ancestor.widget;
}

當執行 dependOnInheritedWidgetOfExactType 時,會把 MediaQuery 的 InheritedElement 塞到 Element 身上的 _dependencies 中,同時也會呼叫 ancestor.updateDependencies,把自己也塞到 InheritedElement 的 _dependents 中。

當 InheritedElement 發生改變時,就會呼叫身上的 notifyClients,從而更新所有的 dependents。

@override
void notifyClients(InheritedWidget oldWidget) {
assert(_debugCheckOwnerBuildTargetExists('notifyClients'));
for (final Element dependent in _dependents.keys) {
assert(() {
// check that it really is our descendant
Element? ancestor = dependent._parent;
while (ancestor != this && ancestor != null)
ancestor = ancestor._parent;
return ancestor == this;
}());
// check that it really depends on us
assert(dependent._dependencies!.contains(this));
notifyDependent(oldWidget, dependent);
}
}

回到例子上,也就是當第一個頁面呼叫 MediaQuery.of(context) 時,就已經向 MediaQuery 註冊了一個觀察者,當 MediaQuery 因為鍵盤的出現導致畫面高度發生改變時,第一頁面也就跟著一起重 build 了。

如何解決問題

回到我們的問題上,如何讓第一個頁面不要重 build 呢?以上面這個例子來看,目的只是想依照固定高度比例來設計畫面,可以簡單的使用 Column + Expanded 解決。

但是其實更好的方式是,避免在 build 的過程產生資料,而是應該使用 StatefulWidget,並在 initState 初始化狀態。這樣一來,即便畫面重 build,也能維持當初隨機出來的數字。

小結

我自己覺得 Flutter 把 Widget 設計得十分方便,讓使用者可以用比較少的程式碼就完成功能,但是其中比較困難的就是許多細節被隱藏在框架之中。像是一般情況下,我們幾乎不會碰到 InheritedWidget,更多的是直接使用他的衍生類別或 Wrapper,在這種情況下,我們就很難知道這行程式碼究竟會帶來什麼影響,進而造成一些不預期的狀況。除了明白如何使用框架,有時也需要深入理解框架做了什麼,才能更有效地使用框架。

參考

分享各種 Flutter 開發技巧
留言0
查看全部
發表第一個留言支持創作者!
本文探討如何在 Flutter 中自訂 Tab Bar 特效,提升使用者介面互動性。從基本的 Row 佈局開始,我們逐步實現選中 Tab 動態變化的需求。最後,使用 CustomMultiChildLayout 與 AnimatedSize 實現一個符合設計需求的 Tab Bar,提升整體使用體驗。
本文探討如何有效解決 Flutter 中 PageView 動畫與複雜畫面造成的卡頓問題。透過使用 Provider 優化效能,減少不必要的 Widget 重建,達成更流暢的使用體驗。本文提供範例程式碼及效能分析,讓開發者能夠理解並應用於實際產品中,從而改善應用的效能。
本文介紹如何解決 Flutter 應用程式中 PageView 的卡頓問題。透過使用 DevTools 的 Profile 模式及啟用 Track Widget Builds 功能,分析了 UI phase 的效能瓶頸,識別出 PlayerInfoGameLogView 重新建構的高成本。
本文探討如何在 Flutter 中自訂 Tab Bar 特效,提升使用者介面互動性。從基本的 Row 佈局開始,我們逐步實現選中 Tab 動態變化的需求。最後,使用 CustomMultiChildLayout 與 AnimatedSize 實現一個符合設計需求的 Tab Bar,提升整體使用體驗。
本文探討如何有效解決 Flutter 中 PageView 動畫與複雜畫面造成的卡頓問題。透過使用 Provider 優化效能,減少不必要的 Widget 重建,達成更流暢的使用體驗。本文提供範例程式碼及效能分析,讓開發者能夠理解並應用於實際產品中,從而改善應用的效能。
本文介紹如何解決 Flutter 應用程式中 PageView 的卡頓問題。透過使用 DevTools 的 Profile 模式及啟用 Track Widget Builds 功能,分析了 UI phase 的效能瓶頸,識別出 PlayerInfoGameLogView 重新建構的高成本。
你可能也想看
Google News 追蹤
Thumbnail
這個秋,Chill 嗨嗨!穿搭美美去賞楓,裝備款款去露營⋯⋯你的秋天怎麼過?秋日 To Do List 等你分享! 秋季全站徵文,我們準備了五個創作主題,參賽還有機會獲得「火烤兩用鍋」,一起來看看如何參加吧~
Thumbnail
這次的side bar不一樣了,上一次我好像做的太簡單了,所以完讀率只有14%,我好難過啊(;´д`)ゞ,所以我這準備的內容有多一點。 這次的目標 按鈕的排版 按鈕滑過去會有顏色變化 Side bar能夠展開 箭頭能夠移動至被選擇的物件上 宣告 這次我創了兩個檔案 SideBa
Thumbnail
隨著科技的不斷發展,網頁設計已經從過去的靜態頁面演變為充滿動態、互動性和個性化的體驗。本文介紹了網站設計的最新趨勢,包括夜間模式、無障礙設計、響應式設計、聊天機器人和虛擬助手等功能。
Thumbnail
這篇文章教你如何製作側邊欄,包括準備工作、HTML和CSS的部分,還有一些互動效果。文章涵蓋了連結、圖片、超連結、大小、顏色、排版、flex和滑鼠互動等內容。
Thumbnail
極簡網頁設計依然是主流,以其乾淨、有序的視覺效果和強調內容的功能性,幫助用戶集中注意力於重要信息。
Thumbnail
在當今的IG時代,多媒體內容已成為網頁設計不可或缺的一部分,它不僅豐富了用戶的互動體驗,還能顯著提升網站的吸引力和溝通效率。本文將探討多媒體內容對用戶體驗(UX)的影響、SEO最佳實踐與多媒體的整合方法,並提供成功整合多媒體的網站案例。
Thumbnail
作為一個非常不專業的前端初學者,有陣子常常卡在公司官網,要插入 Youtube 影片無法RWD(響應式)的問題。 跟不熟悉 網頁技術的朋友們介紹,RWD 就是指網頁的排版能跟著螢幕的大小縮放、變化編排,在這個人手一機的時代,特別重要。
Thumbnail
響應式網頁設計(Responsive Web Design, RWD)已成為現代網站開發不可或缺的一部分。響應式設計不僅能夠確保網站在各種設備上都能提供良好的用戶體驗,而且對於搜索引擎優化(SEO)也有著重要的影響。本文將探討響應式設計的最新技術、其對SEO的優勢以及一些成功的實際案例。
圖片大小 漂亮的圖片讓人賞心悅目,對網站美化也是一大加分項,但若是為了呈現自家商品或吸引人的圖片搭配文字,而塞進過量的圖片,導致網站本身太重跑得太慢,容易使客人失去耐性。|SEO工具 隨著時代的進步網路速度也與時俱進,但若網站本身太重,就算網路狀況再良好也無法馬上將網站載好,根據統計,大多數人的
透過Responsive網頁設計技術,能夠讓您的網站在不同裝置上顯示良好。此外,我們的服務還包括多種語言介面支援、內容管理系統、無限頁數網站、無限網上影片、無限網上表格以及專業的Banner設計。
Thumbnail
UI(使用者介面)設計對於用戶體驗至關重要。一個好的UI設計可以讓用戶輕鬆地與應用互動,而糟糕的設計則可能導致用戶感到困惑甚至沮喪。本文將探討五個UI 設計在App開發中常見的錯誤,並提供相應的解決策略。
Thumbnail
這個秋,Chill 嗨嗨!穿搭美美去賞楓,裝備款款去露營⋯⋯你的秋天怎麼過?秋日 To Do List 等你分享! 秋季全站徵文,我們準備了五個創作主題,參賽還有機會獲得「火烤兩用鍋」,一起來看看如何參加吧~
Thumbnail
這次的side bar不一樣了,上一次我好像做的太簡單了,所以完讀率只有14%,我好難過啊(;´д`)ゞ,所以我這準備的內容有多一點。 這次的目標 按鈕的排版 按鈕滑過去會有顏色變化 Side bar能夠展開 箭頭能夠移動至被選擇的物件上 宣告 這次我創了兩個檔案 SideBa
Thumbnail
隨著科技的不斷發展,網頁設計已經從過去的靜態頁面演變為充滿動態、互動性和個性化的體驗。本文介紹了網站設計的最新趨勢,包括夜間模式、無障礙設計、響應式設計、聊天機器人和虛擬助手等功能。
Thumbnail
這篇文章教你如何製作側邊欄,包括準備工作、HTML和CSS的部分,還有一些互動效果。文章涵蓋了連結、圖片、超連結、大小、顏色、排版、flex和滑鼠互動等內容。
Thumbnail
極簡網頁設計依然是主流,以其乾淨、有序的視覺效果和強調內容的功能性,幫助用戶集中注意力於重要信息。
Thumbnail
在當今的IG時代,多媒體內容已成為網頁設計不可或缺的一部分,它不僅豐富了用戶的互動體驗,還能顯著提升網站的吸引力和溝通效率。本文將探討多媒體內容對用戶體驗(UX)的影響、SEO最佳實踐與多媒體的整合方法,並提供成功整合多媒體的網站案例。
Thumbnail
作為一個非常不專業的前端初學者,有陣子常常卡在公司官網,要插入 Youtube 影片無法RWD(響應式)的問題。 跟不熟悉 網頁技術的朋友們介紹,RWD 就是指網頁的排版能跟著螢幕的大小縮放、變化編排,在這個人手一機的時代,特別重要。
Thumbnail
響應式網頁設計(Responsive Web Design, RWD)已成為現代網站開發不可或缺的一部分。響應式設計不僅能夠確保網站在各種設備上都能提供良好的用戶體驗,而且對於搜索引擎優化(SEO)也有著重要的影響。本文將探討響應式設計的最新技術、其對SEO的優勢以及一些成功的實際案例。
圖片大小 漂亮的圖片讓人賞心悅目,對網站美化也是一大加分項,但若是為了呈現自家商品或吸引人的圖片搭配文字,而塞進過量的圖片,導致網站本身太重跑得太慢,容易使客人失去耐性。|SEO工具 隨著時代的進步網路速度也與時俱進,但若網站本身太重,就算網路狀況再良好也無法馬上將網站載好,根據統計,大多數人的
透過Responsive網頁設計技術,能夠讓您的網站在不同裝置上顯示良好。此外,我們的服務還包括多種語言介面支援、內容管理系統、無限頁數網站、無限網上影片、無限網上表格以及專業的Banner設計。
Thumbnail
UI(使用者介面)設計對於用戶體驗至關重要。一個好的UI設計可以讓用戶輕鬆地與應用互動,而糟糕的設計則可能導致用戶感到困惑甚至沮喪。本文將探討五個UI 設計在App開發中常見的錯誤,並提供相應的解決策略。