iT邦幫忙

2025 iThome 鐵人賽

DAY 22
0
自我挑戰組

攜手 AI 從零開始打造一款 Flutter 應用程式系列 第 22

Day 22: 專案打磨 (II) - 建立統一的訊息回饋系統

  • 分享至 

  • xImage
  •  

前言

大家好,歡迎來到第二十二天!昨天,我們透過改造分類選擇器,讓 App 的核心輸入體驗更加專業。今天,我們將繼續我們的「專案打磨」之旅,這次要優化的對象是使用者回饋

目前我們的 App 在執行完各種操作後,給予使用者的提示並不統一:

  • 在 HomePage 滑動刪除時,會彈出一個 SnackBar。
  • 在 AddTransactionPage 儲存成功後,頁面直接返回,使用者可能不確定是否真的儲存成功。

一個優秀的 App 應該能像一個體貼的助理,在使用者完成每個操作後,都給予清晰且一致的肯定或提示。今天,我們的目標就是建立一個中央化的訊息服務 (MessengerService),讓我們可以用一行程式碼,在 App 的任何地方,輕鬆地彈出風格統一的成功或失敗提示框。

Step 1: 跨越 BuildContext 的鴻溝 - GlobalKey

要顯示一個 SnackBar,我們需要呼叫 ScaffoldMessenger.of(context).showSnackBar(...)。這裡的 context 是關鍵,它代表了 Widget 在元件樹中的位置。這在 Widget 內部很容易取得,但如果我們想在 Service(例如 FirestoreService)或是一個沒有 context 的地方顯示提示,該怎麼辦?

我們可以使用 GlobalKey<ScaffoldMessengerState>

GlobalKey 是一個可以在整個 App 中保持唯一的鑰匙。我們可以將這把鑰匙「鎖」在我們 App 最頂層的 ScaffoldMessenger 上,之後就可以在任何地方,用這把鑰匙直接「打開」它並顯示 SnackBar,完全無需傳遞 context

Step 2: 建立 MessengerService - 我們的訊息中心

讓我們來建立一個專門處理訊息顯示的服務。

  1. lib/services 資料夾下,建立一個新檔案 messenger_service.dart
  2. 在檔案中定義我們的 GlobalKeyMessengerService 類別。
// 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,
        ),
      );
    }
  }
}

Step 3: 將 GlobalKey 綁定到 MaterialApp

鑰匙已經打造好了,現在需要把它插到鎖上。

打開 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

Step 4: 在 App 中全面應用 MessengerService

最後一步,讓我們來享受重構的果實!我們將把散落在各處的 SnackBar 呼叫和 print 日誌,替換為統一的 MessengerService 呼叫。

  1. HomePage (滑動刪除)
// lib/main.dart -> _HomePageState -> Dismissible -> onDismissed
onDismissed: (direction) {
  _firestoreService.deleteTransaction(
    userId: user!.uid,
    transactionId: transaction.id,
  );
  // 替換:使用統一的成功提示
  MessengerService.showSuccess('${transaction.title} 已刪除');
},
  1. AddTransactionPage (儲存/編輯)
      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');
      }
  1. AuthPage (登入/註冊)
  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.showSuccessMessengerService.showError

現在,App 中所有的核心操作,無論成功或失敗,都會彈出風格一致、顏色分明的提示訊息,使用者體驗大大提升!

https://ithelp.ithome.com.tw/upload/images/20251006/20163912XJaCHkYAYh.png

今日結語

今天,我們再次透過重構,提升了 App 的品質。我們建立了一個中央化的 MessengerService,它帶來了三大好處:

  1. 一致性:所有回饋訊息的風格、顏色、行為都保持統一。
  2. 便利性:只需一行 MessengerService.showSuccess('...') 即可在任何地方呼叫,程式碼更簡潔。
  3. 解耦:我們的業務邏輯層(例如 Service)現在也可以向 UI 發送訊息,而無需依賴 BuildContext,架構更清晰。

在我們的主頁上,還有一個非常重要的 UI 元素至今仍然是靜態的——「本月總支出」卡片。明天,我們將讓這個數字也「活」起來,學習如何利用 Stream 的強大能力,即時計算並顯示使用者當前的總消費金額。


上一篇
Day 21: 專案打磨 (I) - 專業的分類選擇器
下一篇
Day 23: 讓數字說話 - 即時計算本月總支出
系列文
攜手 AI 從零開始打造一款 Flutter 應用程式23
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言