form_flutter 0.2.0 copy "form_flutter: ^0.2.0" to clipboard
form_flutter: ^0.2.0 copied to clipboard

Schema-friendly Flutter forms with reusable field widgets, validators, controller state, and preset catalogs.

example/lib/main.dart

import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:form_flutter/form_flutter.dart';

void main() {
  runApp(const ExampleApp());
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'form_flutter example',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(
          seedColor: const Color(0xFF155EEF),
          brightness: Brightness.light,
        ),
        scaffoldBackgroundColor: const Color(0xFFF4F7FB),
        inputDecorationTheme: InputDecorationTheme(
          filled: true,
          fillColor: Colors.white,
          border: OutlineInputBorder(
            borderRadius: BorderRadius.circular(20),
            borderSide: const BorderSide(color: Color(0xFFD0D5DD)),
          ),
          enabledBorder: OutlineInputBorder(
            borderRadius: BorderRadius.circular(20),
            borderSide: const BorderSide(color: Color(0xFFD0D5DD)),
          ),
          focusedBorder: OutlineInputBorder(
            borderRadius: BorderRadius.circular(20),
            borderSide: const BorderSide(color: Color(0xFF155EEF), width: 1.5),
          ),
        ),
        useMaterial3: true,
      ),
      home: const ExampleHomePage(),
    );
  }
}

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

  @override
  State<ExampleHomePage> createState() => _ExampleHomePageState();
}

class _ExampleHomePageState extends State<ExampleHomePage> {
  Map<String, Object?>? _savedSnapshot;

  final FormFlutterController _controller = FormFlutterController(
    initialValues: const {
      'fullName': '',
      'email': '',
      'phone': '',
      'phoneCountry': 'US',
      'password': '',
      'team': 'product',
      'interests': <String>['analytics'],
      'priority': 65.0,
      'available': true,
      'startDate': null,
    },
  );

