← Назад к вопросам

Как работать с большими формами во Flutter?

1.8 Middle🔥 191 комментариев
#Flutter виджеты#State Management

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI28 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Работа с большими формами во 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.

Как работать с большими формами во Flutter? | PrepBro