Flutter TableCalendar可用性日历报错与橙色标记需求求助
Flutter可用性日历问题解决:空值错误与橙色标记不可用日期
一、修复“type 'Null' is not a subtype of type 'String'”错误
错误原因
- 从iCalendar解析的
DTSTART/DTEND/SUMMARY字段可能为空,直接调用DateTime.parse或赋值给Event的必填name参数会触发空值类型错误。 Event类的name字段定义为final String,不允许为null,但解析时可能获取不到有效SUMMARY值。
修复方案
- 对iCalendar解析字段添加空值校验,提供默认兜底值。
- 遍历事件覆盖的所有日期,确保跨天的不可用日期都被正确标记。
- 新增不可用日期集合,用于快速判断日期状态。
二、实现不可用日期橙色标记
通过TableCalendar的calendarBuilders自定义日期单元格样式,判断当前日期是否属于不可用范围,设置橙色背景及文字样式。
修复后的完整代码
页面代码
import '../utils.dart'; import 'package:flutter/material.dart'; import 'package:table_calendar/table_calendar.dart'; import 'package:icalendar_parser/icalendar_parser.dart'; import 'package:http/http.dart' as http; class TestIcal extends StatefulWidget { const TestIcal({super.key}); @override State<TestIcal> createState() => _TestIcalState(); } class _TestIcalState extends State<TestIcal> { ICalendar? _iCalendar; late final ValueNotifier<List<Event>> _selectedEvents; CalendarFormat _calendarFormat = CalendarFormat.month; RangeSelectionMode _rangeSelectionMode = RangeSelectionMode.toggledOff; DateTime _focusedDay = DateTime.now(); DateTime? _selectedDay; DateTime? _rangeStart; DateTime? _rangeEnd; final Set<DateTime> _unavailableDays = {}; @override void initState() { super.initState(); _selectedEvents = ValueNotifier<List<Event>>([]); _getAssets(); } Map<DateTime, List<Event>> _convertIcalendarToCalendarFormat() { if (_iCalendar == null) return {}; Map<DateTime, List<Event>> events = {}; _unavailableDays.clear(); for (var eventData in _iCalendar!.data) { final startStr = eventData["DTSTART"] as String? ?? ""; final endStr = eventData["DTEND"] as String? ?? ""; final eventName = eventData["SUMMARY"] as String? ?? "不可用"; if (startStr.isEmpty || endStr.isEmpty) continue; DateTime start; DateTime end; try { start = DateTime.parse(startStr); end = DateTime.parse(endStr); } catch (e) { print("日期解析错误: $e"); continue; } final event = Event( name: eventName, start: start, end: end, color: Colors.orange, ); for (var day = start; day.isBefore(end); day = day.add(const Duration(days: 1))) { final normalizedDay = DateTime(day.year, day.month, day.day); _unavailableDays.add(normalizedDay); if (events[normalizedDay] == null) { events[normalizedDay] = [event]; } else { events[normalizedDay]?.add(event); } } } return events; } Future<void> _getAssets() async { http.Response response = await http.get( Uri.parse('https://login.smoobu.com/ical/1400834.ics?s=EaTkC2hLcm'), ); if (response.statusCode != 200) { print("请求错误: ${response.statusCode}"); return; } final iCalendar = ICalendar.fromString(response.body); setState(() { _iCalendar = iCalendar; _convertIcalendarToCalendarFormat(); }); } List<Event> _getEventsForDay(DateTime day) { final normalizedDay = DateTime(day.year, day.month, day.day); return _convertIcalendarToCalendarFormat()[normalizedDay] ?? []; } List<Event> _getEventsForRange(DateTime start, DateTime end) { final days = daysInRange(start, end); return [ for (final d in days) ..._getEventsForDay(d), ]; } void _onDaySelected(DateTime selectedDay, DateTime focusedDay) { if (!isSameDay(_selectedDay, selectedDay)) { setState(() { _selectedDay = selectedDay; _focusedDay = focusedDay; _rangeStart = null; _rangeEnd = null; _rangeSelectionMode = RangeSelectionMode.toggledOff; }); _selectedEvents.value = _getEventsForDay(selectedDay); } } void _onRangeSelected(DateTime? start, DateTime? end, DateTime focusedDay) { setState(() { _selectedDay = null; _focusedDay = focusedDay; _rangeStart = start; _rangeEnd = end; _rangeSelectionMode = RangeSelectionMode.toggledOn; }); if (start != null && end != null) { _selectedEvents.value = _getEventsForRange(start, end); } else if (start != null) { _selectedEvents.value = _getEventsForDay(start); } else if (end != null) { _selectedEvents.value = _getEventsForDay(end); } } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('可用性日历'), ), body: SingleChildScrollView( padding: const EdgeInsets.all(16), child: Column( children: [ TableCalendar<Event>( focusedDay: _focusedDay, firstDay: DateTime.now(), lastDay: DateTime(2025, 12, 31), calendarFormat: _calendarFormat, rangeSelectionMode: _rangeSelectionMode, eventLoader: _getEventsForDay, selectedDayPredicate: (day) => isSameDay(_selectedDay, day), rangeStartDay: _rangeStart, rangeEndDay: _rangeEnd, onDaySelected: _onDaySelected, onRangeSelected: _onRangeSelected, onFormatChanged: (format) { if (_calendarFormat != format) { setState(() { _calendarFormat = format; }); } }, calendarBuilders: CalendarBuilders( defaultBuilder: (context, day, focusedDay) { final normalizedDay = DateTime(day.year, day.month, day.day); if (_unavailableDays.contains(normalizedDay)) { return Container( margin: const EdgeInsets.all(4), decoration: BoxDecoration( color: Colors.orange.withOpacity(0.3), borderRadius: BorderRadius.circular(8), ), child: Center( child: Text( '${day.day}', style: const TextStyle(color: Colors.orange), ), ), ); } return null; }, selectedBuilder: (context, day, focusedDay) { final normalizedDay = DateTime(day.year, day.month, day.day); final isUnavailable = _unavailableDays.contains(normalizedDay); return Container( margin: const EdgeInsets.all(4), decoration: BoxDecoration( color: isUnavailable ? Colors.orange : Colors.blue, borderRadius: BorderRadius.circular(8), ), child: Center( child: Text( '${day.day}', style: const TextStyle(color: Colors.white), ), ), ); }, ), ), const SizedBox(height: 16), ValueListenableBuilder<List<Event>>( valueListenable: _selectedEvents, builder: (context, events, _) { return ListView.builder( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), itemCount: events.length, itemBuilder: (context, index) { final event = events[index]; return Card( color: event.color.withOpacity(0.1), child: ListTile( title: Text(event.name), subtitle: Text('${event.start?.toString().split(' ')[0]} 至 ${event.end?.toString().split(' ')[0]}'), textColor: event.color, ), ); }, ); }, ), ], ), ), ); } }
utils代码
import 'dart:ui'; List<DateTime> daysInRange(DateTime start, DateTime end) { final days = <DateTime>[]; for (var day = start; day.isBefore(end); day = day.add(const Duration(days: 1))) { days.add(DateTime(day.year, day.month, day.day)); } return days; } class Event { final String name; final DateTime? start; final DateTime? end; Color color; Event({required this.name, this.start, this.end, required this.color}); }
内容的提问来源于stack exchange,提问作者Gede Destrawan
相关产品推荐
相关产品推荐

