You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Flutter TableCalendar可用性日历报错与橙色标记需求求助

Flutter可用性日历问题解决:空值错误与橙色标记不可用日期

一、修复“type 'Null' is not a subtype of type 'String'”错误

错误原因

  1. 从iCalendar解析的DTSTART/DTEND/SUMMARY字段可能为空,直接调用DateTime.parse或赋值给Event的必填name参数会触发空值类型错误。
  2. Event类的name字段定义为final String,不允许为null,但解析时可能获取不到有效SUMMARY值。

修复方案

  1. 对iCalendar解析字段添加空值校验,提供默认兜底值。
  2. 遍历事件覆盖的所有日期,确保跨天的不可用日期都被正确标记。
  3. 新增不可用日期集合,用于快速判断日期状态。

二、实现不可用日期橙色标记

通过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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.04 08:01:02