iT邦幫忙

2023 iThome 鐵人賽

DAY 11
0
Mobile Development

攜手神隊友ChatGPT:攝護腺自我照護App開發歷程!系列 第 11

D11-實戰指南,帶著 SQLite 踏上偉大的航道吧!

  • 分享至 

  • xImage
  •  

Part1:今日目標

1.前言
2.介面功能設計
3.程式實作: Score_History_DB.dart

Part2:今日內容

1.前言

今天的鐵人賽內容,將應用昨天文章: D10-輕巧強大SQLite: Flutter開發的秘密武器所學到的內容做改寫,撰寫具有基本資料庫功能的程式碼,包括:

  • 資料庫創建
  • 插入新資料: 測驗總分 & 測驗日期
  • 取得資料庫特定日期的數據

2.介面功能設計

這份 Score_History_DB.dart 程式碼提供以下功能和介面:

(1) 分數歷史記錄頁面

  • 使用 ScoreHistoryDBPage widget ,該 widget 包含一個名稱為 "Score History" 的頂部導航欄(AppBar)。
  • 顯示分數歷史記錄的介面。

(2) 分數歷史記錄顯示

  • 使用 FutureBuilder widget ,根據 _scoreHistoryDB.getAllScores() 方法返回的數據 Future 來構建分數歷史記錄的 UI。
  • 顯示分數記錄列表,每個分數記錄都包含了總分和測驗日期。
  • 如果數據正在載入,則顯示進度指示器。如果沒有分數歷史記錄數據可用,則顯示文字 "No history records available."。

(3) 資料庫互動

  • 使用 ScoreHistoryDB 類別來管理分數歷史記錄的資料庫操作。
  • 資料庫包括分數記錄表,用於儲存分數記錄的總分和測驗日期。
  • 支援插入新的分數記錄,如果已存在相同日期的記錄則更新。
  • 支援獲取所有分數記錄,並按照日期顯示每個日期的最新分數記錄。

總結,這個 Flutter 應用程式提供了一個分數歷史記錄的介面,可以查看和管理分數記錄。它使用了 SQLite 資料庫來持久化存儲分數記錄,並在使用者介面來顯示這些測驗記錄。

3.程式實作: Score_History_DB.dart

Part_1: 程式架構

https://ithelp.ithome.com.tw/upload/images/20230914/20120073ZU2kSXKvCQ.png

Part_2: 程式碼解說

(1)class ScoreHistoryDBPage extends StatefulWidget {}
  • 定義一個狀態Stateful Widget,名稱為 ScoreHistoryDBPage。
  • 這個 widget 擁有一個 _ScoreHistoryDBPageState 類的內部狀態,用於管理該 widget 的狀態變化。
class ScoreHistoryDBPage extends StatefulWidget {
  @override
  _ScoreHistoryDBPageState createState() => _ScoreHistoryDBPageState();
}
(2)class _ScoreHistoryDBPageState extends State<ScoreHistoryDBPage> {}
  • 定義了一個狀態類 _ScoreHistoryDBPageState,該狀態用於在 UI 上顯示分數歷史記錄的頁面: Score History
  • 從 ScoreHistoryDB 資料庫中獲取數據並以適當的方式顯示在 UI 上,是一個展示資料庫內容的常見模式。
  • 介面成果
    https://ithelp.ithome.com.tw/upload/images/20230914/20120073gOHU0TRjEt.png
class _ScoreHistoryDBPageState extends State<ScoreHistoryDBPage> {
  // 目的: 用來顯示分數歷史記錄的頁面
  late ScoreHistoryDB _scoreHistoryDB;

  @override
  // 是一個生命週期方法,當這個狀態對象被創建時會自動調用
  void initState() {
    super.initState();
    // 使用單一模式的實例 (Use singleton instance)
    _scoreHistoryDB = ScoreHistoryDB
        .instance; // _scoreHistoryDB 變數被初始化為 ScoreHistoryDB.instance
    // 這表示使用單一模式的方式來獲取 ScoreHistoryDB 的實例,用於與分數歷史記錄資料庫進行互動
  }

