json_driven_dynamic_form 1.0.0 copy "json_driven_dynamic_form: ^1.0.0" to clipboard
json_driven_dynamic_form: ^1.0.0 copied to clipboard

A production-ready Flutter package that generates fully dynamic, validated forms from JSON schemas — no manual widget wiring required.

example/lib/main.dart

import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:json_driven_dynamic_form/json_driven_dynamic_form.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  runApp(const DynamicFormExampleApp());
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'json_driven_dynamic_form Example',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF0F766E)),
        useMaterial3: true,
      ),
      home: const HomePage(),
    );
  }
}

class DemoForm {
  const DemoForm({
    required this.title,
    required this.subtitle,
    required this.assetPath,
    required this.icon,
    this.useMediaPlugins = false,
    this.useCustomRenderer = false,
    this.useAsyncValidator = false,
  });

  final String title;
  final String subtitle;
  final String assetPath;
  final IconData icon;
  final bool useMediaPlugins;
  final bool useCustomRenderer;
  final bool useAsyncValidator;
}

const demos = <DemoForm>[
  DemoForm(
    title: 'Simple Form',
    subtitle: 'Name + email basics',
    assetPath: 'assets/schemas/simple_form.json',
    icon: Icons.short_text,
  ),
  DemoForm(
    title: 'Registration',
    subtitle: 'All Phase 1 field types',
    assetPath: 'assets/schemas/registration_form.json',
    icon: Icons.person_add_alt_1_outlined,
  ),
  DemoForm(
    title: 'Survey',
    subtitle: 'Validation + multiline feedback',
    assetPath: 'assets/schemas/survey_form.json',
    icon: Icons.rate_review_outlined,
  ),
  DemoForm(
    title: 'Selection & Dates',
    subtitle: 'Checkbox, switch, radio, dropdown, chips, pickers',
    assetPath: 'assets/schemas/selection_form.json',
    icon: Icons.tune,
  ),
  DemoForm(
    title: 'Conditional Form',
    subtitle: 'Show / enable fields from other answers',
    assetPath: 'assets/schemas/conditional_form.json',
    icon: Icons.alt_route,
  ),
  DemoForm(
    title: 'Nested Form',
    subtitle: 'Groups + compare password validation',
    assetPath: 'assets/schemas/nested_form.json',
    icon: Icons.account_tree_outlined,
  ),
  DemoForm(
    title: 'Media Upload',
    subtitle: 'Image, camera, file, location + async email',
    assetPath: 'assets/schemas/media_form.json',
    icon: Icons.cloud_upload_outlined,
    useMediaPlugins: true,
    useAsyncValidator: true,
  ),
  DemoForm(
    title: 'Advanced Controls',
    subtitle: 'Slider, rating, color, signature, repeatable, custom',
    assetPath: 'assets/schemas/advanced_form.json',
    icon: Icons.auto_awesome,
    useCustomRenderer: true,
  ),
];

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('json_driven_dynamic_form'),
      ),
      body: ListView.separated(
        padding: const EdgeInsets.all(16),
        itemCount: demos.length,
        separatorBuilder: (_, __) => const SizedBox(height: 12),
        itemBuilder: (context, index) {
          final demo = demos[index];
          return ListTile(
            shape: RoundedRectangleBorder(
              borderRadius: BorderRadius.circular(12),
              side: BorderSide(color: Theme.of(context).dividerColor),
            ),
            leading: Icon(demo.icon),
            title: Text(demo.title),
            subtitle: Text(demo.subtitle),
            trailing: const Icon(Icons.chevron_right),
            onTap: () {
              Navigator.of(context).push(
                MaterialPageRoute<void>(
                  builder: (_) => FormDemoPage(demo: demo),
                ),
              );
            },
          );
        },
      ),
    );
  }
}

class FormDemoPage extends StatefulWidget {
  const FormDemoPage({super.key, required this.demo});

  final DemoForm demo;

