본문 바로가기
SW 개발/FrontEnd(Flutter)

Flutter 사용자 입력 값 검증 위한 Form validation과 TextFormField (Sample code)

by Kibua20 2022. 1. 2.

Flutter에서 사용자 입력을 받기 위한 Widget은 TextField와 TextFormField가 2가지 Class가 있습니다.  TextField은 사용자 입력을 받고, Widget의 GUI를 설정할 수 있는 Widget이고, TextFormField는 TextField 기능을 포함하여 다수의 입력을 받을 때 편리하게 사용하는 Form기능(예를 들어, validation 기능)을 추가한 wraper class입니다.  Form 예제로는 아래 그림과 같이 로그인을 위한 ID, Password 입력,  User 정보 생성을 위한 사용자 입력 UI를 예로 들 수 있습니다.  

 

사용자 입력을 위한  Flutter Widget

  • TextFiled:  사용자 입력받기 위한 Widget Class
  • TextFormField : TextField기능 +  Form 기능(Validation, Form state 관리)

 

Form 구현 예 (ID, Password, Submit 버튼 입력)

 

참고로, TextField와 TextFormField외 Styling 방법은 별도의 포스팅으로 정리하였습니다. 

 

Form Validation 사용법

Flutter Form Validation 기능을 사용하기 위해서는 아래 3단계가 필요합니다. Flutter  Form Validation 공식 문서는 https://docs.flutter.dev/cookbook/forms/validation입니다.

  1. Global key로 Form 선언
  2. TextFormField에 validator, onSaved,  autovalidateMode 추가  
  3. Button Submit 시 onPress() 함수 추가 

 

Step 1. Global key로 Form  key선언

Form Widget을 상단에 선언하고 GlobalKey <FormState>로 form key를 생성하고  Form key 값으로 설정합니다 (key: _formKey).  form key는 이후에 save와 validation 시 사용할 reference입니다.  주의해야 할 점은 GlobalKey <MyCustomFormState>가 아니고, 항상 GlobalKey <FormState>입니다. 

 

Step 1. Global key로 Form&amp;amp;amp;nbsp; key 선언

 

Step 2. TextFormField에 validator, onSaved,  autovalidateMode 추가

Form Widget의 children에 TextFormField를 추가합니다. TextFormField에서 입력받은 값을 저장하는 state를 선언하고 onSaved, validator, autovalidateMode을 설정합니다. 

 

  • onSaved 함수:  onSaved(value)로 전달하는 value 값이 사용자 입력 값이고, setState() 함수에서 value 값을 state에 저장하고 setState() 호출하여 UI를 다시 렌더링 합니다. 
  • validator 함수:  validator(value)로 전달하는 value값의 조건을 확인합니다. Validation 조건이 맞지 않는 경우 TextFormField 하단에 에러 메시지를 리턴하고, 조건에 맞는 정상 값은 null 을  return 합니다.
  • autovalidateMode: 사용자 입력 값을 onSaved() 호출할 때 validation 할지, 변경할 때마다 항상 validation을 할지 설정합니다. AutovalidationMode.always로 설정하면 onChange()를 별도로 처리할 필요 없이 사용자가 입력할 때마다 validation을 진행합니다.

&amp;amp;amp;nbsp;TextFormField에 validator, onSaved,&amp;amp;amp;nbsp; autovalidateMode 추가

 

Step 3. Button Submit 시 onPress() 함수 추가 

마지막 단계는 Submit 버튼 누를 때 처리하는 onPressed() 함수를 구현합니다. Step 1에서 선언한 _formKey 값을 활용하여 validate 결과를 확인하고  _formKey.currentState!. save() 함수로 각 TextFormField의 onSaved() 함수를 호출하여 state 값을 저장합니다. ScaffoldMessenger는 정상 처리 결과를 확인하기 위한 임시 코드이고, 실제에서 사용자 입력한 값을 LocalStorage에 값을 저장 또는 서버에 값을 전달합니다. 

Button Submit 시 onPress() 함수 추가&amp;amp;amp;nbsp;

 

에러 수정 내용

Form 구현 시 발행했던 error 수정 내용입니다. 

 