  @override
  // build() 方法是另一個生命週期方法,當 UI 需要被構建時會自動調用
  Widget build(BuildContext context) {
    return Scaffold(
      // 在這個方法中,Scaffold widget 被返回,該 widget 包含了一個 AppBar 和 Body 區域。
      appBar: AppBar(
        // 應用程式的頂部導航欄,標題被設置為 "Score History"
        title: Text('Score History'),
      ),
      body: FutureBuilder<List<Map<String, dynamic>>>(
        // body 區域是 FutureBuilder widget 
        // 它根據 _scoreHistoryDB.getAllScores() 方法返回的 Future 來構建 UI
        future: _scoreHistoryDB.getAllScores(),
        builder: (context, snapshot) {
          // 在 FutureBuilder 的 builder 方法中,根據不同的 snapshot 狀態來構建不同的 UI
          if (snapshot.connectionState == ConnectionState.waiting) {
            // 如果連接狀態為等待中(ConnectionState.waiting)
            return CircularProgressIndicator(); // 則顯示一個進度指示器
          } else if (snapshot.hasData && snapshot.data!.length > 0) {
            // 如果 snapshot 中有數據且數據長度大於 0 (表示有歷史紀錄)
            final scoreList = snapshot.data!;
            return ListView.builder(
              // 則將數據解析並顯示在一個 ListView 中,每個分數記錄都作為 ListTile 顯示
              itemCount: scoreList.length,
              itemBuilder: (context, index) {
                return ListTile(
                  title: Text('Score: ${scoreList[index]['totalScore']}'),  // 取得測驗總分
                  subtitle:
                      Text('Date: ${scoreList[index]['measurementDate']}'),  // 取得測驗日期
                );
              },
            );
          } else {
            // 如果 snapshot 中沒有數據,則顯示文字 "No history records available."
            return Text('No history records available.');
          }
        },
      ),
    );
  }
}
(3)class ScoreHistoryDB {}
  • 這段程式碼定義了一個名為 ScoreHistoryDB 的類別,該類別負責管理和處理分數歷史記錄的資料庫操作,包括:
    • 資料庫的創建: openDB()
    • 插入分數: insertScore()
    • 獲取所有分數等: getAllScores()
  • 使用單一模式確保只有一個資料庫實例存在,這樣的設計可以方便地在應用程式中管理和操作分數歷史記錄的數據。
  • 這個類別的程式架構如下圖:
    https://ithelp.ithome.com.tw/upload/images/20230914/20120073VzGfOG66EZ.png
class ScoreHistoryDB {
  // ScoreHistoryDB 是一個類別,用於管理分數歷史記錄的資料庫操作
  static final ScoreHistoryDB _instance = ScoreHistoryDB
      ._privateConstructor(); // _instance 是一個靜態屬性,用於保存 ScoreHistoryDB 的單一實例
  static Database? _database; // _database 是一個靜態屬性,用於保存資料庫實例

  // 一個私有的單一模式(Singleton class)
  // 確保只有在內部才能創建 ScoreHistoryDB 的實例
  ScoreHistoryDB._privateConstructor();

  // ScoreHistoryDB get instance 是一個靜態方法,用於獲取 ScoreHistoryDB 的單一實例
  static ScoreHistoryDB get instance => _instance;

  // database 是一個異步 getter 方法,用於獲取資料庫實例
  Future<Database> get database async {
    if (_database != null) return _database!; // 如果 _database 不為空,則直接返回
    _database = await openDB(); // 否則打開資料庫
    return _database!;
  }

  // openDB() 是一個異步方法,用於打開資料庫
  Future<Database> openDB() async {
    print("Opening database...");
    _database = await openDatabase(
      // 它在資料庫創建時執行一些初始化操作,例如創建分數記錄表
      'score_history.db',
      version: 1,
      onCreate: (db, version) {
        db.execute('''
          CREATE TABLE IF NOT EXISTS scores (
            id INTEGER PRIMARY KEY,
            totalScore INTEGER,
            measurementDate TEXT
          )
        ''');
      },
    );
    print("Database opened successfully");
    return _database!;
  }

