大家好,歡迎來到第九天!昨天,我們試著打造了一個具備完整驗證功能的表單頁面,讓 App 從「只能看」進化到了「能夠互動」。但我們也留下了一個懸念:當我們在表單頁按下「儲存」後,資料被印出就消失了,主畫面的列表紋風不動。
這個問題觸及了 App 開發的核心——狀態管理 (State Management)。所謂「狀態」,就是驅動 App 畫面的資料。我們今天的任務,就是學習 Flutter 中最基礎也最重要的一種跨頁面狀態管理方式,讓「新增消費」頁面的資料能夠安全地「回家」,並更新主畫面的列表。
這裡我們可以往前翻看 Day 4 的文章,有提到有關 StatelessWidget
(無狀態元件) 和 StatefulWidget
(有狀態元件)這兩個 Widget 的特性。
StatelessWidget
的特性是「不可變 (immutable)」。一旦它被建立,它內部的所有屬性(包括我們的 transactions 列表)都不能再被更改。這就是為什麼我們無法直接新增資料到列表中的原因。
這裡只要將 HomePage
轉換為 StatefulWidget
,賦予它「持有並管理可變狀態」的能力,就可以解決了。
HomePage
的 extends StatelessWidget
改為 extends StatefulWidget
。_HomePageState
類別,並將原本 HomePage
的 build
方法和 transactions
列表剪下並貼上到這個新類別中。// lib/main.dart
import 'package:snapsaver/models/transaction.dart';
// 將 Transaction 類別移到 models/transaction.py 統一管理
// ... MyApp class ...
class HomePage extends StatefulWidget {
// 因為 HomePage 不再持有狀態,建構子可以再次變回 const
const HomePage({super.key});
@Override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
// 將 transactions 列表移動到 State 內部
List<Transaction> transactions = [
// ... 我們的假資料 ...
];
@override
Widget build(BuildContext context) {
return Scaffold(
// ... 原本的 build 內容 ...
);
}
}
現在,我們需要修改「新增消費」頁面,讓它在關閉時能夠攜帶資料。Navigator.pop
方法可以接受一個可選的參數,這個參數就是我們要回傳的資料。
// lib/add_transaction_page.dart -> _submitForm method
import 'package:snapsaver/models/transaction.dart';
void _submitForm() {
if (_formKey.currentState!.validate()) {
// 1. 根據表單內容,建立一個新的 Transaction 物件
final newTransaction = Transaction(
title: _titleController.text,
amount: double.parse(_amountController.text),
category: '測試分類', // 暫時寫死
date: DateTime.now(),
);
// 2. 將 newTransaction 作為參數傳入 pop 方法
Navigator.pop(context, newTransaction);
}
}
資料已經被送出,HomePage 該如何接收呢?Navigator.push
方法會回傳一個 Future
。Future
代表一個「未來才會完成的操作結果」。我們可以透過 async / await
語法,來「等待」這個 Future
完成,並取得它回傳的資料。
回到 lib/main.dart
,我們需要修改 FloatingActionButton
的 onPressed
行為。
// lib/main.dart -> _HomePageState class
// 建立一個新的 async 方法來處理導航與接收資料
void _navigateToAddTransactionPage() async {
// 使用 await "等待" AddTransactionPage 關閉並回傳資料
final result = await Navigator.push(
context,
MaterialPageRoute(builder: (context) => const AddTransactionPage()),
);
// 當 result 不是 null 時 (代表使用者是儲存而不是直接返回)
// 我們需要更新狀態
if (result != null && result is Transaction) {
// 下一步驟會實作這裡
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
// ...
floatingActionButton: FloatingActionButton(
// 將 onPressed 指向我們的新方法
onPressed: _navigateToAddTransactionPage,
child: const Icon(Icons.add),
),
// ...
);
}
這是最後,也是最神奇的一步。當我們在 StatefulWidget
中想更新資料並重繪畫面時,必須使用 setState
方法。
setState
會做兩件事:
build
方法來更新畫面!」將 Step 4 中留空的 if
區塊補上:
// lib/main.dart -> _HomePageState -> _navigateToAddTransactionPage method
if (result != null && result is Transaction) {
// 呼叫 setState 來更新 UI
setState(() {
// 在這裡更新我們的列表
// 使用 insert(0, ...) 將新項目加到列表最前面,以便立即看到
transactions.insert(0, result);
});
}
至關重要:所有會改變 UI 狀態的資料操作,都必須放在 setState
的回呼函式中!
現在,重啟你的 App。點擊 +
按鈕,新增一筆消費,按下儲存... 見證奇蹟的時刻到了!你會發現,主畫面的列表立刻出現了你剛剛新增的那筆資料!
今天我們完成了 App 功能的閉環,實現了真正的資料互動!我們深入了解了 Flutter 狀態管理的基本功:
StatelessWidget
與 StatefulWidget
的核心差異。StatelessWidget
重構為 StatefulWidget
以管理可變狀態。Navigator.pop(context, data)
回傳資料的技巧。async/await
等待 Navigator.push
的回傳結果。setState
是觸發 StatefulWidget
畫面重建的唯一真理。我們今天所學的,是 Flutter 中最基礎、但使用最頻繁的「手動」狀態管理模式。
然而,我們的 App 現在有一個新問題:所有新增的資料都只存在於記憶體中。一旦你關閉並重啟 App,它們就會消失。該如何讓資料被永久保存下來呢?
明天,我們將迎來一個重大的里程碑:引入後端服務!我們將學習如何設定 Firebase,並使用 Firestore 雲端資料庫來永久儲存我們的消費紀錄。