  late final List<FormFlutterField<dynamic>> _fields = [
    FormFlutterTextField(
      name: 'fullName',
      label: 'Full name',
      decorationOverride: const InputDecoration(
        prefixIcon: Icon(Icons.person_outline),
        fillColor: Color(0xFFF8FBFF),
      ),
      validator: FormFlutterValidators.combine([
        FormFlutterValidators.requiredText(),
        FormFlutterValidators.minLength(3),
      ]),
    ),
    FormFlutterTextField(
      name: 'email',
      label: 'Email',
      keyboardType: TextInputType.emailAddress,
      decorationOverride: const InputDecoration(
        prefixIcon: Icon(Icons.mail_outline),
      ),
      validator: FormFlutterValidators.combine([
        FormFlutterValidators.requiredText(),
        FormFlutterValidators.email(),
      ]),
      asyncValidator: FormFlutterValidators.uniqueEmail(
        (email, _) async => email != 'taken@example.com',
      ),
    ),
    FormFlutterPhoneField(
      name: 'phone',
      label: 'Phone',
      countryFieldName: 'phoneCountry',
      initialCountryCode: 'US',
      showCountryFlagInSelector: false,
      showCountryIsoCodeInSelector: true,
      showCountryDialCodeInSelector: true,
      nationalNumberHintText: 'Enter phone number',
      validator: FormFlutterValidators.combine([
        FormFlutterValidators.requiredText(),
        FormFlutterValidators.numericText(),
      ]),
    ),
    FormFlutterTextField(
      name: 'password',
      label: 'Password',
      hintText: 'Create password',
      obscureText: true,
      enableObscureTextToggle: true,
      decorationOverride: const InputDecoration(
        prefixIcon: Icon(Icons.lock_outline),
      ),
      validator: FormFlutterPresetValidators.strongPassword(),
    ),
    FormFlutterDropdownField<String>(
      name: 'team',
      label: 'Team',
      decorationOverride: const InputDecoration(
        prefixIcon: Icon(Icons.groups_2_outlined),
      ),
      options: const [
        FormFlutterOption(
          value: 'design',
          label: 'Design',
          color: Color(0xFF7C3AED),
          icon: Icons.brush_outlined,
          indicatorSize: 16,
        ),
        FormFlutterOption(
          value: 'product',
          label: 'Product',
          color: Color(0xFFEA580C),
          icon: Icons.insights_outlined,
          indicatorSize: 16,
        ),
        FormFlutterOption(
          value: 'engineering',
          label: 'Engineering',
          color: Color(0xFF2563EB),
          icon: Icons.code,
          indicatorSize: 16,
        ),
      ],
      validator: FormFlutterValidators.requiredSelection<String>(),
    ),
    FormFlutterMultiSelectField<String>(
      name: 'interests',
      label: 'Interests',
      options: [
        const FormFlutterOption(
          value: 'automation',
          label: 'Automation',
          color: Color(0xFFEA580C),
          backgroundColor: Color(0xFFFFF7ED),
          selectedColor: Color(0xFFEA580C),
          selectedTextColor: Colors.white,
          icon: Icons.bolt_outlined,
        ),
        FormFlutterOption(
          value: 'analytics',
          label: 'Analytics',
          color: const Color(0xFFDC2626),
          backgroundColor: const Color(0xFFFEF2F2),
          selectedColor: const Color(0xFFDC2626),
          selectedTextColor: Colors.white,
          icon: Icons.monitor_outlined,
        ),
        const FormFlutterOption(
          value: 'accessibility',
          label: 'Accessibility',
          color: Color(0xFF7C3AED),
          backgroundColor: Color(0xFFF5F3FF),
          selectedColor: Color(0xFF7C3AED),
          selectedTextColor: Colors.white,
          icon: Icons.accessibility_new,
        ),
      ],
      optionBuilder: (context, option, isSelected) {
        return AnimatedContainer(
          duration: const Duration(milliseconds: 180),
          padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
          decoration: BoxDecoration(
            color: isSelected
                ? (option.selectedColor ?? option.color ?? Colors.blue)
                : (option.backgroundColor ?? Colors.white),
            borderRadius: BorderRadius.circular(18),
            border: Border.all(
              color:
                  option.borderColor ?? option.color ?? const Color(0xFFD0D5DD),
            ),
          ),
          child: Row(
            mainAxisSize: MainAxisSize.min,
            children: [
              if (option.icon != null) ...[
                Icon(
                  option.icon,
                  size: 16,
                  color: isSelected
                      ? (option.selectedTextColor ?? Colors.white)
                      : (option.color ?? const Color(0xFF344054)),
                ),
                const SizedBox(width: 8),
              ],
              Text(
                option.label,
                style: TextStyle(
                  fontWeight: FontWeight.w700,
                  color: isSelected
                      ? (option.selectedTextColor ?? Colors.white)
                      : (option.textColor ?? const Color(0xFF344054)),
                ),
              ),
            ],
          ),
        );
      },
      validator: FormFlutterValidators.minItems<String>(1),
    ),
    FormFlutterSliderField(
      name: 'priority',
      label: 'Priority',
      min: 0,
      max: 100,
      divisions: 20,
      unitLabel: 'pts',
      activeColor: const Color(0xFF155EEF),
      inactiveColor: const Color(0xFFD0D5DD),
      thumbColor: const Color(0xFF155EEF),
      valueStyle: const TextStyle(
        fontWeight: FontWeight.w700,
        color: Color(0xFF155EEF),
      ),
    ),
    FormFlutterDateField(
      name: 'startDate',
      label: 'Start date',
      decorationOverride: const InputDecoration(
        prefixIcon: Icon(Icons.calendar_month_outlined),
      ),
      validator: FormFlutterValidators.requiredDate(),
    ),
    FormFlutterSwitchField(
      name: 'available',
      label: 'Available for contact',
      activeColor: const Color(0xFF155EEF),
      titleStyle: const TextStyle(fontWeight: FontWeight.w700),
      decoration: const InputDecoration(fillColor: Color(0xFFF8FBFF)),
    ),
  ];

  void _exportSnapshot() {
    setState(() {
      _savedSnapshot = _controller.toJson();
    });
    ScaffoldMessenger.of(context).showSnackBar(
      const SnackBar(content: Text('Serialized form state saved from controller.')),
    );
  }