  @override
  State<FormDemoPage> createState() => _FormDemoPageState();
}

class _FormDemoPageState extends State<FormDemoPage> {
  late final DynamicFormController _controller;
  late final DynamicFormPluginRegistry _plugins;
  Map<String, dynamic>? _schema;
  Object? _loadError;

  @override
  void initState() {
    super.initState();
    _controller = DynamicFormController();
    _plugins = DynamicFormPluginRegistry();

    if (widget.demo.useMediaPlugins) {
      _plugins.setMediaServices(mockMediaServices());
    }

    if (widget.demo.useCustomRenderer) {
      _plugins.registerRenderer('badge', (ctx) {
        return TextFormField(
          initialValue: ctx.controller.getValue(ctx.field.key)?.toString() ?? '',
          decoration: InputDecoration(
            labelText: ctx.field.label,
            hintText: ctx.field.effectivePlaceholder,
            prefixIcon: const Icon(Icons.badge_outlined),
            filled: true,
            border: OutlineInputBorder(
              borderRadius: BorderRadius.circular(ctx.theme.borderRadius),
            ),
          ),
          onChanged: (v) => ctx.controller.updateField(ctx.field.key, v),
        );
      });
    }

    if (widget.demo.useAsyncValidator) {
      _controller.registerAsyncValidator('uniqueEmail', (value, context) async {
        await Future<void>.delayed(const Duration(milliseconds: 250));
        if (value?.toString().toLowerCase() == 'taken@example.com') {
          return context.rule?.message ?? 'Email already registered';
        }
        return null;
      });
    }

    _loadSchema();
  }

  Future<void> _loadSchema() async {
    try {
      final raw = await rootBundle.loadString(widget.demo.assetPath);
      final decoded = jsonDecode(raw);
      setState(() {
        _schema = Map<String, dynamic>.from(decoded as Map);
        _loadError = null;
      });
    } catch (e) {
      setState(() => _loadError = e);
    }
  }

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

  void _showResult(Map<String, dynamic> values) {
    showDialog<void>(
      context: context,
      builder: (context) => AlertDialog(
        title: const Text('Submitted values'),
        content: SingleChildScrollView(
          child: Text(
            const JsonEncoder.withIndent('  ').convert(
              values.map((key, value) {
                if (value is MediaFileValue) return MapEntry(key, value.toJson());
                if (value is LocationValue) return MapEntry(key, value.toJson());
                return MapEntry(key, value);
              }),
            ),
          ),
        ),
        actions: [
          TextButton(
            onPressed: () => Navigator.of(context).pop(),
            child: const Text('OK'),
          ),
        ],
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text(widget.demo.title)),
      body: _loadError != null
          ? Center(child: Text('Failed to load schema:\n$_loadError'))
          : _schema == null
              ? const Center(child: CircularProgressIndicator())
              : DynamicForm(
                  json: _schema!,
                  controller: _controller,
                  plugins: _plugins,
                  theme: const DynamicFormTheme(
                    fieldSpacing: 18,
                    borderRadius: 12,
                    animateFields: true,
                    groupDecoration: true,
                  ),
                  onSubmit: (_) => _showResult(_controller.nestedValues),
                  onValidationFailed: (errors) {
                    ScaffoldMessenger.of(context).showSnackBar(
                      SnackBar(
                        content: Text(
                          'Please fix ${errors.length} field'
                          '${errors.length == 1 ? '' : 's'}.',
                        ),
                      ),
                    );
                  },
                ),
    );
  }
}
0
likes
160
points
116
downloads

Documentation

Documentation
API reference

Publisher

unverified uploader

Weekly Downloads

A production-ready Flutter package that generates fully dynamic, validated forms from JSON schemas — no manual widget wiring required.

Repository (GitHub)
View/report issues
Contributing

Topics

#form #json #dynamic-forms #validation #widget

License

MIT (license)

Dependencies

flutter

More

Packages that depend on json_driven_dynamic_form