form_ui_builder 0.0.4
form_ui_builder: ^0.0.4 copied to clipboard
A Flutter package for building dynamic, data-driven forms with conditional visibility, auto-calculations, multi-field validation, and real-time validation support.
example/lib/main.dart
import 'dart:convert';
import 'package:example/bloc/main_bloc.dart';
import 'package:example/mock_data.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:form_ui_builder/form_ui_builder.dart';
void main() async {
// Initialize the form_ui_builder package dependencies
// This must be called before using any form builder features
await FormBuilder.initialize();
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Form UI Builder Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const MyHomePage(title: 'Form UI Builder Demo'),
);
}
}
class MyHomePage extends StatelessWidget {
const MyHomePage({super.key, required this.title});
final String title;
void _buildFormWithFacade(BuildContext context) {
// NEW APPROACH: Using FormBuilder facade (Recommended)
// This is the simplest way to create a form
// Parse JSON string to List<Map<String, dynamic>>
final jsonData = jsonDecode(mockJsonData) as List<dynamic>;
final List<Map<String, dynamic>> childrenJson =
jsonData[0]['childrens'][0]['childrens'].cast<Map<String, dynamic>>();
final result = FormBuilder.buildFromJson(
childrenJson,
onSubmit: (result) {
// Handle form submission
if (context.mounted) {
Navigator.of(context).pop();
_showSuccessDialog(context, result);
}
},
);
result.fold(
(failure) {
// Handle error
showSnackBar(context, failure.message);
},
(formScreen) {
// Navigate to form screen
Navigator.of(
context,
).push(MaterialPageRoute(builder: (_) => formScreen));
},
);
}
void _buildFormWithBloc(BuildContext context) {
// OLD APPROACH: Using BLoC directly (More control)
// Use this when you need more control over the BLoC lifecycle
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => BlocProvider(
create: (_) => MainBloc(),
child: const LegacyFormBuilderExample(),
),
),
);
}
void _showSuccessDialog(BuildContext context, FormSubmissionResult result) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Form Submitted Successfully! ✅'),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Submitted Values:',
style: TextStyle(fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
...result.valueMap.entries.map((entry) {
final fieldConfig = result.parameterMap[entry.key];
final fieldName =
fieldConfig?.elementLabel.en ?? 'Field ${entry.key}';
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Text('$fieldName: ${entry.value.value}'),
);
}),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('OK'),
),
],
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: Text(title),
),
body: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.all(24.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.dynamic_form,
size: 64,
color: Colors.deepPurple,
),
const SizedBox(height: 24),
const Text(
'Form UI Builder',
style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
const Text(
'Choose your approach to build dynamic forms',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 16, color: Colors.grey),
),
const SizedBox(height: 48),
// NEW APPROACH Card
Card(
elevation: 4,
child: Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
children: [
const Row(
children: [
Icon(Icons.auto_awesome, color: Colors.green),
SizedBox(width: 8),
Text(
'New: FormBuilder Facade',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
SizedBox(width: 8),
Chip(
label: Text(
'Recommended',
style: TextStyle(fontSize: 10),
),
backgroundColor: Colors.green,
labelStyle: TextStyle(color: Colors.white),
),
],
),
const SizedBox(height: 12),
const Text(
'Simplest way to build forms. One-line form creation with automatic BLoC setup.',
textAlign: TextAlign.center,
style: TextStyle(color: Colors.grey),
),
const SizedBox(height: 16),
ElevatedButton.icon(
onPressed: () => _buildFormWithFacade(context),
icon: const Icon(Icons.rocket_launch),
label: const Text('Build with Facade'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(
horizontal: 32,
vertical: 16,
),
textStyle: const TextStyle(fontSize: 16),
),
),
const SizedBox(height: 8),
const Text(
'70% less boilerplate code',
style: TextStyle(
fontSize: 12,
fontStyle: FontStyle.italic,
color: Colors.green,
fontWeight: FontWeight.bold,
),
),
],
),
),
),
const SizedBox(height: 24),
// OLD APPROACH Card
Card(
elevation: 4,
child: Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
children: [
const Row(
children: [
Icon(Icons.settings, color: Colors.blue),
SizedBox(width: 8),
Text(
'Classic: BLoC Approach',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
],
),
const SizedBox(height: 12),
const Text(
'Manual BLoC setup for maximum control. Use when you need fine-grained lifecycle management.',
textAlign: TextAlign.center,
style: TextStyle(color: Colors.grey),
),
const SizedBox(height: 16),
OutlinedButton.icon(
onPressed: () => _buildFormWithBloc(context),
icon: const Icon(Icons.code),
label: const Text('Build with BLoC'),
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(
horizontal: 32,
vertical: 16,
),
textStyle: const TextStyle(fontSize: 16),
),
),
],
),
),
),
const SizedBox(height: 32),
const Divider(),
const SizedBox(height: 16),
const Text(
'Both approaches produce the same form\nwith identical functionality.',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 14,
fontStyle: FontStyle.italic,
color: Colors.grey,
),
),
],
),
),
),
);
}
}
// Legacy example showing the old approach with manual BLoC setup
class LegacyFormBuilderExample extends StatelessWidget {
const LegacyFormBuilderExample({super.key});
@override
Widget build(BuildContext context) {
return BlocConsumer<MainBloc, MainState>(
listener: (context, state) {
if (state.status == MainStatus.failure) {
showSnackBar(context, state.errorMessage);
}
if (state.formLogicBundle.parameterMap.isNotEmpty &&
!state.hasNavigated) {
context.read<MainBloc>().add(
const SetHasNavigatedEvent(hasNavigated: true),
);
Navigator.of(context)
.push(
MaterialPageRoute(
builder: (context) => BlocProvider(
create: (_) => FormUiBuilderBloc(
buildFormLogicBundle: getIt(),
validateTextFieldValue: getIt(),
updateFieldWithDependencies: getIt(),
validateFieldOnBlur: getIt(),
updateVisibilityState: getIt(),
validationOnSubmit: getIt(),
initialFormLogicBundle: state.formLogicBundle,
),
child: FormUiBuilderScreen(
onSubmit: (result) {
Navigator.of(context).pop(result);
},
),
),
),
)
.then((result) {
// Reset the form state when user returns
if (context.mounted) {
context.read<MainBloc>().add(const ResetFormEvent());
// Handle FormSubmissionResult
if (result != null) {
_showSuccessDialog(context, result);
}
}
});
}
},
builder: (context, state) {
return Scaffold(
appBar: AppBar(title: const Text('Classic BLoC Approach')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Padding(
padding: EdgeInsets.all(16.0),
child: Text(
'Choose Form Data Source:',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
),
const SizedBox(height: 20),
ElevatedButton.icon(
onPressed: () {
context.read<MainBloc>().add(const GetParameterEvent());
},
icon: const Icon(Icons.code),
label: const Text('Build UI from JSON'),
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(
horizontal: 32,
vertical: 16,
),
textStyle: const TextStyle(fontSize: 16),
),
),
const SizedBox(height: 16),
ElevatedButton.icon(
onPressed: () {
context.read<MainBloc>().add(
const GetParameterFromMapEvent(),
);
},
icon: const Icon(Icons.map),
label: const Text('Build UI from Parameter Map'),
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(
horizontal: 32,
vertical: 16,
),
textStyle: const TextStyle(fontSize: 16),
),
),
],
),
),
);
},
);
}
void _showSuccessDialog(BuildContext context, FormSubmissionResult result) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Form Submitted Successfully! ✅'),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Submitted Values:',
style: TextStyle(fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
...result.valueMap.entries.map((entry) {
final fieldConfig = result.parameterMap[entry.key];
final fieldName =
fieldConfig?.elementLabel.en ?? 'Field ${entry.key}';
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Text('$fieldName: ${entry.value.value}'),
);
}),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('OK'),
),
],
),
);
}
}