  void _importSnapshot() {
    final savedSnapshot = _savedSnapshot;
    if (savedSnapshot == null) {
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text('Export a snapshot first to import it.')),
      );
      return;
    }

    _controller.fromJson(savedSnapshot);
    ScaffoldMessenger.of(context).showSnackBar(
      const SnackBar(content: Text('Controller restored from serialized state.')),
    );
  }

  void _resetForm() {
    _controller.reset();
    ScaffoldMessenger.of(context).showSnackBar(
      const SnackBar(content: Text('Form reset to initial values.')),
    );
  }

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: DecoratedBox(
        decoration: const BoxDecoration(
          gradient: LinearGradient(
            begin: Alignment.topLeft,
            end: Alignment.bottomRight,
            colors: [Color(0xFFEFF6FF), Color(0xFFF4F7FB), Color(0xFFFFF7ED)],
          ),
        ),
        child: SafeArea(
          child: Center(
            child: ConstrainedBox(
              constraints: const BoxConstraints(maxWidth: 980),
              child: Padding(
                padding: const EdgeInsets.all(24),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    const _ExampleHeader(),
                    const SizedBox(height: 20),
                    Expanded(
                      child: LayoutBuilder(
                        builder: (context, constraints) {
                          final compact = constraints.maxWidth < 820;
                          final form = _ExampleCard(
                            child: DynamicFormFlutter(
                              controller: _controller,
                              fields: _fields,
                              submitLabel: 'Submit example form',
                              header: Column(
                                crossAxisAlignment: CrossAxisAlignment.start,
                                children: [
                                  Text(
                                    'Styled example',
                                    style: Theme.of(context)
                                        .textTheme
                                        .headlineSmall
                                        ?.copyWith(fontWeight: FontWeight.w800),
                                  ),
                                  const SizedBox(height: 8),
                                  Text(
                                    'This sample uses decoration overrides, option colors, icons, a country-aware phone field, a custom chip builder, and controller serialization helpers.',
                                    style: Theme.of(context)
                                        .textTheme
                                        .bodyMedium
                                        ?.copyWith(
                                          color: const Color(0xFF475467),
                                        ),
                                  ),
                                  const SizedBox(height: 18),
                                  Wrap(
                                    spacing: 12,
                                    runSpacing: 12,
                                    children: [
                                      FilledButton.icon(
                                        onPressed: _exportSnapshot,
                                        icon: const Icon(Icons.upload_file_outlined),
                                        label: const Text('Export JSON'),
                                      ),
                                      OutlinedButton.icon(
                                        onPressed: _importSnapshot,
                                        icon: const Icon(Icons.download_outlined),
                                        label: const Text('Import JSON'),
                                      ),
                                      TextButton.icon(
                                        onPressed: _resetForm,
                                        icon: const Icon(Icons.restart_alt_outlined),
                                        label: const Text('Reset'),
                                      ),
                                    ],
                                  ),
                                  const SizedBox(height: 24),
                                  _ExampleGrid(
                                    children: [
                                      _ExampleFieldCard(
                                        child: _fields[0].buildField(_controller),
                                      ),
                                      _ExampleFieldCard(
                                        child: _fields[1].buildField(_controller),
                                      ),
                                      _ExampleFieldCard(
                                        child: _fields[2].buildField(_controller),
                                      ),
                                      _ExampleFieldCard(
                                        child: _fields[3].buildField(_controller),
                                      ),
                                    ],
                                  ),
                                  const SizedBox(height: 14),
                                  _ExampleFieldCard(
                                    child: _fields[4].buildField(_controller),
                                  ),
                                  const SizedBox(height: 14),
                                  _ExampleFieldCard(
                                    child: _fields[5].buildField(_controller),
                                  ),
                                  const SizedBox(height: 14),
                                  _ExampleGrid(
                                    children: [
                                      _ExampleFieldCard(
                                        child: _fields[6].buildField(_controller),
                                      ),
                                      _ExampleFieldCard(
                                        child: _fields[7].buildField(_controller),
                                      ),
                                      _ExampleFieldCard(
                                        child: _fields[8].buildField(_controller),
                                      ),
                                    ],
                                  ),
                                  const SizedBox(height: 24),
                                ],
                              ),
                              onSubmit: (values) {
                                ScaffoldMessenger.of(context).showSnackBar(
                                  SnackBar(
                                    content: Text('Submitted: ${values.asMap()}'),
                                  ),
                                );
                              },
                            ),
                          );

                          final preview = _ExampleCard(
                            dark: true,
                            child: _ExampleStatePreview(
                              controller: _controller,
                              savedSnapshot: _savedSnapshot,
                            ),
                          );

                          if (compact) {
                            return ListView(
                              children: [
                                form,
                                const SizedBox(height: 20),
                                preview,
                              ],
                            );
                          }

                          return Row(
                            crossAxisAlignment: CrossAxisAlignment.start,
                            children: [
                              Expanded(flex: 7, child: form),
                              const SizedBox(width: 20),
                              Expanded(flex: 4, child: preview),
                            ],
                          );
                        },
                      ),
                    ),
                  ],
                ),
              ),
            ),
          ),
        ),
      ),
    );
  }
}

