大家好,歡迎來到第二十二天!昨天,我們透過改造分類選擇器,讓 App 的核心輸入體驗更加專業。今天,我們將繼續我們的「專案打磨」之旅,這次要優化的對象是使用者回饋。
目前我們的 App 在執行完各種操作後,給予使用者的提示並不統一:
一個優秀的 App 應該能像一個體貼的助理,在使用者完成每個操作後,都給予清晰且一致的肯定或提示。今天,我們的目標就是建立一個中央化的訊息服務 (MessengerService
),讓我們可以用一行程式碼,在 App 的任何地方,輕鬆地彈出風格統一的成功或失敗提示框。
要顯示一個 SnackBar
,我們需要呼叫 ScaffoldMessenger.of(context).showSnackBar(...)
。這裡的 context
是關鍵,它代表了 Widget 在元件樹中的位置。這在 Widget 內部很容易取得,但如果我們想在 Service(例如 FirestoreService
)或是一個沒有 context
的地方顯示提示,該怎麼辦?
我們可以使用 GlobalKey<ScaffoldMessengerState>
。
GlobalKey
是一個可以在整個 App 中保持唯一的鑰匙。我們可以將這把鑰匙「鎖」在我們 App 最頂層的 ScaffoldMessenger
上,之後就可以在任何地方,用這把鑰匙直接「打開」它並顯示 SnackBar
,完全無需傳遞 context
。
讓我們來建立一個專門處理訊息顯示的服務。
lib/services
資料夾下,建立一個新檔案 messenger_service.dart
。GlobalKey
和 MessengerService
類別。// lib/services/messenger_service.dart
import 'package:flutter/material.dart';
// 1. 在檔案頂層定義一個 GlobalKey
final GlobalKey<ScaffoldMessengerState> messengerKey =
GlobalKey<ScaffoldMessengerState>();
// 2. 建立一個靜態服務類別,方便在任何地方直接呼叫
class MessengerService {
static void showSuccess(String message) {
// 檢查 key 是否已經綁定到 widget 上
if (messengerKey.currentState != null) {
// 移除當前可能正在顯示的 SnackBar
messengerKey.currentState!.removeCurrentSnackBar();
// 顯示新的 SnackBar
messengerKey.currentState!.showSnackBar(
SnackBar(
content: Text(message),
backgroundColor: Colors.green.shade700,
behavior: SnackBarBehavior.floating, // 讓 SnackBar 浮動
),
);
}
}
static void showError(String message) {
if (messengerKey.currentState != null) {
messengerKey.currentState!.removeCurrentSnackBar();
messengerKey.currentState!.showSnackBar(
SnackBar(
content: Text(message),
backgroundColor: Colors.red.shade700,
behavior: SnackBarBehavior.floating,
),
);
}
}
}
鑰匙已經打造好了,現在需要把它插到鎖上。
打開 lib/main.dart
,在 MyApp
中找到我們的 MaterialApp
Widget,並將 scaffoldMessengerKey
屬性設定為我們剛剛建立的 messengerKey
。
// lib/main.dart
import 'package:snapsaver/services/messenger_service.dart'; // 導入我們的服務
// ...
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
// ... 其他屬性 ...
// 將 GlobalKey 綁定到 MaterialApp
scaffoldMessengerKey: messengerKey,
home: const AuthGate(),
);
}
}
現在起,App 內的所有 Scaffold
都會共用這個由 messengerKey
控制的 ScaffoldMessenger
。
最後一步,讓我們來享受重構的果實!我們將把散落在各處的 SnackBar
呼叫和 print
日誌,替換為統一的 MessengerService
呼叫。
HomePage
(滑動刪除)// lib/main.dart -> _HomePageState -> Dismissible -> onDismissed
onDismissed: (direction) {
_firestoreService.deleteTransaction(
userId: user!.uid,
transactionId: transaction.id,
);
// 替換:使用統一的成功提示
MessengerService.showSuccess('${transaction.title} 已刪除');
},
try {
if (isEditMode) {
await _firestoreService.updateTransaction(
userId: user.uid,
transactionId: widget.transaction!.id,
newTitle: _titleController.text,
newAmount: double.parse(_amountController.text),
newCategory: _selectedCategory!,
);
MessengerService.showSuccess('紀錄已更新');
} else {
await _firestoreService.addTransaction(
userId: user.uid,
title: _titleController.text,
amount: double.parse(_amountController.text),
category: _selectedCategory!,
);
MessengerService.showSuccess('紀錄已新增');
}
} catch (e) {
MessengerService.showError('操作失敗: $e');
}
Future<void> _signUp() async {
try {
await FirebaseAuth.instance.createUserWithEmailAndPassword(
email: _emailController.text.trim(),
password: _passwordController.text.trim(),
);
// 3. 替換:使用 MessengerService 顯示成功訊息
// AuthGate 會自動處理頁面跳轉,所以這裡可以不用顯示成功訊息,
// 但為了示範,我們仍然可以加上。
// MessengerService.showSuccess('註冊成功!');
} on FirebaseAuthException catch (e) {
final message =
e.code == 'email-already-in-use' ? '此 Email 已被註冊' : '註冊失敗: ${e.message}';
// 3. 替換:使用 MessengerService 顯示錯誤訊息
MessengerService.showError(message);
}
}
Future<void> _signIn() async {
try {
await FirebaseAuth.instance.signInWithEmailAndPassword(
email: _emailController.text.trim(),
password: _passwordController.text.trim(),
);
// 登入成功後,AuthGate 會自動切換到 HomePage,
// 所以這裡通常不需要顯示 SnackBar。
} on FirebaseAuthException catch (e) {
final message =
e.code == 'user-not-found' || e.code == 'wrong-password'
? 'Email 或密碼錯誤'
: '登入失敗: ${e.message}';
// 3. 替換:使用 MessengerService 顯示錯誤訊息
MessengerService.showError(message);
}
}
_scaffoldKey
和 _showSnackBar
輔助函式。MessengerService
。_showSnackBar
的地方,根據情境分別替換為 MessengerService.showSuccess
和 MessengerService.showError
。現在,App 中所有的核心操作,無論成功或失敗,都會彈出風格一致、顏色分明的提示訊息,使用者體驗大大提升!
今天,我們再次透過重構,提升了 App 的品質。我們建立了一個中央化的 MessengerService
,它帶來了三大好處:
MessengerService.showSuccess('...')
即可在任何地方呼叫,程式碼更簡潔。BuildContext
,架構更清晰。在我們的主頁上,還有一個非常重要的 UI 元素至今仍然是靜態的——「本月總支出」卡片。明天,我們將讓這個數字也「活」起來,學習如何利用 Stream
的強大能力,即時計算並顯示使用者當前的總消費金額。