當狀態變更時重建 UI
說明如何使用 ChangeNotifier 管理狀態的指南。
學習使用 ListenableBuilder 自動重建 UI,並以 switch 運算式處理所有可能的狀態。
你將完成的事項
Steps
1
介紹
介紹
View 層是你的 UI,在 Flutter 中,這指的是應用程式的元件 (Widget)。
就本教學而言,重要的部分在於將 UI 與 ViewModel 的資料變更連結起來,使其能夠回應。
ListenableBuilder
是一個能夠「監聽」ChangeNotifier
的元件,當所提供的 ChangeNotifier 呼叫 notifyListeners() 時,它會自動重建。
2
建立文章 View 元件
建立文章 View 元件
建立 ArticleView 元件,用來管理頁面的版面配置和 ViewModel 的生命週期。
由於它必須在渲染之前明確初始化資料擷取,因此將其實作為 StatefulWidget。
從建立基本的有狀態結構開始:
import 'package:flutter/material.dart';
class ArticleView extends StatefulWidget {
const ArticleView({super.key});
@override
State<ArticleView> createState() => _ArticleViewState();
}
class _ArticleViewState extends State<ArticleView> {
// The view model will be instantiated here next.
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Wikipedia Flutter')),
body: const Center(child: Text('Loading...')),
);
}
}
3
實例化文章 View Model
實例化文章 View Model
接著,初始化 ArticleViewModel 並將其對應到狀態的生命週期。
在 initState() 中提供 ViewModel 並執行 fetchArticle():
class ArticleView extends StatefulWidget {
const ArticleView({super.key});
@override
State<ArticleView> createState() => _ArticleViewState();
}
class _ArticleViewState extends State<ArticleView> {
final ArticleViewModel viewModel = ArticleViewModel(ArticleModel());
@override
void initState() {
super.initState();
viewModel.fetchArticle();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Wikipedia Flutter')),
body: const Center(child: Text('Loading...')),
);
}
}
4
更新應用程式以加入文章 View
更新應用程式以加入文章 View
透過更新 MainApp 以加入已完成的 ArticleView,將所有部分串聯在一起。
用以下更新後的版本取代現有的 MainApp:
class MainApp extends StatelessWidget {
const MainApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(home: ArticleView());
}
}
這個變更將從基於主控台的測試切換到具備適當狀態管理的完整 UI 體驗。
5
監聽狀態變更
監聽狀態變更
將 UI 包覆在 ListenableBuilder
中以監聽狀態變更,並傳入一個 ChangeNotifier 物件。
在此範例中,ArticleViewModel 繼承了 ChangeNotifier。
class ArticleView extends StatefulWidget {
const ArticleView({super.key});
@override
State<ArticleView> createState() => _ArticleViewState();
}
class _ArticleViewState extends State<ArticleView> {
final ArticleViewModel viewModel = ArticleViewModel(ArticleModel());
@override
void initState() {
super.initState();
viewModel.fetchArticle();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Wikipedia Flutter')),
body: ListenableBuilder(
listenable: viewModel,
builder: (context, child) {
return const Center(child: Text('Loading...'));
},
),
);
}
}
ListenableBuilder 使用建構器 (builder) 模式,
這個模式需要一個回呼(callback)而非 child 元件來建置其下方的元件樹。
這些元件非常靈活,因為你可以在回呼(callback)中執行操作,
根據狀態建置不同的元件。
6
處理 View Model 的可能狀態
處理 View Model 的可能狀態
回想 ArticleViewModel,它有三個 UI 會關注的屬性:
Summary? summarybool isLoadingException? error
根據這些屬性的組合狀態,UI 可以顯示不同的元件。 使用 Dart 對 switch 運算式的支援, 以簡潔、易讀的方式處理所有可能的組合:
class ArticleView extends StatefulWidget {
const ArticleView({super.key});
@override
State<ArticleView> createState() => _ArticleViewState();
}
class _ArticleViewState extends State<ArticleView> {
final ArticleViewModel viewModel = ArticleViewModel(ArticleModel());
@override
void initState() {
super.initState();
viewModel.fetchArticle();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Wikipedia Flutter')),
body: Center(
child: ListenableBuilder(
listenable: viewModel,
builder: (context, _) {
return switch ((
viewModel.isLoading,
viewModel.summary,
viewModel.error,
)) {
(true, _, _) => const CircularProgressIndicator(),
(_, _, final Exception e) => Text('Error: $e'),
(_, final summary?, _) => ArticlePage(
summary: summary,
nextArticleCallback: viewModel.fetchArticle,
),
_ => const Text('Something went wrong!'),
};
},
),
),
);
}
}
這是一個很好的範例,說明像 Flutter 這樣的宣告式、響應式框架 和像 MVVM 這樣的模式如何相互配合: UI 根據狀態渲染,並在狀態需要時自動更新, 但 UI 本身不管理任何狀態或更新自身的過程。 商業邏輯與渲染完全分離。
7
完成 UI
完成 UI
剩下的唯一工作是使用 View Model 提供的屬性和方法來建置 UI。
現在建立一個 ArticlePage 元件,用於顯示實際的文章內容。
這個可重複使用的元件接受摘要資料和一個回呼(callback)函式:
class ArticlePage extends StatelessWidget {
const ArticlePage({
super.key,
required this.summary,
required this.nextArticleCallback,
});
final Summary summary;
final VoidCallback nextArticleCallback;
@override
Widget build(BuildContext context) {
return const Center(
child: Text('Article content will be displayed here...'),
);
}
}
8
加入可捲動的版面配置
加入可捲動的版面配置
用可捲動的直欄版面配置取代佔位符:
class ArticlePage extends StatelessWidget {
const ArticlePage({
super.key,
required this.summary,
required this.nextArticleCallback,
});
final Summary summary;
final VoidCallback nextArticleCallback;
@override
Widget build(BuildContext context) {
return const SingleChildScrollView(
child: Column(
children: [Text('Article content will be displayed here...')],
),
);
}
}
9
加入文章內容與按鈕
加入文章內容與按鈕
以文章元件和導覽按鈕完成版面配置:
class ArticlePage extends StatelessWidget {
const ArticlePage({
super.key,
required this.summary,
required this.nextArticleCallback,
});
final Summary summary;
final VoidCallback nextArticleCallback;
@override
Widget build(BuildContext context) {
return SingleChildScrollView(
child: Column(
children: [
ArticleWidget(summary: summary),
ElevatedButton(
onPressed: nextArticleCallback,
child: const Text('Next random article'),
),
],
),
);
}
}
10
建立 ArticleWidget
建立 ArticleWidget
ArticleWidget 負責以適當的樣式和條件式渲染來顯示實際的文章內容。
設定基本文章結構
#從接受 summary 參數的元件開始:
class ArticleWidget extends StatelessWidget {
const ArticleWidget({super.key, required this.summary});
final Summary summary;
@override
Widget build(BuildContext context) {
return const Text('Article content will be displayed here...');
}
}
加入內距(padding)和直欄版面配置
#將內容包覆在適當的內距(padding)和版面配置中:
class ArticleWidget extends StatelessWidget {
const ArticleWidget({super.key, required this.summary});
final Summary summary;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(8),
child: Column(
spacing: 10,
children: [const Text('Article content will be displayed here...')],
),
);
}
}
加入條件式圖片顯示
#加入只在有圖片時才顯示的文章圖片:
class ArticleWidget extends StatelessWidget {
const ArticleWidget({super.key, required this.summary});
final Summary summary;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(8),
child: Column(
spacing: 10,
children: [
if (summary.hasImage) Image.network(summary.originalImage!.source),
const Text('Article content will be displayed here...'),
],
),
);
}
}
以樣式化文字內容完成
#以具備適當樣式的標題、描述和摘錄取代佔位符文字:
class ArticleWidget extends StatelessWidget {
const ArticleWidget({super.key, required this.summary});
final Summary summary;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(8),
child: Column(
spacing: 10,
children: [
if (summary.hasImage) Image.network(summary.originalImage!.source),
Text(
summary.titles.normalized,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.displaySmall,
),
if (summary.description != null)
Text(
summary.description!,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.bodySmall,
),
Text(summary.extract),
],
),
);
}
}
這個元件展示了幾個重要的 UI 概念:
-
條件式渲染:
if陳述式只在內容可用時才顯示。 - 文字樣式: 不同的文字樣式利用 Flutter 的主題系統建立視覺層次。
-
適當的間距:
spacing參數提供一致的垂直間距。 -
溢位處理:
TextOverflow.ellipsis防止文字破壞版面配置。
11
執行完整的應用程式
執行完整的應用程式
最後一次熱重載應用程式。你現在應該會看到:
- 初始文章載入時的讀取轉圈動畫。
- 文章的標題、描述和摘要摘錄。
- 圖片(如果文章有的話)。
- 載入另一篇隨機文章的按鈕。
要查看響應式 UI 的實際運作, 請點擊 Next random article 按鈕。 應用程式會顯示載入狀態,擷取新資料,並自動更新畫面。
12
回顧
回顧
你完成了什麼
以下是你在本課程中建置和學習內容的摘要。使用 ListenableBuilder 自動重建 UI
ListenableBuilder 監聽你的 ViewModel,並在每次呼叫 notifyListeners() 時自動重建其子元件。 在 MVVM 模式中,這是 ViewModel 與 View 之間的關鍵連結。
以 switch 運算式處理所有可能的狀態
使用 switch 運算式,你為所有可能的狀態組合提供了適當的使用者介面, 條件式地顯示載入轉圈動畫、錯誤訊息, 或實際的文章內容。 有了這樣的處理,UI 現在更加健全且完整。
建置具備適當樣式的完整 View 層
你建立了具備條件式渲染、文字樣式、 適當間距和溢位處理的 ArticleView、ArticlePage 和 ArticleWidget。 這些是你在每個 Flutter 應用程式中都會用到的核心 UI 模式。
完成 MVVM 架構
你已建置了一個具備 Model(資料操作)、 ViewModel(狀態管理)和 View(響應式 UI)層的完整應用程式。 這種關注點分離有助於讓你的程式碼更易於測試、維護和擴展。
13
自我測試
自我測試
ListenableBuilder 測驗
1 / 2-
監聽 ChangeNotifier,並在 `notifyListeners()` 被呼叫時自動重建其子元件。
That's right!
ListenableBuilder 監聽一個 Listenable,並在收到通知時重建其 builder 函式。
-
根據 ChangeNotifier 建立動畫。
Not quite
ListenableBuilder 是在狀態變更時重建 UI,而非專門用於動畫。
-
手動控制元件何時應該重建。
Not quite
當 notifyListeners() 被呼叫時,重建會自動發生;你無法手動控制它。
-
快取元件建置以提升效能。
Not quite
ListenableBuilder 是關於響應式更新,而非快取。
-
只在元件首次掛載時。
Not quite
它在每次 `notifyListeners()` 被呼叫時都會重建,不只是在掛載時。
-
當父元件重建時。
Not quite
ListenableBuilder 根據 Listenable 重建,而非根據父元件的重建。
-
每次應用程式的畫面更新時。
Not quite
ListenableBuilder 只在收到通知時重建,而非每一幀都重建。
-
當它所監聽的 Listenable 呼叫 notifyListeners() 時。
That's right!
ListenableBuilder 訂閱 Listenable,並在每次呼叫 `notifyListeners()` 時重建其 builder 函式。
Unless stated otherwise, the documentation on this site reflects Flutter 3.44.0. Page last updated on 2026-06-14. View source or report an issue.