[Flutter] 폼 입력값 검증하기-Form Validation

Flutter에서 폼 위젯을 이용하여 사용자로부터 값을 입력받을 때 가장 중요한 것이 폼 입력값 검증입니다. 입력값을 검증하는 방법에 대해서 알아보고 리팩토링을 통해서 입력값의 종류에 따라서 어떻게 다른 검증 방법을 적용할 수 있을지에 대해서도 알아보도록 하겠습니다.

TextFormField 정의하기

Flutter에서 사용자의 입력을 받기 위해서 주로 TextFormField를 이용합니다. 아래와 같이 TextFormField를 사용자의 기호에 맞게 스타일을 적용하여 일관성 있는 FormField를 사용할 수 있습니다. 이 때 전달하는 요소로써 controller, txtLabel, 그리고 type 세가지가 있는데 controller는 TextEditingController를 전달하여 입력값을 관리합니다. txtLabel은 각 입력요소 별 이름을 나타내며, type은 TextInputType으로써 숫자입력이나 문자입력, 그 외에도 이메일 등의 여러가지 입력 자판 스타일을 지정할 수 있습니다.

마지막으로 설정하는 validator의 경우에는 사용자의 입력값을 검증하는 역할을 하게 되는데요. 사용자가 입력값을 입력하지 않았거나 혹은 다른 형식의 값을 입력하였을 때 이에 경고를 주는 역할을 합니다. CustomTextFormField의 경우 사용자 입력폼의 종류에 따라서 각기 다른 validator가 필요할 수 있습니다. 이를 구분하기 위해서 validatorType을 문자열로 별도로 지정받고 이를 switch 구문을 통해서 각기 다른 validator를 작성해 주었습니다.

Student Id의 경우 8자리의 숫자를 입력받도록 하였고 숫자가 아닌 값이 입력되거나 8자리가 아닌 값이 입력되는 경우 이를 걸러낼 수 있도록 하였습니다.

class CustomTextFormField extends StatelessWidget {
  const CustomTextFormField({
    super.key,
    required TextEditingController controller,
    required String txtLabel,
    required TextInputType type,
    required String validatorType,
  })  : _controller = controller,
        _txtLabel = txtLabel,
        _type = type,
        _validatorType = validatorType;

  final TextEditingController _controller;
  final String _txtLabel;
  final TextInputType _type;
  final String _validatorType;

