iT邦幫忙

2025 iThome 鐵人賽

DAY 21
0
Mobile Development

《30 天 Flutter:跨平台 AI 行程規劃 App》系列 第 21

Day 21 - 從單機到分享:讓旅伴也能擁有這份完美行程

  • 分享至 

  • xImage
  •  

前幾天已經成功地讓使用者即使在沒有網路的情況下,也能安心地規劃行程。但我開始思考,一個完美的行程,如果只能自己用,是不是有點可惜?

我想做的,不只是讓 App 在離線時能夠運作,而是要讓它能打破裝置的界線,真正實現資料的自由流通。

今天,將實作分享功能,這意味著將建立一個完整的匯入與匯出流程。這將允許使用者將辛苦規劃的行程打包成一個檔案,輕鬆分享給旅伴~


匯出流程:將行程打包成 JSON 檔案

要讓資料能夠被分享,第一步就是將它從資料庫中「取出」並轉換成一個通用格式。我選擇使用 JSON,因為它輕量且易於跨平台傳輸。這個過程主要分為三個步驟:

1. 從資料庫讀取資料

為了匯出一個完整的行程,需要一個函式,將 Drift 資料庫中的關聯資料,轉換成一個適合 JSON 輸出的物件。此外,在進行資料匯出與匯入時,除了基本的流程外,還有一些關鍵細節需要注意,以確保資料的完整性與相容性。

  • 資料格式與型別轉換

    • 時間與時區:為確保跨裝置資料一致,建議在資料庫中統一儲存 UTC 時間。在匯出成 JSON 時,建議統一成 RFC3339 格式。
    • 空值與必填:JSON 中 null 的處理至關重要。建議在將資料庫的空值轉換成 JSON 時,為前端必填欄位補上預設值,避免應用程式出錯。
  • 效能與架構考量

    • 巢狀關聯:處理如「行程下有多個活動」這類巢狀資料時,避免在迴圈中逐筆查詢(N+1 問題)。建議使用 JOIN 或一次性 IN 查詢,再在記憶體中將資料組合成巢狀結構,以大幅提升效能,這部分在 Day 17 - Drift CRUD 入門 中已有詳細介紹,這裡便不再贅述。
    • 版本相容性:為應對未來 App 版本更新時資料庫結構的改變,建議在匯出的 JSON 裡加入 schemaVersionappVersion 欄位,方便在匯入時進行相容性檢查與處理。

2. 使用 share_plus 套件分享檔案

要讓使用者能夠將行程匯出成 JSON 檔案並分享,將使用 share_plus 套件。首先,在你的 pubspec.yaml 檔案中加入這個套件:

dependencies:
  share_plus: ^11.1.0

接著,可以參考以下程式碼來實作匯出與分享的邏輯:

/// 匯出指定行程為 JSON 檔,並透過 share_plus 分享出去
Future<void> exportTripAsJson(int tripId, String tripName) async {
  // 1. 從資料庫取出指定行程的完整資料(含活動與子活動)
  final exportData = await db.tripsDao.getTripDataForExport(tripId);

  // 2. 如果找不到該行程,直接結束
  if (exportData == null) {
    // 處理找不到行程的情況,例如顯示一個提示訊息
    return;
  }

  // 3. 將 Map 資料轉成 JSON 字串
  String jsonString = jsonEncode(exportData);

  // 4. 準備分享的檔案資料
  final XFile fileToShare = XFile.fromData(
    // 將 JSON 字串轉成二進位資料 (UTF-8 編碼)
    utf8.encode(jsonString),
    // 指定 MIME type 為 application/json
    mimeType: 'application/json',
    // 讓分享的檔案名稱固定為「{tripName}.json」
    name: '$tripName.json',
  );

  // 5. 呼叫 share_plus 執行分享動作(跳出系統的分享視窗)
  await Share.shareXFiles([fileToShare], text: '分享我的行程:$tripName');
}

