Form 으로 손쉽게 여러개의 텍스트필드 상태관리하기!
서론
Flutter 에서 기본적으로 TextField
입력을 받으려면 기본적으로
TextEditingController
를 사용해야 합니다. TextField
가 하나면 괜찮지만 여러개가 되면 될수록 컨트롤러 관리가 굉장히
어려워지죠. 하지만 TextFormField
라는 TextField
의 살짝
변형된 위젯을 이용하면 쉽게 validation
과 값을 받아올 수 있습니다.
이번 시간엔 Form 을 사용해서 여러개의 TextFormField
를
관리하는 방법에 대해 알아보겠습니다.
기본 레이아웃
일단 기본적인 레이아웃을 생성 해보겠습니다. 아래 코드를 참조해주세요.
import 'package:codefactory_youtube_flutter_tutorial/Layouts/DefaultAppbarLayout.dart';
import 'package:flutter/material.dart';
class FormScreen extends StatefulWidget {
_FormScreenState createState() => _FormScreenState();
}
class _FormScreenState extends State<FormScreen> {
Widget build(BuildContext context) {
return DefaultAppbarLayout(
child: Column(
children: [
// 여기에 폼을 작성할거예요!
],
),
);
}
}
DefaultAppbarLayout
은 제가 제작한 기본 레이아웃 위젯입니다.
그냥 Scaffold
를 래퍼로 사용하는 위젯이라 보면 되겠습니다.
자세한 코드는 제 레포지토리를 참고 해주세요!
Form 위젯 사용하기
Column
의 children
파라미터에 폼을 작성해보도록 하겠습니다.
Form
이라는 위젯은 child
파라미터와 Key
파라미터를
받습니다. child
에는 TextFormField
들을 넣어주면되고
key
에는 GlobalKey
를 넣어주면 됩니다. 이 key
는
나중에 폼 내부의 TextFormField
값들을 저장하고 validation
을 진행하는데 사용됩니다.
final formKey = GlobalKey<FormState>();
Widget build(BuildContext context) {
return DefaultAppbarLayout(
child: Form(
key: this.formKey,
child: Column(
children: [
// 여기에 TextFormField 들을 입력할거예요!
],
),
),
);
}
TextFormField 위젯 생성 함수
TextFormField
를 생성하는 함수를 따로 만들어서 텍스트필드를
생성해보도록 할게요. TextFormField
에서 기본적으로 제공해주는
label 파라미터도 있지만 저는 개인적으로 직접 Text
위젯으로
label 을 제작하는걸 좋아합니다.
renderTextFormField({
String label,
FormFieldSetter onSaved,
FormFieldValidator validator,
}) {
assert(onSaved != null);
assert(validator != null);
return Column(
children: [
Row(
children: [
Text(
label,
style: TextStyle(
fontSize: 12.0,
fontWeight: FontWeight.w700,
),
),
],
),
TextFormField(
onSaved: onSaved,
validator: validator,
),
],
);
}
TextFormField 의 onSaved, validator 파라미터
보시다시피 TextFormField
는 onSaved
와 validator
파라미터를 받습니다. onSaved
의 시그니처는 FormFieldSetter
라는 typedef
고 validator
의 시그니처는 FormFieldValidator
입니다. 둘 다 String
값을 받고 있고 validator
는
String
의 리턴값 또한 받습니다. 리턴되는 String
은
에러메세지로 사용되게 됩니다.
build(BuildContext context) {
return DefaultAppbarLayout(
child: Form(
key: this.formKey,
child: Padding(
padding: EdgeInsets.all(16.0),
child: Column(
children: [
renderTextFormField(
label: '이름',
onSaved: (val) {},
validator: (val) {
return null;
},
),
renderTextFormField(
label: '이메일',
onSaved: (val) {},
validator: (val) {
return null;
},
),
renderTextFormField(
label: '비밀번호',
onSaved: (val) {},
validator: (val) {
return null;
},
),
renderTextFormField(
label: '주소',
onSaved: (val) {},
validator: (val) {
return null;
},
),
renderTextFormField(
label: '닉네임',
onSaved: (val) {},
validator: (val) {
return null;
},
),
],
),
),
),
);
}
Widget
위 코드를 작성하면 아래와 같은 화면을 볼 수 있습니다.
패딩 추가하기
텍스트 필드간의 간격을 넣기 위해서 renderTextFormField
의 맨 아래에 Container
를 추가해주겠습니다.
renderTextFormField({
String label,
FormFieldSetter onSaved,
FormFieldValidator validator,
}) {
assert(onSaved != null);
assert(validator != null);
return Column(
children: [
Row(
children: [
Text(
label,
style: TextStyle(
fontSize: 12.0,
fontWeight: FontWeight.w700,
),
),
],
),
TextFormField(
onSaved: onSaved,
validator: validator,
),
Container(height: 16.0),
],
);
}
폼 저장버튼 생성
이제는 폼 입력을 받을 버튼을 제작 해보겠습니다.
renderButton() {
return RaisedButton(
color: Colors.blue,
onPressed: () {},
child: Text(
'저장하기!',
style: TextStyle(
color: Colors.white,
),
),
);
}
폼 저장 후 Snackbar 보여주기
모든 버튼은 onPressed
파라미터를 통해 클릭했을때
액션을 받을 수 있죠. 저희는 이 onPressed
파라미터를
이용해서 폼을 검증하고 저장 해보도록 하겠습니다.
renderButton() {
return RaisedButton(
color: Colors.blue,
onPressed: () async {
if(this.formKey.currentState.validate()){
// validation 이 성공하면 true 가 리턴돼요!
Get.snackbar(
'저장완료!',
'폼 저장이 완료되었습니다!',
backgroundColor: Colors.white,
);
}
},
child: Text(
'저장하기!',
style: TextStyle(
color: Colors.white,
),
),
);
}
formKey.currentState.validate()
함수를 실행하면
Form
내부에 있는 TextFormField
들의 validation
결과에 따라 성공이면 true 를 리턴해주고 실패하면 false 를
리턴해줍니다. 일단은 성공하면 스낵바로 '저장이 됐습니다!' 라는
메세지를 보여주도록 할게요.
Validator 파라미터 작성하기
위 스크린샷과 같이 저장하기 버튼을 누르면 이제 스넥바가 떠서
저장이 완료되었다는 걸 알려준다는걸 알 수 있어요. 하지만 벌써
문제점이 보이죠? 텍스트필드에 아무것도 입력을 하지 않아도
저장이됩니다. 저희가 모든 텍스트 필드의 validation
파라미터를
return null
로 저장해서 그래요. 그럼 이번엔 텍스트필드별로
적절한 에러 메세지를 작성 해보도록 하겠습니다.
build(BuildContext context) {
return DefaultAppbarLayout(
child: Form(
key: this.formKey,
child: Padding(
padding: EdgeInsets.all(16.0),
child: Column(
children: [
renderTextFormField(
label: '이름',
onSaved: (val) {},
validator: (val) {
if(val.length < 1) {
return '이름은 필수사항입니다.';
}
if(val.length < 2) {
return '이름은 두글자 이상 입력 해주셔야합니다.';
}
return null;
},
),
renderTextFormField(
label: '이메일',
onSaved: (val) {
},
validator: (val) {
if(val.length < 1) {
return '이메일은 필수사항입니다.';
}
if(!RegExp(
r'^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$')
.hasMatch(val)){
return '잘못된 이메일 형식입니다.';
}
return null;
},
),
renderTextFormField(
label: '비밀번호',
onSaved: (val) {},
validator: (val) {
if(val.length < 1) {
return '비밀번호는 필수사항입니다.';
}
if(val.length < 8){
return '8자 이상 입력해주세요!';
}
return null;
},
),
renderTextFormField(
label: '주소',
onSaved: (val) {},
validator: (val) {
if(val.length < 1) {
return '주소는 필수사항입니다.';
}
return null;
},
),
renderTextFormField(
label: '닉네임',
onSaved: (val) {},
validator: (val) {
if(val.length < 1) {
return '닉네임은 필수사항입니다.';
}
if(val.length < 8) {
return '닉네임은 8자 이상 입력해주세요!';
}
return null;
},
),
renderButton(),
],
),
),
),
);
}
Widget
빌드 함수를 위와 같이 변경해서 validator 를 모두 입력한 다음에 저장하기 버튼을 다시 눌러보겠습니다.
이제 입력된 값이 없는 텍스트 필드에서는 에러가 나는걸 볼 수 있습니다. 그럼 validation 조건에 부합하게 텍스트필드에 값을 집어넣고 저장하기 버튼을 눌러볼게요.
onSaved 파라미터 작성하기
validation 조건을 만족 시키면 문제없이 스넥바가 뜨는걸
볼 수 있습니다. 하지만 실제 저장을 했을때 저희가 텍스트필드의
값을 받아볼 수 있는 방법이 현재는 없습니다. 그럼 값을 받아보기
위해 onSaved
파라미터에서 저장시 위젯의 변수에 값을 저장하는
기능을 작성해보겠습니다.
final formKey = GlobalKey<FormState>();
String name = '';
String email = '';
String password = '';
String address = '';
String nickname = '';
Widget build(BuildContext context) {
return DefaultAppbarLayout(
child: Form(
key: this.formKey,
child: Padding(
padding: EdgeInsets.all(16.0),
child: Column(
children: [
renderTextFormField(
label: '이름',
onSaved: (val) {
setState(() {
this.name = val;
});
},
validator: (val) {
if(val.length < 1) {
return '이름은 필수사항입니다.';
}
if(val.length < 2) {
return '이름은 두글자 이상 입력 해주셔야합니다.';
}
return null;
},
),
renderTextFormField(
label: '이메일',
onSaved: (val) {
setState(() {
this.email = val;
});
},
validator: (val) {
if(val.length < 1) {
return '이메일은 필수사항입니다.';
}
if(!RegExp(
r'^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$')
.hasMatch(val)){
return '잘못된 이메일 형식입니다.';
}
return null;
},
),
renderTextFormField(
label: '비밀번호',
onSaved: (val) {
setState(() {
this.password = val;
});
},
validator: (val) {
if(val.length < 1) {
return '비밀번호는 필수사항입니다.';
}
if(val.length < 8){
return '8자 이상 입력해주세요!';
}
return null;
},
),
renderTextFormField(
label: '주소',
onSaved: (val) {
setState(() {
this.address = val;
});
},
validator: (val) {
if(val.length < 1) {
return '주소는 필수사항입니다.';
}
return null;
},
),
renderTextFormField(
label: '닉네임',
onSaved: (val) {
setState(() {
this.nickname = val;
});
},
validator: (val) {
if(val.length < 1) {
return '닉네임은 필수사항입니다.';
}
if(val.length < 8) {
return '닉네임은 8자 이상 입력해주세요!';
}
return null;
},
),
renderButton(),
],
),
),
),
);
}
값을 보여주는 위젯 작성하기
이렇게 작성하고 실제 저장시 값을 조회해볼 수 있도록 함수를 하나 더 작성해보도록 하겠습니다.
renderValues(){
return Column(
children: [
Text(
'name: $name'
),
Text(
'email: $email'
),
Text(
'password: $password',
),
Text(
'address: $address',
),
Text(
'nickname: $nickname',
),
],
);
}
Form 의 Save 함수
renderValues
함수를 build
함수에 넣었는데
저장을 눌러도 안타깝게도 값이 보이지 않습니다. 왜그럴까요?
그 이유는 저희가 폼의 validate
함수만 실행하고 save
함수를 실행하지 않았기 때문이예요. 버튼의 onPressed
콜백을
조금 변경 해볼게요.
renderButton() {
return RaisedButton(
color: Colors.blue,
onPressed: () async {
if (this.formKey.currentState.validate()) {
// validation 이 성공하면 true 가 리턴돼요!
// validation 이 성공하면 폼 저장하기
this.formKey.currentState.save();
Get.snackbar(
'저장완료!',
'폼 저장이 완료되었습니다!',
backgroundColor: Colors.white,
);
}
},
child: Text(
'저장하기!',
style: TextStyle(
color: Colors.white,
),
),
);
}
이제는 값을 저장하면 아래와같이 저장한 값이 화면에 보이고 스넥바까지 정상적으로 뜨는걸 볼 수 있습니다.
마지막 꿀팁
TextField
에 onChanged
파라미터를 받아서 상태관리를
하는것보다 훨씬 편하죠? 여기에서 한가지 유용한 팁을 드린다면
TextFormField
에 autovalidateMode
를 AutovalidateMode.always
로 지정해보세요! 그럼 저장하기 버튼을 누르기 전에 각 TextFormField
가 자동으로 validation 을 진행하는걸 볼 수 있습니다.