  @override
  Widget build(BuildContext context) {
    return TextFormField(
      controller: _controller,
      keyboardType: _type,
      decoration: InputDecoration(
        border: const OutlineInputBorder(),
        label: Text(_txtLabel),
      ),
      validator: (value) {
        switch (_validatorType) {
          case 'text':
            if (value == null || value.isEmpty) {
              return '$_txtLabel cannot be empty';
            }
            return null;
          case 'id':
            if (value == null || value.isEmpty) {
              return '$_txtLabel cannot be empty';
            }
            final number = num.tryParse(value);

            if (number.toString().length != 8) {
              return 'Studnet Id is 8 digit value only, cannot be start with 0';
            }

            if (number == null) {
              return 'Student Id is only valid number value';
            }
            return null;
        }
        return null;
      },
    );
  }

Validator 적용하기

앞에서 정의한 CustomTextFormField를 적용하는 예제는 아래와 같습니다. 일단 Stateful widget에서 구현이 되어야 합니다. validator를 이용하기 위해서는 TextFormField가 Form 위젯에 묶여 있어야 합니다. 그래서 위젯의 최상단에 _formKey를 정의하게 되는데 GlobalKey 형태로 정의하여 Form 위젯이 key값으로 전달합니다. 이렇게 정의를 하면 Form 하위의 위젯에서 validator에 의해서 잘못된 입력으로 판단되는 값이 있을 경우 _formKey 값으로 전달하게 됩니다.

class _AddStudentScreenState extends State<AddStudentScreen> {
  final _formKey = GlobalKey<FormState>();
...
body: Form(
        key: _formKey,
        child: Column(
          children: [
...

실제로 데이터를 처리할 때 _formKey.currentState.validate()의 값으로 Form 검증과정을 통과했는지를 확인할 수 있습니다.

 void addStudent() {
    final isValid = _formKey.currentState?.validate();
    if (isValid != null && isValid) {
...

전체 코드

위 내용을 모두 종합하여 add_student_screen.dart를 만들어 보면 아래와 같이 나타낼 수 있습니다.

import 'package:flutter/material.dart';
import '../data/local/db/app_db.dart';
import 'package:drift/drift.dart' as drift;

enum Gender { male, female }

class AddStudentScreen extends StatefulWidget {
  const AddStudentScreen({super.key});

  @override
  State<AddStudentScreen> createState() => _AddStudentScreenState();
}

class _AddStudentScreenState extends State<AddStudentScreen> {
  final _formKey = GlobalKey<FormState>();
  late AppDb _db;
  final TextEditingController _studentNameController = TextEditingController();
  final TextEditingController _studentIdController = TextEditingController();
  Gender genderSelection = Gender.male;
  String enumValue = 'male';

  @override
  void initState() {
    super.initState();
    _db = AppDb();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Add Student'),
        centerTitle: true,
        actions: [
          IconButton(
            onPressed: () => addStudent(),
            icon: const Icon(Icons.save),
          ),
        ],
      ),
      body: Form(
        key: _formKey,
        child: Column(
          children: [
            Padding(
              padding: const EdgeInsets.all(8.0),
              child: Column(
                children: [
                  CustomTextFormField(
                    controller: _studentIdController,
                    txtLabel: 'Student Id',
                    type: TextInputType.number,
                    validatorType: 'id',
                  ),
                  const SizedBox(height: 16),
                  CustomTextFormField(
                    controller: _studentNameController,
                    txtLabel: 'Student Name',
                    type: TextInputType.name,
                    validatorType: 'text',
                  ),
                  const SizedBox(height: 16),
                  RadioListTile<Gender>(
                    title: const Text('male'),
                    value: Gender.male,
                    groupValue: genderSelection,
                    onChanged: (Gender? value) {
                      setState(() {
                        if (value != null) {
                          genderSelection = value;
                          enumValue = value.name;
                        }
                      });
                    },
                  ),
                  RadioListTile<Gender>(
                    title: const Text('female'),
                    value: Gender.female,
                    groupValue: genderSelection,
                    onChanged: (Gender? value) {
                      setState(() {
                        if (value != null) {
                          genderSelection = value;
                          enumValue = value.name;
                        }
                      });
                    },
                  ),
                ],
              ),
            )
          ],
        ),
      ),
    );
  }

  void addStudent() {
    final isValid = _formKey.currentState?.validate();
    if (isValid != null && isValid) {
      final entity = StudentInfosCompanion(
          studentId: drift.Value(_studentIdController.text),
          name: drift.Value(_studentNameController.text),
          gender: drift.Value(enumValue));
      _db.insertStudent(entity).then(
            (value) => ScaffoldMessenger.of(context).showMaterialBanner(
              MaterialBanner(
                content: Text('New Student inserted: $value'),
                actions: [
                  TextButton(
                    onPressed: () => ScaffoldMessenger.of(context)
                        .hideCurrentMaterialBanner(),
                    child: const Text('Close'),
                  )
                ],
              ),
            ),
          );
    }
  }
}

class CustomTextFormField extends StatelessWidget {
  const CustomTextFormField({
    super.key,
    required TextEditingController controller,
    required String txtLabel,
    required TextInputType type,
    required String validatorType,
  })  : _controller = controller,
        _txtLabel = txtLabel,
        _type = type,
        _validatorType = validatorType;

  final TextEditingController _controller;
  final String _txtLabel;
  final TextInputType _type;
  final String _validatorType;

  @override
  Widget build(BuildContext context) {
    return TextFormField(
      controller: _controller,
      keyboardType: _type,
      decoration: InputDecoration(
        border: const OutlineInputBorder(),
        label: Text(_txtLabel),
      ),
      validator: (value) {
        switch (_validatorType) {
          case 'text':
            if (value == null || value.isEmpty) {
              return '$_txtLabel cannot be empty';
            }
            return null;
          case 'id':
            if (value == null || value.isEmpty) {
              return '$_txtLabel cannot be empty';
            }
            final number = num.tryParse(value);

            if (number.toString().length != 8) {
              return 'Studnet Id is 8 digit value only, cannot be start with 0';
            }

            if (number == null) {
              return 'Student Id is only valid number value';
            }
            return null;
        }
        return null;
      },
    );
  }
}

예시 화면

실제 화면 예시를 보여드리면 다음과 같습니다. Student Id를 8자리를 입력하였지만 숫자로 입력하지 않은 경우 에러 메시지를 띄우게 됩니다. text 타입으로 설정한 Student Name의 경우 입력값이 없을 경우 에러 메시지를 표시합니다.

Leave a Comment