flutter_ai_forms 1.0.1
flutter_ai_forms: ^1.0.1 copied to clipboard
A provider-agnostic, schema-driven Flutter dynamic form engine for AI-generated interfaces, validation, conditional fields, and extensible widgets.
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.
Features #
- AI Provider Agnostic: Compatible with OpenAI, Gemini, Claude, Ollama, OpenRouter, and Hugging Face via the
AiFormGeneratorcontract. - 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, andrequiredWhenrules 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.