flutter_ai_forms

A provider-agnostic, schema-driven Flutter dynamic form engine designed for AI-generated interfaces, dynamic validation, conditional field rendering, schema repair, and custom component extensions.

pub package License: MIT


Features

  • AI Provider Agnostic: Compatible with OpenAI, Gemini, Claude, Ollama, OpenRouter, and Hugging Face via the AiFormGenerator contract.
  • Self-Healing Schema Normalization (SchemaNormalizer): Automatically sanitizes malformed LLM outputs (such as invalid type names, string-based booleans, or markdown block wrapping) without throwing exceptions.
  • Backend Dynamic API Integration: Parses dynamic JSON schemas from REST or GraphQL endpoints directly into interactive Flutter forms.
  • Dynamic Conditional Logic: Evaluates visibleWhen, enabledWhen, and requiredWhen rules in real time.
  • Extensible Field Registry: Supports registering custom field widgets via AiFormFieldRegistry.
  • 13 Built-in Field Types: Supports Text, Email, Password, Number, Phone, Dropdown, Radio, Checkbox, Multi-Select, Date, Time, Slider, and File input types.

Getting Started

Add flutter_ai_forms to your pubspec.yaml:

dependencies:
  flutter:
    sdk: flutter
  flutter_ai_forms: ^1.0.0

Usage Example

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

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

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

  @override
  Widget build(BuildContext context) {
    final formSchema = FormSchema.fromJson({
      "title": "Customer Registration",
      "fields": [
        {
          "id": "full_name",
          "type": "text",
          "label": "Full Name",
          "required": true
        },
        {
          "id": "email",
          "type": "email",
          "label": "Email Address",
          "required": true
        },
        {
          "id": "employment_status",
          "type": "dropdown",
          "label": "Employment Status",
          "options": ["Employed", "Student", "Unemployed"]
        },
        {
          "id": "company",
          "type": "text",
          "label": "Company Name",
          "visibleWhen": { "field": "employment_status", "equals": "Employed" }
        }
      ]
    });

    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: const Text('flutter_ai_forms')),
        body: SingleChildScrollView(
          child: AiForm(
            schema: formSchema,
            onSubmit: (FormResult result) {
              if (result.isValid) {
                print('Form Values: ${result.values}');
              }
            },
          ),
        ),
      ),
    );
  }
}

AI Model Integration

When generating forms dynamically using LLMs (OpenAI, OpenRouter, Gemini, Claude, Ollama), follow these guidelines for optimal performance, accuracy, and reliability.

1. System Prompt Blueprint

Include explicit JSON structure constraints in the system prompt to enforce valid schema output:

You are a dynamic UI schema generator. 
Generate a valid JSON object matching this structure:

{
  "title": "Form Title",
  "description": "Optional form description",
  "submitLabel": "Submit",
  "fields": [
    {
      "id": "unique_field_id",
      "type": "text|email|password|number|phone|dropdown|radio|checkbox|multi_select|date|time|slider|file",
      "label": "Human Readable Label",
      "placeholder": "Optional hint",
      "defaultValue": null,
      "required": true|false,
      "options": ["Option 1", "Option 2"],
      "min": 0,
      "max": 100,
      "visibleWhen": { "field": "other_id", "equals": "target_value" },
      "enabledWhen": { "field": "other_id", "notEquals": "value" },
      "requiredWhen": { "field": "other_id", "equals": "value" }
    }
  ]
}

Return raw valid JSON only without markdown formatting or introductory text.

2. Implementing AiFormGenerator

Implement the AiFormGenerator interface and leverage SchemaNormalizer to safely parse and sanitize the model response:

import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:flutter_ai_forms/flutter_ai_forms.dart';

class OpenAiFormGenerator implements AiFormGenerator {
  final String apiKey;
  final String model;

  OpenAiFormGenerator({
    required this.apiKey,
    this.model = 'gpt-4o-mini',
  });

  @override
  Future<FormSchema> generate(String userPrompt) async {
    final response = await http.post(
      Uri.parse('https://api.openai.com/v1/chat/completions'),
      headers: {
        'Content-Type': 'application/json',
        'Authorization': 'Bearer $apiKey',
      },
      body: jsonEncode({
        'model': model,
        'response_format': {'type': 'json_object'},
        'messages': [
          {
            'role': 'system',
            'content': 'You generate FormSchema JSON for Flutter dynamic forms. Return valid JSON only.',
          },
          {
            'role': 'user',
            'content': 'Generate a dynamic form for: $userPrompt',
          },
        ],
        'temperature': 0.2,
      }),
    );

    if (response.statusCode != 200) {
      throw Exception('Failed to generate form schema: ${response.body}');
    }

    final jsonResponse = jsonDecode(response.body);
    final rawJsonContent = jsonResponse['choices'][0]['message']['content'];

    final parseResult = SchemaNormalizer.normalize(rawJsonContent);
    
    if (!parseResult.isValid) {
      throw Exception('Schema parsing error: ${parseResult.error}');
    }

    return parseResult.schema;
  }
}

3. Real-Time AI Generation UI Component

class DynamicAiFormScreen extends StatefulWidget {
  final AiFormGenerator generator;

  const DynamicAiFormScreen({super.key, required this.generator});

  @override
  State<DynamicAiFormScreen> createState() => _DynamicAiFormScreenState();
}

class _DynamicAiFormScreenState extends State<DynamicAiFormScreen> {
  final TextEditingController _promptController = TextEditingController();
  FormSchema? _generatedSchema;
  bool _isLoading = false;
  String? _errorMessage;

