unified_fields 0.1.6
unified_fields: ^0.1.6 copied to clipboard
Standalone unified form fields, pickers, and date/time UI for Flutter apps.
example/lib/main.dart
import 'package:flutter/material.dart';
import 'package:unified_fields/unified_fields.dart';
void main() {
WidgetsFlutterBinding.ensureInitialized();
runApp(
UnifiedInputThemeScope(
data: const UnifiedInputThemeData(
requiredIconColor: Color(0xFF1565C0),
requiredIconSize: 9,
validationColor: Color(0xFFD32F2F),
disabledFieldOpacity: 0.38,
placeholderOpacityWhenDisabled: 0.38,
pickerSheetBackgroundColor: Color(0xFFF5F7FA),
defaultSuffixIcons: UnifiedInputDefaultSuffixIcons(
date: Icons.calendar_month_outlined,
time: Icons.access_time,
duration: Icons.timelapse_outlined,
picker: Icons.unfold_more,
),
pickerHeaderStyle: const UnifiedInputPickerHeaderStyle(
padding: EdgeInsets.fromLTRB(16, 14, 8, 14),
),
multiPickerCheckboxStyle: const UnifiedInputMultiPickerCheckboxStyle(
borderRadius: 4,
fillColor: Color(0xFF1565C0),
),
),
child: const UnifiedFieldsDemoApp(),
),
);
}
/// Demo entry. Wires every notable widget from the `unified_fields` package
/// into a single `Form` so you can see validate / save / reset working with
/// the same `GlobalKey<FormState>` for every field type.
class UnifiedFieldsDemoApp extends StatelessWidget {
const UnifiedFieldsDemoApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'unified_fields demo',
debugShowCheckedModeBanner: false,
theme: ThemeData(useMaterial3: true, colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xff2A5CFF))),
home: const DemoHomePage(),
);
}
}
/// Demo page hosting the showcase and a simple form that exercises every
/// `UnifiedForm*` wrapper.
class DemoHomePage extends StatefulWidget {
const DemoHomePage({super.key});
@override
State<DemoHomePage> createState() => _DemoHomePageState();
}
class _DemoHomePageState extends State<DemoHomePage> {
final _formKey = GlobalKey<FormState>();
final _name = UnifiedInputPicker<String>(initialValue: '');
final _nameC = UnifiedTextFieldController();
final _country = UnifiedInputPicker<String>(initialValue: null);
final _countryController = UnifiedPickerFieldController<String>();
final _flavors = UnifiedInputPicker<List<String>>(initialValue: const []);
final _date = UnifiedInputPicker<DateTime>(initialValue: null);
final _time = UnifiedInputPicker<TimeOfDay>(initialValue: null);
final _duration = UnifiedInputPicker<Duration>(initialValue: const Duration(minutes: 5));
late final CustomizableSinglePickerController<String> _customSingle;
late final CustomizableMultiPickerController<String> _customMulti;
static const _countries = <String>['Iran', 'Türkiye', 'Germany', 'Japan', 'Brazil', 'Canada'];
static const _flavorChoices = <String>['Sweet', 'Acidic', 'Nutty', 'Floral', 'Chocolate'];
String _savedSummary = '';
@override
void initState() {
super.initState();
_customSingle = CustomizableSinglePickerController<String>(valueToString: (e) => e, initialKind: CustomizablePickerInputKind.typed, initialTyped: '');
_customMulti = CustomizableMultiPickerController<String>(valueToString: (e) => e);
}
@override
void dispose() {
_name.dispose();
_country.dispose();
_flavors.dispose();
_date.dispose();
_time.dispose();
_duration.dispose();
_customSingle.dispose();
_customMulti.dispose();
super.dispose();
}
Future<List<String>> _loadAsyncCountries() async {
await Future<void>.delayed(const Duration(milliseconds: 350));
return _countries;
}
void _onValidate() {
if (_formKey.currentState?.validate() ?? false) {
_formKey.currentState!.save();
setState(() {
_savedSummary = [
'name: ${_name.value ?? ''}',
'country: ${_country.value ?? '—'}',
'flavors: ${_flavors.value?.join(', ') ?? ''}',
'date: ${_date.value?.toIso8601String().split('T').first ?? '—'}',
'time: ${_time.value?.format(context) ?? '—'}',
'duration: ${_duration.value}',
'custom single: ${_customSingle.fieldDisplayText}',
'custom multi: ${_customMulti.fieldDisplayText}',
].join('\n');
});
}
}
void _onReset() {
_formKey.currentState?.reset();
setState(() => _savedSummary = '');
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('unified_fields demo'),
actions: [
IconButton(
tooltip: 'Open full showcase',
icon: const Icon(Icons.dashboard_customize_outlined),
onPressed: () => Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (_) => Scaffold(
appBar: AppBar(),
body: const SafeArea(
child: Padding(padding: EdgeInsets.all(8.0), child: UnifiedInputsShowcasePage()),
),
),
),
),
),
IconButton(
tooltip: 'Open full showcase',
icon: const Icon(Icons.clear),
onPressed: () {
_nameC.requestFocus();
// _countryController.openPicker(context);
// _country.clear();
// _countryController.openPicker(context, items: [], label: "label");
// _countryController.clear();
// setState((){});
},
),
],
),
body: SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: UnifiedFormFieldScope(
autovalidateMode: AutovalidateMode.onUserInteraction,
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
'A small form using every UnifiedForm* widget. '
'Tap Validate to run validators and save state, '
'or Reset to restore each field with its resetValue.',
style: Theme.of(context).textTheme.bodyMedium,
),
const SizedBox(height: 12),
_ThemeScopeDemoCard(),
const SizedBox(height: 12),
UnifiedFormTextField(
label: 'Full name',
decoration: UnifiedInputDecoration(
labelInRow: true,
height: 40,
// headerBackgroundColor: Colors.red,
// backgroundColor: Colors.blue
// borderRadius: BorderRadius.circular(5)
),
placeholder: 'e.g. Ada Lovelace',
isRequired: true,
binding: _name,
fieldController: _nameC,
// locked: true,
// disabled: true,
resetValue: () => '',
validator: (v) => (v == null || v.trim().isEmpty) ? 'Required' : null,
),
const SizedBox(height: 12),
UnifiedFormSinglePickerField<String>(
label: 'Country',
decoration: UnifiedInputDecoration(height: 40, labelInRow: true),
placeholder: 'Pick one',
isRequired: true,
items: _countries,
fieldController: _countryController,
binding: _country,
validator: (v) => v == null ? 'Pick a country' : null,
),
const SizedBox(height: 12),
UnifiedFormMultiPickerField<String>(label: 'Flavors', placeholder: 'Add some', items: _flavorChoices, values: _flavors.value ?? const [], binding: _flavors, resetValue: () => const <String>[]),
const SizedBox(height: 12),
UnifiedFormDateField(
label: 'Date (form, wheels)',
placeholder: 'Tap to pick',
isRequired: true,
binding: _date,
min: DateTime(2020),
max: DateTime(2035),
pickerStyle: UnifiedFieldsDatePickerStyle.wheels,
initialCalendarKind: UnifiedFieldsCalendarKind.gregorian,
pickerGranularity: UnifiedFieldsDatePickerGranularity.day,
resetValue: () => null,
validator: (v) => v == null ? 'Pick a date' : null,
),
const SizedBox(height: 12),
UnifiedDateField(
label: 'Date',
placeholder: 'Tap to pick',
isRequired: true,
binding: _date,
min: DateTime(2020),
showCalendarKindToggle: false,
initialCalendarKind: UnifiedFieldsCalendarKind.jalali,
max: DateTime(2035),
// pickerGranularity: UnifiedFieldsDatePickerGranularity.year,
validator: (v) => v.trim().isEmpty ? 'Pick a date' : null,
),
const SizedBox(height: 12),
UnifiedTimeOfDayField(
pickerStyle: UnifiedFieldsTimePickerStyle.wheels,
pickerGranularity: UnifiedFieldsTimeGranularity.hoursMinutesSeconds, // or .hours / .hoursMinutes
initialCalendarKind: UnifiedFieldsCalendarKind.jalali,
showCalendarKindToggle: true,
value: TimeOfDay(hour: 14, minute: 30),
),
const SizedBox(height: 12),
UnifiedDurationField(
// locked: true,
// isDisabled: true,
// showCalendarKindToggle: false,
// pickerStyle: UnifiedFieldsDurationPickerStyle.wheels, // default
// granularity: UnifiedDurationGranularity.hoursMinutesSeconds, // or .hours / .hoursMinutesSeconds
// initialCalendarKind: UnifiedFieldsCalendarKind.jalali,
// value: const Duration(hours: 1, minutes: 30),
// pickerColumns: [UnifiedFieldsDurationColumn.year,UnifiedFieldsDurationColumn.month,UnifiedFieldsDurationColumn.week],
),
const SizedBox(height: 12),
UnifiedFormTimeOfDayField(label: 'Time', placeholder: 'Tap to pick', binding: _time, resetValue: () => null),
const SizedBox(height: 12),
UnifiedFormDurationField(label: 'Duration', placeholder: 'Tap to edit', binding: _duration, granularity: UnifiedDurationGranularity.hoursMinutesSeconds, resetValue: () => Duration.zero),
const SizedBox(height: 12),
UnifiedFormAsyncPickerField<String>(label: 'Country (async)', placeholder: 'Loads on tap', itemProvider: _loadAsyncCountries),
const SizedBox(height: 12),
UnifiedFormCustomizablePickerField<String>(
label: 'Origin (free text or pick)',
placeholder: 'Type or open the sheet',
items: _countries,
pickerController: _customSingle,
resetValue: () => const CustomizableSinglePickerSnapshot<String>.empty(),
),
const SizedBox(height: 12),
UnifiedFormCustomizableMultiPickerField<String>(
label: 'Tags',
placeholder: 'Type or pick multiple',
items: _flavorChoices,
pickerController: _customMulti,
allowFreeText: false,
resetValue: () => const CustomizableMultiPickerSnapshot<String>.empty(),
),
const SizedBox(height: 20),
Row(
children: [
Expanded(
child: FilledButton(onPressed: _onValidate, child: const Text('Validate + Save')),
),
const SizedBox(width: 12),
Expanded(
child: OutlinedButton(onPressed: _onReset, child: const Text('Reset')),
),
],
),
if (_savedSummary.isNotEmpty) ...[
const SizedBox(height: 16),
Container(
width: double.infinity,
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(color: Theme.of(context).colorScheme.surfaceContainerHighest, borderRadius: BorderRadius.circular(12)),
child: Text(_savedSummary, style: const TextStyle(fontFamily: 'monospace')),
),
],
const SizedBox(height: 32),
],
),
),
),
),
),
);
}
}
/// Contrasts app-wide [UnifiedInputThemeScope] (from [main]) with a local override.
class _ThemeScopeDemoCard extends StatelessWidget {
const _ThemeScopeDemoCard();
@override
Widget build(BuildContext context) {
return Card(
elevation: 0,
color: Theme.of(context).colorScheme.surfaceContainerHighest,
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
'UnifiedInputThemeScope',
style: Theme.of(context).textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w600),
),
const SizedBox(height: 4),
Text(
'The whole app uses the scope from main() (blue required *, custom picker icons). '
'This card adds a nested scope with orange required icons.',
style: Theme.of(context).textTheme.bodySmall,
),
const SizedBox(height: 12),
const UnifiedTextField(
label: 'App scope (blue *)',
isRequired: true,
initialValue: 'Uses main() theme',
),
const SizedBox(height: 12),
UnifiedInputThemeScope(
data: const UnifiedInputThemeData(
requiredIconColor: Color(0xFFE65100),
requiredIconSize: 11,
validationColor: Color(0xFF6A1B9A),
disabledFieldColor: Color(0xFF757575),
disabledFieldOpacity: 0.5,
),
child: const Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
UnifiedTextField(
label: 'Nested scope (orange *)',
isRequired: true,
initialValue: 'Overrides required icon only',
),
SizedBox(height: 12),
UnifiedTextField(
label: 'Disabled (nested grey)',
isDisabled: true,
initialValue: 'Themed disabled value',
),
],
),
),
],
),
),
);
}
}