에러 1. Error: A value of type 'String?' can't be assigned to a variable of type 'String' because 'String?

[에러 메시지]

lib/main.dart:53:31:  Error: A value of type 'String?' can't be assigned to a variable of type 'String' because 'String?' is nullable and 'String' isn't.
                  this.name = value ;
                              ^

[수정 내용]

this.name = value;  →   this.name = value as String;

 

에러 2.  Null Safety (링크)  Error: Method 'save' cannot be called on 'FormState?' because it is potentially null.

[에러 메시지] 

lib/main.dart:71:41: Error: Method 'save' cannot be called on 'FormState?' because it is potentially null.
 - 'FormState' is from 'package:flutter/src/widgets/form.dart' ('/usr/local/Caskroom/flutter/2.8.0/flutter/packages/flutter/lib/src/widgets/form.dart').
Try calling using ?. instead.
                  _formKey.currentState.save();
                                        ^^^^
/usr/local/Caskroom/flutter/2.8.0/flutter/packages/flutter/lib/src/widgets/framework.dart:171:10: Context: 'currentState' refers to a property so it couldn't be promoted.
See http://dart.dev/go/non-promo-property
  T? get currentState {
                  _formKey.currentState!.save();
This is due to Dart's flow analysis.
Your formKey is an instance variable and thus it cannot be detected by flow analysis that it is definitely not null even after your if check.
Use it like this instead formKey.currentState!.save();

[수정 내용] currentState에 대한 null check를 추가합니다. 

if (formKey.currentState != null)  formKey.currentState.save(); 를  formKey.currentState!.save(); 표시합니다. 

_formKey.currentState.save();  → formKey.currentState!.save();

 

TextFormField의 유용한 설정 옵션

입력 가능한 메시지를 포맷을 설정 

inputFormatters을 사용하면  특정 조건 (e.g 정규식 또는 숫자만)에 맞는 값만 입력을 허용 또는 거절할 수 있도록 설정합니다. 이 경우 입력 자체가 무시되고 에러 메시지도 출력하지 않습니다.   

inputFormatters: [FilteringTextInputFormatter(RegExp('[0-9]'), allow:false), ],

 

InputDecoration 설정 

아이콘, hint, label 메시지를 출력합니다. 

decoration: const InputDecoration(
  icon: Icon(Icons.person),
  hintText: 'Hint Text',
  labelText: 'Label Text',
),

 

입력 Focust 설정 (focusNode)

입력 Focus를 설정하기 위해서는 focusNode를 설정합니다. (참고 링크 1 링크 2)

  1. FocusNode를 만듭니다. 
  2. FocusNode를 텍스트 필드에 전달합니다. 
  3. 버튼을 누를 때, 포커스를 텍스트 필드에 줍니다.
focusNode: _passwordFocusNode,
textInputAction: TextInputAction.done,
keyboardType: TextInputType.text ,
decoration: InputDecoration(  labelText: "비밀번호",  suffixIcon: Icon(Icons.lock),),

 

비밀 번호 숨김

비밀 번호를 입력 시 문자를 '*'로 표시합니다.

obscureText: true,

 

Sample 소스 코드 

앞에서 설명한 내용의 전체 소스 코드입니다. GitHub에 flutter/a02form폴더의 main_form_validation.dart에 올렸습니다.

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: 'Flutter Demo',
      home: FormScreen(title: 'Flutter Form Validation'),
    );
  }
}

class FormScreen extends StatefulWidget {
  const FormScreen({Key? key, required this.title}) : super(key: key);
  final String title;

  @override
  State<FormScreen> createState() => _FormScreenState();
}

class _FormScreenState extends State<FormScreen> {
  // Create a global key that uniquely identifies the Form widget and allows validation of the form.
  // Note: This is a GlobalKey<FormState>, not a GlobalKey<MyCustomFormState>.
  final _formKey = GlobalKey<FormState>();

