Как работать с большими формами во Flutter?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Работа с большими формами во Flutter
Большие формы — частая задача в мобильных приложениях. Без правильной архитектуры они становятся болезненным местом. Вот проверенный подход.
Проблема без структуры
// ❌ Плохо: всё в одном State
class SignUpPage extends StatefulWidget {
@override
State<SignUpPage> createState() => _SignUpPageState();
}
class _SignUpPageState extends State<SignUpPage> {
String? email;
String? password;
String? confirmPassword;
String? firstName;
String? lastName;
String? phone;
String? address;
String? city;
String? country;
String? zipCode;
// ... ещё 20 полей
bool isLoading = false;
String? errorMessage;
@override
Widget build(BuildContext context) {
// Огромный метод build()
return Scaffold(...);
}
void submit() {
// Дублирование валидации
// Сложная логика
}
}
Решение 1: Разбиение на части (шаги)
Большую форму часто имеет смысл разбить на логические шаги:
class SignUpWizard extends StatefulWidget {
@override
State<SignUpWizard> createState() => _SignUpWizardState();
}
class _SignUpWizardState extends State<SignUpWizard> {
int currentStep = 0;
final formData = <String, dynamic>{};
final steps = [
FormStep(
title: 'Personal Info',
fields: ['firstName', 'lastName', 'email'],
),
FormStep(
title: 'Address',
fields: ['address', 'city', 'country', 'zipCode'],
),
FormStep(
title: 'Password',
fields: ['password', 'confirmPassword'],
),
];
@override
Widget build(BuildContext context) {
return Stepper(
currentStep: currentStep,
steps: steps.asMap().entries.map((entry) {
return Step(
title: Text(entry.value.title),
content: _buildStepContent(entry.value),
);
}).toList(),
onStepContinue: _nextStep,
onStepCancel: _previousStep,
);
}
Widget _buildStepContent(FormStep step) {
return Column(
children: step.fields.map((field) {
return TextFormField(
decoration: InputDecoration(labelText: field),
onChanged: (value) => formData[field] = value,
);
}).toList(),
);
}
void _nextStep() {
if (currentStep < steps.length - 1) {
setState(() => currentStep++);
} else {
_submitForm();
}
}
void _previousStep() {
if (currentStep > 0) {
setState(() => currentStep--);
}
}
}
Решение 2: Использование Form и GlobalKey
FormState дает инструменты для валидации и сохранения состояния:
class PersonalInfoForm extends StatefulWidget {
final Function(Map<String, dynamic>) onSubmit;
const PersonalInfoForm({required this.onSubmit});
@override
State<PersonalInfoForm> createState() => _PersonalInfoFormState();
}
class _PersonalInfoFormState extends State<PersonalInfoForm> {
final _formKey = GlobalKey<FormState>();
final _formData = <String, dynamic>{};
@override
Widget build(BuildContext context) {
return Form(
key: _formKey,
child: Column(
children: [
TextFormField(
decoration: InputDecoration(labelText: 'First Name'),
validator: (value) {
if (value?.isEmpty ?? true) return 'Required';
return null;
},
onSaved: (value) => _formData['firstName'] = value,
),
TextFormField(
decoration: InputDecoration(labelText: 'Last Name'),
validator: (value) {
if (value?.isEmpty ?? true) return 'Required';
return null;
},
onSaved: (value) => _formData['lastName'] = value,
),
TextFormField(
decoration: InputDecoration(labelText: 'Email'),
validator: (value) {
if (!value!.contains('@')) return 'Invalid email';
return null;
},
onSaved: (value) => _formData['email'] = value,
),
SizedBox(height: 20),
ElevatedButton(
onPressed: _submit,
child: Text('Next'),
),
],
),
);
}
void _submit() {
if (_formKey.currentState!.validate()) {
_formKey.currentState!.save();
widget.onSubmit(_formData);
}
}
}
Решение 3: Использование состояния с Provider
Для более сложных форм используй паттерн состояния (state management):
class FormState extends ChangeNotifier {
final formData = <String, dynamic>{};
final errors = <String, String>{};
bool _isLoading = false;
bool get isLoading => _isLoading;
void updateField(String key, dynamic value) {
formData[key] = value;
notifyListeners();
}
void setError(String key, String message) {
errors[key] = message;
notifyListeners();
}
void clearError(String key) {
errors.remove(key);
notifyListeners();
}
Future<void> submit() async {
_isLoading = true;
notifyListeners();
try {
// API call
await api.submitForm(formData);
_isLoading = false;
} catch (e) {
setError('form', 'Submission failed');
_isLoading = false;
}
notifyListeners();
}
}
// В UI:
class SignUpForm extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => FormState(),
child: Consumer<FormState>(
builder: (context, formState, _) {
return Column(
children: [
TextFormField(
decoration: InputDecoration(
labelText: 'Email',
errorText: formState.errors['email'],
),
onChanged: (value) {
formState.updateField('email', value);
formState.clearError('email');
},
),
// ... другие поля
ElevatedButton(
onPressed: formState.isLoading ? null : formState.submit,
child: formState.isLoading
? CircularProgressIndicator()
: Text('Sign Up'),
),
],
);
},
),
);
}
}
Лучшие практики
1. Разделение на подформы: Большую форму раздели на части (PersonalInfoForm, AddressForm, etc.). Каждая отвечает за свои данные.
2. Валидация на месте:
String? validateEmail(String? value) {
if (value == null || value.isEmpty) {
return 'Email is required';
}
if (!RegExp(r'^[^@]+@[^@]+\.[^@]+$').hasMatch(value)) {
return 'Enter a valid email';
}
return null;
}
3. Использование ListView для прокрутки:
SingleChildScrollView(
child: Column(
children: [...formFields],
),
)
4. Сохранение прогресса:
Future<void> saveProgress() async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString('formData', jsonEncode(formData));
}
Future<void> loadProgress() async {
final prefs = await SharedPreferences.getInstance();
final saved = prefs.getString('formData');
if (saved != null) {
formData.addAll(jsonDecode(saved));
}
}
5. Обработка ошибок API:
try {
await api.submit(formData);
// Success
} on ValidationException catch (e) {
// Если сервер вернул ошибки валидации
e.errors.forEach((key, message) {
setFieldError(key, message);
});
} catch (e) {
// Общая ошибка
showErrorDialog(context, 'Something went wrong');
}
Архитектура большой формы
FormPage
├── PersonalInfoSection (State)
│ ├── FirstNameField
│ ├── LastNameField
│ └── EmailField
├── AddressSection (State)
│ ├── AddressField
│ ├── CityField
│ └── CountryField
└── Actions
├── NextButton
└── CancelButton
Каждая секция управляет своим State, родитель координирует переходы между ними. Так достигается чистота и тестируемость.
Тестирование форм
testWidgets('Form validation works', (WidgetTester tester) async {
await tester.pumpWidget(MyForm());
await tester.enterText(find.byType(TextFormField).first, 'invalid');
await tester.tap(find.byType(ElevatedButton));
await tester.pumpWidget(MyForm());
expect(find.text('Invalid email'), findsOneWidget);
});
Большие формы требуют правильной архитектуры с самого начала. Лучше потратить время на структуру, чем потом распутывать спагетти из State.