A production-grade Flutter form engine demonstrating Domain-Driven Design (DDD) and Clean Architecture principles.
Build complex forms from JSON with strict separation of concerns, offline support, and zero coupling between business logic and UI.
Most Flutter form systems have one fatal flaw: UI logic is tightly coupled to business logic.
This means:
- ❌ Changing one field type requires touching 5+ files
- ❌ Business rules are scattered across widgets
- ❌ Testing validation without UI is impossible
- ❌ Adding new features means debugging unrelated code breaking
Result: Technical debt accumulates fast. Even simple changes become risky.
JSON-Driven Form Engine with Domain-Driven Design
Define your entire form in JSON. The app renders it. Business logic stays pure, testable, and independent.
{
"id": "contact-form-v1",
"title": "Contact Us",
"description": "Get in touch with us",
"questions": [
{
"id": "email",
"type": "short_text",
"title": "Email Address",
"description": "Your email address",
"isRequired": true,
"options": null
},
{
"id": "message",
"type": "long_text",
"title": "Message",
"description": "Your message to us",
"isRequired": true,
"options": null
},
{
"id": "rating",
"type": "single_choice",
"title": "How would you rate us?",
"description": "",
"isRequired": false,
"options": [
{"id": "excellent", "title": "Excellent"},
{"id": "good", "title": "Good"},
{"id": "average", "title": "Average"},
{"id": "poor", "title": "Poor"}
]
}
]
}Add new field types? Just extend the factory. Existing code remains untouched.
- Render any form from JSON without code changes
- Support for multiple field types:
short_text,long_text,single_choice(radio buttons) - Easy to add new field types via switch statement pattern
- Domain layer contains ALL business rules (100% testable, zero Flutter dependencies)
- BLoC pattern for predictable state management
- Value objects ensure type safety and validation
- Forms cached via Hive for instant access
- Submit forms offline; sync when connected
- Users never lose data
- Lazy load form fields
- Smart caching with Hive reduces network requests
- Fast load times even for complex forms
- BLoC state management for predictable UI updates
- Error handling with proper state management
- Type-safe with modern Dart patterns
┌─────────────────────────────────────┐
│ PRESENTATION (UI, BLoC, Widgets) │
│ Depends on: Application Layer │
└────────────────┬────────────────────┘
│
┌────────────────▼────────────────────┐
│ APPLICATION (BLoC, Use Cases) │
│ Depends on: Domain Layer │
└────────────────┬────────────────────┘
│
┌────────────────▼────────────────────┐
│ DOMAIN (Business Rules, Entities) │
│ Zero dependencies (Pure Dart) │
└────────────────┬────────────────────┘
│
┌────────────────▼────────────────────┐
│ INFRASTRUCTURE (Hive, DTOs, JSON) │
│ Implements Domain repositories │
└─────────────────────────────────────┘
Form State Management with BLoC:
class FormBloc extends Bloc<FormEvent, FormState> {
FormBloc() : super(FormState.initial()) {
on<FormEvent>((event, emit) {
event.when(
saveTitle: (title) => emit(state.copyWith(title: FormTitle(title))),
answerChanged: (questionId, answer) => emit(
state.copyWith(answers: {...state.answers, questionId: answer})
),
);
});
}
}Dynamic Field Rendering:
Widget _buildQuestionInput(context, question, selectedValue, FormBloc bloc) {
switch (question.type) {
case FormInputTypeEnum.singleChoice:
return Column(children: question.options?.map<Widget>((option) {
return RadioListTile<String>(
title: Text(option.title),
value: option.id,
groupValue: selectedValue,
onChanged: (value) => bloc.add(
FormEvent.answerChanged(questionId: question.id, answer: value!)
),
);
}).toList() ?? []);
case FormInputTypeEnum.shortText:
return TextFormField(
initialValue: selectedValue?.toString() ?? '',
onChanged: (value) => bloc.add(
FormEvent.answerChanged(questionId: question.id, answer: value)
),
);
case FormInputTypeEnum.longText:
return TextFormField(
initialValue: selectedValue?.toString() ?? '',
maxLines: 4,
onChanged: (value) => bloc.add(
FormEvent.answerChanged(questionId: question.id, answer: value)
),
);
}
}lib/
├── main.dart # App entry point with Hive setup
├── injection.dart # Dependency injection setup
│
├── application/
│ └── form/
│ ├── form_bloc.dart # BLoC for form state management
│ ├── form_event.dart # Form events (saveTitle, answerChanged)
│ └── form_state.dart # Form state with answers map
│
├── domain/
│ └── core/
│ └── _enums.dart # FormInputTypeEnum definition
│
├── infrastructure/
│ └── form/
│ └── dtos/
│ ├── form_dto.dart # Form data transfer objects
│ └── hive_adapters.dart # Hive type adapters
│
└── presentation/
├── core/
│ └── app_widget.dart # Main app widget
└── form/
└── form_page.dart # Main form rendering page
Forms are stateful. BLoC makes state transitions explicit and testable.
// Current implementation uses events and state
on<FormEvent>((event, emit) {
event.when(
saveTitle: (title) => emit(state.copyWith(title: FormTitle(title))),
answerChanged: (questionId, answer) => emit(
state.copyWith(answers: {...state.answers, questionId: answer})
),
);
});Fast local storage for forms and user data.
// Setup in main.dart
await Hive.initFlutter();
Hive.registerAdapter(FormDtoAdapter());
Hive.registerAdapter(FormQuestionDtoAdapter());
final formBox = await Hive.openBox('forms_v3');Type-safe field type definitions:
enum FormInputTypeEnum {
singleChoice,
shortText,
longText,
// Easy to extend with new types
}- Flutter 3.8.1+
- Dart 3.8.1+
# Clone repository
git clone https://github.com/GodswillErondu/forms_project.git
cd forms_project
# Install dependencies
flutter pub get
# Run code generation for Hive adapters
flutter pub run build_runner build
# Run the app
flutter runKey packages used:
flutter_bloc: ^9.1.1- State managementhive: ^2.2.3&hive_flutter: ^1.1.0- Local storagedartz: ^0.10.1- Functional programming utilitiesfreezed_annotation: ^3.0.0- Immutable classesget_it: ^8.0.3- Dependency injectioninjectable: ^2.5.0- Code generation for DI
✅ Single Responsibility — Each widget/class has one clear purpose
✅ State Management — Predictable state updates with BLoC
✅ Type Safety — Enum-based field types, no magic strings
✅ Offline-First — Hive storage for instant access
✅ Extensibility — Easy to add new field types via enum extension
✅ Clean Architecture — Clear separation between layers
MIT License — See LICENSE file for details.
This is a showcase project demonstrating best practices in Flutter architecture. Use it as a reference for your own projects. Questions? Open an issue.
Most developers build features. This project shows how to build systems that scale.
At Transferxo, we used these principles to:
- Ship from 0 → 1,000 users with zero downtime
- Move 30% faster as the team grew
- Never rewrite the codebase
Same principles. Same approach. Used across production apps handling real users and real money.
That's what makes this portfolio piece powerful.