  Future<void> _generateForm() async {
    if (_promptController.text.trim().isEmpty) return;

    setState(() {
      _isLoading = true;
      _errorMessage = null;
    });

    try {
      final schema = await widget.generator.generate(_promptController.text);
      setState(() {
        _generatedSchema = schema;
      });
    } catch (e) {
      setState(() {
        _errorMessage = e.toString();
      });
    } finally {
      setState(() {
        _isLoading = false;
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('AI Dynamic Form Generator')),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          children: [
            Row(
              children: [
                Expanded(
                  child: TextField(
                    controller: _promptController,
                    decoration: const InputDecoration(
                      hintText: 'Enter form description (e.g. Event Registration Form)',
                      border: OutlineInputBorder(),
                    ),
                  ),
                ),
                const SizedBox(width: 8),
                ElevatedButton(
                  onPressed: _isLoading ? null : _generateForm,
                  child: _isLoading
                      ? const CircularProgressIndicator()
                      : const Text('Generate'),
                ),
              ],
            ),
            const SizedBox(height: 20),
            if (_errorMessage != null)
              Text(_errorMessage!, style: const TextStyle(color: Colors.red)),
            if (_generatedSchema != null)
              Expanded(
                child: SingleChildScrollView(
                  child: AiForm(
                    schema: _generatedSchema!,
                    onSubmit: (result) {
                      if (result.isValid) {
                        ScaffoldMessenger.of(context).showSnackBar(
                          SnackBar(content: Text('Form Submitted: ${result.values}')),
                        );
                      }
                    },
                  ),
                ),
              ),
          ],
        ),
      ),
    );
  }
}

Dynamic Backend API Integration

flutter_ai_forms can render forms directly from dynamic JSON definitions served by backend applications (Node.js, Go, FastAPI, Django, Laravel, Spring).

Fetching and Rendering Forms from REST Endpoints

import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:flutter_ai_forms/flutter_ai_forms.dart';

class DynamicBackendFormScreen extends StatefulWidget {
  final String formId;
  final String apiBaseUrl;

  const DynamicBackendFormScreen({
    super.key,
    required this.formId,
    required this.apiBaseUrl,
  });

  @override
  State<DynamicBackendFormScreen> createState() => _DynamicBackendFormScreenState();
}

class _DynamicBackendFormScreenState extends State<DynamicBackendFormScreen> {
  late Future<FormSchema> _formSchemaFuture;

  @override
  void initState() {
    super.initState();
    _formSchemaFuture = _fetchFormSchema();
  }

  Future<FormSchema> _fetchFormSchema() async {
    final url = Uri.parse('${widget.apiBaseUrl}/api/v1/forms/${widget.formId}');
    final response = await http.get(url);

    if (response.statusCode == 200) {
      final parseResult = SchemaNormalizer.normalize(response.body);
      return parseResult.schema;
    } else {
      throw Exception('Failed to load dynamic form schema: ${response.statusCode}');
    }
  }

  Future<void> _submitFormToBackend(Map<String, dynamic> formValues) async {
    final submitUrl = Uri.parse('${widget.apiBaseUrl}/api/v1/forms/${widget.formId}/submissions');
    
    final response = await http.post(
      submitUrl,
      headers: {'Content-Type': 'application/json'},
      body: jsonEncode(formValues),
    );

    if (response.statusCode == 200 || response.statusCode == 201) {
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          const SnackBar(content: Text('Form submitted successfully.')),
        );
      }
    } else {
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text('Submission failed: ${response.body}')),
        );
      }
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Dynamic API Form')),
      body: FutureBuilder<FormSchema>(
        future: _formSchemaFuture,
        builder: (context, snapshot) {
          if (snapshot.connectionState == ConnectionState.waiting) {
            return const Center(child: CircularProgressIndicator());
          } else if (snapshot.hasError) {
            return Center(
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  Text('Error loading form: ${snapshot.error}'),
                  const SizedBox(height: 12),
                  ElevatedButton(
                    onPressed: () => setState(() {
                      _formSchemaFuture = _fetchFormSchema();
                    }),
                    child: const Text('Retry'),
                  ),
                ],
              ),
            );
          } else if (snapshot.hasData) {
            return SingleChildScrollView(
              child: AiForm(
                schema: snapshot.data!,
                onSubmit: (FormResult result) {
                  if (result.isValid) {
                    _submitFormToBackend(result.values);
                  }
                },
              ),
            );
          }
          return const Center(child: Text('No Form Data Available'));
        },
      ),
    );
  }
}

Performance and Best Practices

Objective Recommended Technique Benefit
Reduce Latency Configure temperature: 0.1-0.2 and response_format: {"type": "json_object"} Reduces output token generation overhead and enforces valid JSON formatting.
Schema Sanitation Process responses via SchemaNormalizer.normalize(jsonString) Prevents runtime exceptions caused by malformed JSON, markdown fences, or unrecognized field types.
Network Optimization Cache parsed FormSchema instances locally Eliminates redundant network calls for static or infrequently modified schemas.
Form State Management Programatically interact via AiFormController Enables external form state inspection, programatic updates, and custom validation triggers.
Custom Field Renderers Register custom widgets using AiFormFieldRegistry Allows integration of specialized UI components (e.g. Signature fields, Color Pickers, Rating systems).

Schema Repair Example

// Example of imprecise raw JSON output generated by an LLM
String rawLlmString = '''
```json
{
  "title": "User Registration",
  "fields": [
    { "id": "email", "type": "emal", "label": "Email", "required": "yes" },
    { "id": "age", "type": "numb", "label": "Age", "min": "18" }
  ]
}

''';

// Normalize output to produce a strictly typed FormSchema FormParseResult parseResult = SchemaNormalizer.normalize(rawLlmString);

FormSchema schema = parseResult.schema; print(schema.fields.first.type); // Output: "email" print(schema.fields.first.required); // Output: true


---

## License

This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.

Libraries

flutter_ai_forms