iT邦幫忙

2025 iThome 鐵人賽

DAY 12
0
Mobile Development

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

Day 12 - 從靜態畫面到功能實現:核心行程 UI 宣告完成

  • 分享至 

  • xImage
  •  

今天的重點是把 資料模型狀態管理 串起來,然後透過 UI 驗證 時間區塊防呆設計,確認資料流與使用流程是否順暢。今天主要目標頁面:

  • 添加行程頁
  • 行程總覽頁

這些頁面都會跟資料模型緊密結合,UI 的變動、資料流的更新都能同步反映。


專案資料夾整理

先把資料與畫面元件整理好,方便維護與擴充:

lib/
├─ models/                   # 資料模型(用 json_serializable 生成)
│   ├─ trip.dart              
│   ├─ activity.dart          
│   └─ child_activity.dart    
└─ views/                     
       └─ activity_editor/    # 行程編輯相關頁面
              └─ activity_editor_view.dart  
              └─ child_activity_form.dart   
       └─ trip_detail_view.dart  # 行程總覽頁面

清楚區分資料模型與畫面元件,維護起來更直覺。


Model 定義與語法糖的好處

我們使用 json_serializable 自動生成資料模型,避免手動轉換 JSON,提高開發效率。用到套件:

  • json_annotation:標記需要轉換的屬性
  • json_serializable:生成 fromJson / toJson
  • build_runner:執行生成命令,產生 .g.dart

範例:

part 'trip.g.dart';

@JsonSerializable(explicitToJson: true)
class Trip {
  final String id;
  final String title;
  final List<Activity> activities;

  Trip({required this.id, required this.title, required this.activities});

  Trip copyWith({String? id, String? title, List<Activity>? activities}) {
    return Trip(
      id: id ?? this.id,
      title: title ?? this.title,
      activities: activities ?? this.activities,
    );
  }

  factory Trip.fromJson(Map<String, dynamic> json) => _$TripFromJson(json);
  Map<String, dynamic> toJson() => _$TripToJson(this);
}

為什麼用 json_serializable

  • 避免手動錯誤
  • 修改 model 後只需重新生成 .g.dart
  • JSON 轉換統一管理,易於維護

生成指令:

flutter pub run build_runner build --delete-conflicting-outputs

每次修改 model 都要重新執行。


Riverpod 狀態管理流程

把 model 與 UI 串起來的重點就是 Riverpod。

流程:

  1. Provider 定義:把 model 包裝在 provider
  2. UI 讀取:畫面用 Consumerref.watch 監聽資料
  3. 更新狀態:透過 provider 修改 model,UI 自動刷新

主要套件:

  • flutter_riverpod:核心狀態管理框架,提供 Provider、Notifier、AsyncNotifier

示範:

class TripListNotifier extends AsyncNotifier<List<Trip>> {
  @override
  FutureOr<List<Trip>> build() => [];
  
  Future<void> addTrip(Trip newTrip) async { /*...*/ }
  Future<void> removeTrip(String tripId) async { /*...*/ }
}

final tripListProvider = AsyncNotifierProvider<TripListNotifier, List<Trip>>(
  () => TripListNotifier(),
);

when 處理不同狀態:

ref.watch(tripListProvider).when(
  data: (trips) => ListView.builder(
    itemCount: trips.length,
    itemBuilder: (context, index) => Text(trips[index].title),
  ),
  loading: () => const CircularProgressIndicator(),
  error: (err, stack) => Text('發生錯誤:$err'),
);

這樣不管 model 變動或資料異步載入,UI 都能正確呈現。


UI 與時間區塊防呆設計

為了提升使用者體驗,時間區塊有兩項優化:

防呆設計

系統檢查輸入時間,確保:

  • 開始時間早於結束時間
  • 停留時間不可為負數
  • 結束時間晚於開始時間

自動計算時間

  • 任填兩項(開始時間 / 停留時間 / 結束時間),系統自動計算第三項
  • 後續調整時間時,會彈提示詢問是否同步更新
防呆設計 時間計算

今日成果展示

子行程添加 行程頁展示 今日成果

上一篇
Day 11 - 從填空題到選擇題:Flutter 輸入元件技術拆解
下一篇
Day 13 - 行程拖曳功能開發:LongPressDraggable 與 DragTarget 的選擇與實作
系列文
《30 天 Flutter:跨平台 AI 行程規劃 App》20
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言