📆 FlutterFlow 高機能料金カレンダー開発ガイド
FlutterFlowで実装された高機能な料金計算カレンダー、およびその料金内訳表示コンポーネントの技術仕様と開発フローをまとめます。
✅ 主なコンポーネント
コンポーネント名 | 役割 |
---|---|
CustomDateRangePicker | 宿泊期間を選択するカレンダーウィジェット |
PriceBreakdownView | 選択期間の料金内訳をカテゴリ別に表示 |
🏗 システム概要と設計思想
- 関心の分離 (Separation of Concerns) を徹底し、UI層・サービス層・データモデル層に分割。
- 料金計算ルールの変更はサービス層(PricingService)のみ修正すればOK。
🗄 Firestoreデータ設計
フィールド名 | データ型 | 説明 |
---|---|---|
basePrice | Number (Double) | 1泊あたりの基本料金 |
weekendSurcharge | Number (Double) | 土日祝日追加料金 |
specialSurcharges | List | 特別料金期間リスト(startDate, endDate, surcharge) |
📦 必須依存パッケージ(pubspec.yaml)
dependencies:
table_calendar: ^3.0.9
intl: ^0.18.1
holiday_jp: ^0.6.0
cloud_firestore: ^5.5.0
🛠 カスタムウィジェット実装例
1. データモデル
class SpecialPeriod { final DateTime startDate; final DateTime endDate; final double surcharge; SpecialPeriod({required this.startDate, required this.endDate, required this.surcharge}); }
enum PriceCategory { base, weekend, special }
class DailyPriceInfo { final PriceCategory category; final double price; DailyPriceInfo({required this.category, required this.price}); }
class PriceRangeDetail { final String rangeString; final int numberOfNights; final double pricePerNight; final double subtotal; PriceRangeDetail({required this.rangeString, required this.numberOfNights, required this.pricePerNight, required this.subtotal}); }
2. サービス層
class BookingService { final DocumentReference roomRef; BookingService({required this.roomRef}); Future<List<SpecialPeriod>> fetchSpecialSurchargePeriods() async { List<SpecialPeriod> periods = []; try { final roomDoc = await roomRef.get(); if (roomDoc.exists && roomDoc.data() != null) { final data = roomDoc.data() as Map<String, dynamic>; if (data.containsKey('specialSurcharges')) { final List<dynamic>? surchargeList = data['specialSurcharges']; if (surchargeList != null) { periods = surchargeList.map((item) { final map = item as Map<String, dynamic>; return SpecialPeriod( startDate: (map['startDate'] as Timestamp).toDate(), endDate: (map['endDate'] as Timestamp).toDate(), surcharge: (map['surcharge'] as num).toDouble(), ); }).toList(); } } } } catch (e) { print('特別料金の取得に失敗しました: $e'); } return periods; } }
class PricingService { final double basePrice; final double? weekendSurcharge; final List<SpecialPeriod> specialSurchargePeriods; PricingService({required this.basePrice, this.weekendSurcharge, required this.specialSurchargePeriods}); DailyPriceInfo getDailyPriceInfo(DateTime date) { double dailySurcharge = 0; PriceCategory category = PriceCategory.base; for (final period in specialSurchargePeriods) { final normalizedDate = DateTime(date.year, date.month, date.day); final normalizedStart = DateTime(period.startDate.year, period.startDate.month, period.startDate.day); final normalizedEnd = DateTime(period.endDate.year, period.endDate.month, period.endDate.day); if (!normalizedDate.isBefore(normalizedStart) && !normalizedDate.isAfter(normalizedEnd)) { dailySurcharge = period.surcharge; category = PriceCategory.special; return DailyPriceInfo(category: category, price: basePrice + dailySurcharge); } } final isWeekend = date.weekday == DateTime.saturday || date.weekday == DateTime.sunday; final isHoliday = holiday_jp.isHoliday(date); if (isWeekend || isHoliday) { dailySurcharge = weekendSurcharge ?? 0; category = PriceCategory.weekend; } return DailyPriceInfo(category: category, price: basePrice + dailySurcharge); } }
3. 料金内訳表示 PriceBreakdownView
class PriceBreakdownView extends StatefulWidget { // ...省略(引数など)... @override _PriceBreakdownViewState createState() => _PriceBreakdownViewState(); }
class _PriceBreakdownViewState extends State<PriceBreakdownView> { // ...省略(状態変数など)... void _generateBreakdown() { final start = widget.startDate; final end = widget.endDate; if (start == null || end == null) { _priceBreakdown = {}; _totalNights = 0; _totalPrice = 0; return; } final pricingService = PricingService( basePrice: widget.basePrice, weekendSurcharge: widget.weekendSurcharge, specialSurchargePeriods: _specialPeriods, ); Map<PriceCategory, List<PriceRangeDetail>> breakdown = { PriceCategory.base: [], PriceCategory.weekend: [], PriceCategory.special: [], }; double runningTotalPrice = 0; int runningTotalNights = 0; DateTime? rangeStart; DailyPriceInfo? currentPriceInfo; for (DateTime date = start; date.isBefore(end); date = date.add(const Duration(days: 1))) { runningTotalNights++; runningTotalPrice += pricingService.getDailyPriceInfo(date).price; final priceInfo = pricingService.getDailyPriceInfo(date); if (rangeStart == null) { rangeStart = date; currentPriceInfo = priceInfo; } else if (priceInfo.category != currentPriceInfo!.category || priceInfo.price != currentPriceInfo.price) { _addRangeToList(breakdown, currentPriceInfo, rangeStart, date.add(Duration(days: -1))); rangeStart = date; currentPriceInfo = priceInfo; } } if (rangeStart != null && currentPriceInfo != null) { _addRangeToList(breakdown, currentPriceInfo, rangeStart, end.add(Duration(days: -1))); } _priceBreakdown = breakdown; _totalNights = runningTotalNights; _totalPrice = runningTotalPrice; } // ...省略(buildメソッドなど)... }
🧠 使用例
- FirestoreのroomsコレクションからroomRefId, basePrice, weekendSurcharge, specialSurchargesを取得
- AppStateでstartDate, endDateを管理し、PriceBreakdownViewに渡す
- カレンダー選択と料金内訳表示が連動
✅ まとめ
- 責務分離・拡張性重視の設計
- 料金カテゴリ追加も容易
- Firestore連携・祝日判定もサポート
今後の拡張例:
- 超繁忙期や特別割引など新カテゴリ追加
- 料金計算ロジックの柔軟なカスタマイズ
- UIテーマや多言語対応の強化