dynamic_pages 0.1.0
dynamic_pages: ^0.1.0 copied to clipboard
Schema-driven, type-safe generated Flutter pages for Dart models.
dynamic_pages #
Build production-oriented Flutter forms from typed Dart/Freezed models or a constrained server schema. The generator emits typed metadata and a small page wrapper; maintained runtime widgets handle rendering, validation, loading, errors, and themes.
Version 0.1 focuses deliberately on forms. Details, list, and CRUD page renderers can be added without changing the schema-first architecture.
Features #
source_gensupport for regular Dart and Freezed factory models- text, email, number, date, enum dropdown, checkbox, and custom fields
- create and edit modes through
initialValue - synchronous and asynchronous validation
- loading state and API field-error mapping
- scroll to the first invalid field
- dirty-state confirmation
- application-wide and per-page themes
- custom field builders plus header and footer composition
- constrained JSON schemas for server-driven forms
Typed model generation #
Add dynamic_pages and your preferred model packages, then add
build_runner to dev_dependencies.
import 'package:dynamic_pages/dynamic_pages.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'user_form.freezed.dart';
part 'user_form.g.dart';
enum Role { admin, member }
@freezed
@GenerateFormPage(title: 'Create user', submitLabel: 'Save')
abstract class UserForm with _$UserForm {
const factory UserForm({
@FormTextField(label: 'Name', required: true, minLength: 2)
required String name,
@FormEmailField(label: 'Email', required: true)
required String email,
@FormDateField(label: 'Birth date')
DateTime? birthDate,
@FormDropdownField(label: 'Role', source: Role.values)
required Role role,
@FormCheckboxField(label: 'Receive product updates')
@Default(false)
bool receiveUpdates,
}) = _UserForm;
}
Generate both the Freezed implementation and page metadata:
dart run build_runner build --delete-conflicting-outputs
The generated UserFormPage has a typed submit callback:
UserFormPage(
initialValue: existingUser,
onSubmit: (value) async {
await repository.save(value);
},
);
You can also render the generated schema directly:
ModelFormPage<UserForm>(
schema: userFormPageSchema,
onSubmit: repository.save,
);
Themes #
Use Flutter's extension mechanism for application-wide defaults:
MaterialApp(
theme: ThemeData(
extensions: const [
DynamicPageThemeData(
contentMaxWidth: 640,
contentPadding: EdgeInsets.all(32),
fieldSpacing: 20,
),
],
),
);
Any page can override only the tokens it needs:
UserFormPage(
theme: const DynamicPageThemeData(
contentMaxWidth: 520,
fieldSpacing: 12,
),
onSubmit: repository.save,
);
Themes resolve in this order: package fallback, application
ThemeData.extensions, then the page override.
Custom fields and composition #
UserFormPage(
header: const ProfileHeader(),
footer: const TermsSection(),
fieldBuilders: {
'avatar': (context, data) => AvatarPicker(
value: data.value,
errorText: data.errorText,
onChanged: data.onChanged,
),
},
onSubmit: repository.save,
);
Annotate non-standard values with @CustomFormField so missing builders are
reported clearly in the UI.
Server-driven forms #
Server schemas use a small domain vocabulary rather than arbitrary Flutter widgets:
DynamicSchemaPage.fromJson(
schema: {
'type': 'form',
'title': 'Create user',
'submitLabel': 'Save',
'theme': {
'contentWidth': 'narrow',
'density': 'comfortable',
},
'fields': [
{
'key': 'email',
'type': 'email',
'label': 'Email',
'required': true,
},
{
'key': 'role',
'type': 'dropdown',
'label': 'Role',
'options': [
{'value': 'admin', 'label': 'Administrator'},
{'value': 'member', 'label': 'Member'},
],
},
],
},
data: responseData,
onSubmit: api.saveDynamicForm,
);
Supported server theme tokens are constrained widths, density, and padding.
Unknown page and field types throw FormatException.
Mapping API errors #
Throw FormSubmissionException from a submit callback:
throw const FormSubmissionException(
{
'email': ['Email is already registered'],
},
message: 'Please correct the highlighted fields.',
);
The renderer assigns each message to the matching field and scrolls to the first error.