  // insertScore() 是一個異步方法,用於插入分數記錄到資料庫
  // 這個函式返回一個 Future 物件,表示這個操作是一個非同步操作,並且不會返回任何值
  // (int totalScore, String measurementDate):函式的兩個參數,分別是症狀總分數和測量日期。將這些值作為引數傳遞給函式
  // await 關鍵字:在 Dart 中,用於等待非同步操作儲存的關鍵字
  // 它將使程式碼等待資料庫插入操作儲存,然後再繼續執行後續的程式碼
  // _database 是類中的一個初始化的資料庫連接
  Future<void> insertScore(int totalScore, String measurementDate) async {
    // print("Inserting score: $totalScore on ${measurementDate}");
    // 'scores' 是資料表的名稱,'measurementDate = ?' 是查詢的條件
    // 表示在 'scores' 表中搜尋 'SUBSTR(measurementDate, 1, 10) = ?' 值等於 whereArgs 變數的記錄
    final existingRecord = await _database!.query(
      'scores',
      where: 'SUBSTR(measurementDate, 1, 10) = ?',
      whereArgs: [
        measurementDate.substring(0, 10)
      ], // 對於 SQLite 來說,日期的字串是以 0 為基底的索引
    );
    // print("existingRecord: $existingRecord");  // Debug: 列印變數內容

    if (existingRecord.isNotEmpty) {  // 如果結果不為空,表示資料庫中已經存在該日期的記錄
      await _database!.update(  // 如果存在相同日期的記錄,則使用 _database!.update() 方法來更新該日期的記錄
        'scores',
        {'totalScore': totalScore, 'measurementDate': measurementDate},
        where: 'SUBSTR(measurementDate, 1, 10) = ?',
        whereArgs: [measurementDate.substring(0, 10)],
      );
      print("Score updated successfully");
    } else {  // 如果不存在相同日期的記錄,則使用 _database!.insert() 方法來插入一條新的記錄
      await _database!.insert(  // 這行程式碼呼叫 _database 中的 insert 方法(由資料庫庫或框架提供的內建方法)
        // 將一個新的分數紀錄插入到 'scores' 表中。這個表是儲存分數紀錄的地方。
        // 'totalScore' 和 'measurementDate' 是列名,totalScore 和 measurementDate 則是你傳遞給函式的參數
        'scores',
        {'totalScore': totalScore, 'measurementDate': measurementDate.substring(0, 10)},
      );
      print("Score inserted successfully");  // 在終端機(Terminal)顯示插入資料成功的訊息
    }
  }

  // getAllScores() 是一個異步方法,用於獲取所有分數記錄,按照 id 由大到小排列
  // 實現在回傳結果中去除重複日期,並僅保留當天時間最新的記錄,循以下步驟:
  // 1.使用 GROUP BY 來按日期分組 & 使用 MAX 函數找出每個日期分組中的最大時間,即當天最新的時間。
  // 2.使用 JOIN 來連接原始資料表和包含最大時間的結果,以選取對應的記錄。
  Future<List<Map<String, dynamic>>> getAllScores() async {
    print("Fetching all scores...");
    // 1.建立子查詢,找出每個日期分組中的最新時間 (id數值最大的那筆)
    String subquery = '''
    SELECT SUBSTR(measurementDate, 1, 10) AS date, MAX(id) AS lastId
    FROM scores
    GROUP BY date
    ''';

    // 2.使用子查詢和 JOIN 來取得最新的記錄, 不能left join
    String query = '''
    SELECT s.*
    FROM scores s
    JOIN ($subquery) t ON SUBSTR(s.measurementDate, 1, 10) = t.date AND s.id = t.lastId
    ORDER BY s.id DESC
    ''';
    List<Map<String, dynamic>> result = await _database!.rawQuery(query);
    print("Fetched ${result.length} scores");
    return result;
  }
}

「一個人一生可以愛上很多人。等你終於得到屬於你的幸福,你就會明白以前的傷痛其實是一種財富,它讓你學會把握和珍惜你愛的人。」 –電影【鐵達尼號】TITANIC
One may fall in love with many people during the lifetime. When you finally get your own happiness, you will understand the previous sadness is kind of treasure, which makes you better to hold and cherishthe people you love.-TITANIC
今天要聽林曉培的《心動》,金城武的帥臉怎能讓人不心動haha

https://ithelp.ithome.com.tw/upload/images/20230914/20120073I4UQmfWo7U.jpg
圖片來源


上一篇
D10-輕巧強大SQLite: Flutter開發的秘密武器
下一篇
D12-慢活片刻,Dart x Flutter 學習之旅
系列文
攜手神隊友ChatGPT:攝護腺自我照護App開發歷程!30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言