  String _id = '';
  String _password = '';

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Form(
        key: _formKey,
        child: Column(
          mainAxisAlignment: MainAxisAlignment.start,
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Padding(
                padding: const EdgeInsets.all(20),
                child: Column(
                  children: [
                    const Text(
                      'name',
                      style: TextStyle(
                        fontSize: 25.0,
                      ),
                    ),
                    TextFormField(
                      autovalidateMode: AutovalidateMode.always,
                      decoration: const InputDecoration(
                        icon: Icon(Icons.person),
                        hintText: 'Hint Text',
                        labelText: 'Label Text',
                      ),
                      onSaved: (value) {
                        setState(() {
                          _id = value as String;
                        });
                      },
                      validator: (value) {
                        if (value == null || value.isEmpty) {
                          return 'Please enter some text';
                        }
                        return null;
                      },
                    ),
                  ],
                )),
            Padding(
                padding: const EdgeInsets.fromLTRB(20, 20, 20, 20),
                child: Column(
                  children: [
                    const Text(
                      'password',
                      style: TextStyle(
                        fontSize: 25.0,
                      ),
                    ),
                    TextFormField(

                        autovalidateMode: AutovalidateMode.always,
                        onSaved: (value) {
                          setState(() {
                            _password = value as String;
                          });
                        },
                        validator: (value) {
                          if (value == null || value.isEmpty) {
                            return 'Please enter some text';
                          }
                          if (value.toString().length < 8) {
                            return '8자 이상 입력';
                          }
                          if (!RegExp('[0-9]').hasMatch(value)) {
                            return '정규식';
                          }
                          return null;
                        }
                        // inputFormatters: [FilteringTextInputFormatter(RegExp('[0-9]'), allow:false), ],
                        // focusNode: _passwordFocusNode,
                        // keyboardType: TextInputType.text ,
                        // obscureText: true,
                        // decoration: InputDecoration(
                        //   labelText: "비밀번호",
                        //   suffixIcon: Icon(Icons.lock),
                        // ),
                        // textInputAction: TextInputAction.done,
                        ),

                    // SizedBox( height: 16, ),

                    ElevatedButton(
                      onPressed: () {
                        // Validate returns true if the form is valid, or false otherwise.
                        if (_formKey.currentState!.validate()) {
                          // validation 이 성공하면 폼 저장하기
                          _formKey.currentState!.save();

                          // If the form is valid, display a SnackBar. In the real world,
                          // you'd often call a server or save the information in a database.
                          ScaffoldMessenger.of(context).showSnackBar(
                            SnackBar(content: Text(_id + '/' + _password)),
                          );
                        }
                      },
                      child: const Text('Submit'),
                    ),
                  ],
                )),
          ],
        ),
      ),
    );
  }
}

 

관련 글

[SW 개발/FrontEnd(Flutter)] - Flutter 특징과 개발환경 설정 방법

[SW 개발/FrontEnd(Flutter)] - Android Studio와 Visual Code로 Flutter Project 프로젝트 생성

[SW 개발/FrontEnd(Flutter)] - Flutter Stateless와 Stateful Widget 개념과Life Cycle, 대표 Widget(MaterialApp, Scaffold)

[SW 개발/FrontEnd(Flutter)] - Flutter Parent-Child Widget 간 State 전달과 업데이트 방법

[SW 개발/FrontEnd(Flutter)] - Flutter에서 Widget Tree와 layout 디자인 방법

[SW 개발/FrontEnd(Flutter)] - Flutter에서 Material UI Icon과 Cupertino Icon 검색하고 사용하기

[SW 개발/FrontEnd(Flutter)] - Flutter 이미지 처리를 위한 Image, FadeInImage, CachedNetworkImage, ExtendedImage 사용법 및 성능 비교

[SW 개발/FrontEnd(Flutter)] - Flutter TextField 와 TextFormField 스타일 꾸미기

[SW 개발/Python] - Selenium 4.0 개선 사항 정리 - WebDriver 자동 로딩 가능

[SW 개발/Android] - 안드로이드 adb 설치 및 설정 방법

[개발환경/Web Server] - Web Server 성능 및 Load 측정 Tool: Apache AB (Apache Http Server Benchmarking Tool)

[SW 개발/Python] - Python: JSON 개념과 json 모듈 사용법




댓글