這段程式碼首先會從資料庫中取得完整的行程資料,然後將它轉換成一個 JSON 格式的檔案。最後,透過 share_plusshareXFiles 函式,呼叫系統的分享視窗,讓使用者可以選擇任何 App 來傳送這個檔案。


匯入流程:讓 JSON 檔案重生為資料庫紀錄

這個過程是匯出的逆向操作。需要讀取檔案,解析 JSON,最後將資料寫入資料庫。這裡最關鍵的挑戰是,不能使用舊有的 ID,因為資料庫會為新紀錄自動生成 ID,否則會發生衝突。

1. 使用 file_picker 選擇檔案

要讓使用者能夠從手機中匯入 JSON 檔案,將使用 file_picker 套件。首先,在 pubspec.yaml 中加入該套件:

dependencies:
  file_picker: ^10.3.2

程式碼範例

實作檔案選擇與讀取功能:

/// 從使用者手機中選擇 JSON 檔案,並匯入行程資料庫
Future<void> importTripFromJsonFile() async {
  // 1. 開啟檔案選擇器,只允許選擇 .json 檔
  FilePickerResult? result = await FilePicker.platform.pickFiles(
    type: FileType.custom,          // 自訂檔案類型
    allowedExtensions: ['json'],    // 只允許 .json 檔案
  );

  // 2. 檢查使用者是否選擇了檔案
  if (result != null) {
    // 3. 取得使用者選中的檔案
    File file = File(result.files.single.path!);

    // 4. 讀取檔案內容為字串
    String jsonContent = await file.readAsString();

    // 5. 將字串解析成 Map 結構
    Map<String, dynamic> jsonData = jsonDecode(jsonContent);

    // 6. 呼叫資料庫 DAO 的匯入函式,把 JSON 資料存入資料庫
    //    假設你已經實作 importTripFromJsonFile 方法
    await db.tripsDao.importTripFromJsonFile(jsonData);
  }
  // 若 result 為 null,表示使用者取消選擇檔案,直接結束
}

這段程式碼會打開檔案選擇器,並篩選出副檔名為 .json 的檔案。使用者選取後,會讀取檔案內容並解析為 Dart 物件,準備進行後續的資料庫寫入操作。

2. 寫入資料庫

匯入流程中最關鍵的一步,就是將整個行程(包含 Trip、Activities 與 ChildActivities)正確寫入資料庫。為了保證資料的一致性與完整性,使用 Drift 的 transaction 功能,將所有寫入操作包裹在同一個交易中。

Transaction 與 Batch 的角色

  • Transaction
    • 包裹整個匯入流程,確保原子性:要麼全部成功,要麼全部回滾。
    • 適合逐步插入有父子關聯的資料,因為每次 insert 都能立即取得自增 ID,這對建立 Trip → Activities → ChildActivities 的關聯至關重要。
  • Batch
    • 適合一次性插入大量、沒有父子關聯的資料,可以減少 SQL 呼叫次數,提升效能。
    • 在匯入流程中,可在每個 Activity 下的多個 ChildActivities 使用 batch 批次插入,兼顧效能與父子關聯正確性。

整合效能與正確性的做法

  1. 先在 transaction 中插入 Trip,取得 tripId。
  2. 對每個 Activity 單筆插入,取得 activityId。
  3. 對每個 Activity 底下的多個 ChildActivities,可以在 transaction 內使用 batch 批次插入,填入對應的 activityId。

這種設計既確保了資料的原子性,也保證父子關聯正確,對大量子活動的寫入仍能保持高效能,避免部分寫入失敗導致資料不完整。詳細實作可以參考前幾天寫的 Day 19 - Drift 實戰:從卡頓到流暢,打造高效能的批次寫入術 了解如何做批次處理。


今日成果

匯出流程 匯入流程

上一篇
Day 20 - Drift 實戰:告別手動更新,讓 UI 畫面自動同步
下一篇
Day 22 - 行程 APP 沒地圖怎麼行?從零打造地圖互動功能,試試 ChatGPT 的教學體驗
系列文
《30 天 Flutter:跨平台 AI 行程規劃 App》22
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言