class _ExampleStatePreview extends StatelessWidget {
  const _ExampleStatePreview({
    required this.controller,
    required this.savedSnapshot,
  });

  final FormFlutterController controller;
  final Map<String, Object?>? savedSnapshot;

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    return ValueListenableBuilder<Map<String, Object?>>(
      valueListenable: controller.valuesListenable,
      builder: (context, values, _) {
        return ValueListenableBuilder<Map<String, bool>>(
          valueListenable: controller.touchedFieldsListenable,
          builder: (context, touchedFields, _) {
            return ValueListenableBuilder<Map<String, bool>>(
              valueListenable: controller.dirtyFieldsListenable,
              builder: (context, dirtyFields, _) {
                final touchedCount =
                    touchedFields.values.where((value) => value).length;
                final dirtyCount =
                    dirtyFields.values.where((value) => value).length;
                final currentJson = const JsonEncoder.withIndent('  ')
                    .convert(controller.toJson());
                final savedJson = savedSnapshot == null
                    ? 'No exported snapshot yet.'
                    : const JsonEncoder.withIndent('  ').convert(savedSnapshot);

                return Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text(
                      'Live preview',
                      style: theme.textTheme.titleLarge?.copyWith(
                        color: Colors.white,
                        fontWeight: FontWeight.w800,
                      ),
                    ),
                    const SizedBox(height: 8),
                    Text(
                      'Watch values, serialized JSON, and touched/dirty state update together.',
                      style: theme.textTheme.bodyMedium?.copyWith(
                        color: const Color(0xFF94A3B8),
                        height: 1.5,
                      ),
                    ),
                    const SizedBox(height: 16),
                    Wrap(
                      spacing: 8,
                      runSpacing: 8,
                      children: [
                        _PreviewChip(
                          label: 'Touched $touchedCount',
                          tone: const Color(0xFF38BDF8),
                        ),
                        _PreviewChip(
                          label: 'Dirty $dirtyCount',
                          tone: const Color(0xFFF97316),
                        ),
                        _PreviewChip(
                          label: controller.hasDirtyFields ? 'Unsaved' : 'Clean',
                          tone: controller.hasDirtyFields
                              ? const Color(0xFFEF4444)
                              : const Color(0xFF22C55E),
                        ),
                      ],
                    ),
                    const SizedBox(height: 20),
                    _PreviewBlock(
                      title: 'Values',
                      content: values.toString(),
                    ),
                    const SizedBox(height: 14),
                    _PreviewBlock(
                      title: 'Current JSON',
                      content: currentJson,
                    ),
                    const SizedBox(height: 14),
                    _PreviewBlock(
                      title: 'Last Exported JSON',
                      content: savedJson,
                    ),
                  ],
                );
              },
            );
          },
        );
      },
    );
  }
}

