📆 FlutterFlow カスタムウィジェット:CustomDateRangePicker 実装ガイド
宿泊予約アプリや日付管理アプリにおいて、「範囲選択ができて、過去の日付や予約済みの日付をブロックできるカレンダーUI」は非常に重要です。
ここでは、FlutterFlow に組み込む CustomDateRangePicker をカスタムウィジェットで実装し、Firestore の予約情報を使って日付の選択可否を制御する方法を解説します。
✅ 主な機能要件
機能 | 内容 |
---|---|
📆 範囲選択機能 | startDate と endDate の範囲を選択可能 |
🔐 予約ブロック機能 | bookings コレクションから |
🕓 過去日選択不可 | 今日以前の日付は選択不可 |
🌐 多言語対応(ロケール) | 日本語などのロケール指定が可能(例: ja_JP) |
🎨 スタイルカスタマイズ | 色・テキストカラー・ヘッダーなどをウィジェット引数で制御 |
📦 FFAppStateと連携 | 選択された日付をアプリ全体の状態に保存 |
📦 必須依存パッケージ(pubspec.yaml)
dependencies:
table_calendar: ^3.0.9
intl: ^0.18.1
cloud_firestore: ^5.5.0 # FlutterFlowのバージョンに合わせて調整
🛠 カスタムウィジェット実装コード
// Automatic FlutterFlow imports import '/backend/backend.dart'; import '/flutter_flow/flutter_flow_util.dart'; import 'package:flutter/material.dart'; // Begin custom widget code // DO NOT REMOVE OR MODIFY THE CODE ABOVE!
import 'package:table_calendar/table_calendar.dart'; import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:intl/intl.dart';
class CustomDateRangePicker extends StatefulWidget { const CustomDateRangePicker({ Key? key, required this.roomRefId, this.startDate, this.endDate, this.locale, this.selectedColor, this.todayColor, this.disabledColor, this.textColor, this.width, this.height, this.borderRadius, this.elevation, this.headerTextColor, this.headerBackgroundColor, }) : super(key: key);
final DocumentReference roomRefId; final DateTime? startDate; final DateTime? endDate; final String? locale; final Color? selectedColor; final Color? todayColor; final Color? disabledColor; final Color? textColor; final double? width; final double? height; final double? borderRadius; final double? elevation; final Color? headerTextColor; final Color? headerBackgroundColor;
@override _CustomDateRangePickerState createState() => _CustomDateRangePickerState(); }
class _CustomDateRangePickerState extends State<CustomDateRangePicker> { DateTime? _rangeStart; DateTime? _rangeEnd; Map<String, bool> _disabledDates = {};
@override void initState() { super.initState(); _rangeStart = widget.startDate; _rangeEnd = widget.endDate;
FFAppState().update(() { FFAppState().selectedStart = _rangeStart; FFAppState().selectedEnd = _rangeEnd; }); _fetchDisabledDates();
}
Future<void> _fetchDisabledDates() async { final bookings = await FirebaseFirestore.instance .collection('bookings') .where('roomRef', isEqualTo: widget.roomRefId) .where('status', whereIn: ['now', 'pending']) // ← 予約中 or 保留のみ対象 .get();
Map<String, bool> disabled = {}; for (var doc in bookings.docs) { final data = doc.data(); DateTime start = (data['startDate'] as Timestamp).toDate(); DateTime end = (data['endDate'] as Timestamp).toDate(); for (DateTime date = start; !date.isAfter(end); date = date.add(const Duration(days: 1))) { final key = DateFormat('yyyy-MM-dd').format(date); disabled[key] = true; } } setState(() { _disabledDates = disabled; });
}
bool _isDisabled(DateTime day) { final key = DateFormat('yyyy-MM-dd').format(day); return _disabledDates[key] == true; }
bool _isPastDate(DateTime day) { final now = DateTime.now(); return day.isBefore(DateTime(now.year, now.month, now.day)); }
@override Widget build(BuildContext context) { return Material( elevation: widget.elevation ?? 2.0, borderRadius: BorderRadius.circular(widget.borderRadius ?? 12.0), child: Container( width: widget.width ?? double.infinity, height: widget.height ?? 400, decoration: BoxDecoration( borderRadius: BorderRadius.circular(widget.borderRadius ?? 12.0), ), child: TableCalendar( locale: widget.locale ?? 'ja_JP', firstDay: DateTime.utc(2020, 1, 1), lastDay: DateTime.utc(2030, 12, 31), focusedDay: DateTime.now(), calendarStyle: CalendarStyle( todayDecoration: BoxDecoration( color: widget.todayColor ?? Colors.orange, shape: BoxShape.circle, ), rangeStartDecoration: BoxDecoration( color: widget.selectedColor ?? Colors.blue, shape: BoxShape.circle, ), rangeEndDecoration: BoxDecoration( color: widget.selectedColor ?? Colors.blue, shape: BoxShape.circle, ), withinRangeDecoration: BoxDecoration( color: (widget.selectedColor ?? Colors.blue).withOpacity(0.3), shape: BoxShape.circle, ), defaultTextStyle: TextStyle(color: widget.textColor ?? Colors.black), disabledTextStyle: TextStyle(color: widget.disabledColor ?? Colors.grey), ), selectedDayPredicate: (day) => _rangeStart != null && _rangeEnd != null && (day.isAtSameMomentAs(_rangeStart!) || day.isAtSameMomentAs(_rangeEnd!) || (day.isAfter(_rangeStart!) && day.isBefore(_rangeEnd!))), enabledDayPredicate: (day) => !_isDisabled(day) && !_isPastDate(day), onRangeSelected: (start, end, focusedDay) { setState(() { _rangeStart = start; _rangeEnd = end; });
FFAppState().update(() { FFAppState().selectedStart = start; FFAppState().selectedEnd = end; }); }, rangeStartDay: _rangeStart, rangeEndDay: _rangeEnd, calendarFormat: CalendarFormat.month, rangeSelectionMode: RangeSelectionMode.enforced, availableGestures: AvailableGestures.horizontalSwipe, headerStyle: HeaderStyle( formatButtonVisible: false, titleCentered: true, titleTextStyle: TextStyle( color: widget.headerTextColor ?? Colors.black, fontSize: 16, fontWeight: FontWeight.bold, ), decoration: BoxDecoration( color: widget.headerBackgroundColor ?? Colors.transparent, ), ), calendarBuilders: CalendarBuilders( disabledBuilder: (context, day, focusedDay) { final isReserved = _isDisabled(day); final isPast = _isPastDate(day); String message = isReserved ? '予約済みです' : isPast ? '過去の日付は選択できません' : '選択できません'; return Tooltip( message: message, child: Center( child: Text( '✕', style: TextStyle( color: widget.disabledColor ?? Colors.grey, fontWeight: FontWeight.bold, fontSize: 16, ), ), ), ); }, ), ), ), );
} }
🧠 使用例
FlutterFlow のページ上に配置し、以下のような引数を渡すことで動作します:
roomRefId
: 予約対象の部屋(Firestore の DocumentReference)selectedColor
: 選択中の色todayColor
: 今日の日付の背景色disabledColor
: 選択不可日の色locale
: 'ja_JP' などローカル言語指定FFAppState().selectedStart
/selectedEnd
: 選択日範囲を保存するアプリ状態
✅ まとめ
この CustomDateRangePicker は、FlutterFlow 上での予約管理・カレンダー選択に最適化されたカスタムウィジェットです。拡張性が高く、ユーザーの UX 向上にも直結します。
今後以下のような機能拡張も可能です:
- ✅ 空室数の表示
- 📆 月ごとの非アクティブ制御
- 🔄 他ウィジェットとの同期(例:料金計算など)