recurrence_picker 0.3.0 copy "recurrence_picker: ^0.3.0" to clipboard
recurrence_picker: ^0.3.0 copied to clipboard

A Flutter widget for building RFC 5545 recurrence rules (rrule)

example/lib/main.dart

import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:monthly_calendar/monthly_calendar.dart';
import 'package:recurrence_picker/recurrence_picker.dart';
import 'package:rrule/rrule.dart';

import 'l10n/app_localizations.dart';

void main() => runApp(const DemoApp());

class DemoApp extends StatelessWidget {
  const DemoApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Recurrence Picker Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.indigo),
        useMaterial3: true,
      ),
      localizationsDelegates: [
        ...RecurrenceLocalizations.localizationsDelegates,
        ...CalendarLocalizations.localizationsDelegates,
        AppLocalizations.delegate,
      ],
      supportedLocales: {
        ...RecurrenceLocalizations.supportedLocales,
        ...CalendarLocalizations.supportedLocales,
      },
      home: const HomePage(),
    );
  }
}

class HomePage extends StatefulWidget {
  const HomePage({super.key});

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  DateTime _startDate = DateTime.now();
  RecurrenceRule _rule = RecurrenceRule(frequency: Frequency.weekly);

  Future<void> _showStartDateDialog(BuildContext context) async {
    final picked = await showDialog<DateTime>(
      context: context,
      builder: (context) => _StartDateDialog(initialDate: _startDate),
    );
    if (picked != null) setState(() => _startDate = picked);
  }

  @override
  Widget build(BuildContext context) {
    final loc = AppLocalizations.of(context)!;

    return Scaffold(
      appBar: AppBar(title: Text(loc.appTitle)),
      body: SingleChildScrollView(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            // Start date
            _SectionHeader(label: loc.startDate),
            const SizedBox(height: 8),
            _startDatePicker,

            // Recurrence rule
            const SizedBox(height: 16),
            _SectionHeader(label: loc.recurrenceRule),
            const SizedBox(height: 8),
            _recurrencePicker,

            const SizedBox(height: 24),
            const Divider(),
            const SizedBox(height: 12),

            // Results
            _SectionHeader(label: loc.rruleString),
            const SizedBox(height: 4),
            SelectableText(
              _rule.toString(),
              style: Theme.of(
                context,
              ).textTheme.bodyLarge?.copyWith(fontFamily: 'monospace'),
            ),
            const SizedBox(height: 12),
            _SectionHeader(label: loc.description),
            const SizedBox(height: 4),
            _RuleDescription(rule: _rule, startDate: _startDate),
          ],
        ),
      ),
    );
  }

  Widget get _startDatePicker {
    final theme = Theme.of(context);
    return ListTile(
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(12),
      ),
      tileColor: theme.colorScheme.surfaceContainerLow,
      leading: const Icon(Icons.calendar_today),
      title: Text(
        DateFormat.yMMMMd().format(_startDate),
        style: theme.textTheme.titleMedium,
      ),
      trailing: const Icon(Icons.chevron_right),
      onTap: () => _showStartDateDialog(context),
    );
  }

  Widget get _recurrencePicker => RecurrencePicker(
        initialFrequency: Frequency.weekly,
        initialInterval: 1,
        startDate: _startDate,
        showEndOfMonthSelector: true,
        onRecurrenceChanged: (rule) {
          setState(() => _rule = rule);
        },
      );
}

class _SectionHeader extends StatelessWidget {
  final String label;
  const _SectionHeader({required this.label});

  @override
  Widget build(BuildContext context) {
    return Text(
      label,
      style: Theme.of(context).textTheme.titleSmall?.copyWith(
            color: Theme.of(context).colorScheme.primary,
            fontWeight: FontWeight.w600,
          ),
    );
  }
}

/// Displays a human-readable description of a [RecurrenceRule].
class _RuleDescription extends StatefulWidget {
  final RecurrenceRule rule;
  final DateTime startDate;
  const _RuleDescription({required this.rule, required this.startDate});

  @override
  State<_RuleDescription> createState() => _RuleDescriptionState();
}

class _RuleDescriptionState extends State<_RuleDescription> {
  RruleL10n? _l10n;
  String? _languageCode;

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    final languageCode = Localizations.localeOf(context).languageCode;
    if (languageCode != _languageCode) {
      _languageCode = languageCode;
      _initL10n(languageCode);
    }
  }

  Future<void> _initL10n(String languageCode) async {
    final l10n = await createRruleL10n(languageCode);
    if (mounted && languageCode == _languageCode) {
      setState(() => _l10n = l10n);
    }
  }

  @override
  Widget build(BuildContext context) {
    if (_l10n == null) {
      return const SizedBox(
        height: 20,
        width: 20,
        child: CircularProgressIndicator(strokeWidth: 2),
      );
    }
    final text = widget.rule.describe(
      l10n: _l10n!,
      loc: RecurrenceLocalizations.of(context)!,
      verbose: true,
      startDate: widget.startDate,
    );
    return Text(text, style: Theme.of(context).textTheme.bodyLarge);
  }
}

/// A dialog that shows a [MonthlyCalendarView] for picking a start date.
class _StartDateDialog extends StatefulWidget {
  final DateTime initialDate;
  const _StartDateDialog({required this.initialDate});

  @override
  State<_StartDateDialog> createState() => _StartDateDialogState();
}

class _StartDateDialogState extends State<_StartDateDialog> {
  late DateTime _selected;
  late final CalendarController _controller;

  @override
  void initState() {
    super.initState();
    _selected = widget.initialDate;
    _controller = CalendarController();
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return AlertDialog(
      title: const Text('Select start date'),
      content: SizedBox(
        width: 400,
        child: MonthlyCalendarView(
          today: DateTime.now(),
          controller: _controller,
          selectedDate: _selected,
          onDayTapped: (date) {
            setState(() => _selected = date);
          },
        ),
      ),
      actions: [
        TextButton(
          onPressed: () => Navigator.of(context).pop(),
          child: const Text('Cancel'),
        ),
        FilledButton(
          onPressed: () => Navigator.of(context).pop(_selected),
          child: const Text('OK'),
        ),
      ],
    );
  }
}
0
likes
160
points
15
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

A Flutter widget for building RFC 5545 recurrence rules (rrule)

Repository (GitHub)
View/report issues

Topics

#flutter #widget #ui #picker #calendar

License

BSD-3-Clause (license)

Dependencies

collection, flutter, flutter_localizations, intl, rrule

More

Packages that depend on recurrence_picker