class _ExampleHeader extends StatelessWidget {
  const _ExampleHeader();

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    return Container(
      width: double.infinity,
      padding: const EdgeInsets.all(28),
      decoration: BoxDecoration(
        borderRadius: BorderRadius.circular(28),
        gradient: const LinearGradient(
          colors: [Color(0xFF155EEF), Color(0xFF1D4ED8), Color(0xFF7C3AED)],
        ),
      ),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text(
            'form_flutter example',
            style: theme.textTheme.labelLarge?.copyWith(
              color: const Color(0xFFDBEAFE),
              fontWeight: FontWeight.w800,
              letterSpacing: 1.1,
            ),
          ),
          const SizedBox(height: 10),
          Text(
            'Detailed Styling Example',
            style: theme.textTheme.headlineMedium?.copyWith(
              color: Colors.white,
              fontWeight: FontWeight.w900,
            ),
          ),
        ],
      ),
    );
  }
}

class _ExampleCard extends StatelessWidget {
  const _ExampleCard({required this.child, this.dark = false});

  final Widget child;
  final bool dark;

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.all(24),
      decoration: BoxDecoration(
        color: dark ? const Color(0xFF0F172A) : Colors.white.withValues(alpha: 0.94),
        borderRadius: BorderRadius.circular(28),
        border: Border.all(
          color: dark ? const Color(0xFF1F2937) : const Color(0xFFD7E2F2),
        ),
        boxShadow: const [
          BoxShadow(
            color: Color(0x14000000),
            blurRadius: 24,
            offset: Offset(0, 12),
          ),
        ],
      ),
      child: child,
    );
  }
}

class _ExampleGrid extends StatelessWidget {
  const _ExampleGrid({required this.children});

  final List<Widget> children;

  @override
  Widget build(BuildContext context) {
    return LayoutBuilder(
      builder: (context, constraints) {
        if (constraints.maxWidth < 620) {
          return Column(
            children: [
              for (var i = 0; i < children.length; i++) ...[
                children[i],
                if (i != children.length - 1) const SizedBox(height: 14),
              ],
            ],
          );
        }

        return Wrap(
          spacing: 14,
          runSpacing: 14,
          children: [
            for (final child in children)
              SizedBox(width: (constraints.maxWidth - 14) / 2, child: child),
          ],
        );
      },
    );
  }
}

class _PreviewBlock extends StatelessWidget {
  const _PreviewBlock({
    required this.title,
    required this.content,
  });

  final String title;
  final String content;

  @override
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Text(
          title,
          style: Theme.of(context).textTheme.labelLarge?.copyWith(
                color: Colors.white,
                fontWeight: FontWeight.w700,
              ),
        ),
        const SizedBox(height: 8),
        Container(
          width: double.infinity,
          padding: const EdgeInsets.all(16),
          decoration: BoxDecoration(
            color: const Color(0xFF111827),
            borderRadius: BorderRadius.circular(18),
            border: Border.all(color: const Color(0xFF1F2937)),
          ),
          child: Text(
            content,
            style: const TextStyle(
              color: Color(0xFFE5E7EB),
              fontFamily: 'monospace',
              height: 1.5,
            ),
          ),
        ),
      ],
    );
  }
}

class _PreviewChip extends StatelessWidget {
  const _PreviewChip({required this.label, required this.tone});

  final String label;
  final Color tone;

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
      decoration: BoxDecoration(
        color: tone.withValues(alpha: 0.18),
        borderRadius: BorderRadius.circular(999),
        border: Border.all(color: tone.withValues(alpha: 0.28)),
      ),
      child: Text(
        label,
        style: TextStyle(
          color: tone,
          fontWeight: FontWeight.w600,
        ),
      ),
    );
  }
}

class _ExampleFieldCard extends StatelessWidget {
  const _ExampleFieldCard({required this.child});

  final Widget child;

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.all(14),
      decoration: BoxDecoration(
        color: const Color(0xFFF8FAFC),
        borderRadius: BorderRadius.circular(22),
        border: Border.all(color: const Color(0xFFE4E7EC)),
      ),
      child: child,
    );
  }
}
0
likes
160
points
199
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Schema-friendly Flutter forms with reusable field widgets, validators, controller state, and preset catalogs.

Repository (GitHub)
View/report issues

Topics

#form #validation #flutter-widget #dynamic-form #form-builder

License

MIT (license)

Dependencies

device_preview, flutter

More

Packages that depend on form_flutter