Contents
출처Preview화면 구조기본 코드LayoutStatelessWidget을 HomePage와 LoginPage로 나누기라이브러리 사용해서 logo.svg 가져오기LoginPage에 사진 넣기 LoginPage에 Email, Password TextField 만들기Login Button 만들기MTextField와 MButton 모듈화HomePage에 logo와 버튼 넣기Route 설정유효성 검사Design간격 통일 위한 size 파일 만들기MTextField DesignMForm DesignMButton DesignMLogo Design간격 주기Padding 주기완성출처
만들면서 배우는 플러터 앱 프로그래밍 7가지 모바일 앱 UI 제작 & RiverPod 상태 관리
Preview
- From 위젯
- TextFromField 위젯
- Navigator 위젯
- 위젯을 위한 Route
- Svg 위젯
- 앱 전체 디자인을 위한 Theme 사용법 - 나중에 한방에 알려주신다 함
화면 구조


기본 코드
Settings → live 검색 → Flutter - fst 선택 → top level 체크
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp();
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: HomePage(),
);
}
}
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Placeholder(),
);
}
}
Layout
StatelessWidget을 HomePage와 LoginPage로 나누기
LoginPage 구조 먼저 잡을 것이므로 home: LoginPage
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp();
@override
Widget build(BuildContext context) {
return MaterialApp(debugShowCheckedModeBanner: false, home: LoginPage());
}
}
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(body: Placeholder());
}
}
class LoginPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(body: Placeholder());
}
}
Component로 만들어서 넣을 폴더 만들어두기

라이브러리 사용해서 logo.svg 가져오기
logo.svg → 벡터로 된 이미지
pub.dev에서 svg 검색


사진 경로 설정

라이브러리 연결


assets 폴더에 logo.svg 붙여넣기


LoginPage에 사진 넣기
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp();
@override
Widget build(BuildContext context) {
return MaterialApp(debugShowCheckedModeBanner: false, home: LoginPage());
}
}
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(body: Placeholder());
}
}
class LoginPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: ListView(
children: [
SvgPicture.asset(
"assets/logo.svg",
width: 70,
height: 70,
),
Text("Login"),
],
),
);
}
}

이미지를 Column으로 만들기
LoginPage와 HomePage를 Component로 만들기 위함 / 지금은 이미지와 텍스트가 따로 노는 상태
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp();
@override
Widget build(BuildContext context) {
return MaterialApp(debugShowCheckedModeBanner: false, home: LoginPage());
}
}
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(body: Placeholder());
}
}
class LoginPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: ListView(
children: [
Column(
children: [
SvgPicture.asset(
"assets/logo.svg",
width: 70,
height: 70,
),
Text("Login"),
],
),
],
),
);
}
}

Column을 Component로 만들기

MLogo에 변수 추가
재사용 위함
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp();
@override
Widget build(BuildContext context) {
return MaterialApp(debugShowCheckedModeBanner: false, home: LoginPage());
}
}
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(body: Placeholder());
}
}
class LoginPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: ListView(
children: [
MLogo("Login"),
],
),
);
}
}
class MLogo extends StatelessWidget {
String title;
MLogo(this.title);
@override
Widget build(BuildContext context) {
return Column(
children: [
SvgPicture.asset(
"assets/logo.svg",
width: 70,
height: 70,
),
Text("$title"),
],
);
}
}
Component 폴더에 옮기기
m_logo
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
class MLogo extends StatelessWidget {
String title;
MLogo(this.title);
@override
Widget build(BuildContext context) {
return Column(
children: [
SvgPicture.asset(
"assets/logo.svg",
width: 70,
height: 70,
),
Text("$title"),
],
);
}
}
main
import 'package:flutter/material.dart';
import 'package:flutter_login_2/component/m_logo.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp();
@override
Widget build(BuildContext context) {
return MaterialApp(debugShowCheckedModeBanner: false, home: LoginPage());
}
}
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(body: Placeholder());
}
}
class LoginPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: ListView(
children: [
MLogo("Login"),
],
),
);
}
}
LoginPage에 Email, Password TextField 만들기
TextFormField()는 Column으로 만들면 터진다
import 'package:flutter/material.dart';
import 'package:flutter_login_2/component/m_logo.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp();
@override
Widget build(BuildContext context) {
return MaterialApp(debugShowCheckedModeBanner: false, home: LoginPage());
}
}
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(body: Placeholder());
}
}
class LoginPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: ListView(
children: [
MLogo("Login"),
TextFormField(),
TextFormField(),
],
),
);
}
}

decoration으로 TextFormField을 디자인한다
InputDecoration(hintText: “Enter Email") → PlaceHolder와 같음
import 'package:flutter/material.dart';
import 'package:flutter_login_2/component/m_logo.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp();
@override
Widget build(BuildContext context) {
return MaterialApp(debugShowCheckedModeBanner: false, home: LoginPage());
}
}
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(body: Placeholder());
}
}
class LoginPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: ListView(
children: [
MLogo("Login"),
TextFormField(
decoration: InputDecoration(
hintText: "Enter Email",
),
),
],
),
);
}
}

TextFormField를 Component로 만들기

import 'package:flutter/material.dart';
import 'package:flutter_login_2/component/m_logo.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp();
@override
Widget build(BuildContext context) {
return MaterialApp(debugShowCheckedModeBanner: false, home: LoginPage());
}
}
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(body: Placeholder());
}
}
class LoginPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: ListView(
children: [
MLogo("Login"),
MTextField(),
],
),
);
}
}
class MTextField extends StatelessWidget {
@override
Widget build(BuildContext context) {
return TextFormField(
decoration: InputDecoration(
hintText: "Enter Email",
),
);
}
}
MTextField에 변수 추가
isPassword의 기본값은 false
Password Field에 입력하는 순간 obscureText - isPassword: true로 인해 Field에 입력하는 값이 보이지 않게 됨
import 'package:flutter/material.dart';
import 'package:flutter_login_2/component/m_logo.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp();
@override
Widget build(BuildContext context) {
return MaterialApp(debugShowCheckedModeBanner: false, home: LoginPage());
}
}
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(body: Placeholder());
}
}
class LoginPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: ListView(
children: [
MLogo("Login"),
MTextField("Email"),
MTextField("Password", isPassword: true),
],
),
);
}
}
class MTextField extends StatelessWidget {
String name;
bool isPassword;
MTextField(this.name, {this.isPassword = false});
@override
Widget build(BuildContext context) {
return TextFormField(
obscureText: isPassword,
decoration: InputDecoration(hintText: "Enter $name"),
);
}
}

MTextField를 Component 폴더로 옮기기
m_text_field
import 'package:flutter/material.dart';
class MTextField extends StatelessWidget {
String name;
bool isPassword;
MTextField(this.name, {this.isPassword = false});
@override
Widget build(BuildContext context) {
return TextFormField(
obscureText: isPassword,
decoration: InputDecoration(hintText: "Enter $name"),
);
}
}
main
import 'package:flutter/material.dart';
import 'package:flutter_login_2/component/m_logo.dart';
import 'package:flutter_login_2/component/m_text_field.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp();
@override
Widget build(BuildContext context) {
return MaterialApp(debugShowCheckedModeBanner: false, home: LoginPage());
}
}
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(body: Placeholder());
}
}
class LoginPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: ListView(
children: [
MLogo("Login"),
MTextField("Email"),
MTextField("Password", isPassword: true),
],
),
);
}
}
Login Button 만들기
import 'package:flutter/material.dart';
import 'package:flutter_login_2/component/m_logo.dart';
import 'package:flutter_login_2/component/m_text_field.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp();
@override
Widget build(BuildContext context) {
return MaterialApp(debugShowCheckedModeBanner: false, home: LoginPage());
}
}
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(body: Placeholder());
}
}
class LoginPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: ListView(
children: [
MLogo("Login"),
Text("Email"),
MTextField("Email"),
Text("Password"),
MTextField("Password", isPassword: true),
ElevatedButton(onPressed: () {}, child: Text("Login")),
],
),
);
}
}

ElevatedButton을 Component로 만들기

ElevatedButton 재사용 위한 변수 추가
import 'package:flutter/material.dart';
import 'package:flutter_login_2/component/m_logo.dart';
import 'package:flutter_login_2/component/m_text_field.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp();
@override
Widget build(BuildContext context) {
return MaterialApp(debugShowCheckedModeBanner: false, home: LoginPage());
}
}
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(body: Placeholder());
}
}
class LoginPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: ListView(
children: [
MLogo("Login"),
Text("Email"),
MTextField("Email"),
Text("Password"),
MTextField("Password", isPassword: true),
MButton("Login"),
],
),
);
}
}
class MButton extends StatelessWidget {
String name;
MButton(this.name);
@override
Widget build(BuildContext context) {
return ElevatedButton(onPressed: () {}, child: Text("$name"));
}
}
MButton을 component 폴더로 옮기기
m_button
import 'package:flutter/material.dart';
class MButton extends StatelessWidget {
String name;
MButton(this.name);
@override
Widget build(BuildContext context) {
return ElevatedButton(onPressed: () {}, child: Text("$name"));
}
}
main
import 'package:flutter/material.dart';
import 'package:flutter_login_2/component/m_button.dart';
import 'package:flutter_login_2/component/m_logo.dart';
import 'package:flutter_login_2/component/m_text_field.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp();
@override
Widget build(BuildContext context) {
return MaterialApp(debugShowCheckedModeBanner: false, home: LoginPage());
}
}
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(body: Placeholder());
}
}
class LoginPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: ListView(
children: [
MLogo("Login"),
Text("Email"),
MTextField("Email"),
Text("Password"),
MTextField("Password", isPassword: true),
MButton("Login"),
],
),
);
}
}
MTextField와 MButton 모듈화
Column으로 감싼 후 Column을 Form으로 감싼다
Form으로 감싸면 나중에 유효성 검사도 가능
import 'package:flutter/material.dart';
import 'package:flutter_login_2/component/m_button.dart';
import 'package:flutter_login_2/component/m_logo.dart';
import 'package:flutter_login_2/component/m_text_field.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp();
@override
Widget build(BuildContext context) {
return MaterialApp(debugShowCheckedModeBanner: false, home: LoginPage());
}
}
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(body: Placeholder());
}
}
class LoginPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: ListView(
children: [
MLogo("Login"),
Form(
child: Column(
children: [
Text("Email"),
MTextField("Email"),
Text("Password"),
MTextField("Password", isPassword: true),
MButton("Login"),
],
),
),
],
),
);
}
}

Form을 Component로 만들기

import 'package:flutter/material.dart';
import 'package:flutter_login_2/component/m_button.dart';
import 'package:flutter_login_2/component/m_logo.dart';
import 'package:flutter_login_2/component/m_text_field.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp();
@override
Widget build(BuildContext context) {
return MaterialApp(debugShowCheckedModeBanner: false, home: LoginPage());
}
}
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(body: Placeholder());
}
}
class LoginPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: ListView(
children: [
MLogo("Login"),
MForm(),
],
),
);
}
}
class MForm extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Form(
child: Column(
children: [
Text("Email"),
MTextField("Email"),
Text("Password"),
MTextField("Password", isPassword: true),
MButton("Login"),
],
),
);
}
}
MForm을 component 폴더로 옮기기
m_form
import 'package:flutter/material.dart';
class MForm extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Form(
child: Column(
children: [
Text("Email"),
MTextField("Email"),
Text("Password"),
MTextField("Password", isPassword: true),
MButton("Login"),
],
),
);
}
}
main
import 'package:flutter/material.dart';
import 'package:flutter_login_2/component/m_form.dart';
import 'package:flutter_login_2/component/m_logo.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp();
@override
Widget build(BuildContext context) {
return MaterialApp(debugShowCheckedModeBanner: false, home: LoginPage());
}
}
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(body: Placeholder());
}
}
class LoginPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: ListView(
children: [
MLogo("Login"),
MForm(),
],
),
);
}
}
MTextField에 TextEditingController 연결하기
TextEditingController을 사용하면 서버로 데이터를 보낼 때 TextEditingController의 변수명에 연결연산자를 사용하여 Field값의 문자를 가져올 수 있다 (_email.text)
MTextField
import 'package:flutter/material.dart';
class MTextField extends StatelessWidget {
String name;
bool isPassword;
TextEditingController controller;
MTextField(this.name, this.controller, {this.isPassword = false});
@override
Widget build(BuildContext context) {
return TextFormField(
controller: controller,
obscureText: isPassword,
decoration: InputDecoration(hintText: "Enter $name"),
);
}
}
MButton
서버로 Field값들이 전송되도록 함수 만들기
버튼에 함수를 전달하여 버튼 눌렀을 때 서버로 Field값들이 전송되도록 하기
버튼에 함수를 넣을 땐 var 타입으로 넣기 (맞는 타입 일일이 찾아서 지정하기 힘드니까)
import 'package:flutter/material.dart';
class MButton extends StatelessWidget {
String name;
var submit;
MButton(this.name, {this.submit});
@override
Widget build(BuildContext context) {
return ElevatedButton(onPressed: submit, child: Text("$name"));
}
}
MForm
submit 함수를 만들어서 MButton에 전달하기
함수 내부는 Map으로 만든다
(Flutter의 Map이 Json과 똑같은 구조이므로 편하게 서버로 데이터를 전송하기 위해)
MForm에서 MButton을 누르면 HomePage로 이동되도록 Navigator 위젯 사용
import 'package:flutter/material.dart';
import 'package:flutter_login_2/component/m_button.dart';
import 'package:flutter_login_2/component/m_text_field.dart';
class MForm extends StatelessWidget {
TextEditingController _email = TextEditingController();
TextEditingController _password = TextEditingController();
void submit(BuildContext context) {
var requestBody = {"emaii": _email.text, "password": _password.text};
print("전송할 데이터 : $requestBody");
}
@override
Widget build(BuildContext context) {
return Form(
child: Column(
children: [
Text("Email"),
MTextField("Email", _email),
Text("Password"),
MTextField("Password", _password, isPassword: true),
MButton("Login", submit: () => submit(context)),
],
),
);
}
}
HomePage에 logo와 버튼 넣기
import 'package:flutter/material.dart';
import 'package:flutter_login_2/component/m_button.dart';
import 'package:flutter_login_2/component/m_form.dart';
import 'package:flutter_login_2/component/m_logo.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp();
@override
Widget build(BuildContext context) {
return MaterialApp(debugShowCheckedModeBanner: false, home: HomePage());
}
}
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: ListView(
children: [
MLogo("Care Soft"),
MButton("Get Started"),
],
),
);
}
}
class LoginPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: ListView(
children: [
MLogo("Login"),
MForm(),
],
),
);
}
}

Route 설정
Route → HashMap으로 구성되어있음 / 플러터의 Map(Object)은 JSON이랑 똑같이 생김
통신할 때 Map을 던지면 됨 → JSON 형태니까 굳이 바꾸지 않아도 됨 / 던질때 문자열로 바꿔서 던진다
initialRoute → 초기 라우터 설정 / 첫 페이지가 LoginPage가 됨
책 참고하기 → 3장의 73P
import 'package:flutter/material.dart';
import 'package:flutter_login_2/component/m_button.dart';
import 'package:flutter_login_2/component/m_form.dart';
import 'package:flutter_login_2/component/m_logo.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp();
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
initialRoute: "/login",
routes: {
"/home": (context) => HomePage(),
"/login": (context) => LoginPage(),
},
);
}
}
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: ListView(
children: [
MLogo("Care Soft"),
MButton("Get Started"),
],
),
);
}
}
class LoginPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: ListView(
children: [
MLogo("Login"),
MForm(),
],
),
);
}
}
HomePage와 LoginPage를 page 폴더로 옮기기
login_page
import 'package:flutter/material.dart';
import 'package:flutter_login_2/component/m_form.dart';
import 'package:flutter_login_2/component/m_logo.dart';
class LoginPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: ListView(
children: [
MLogo("Login"),
MForm(),
],
),
);
}
}
home_page
import 'package:flutter/material.dart';
import 'package:flutter_login_2/component/m_button.dart';
import 'package:flutter_login_2/component/m_logo.dart';
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: ListView(
children: [
MLogo("Care Soft"),
MButton("Get Started"),
],
),
);
}
}
main
import 'package:flutter/material.dart';
import 'page/home_page.dart';
import 'page/login_page.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp();
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
initialRoute: "/login",
routes: {
"/home": (context) => HomePage(),
"/login": (context) => LoginPage(),
},
);
}
}
MForm에서 버튼 누르면 HomePage로 이동되도록 Navigator 위젯 설정
import 'package:flutter/material.dart';
import 'package:flutter_login_2/component/m_button.dart';
import 'package:flutter_login_2/component/m_text_field.dart';
import 'package:flutter_login_2/size.dart';
class MForm extends StatelessWidget {
TextEditingController _email = TextEditingController();
TextEditingController _password = TextEditingController();
void submit(BuildContext context) {
var requestBody = {"emaii": _email.text, "password": _password.text};
print("전송할 데이터 : $requestBody");
Navigator.pushNamed(context, "/home");
}
@override
Widget build(BuildContext context) {
return Form(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text("Email"),
MTextField("Email", _email),
SizedBox(height: sGap),
Text("Password"),
MTextField("Password", _password, isPassword: true),
SizedBox(height: mGap),
MButton("Login", submit: () => submit(context)),
],
),
);
}
}
HomePage에서 버튼 누르면 이전 화면으로 돌아가도록 Navigator 위젯 설정
import 'package:flutter/material.dart';
import 'package:flutter_login_2/component/m_button.dart';
import 'package:flutter_login_2/component/m_logo.dart';
import 'package:flutter_login_2/size.dart';
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: ListView(
children: [
SizedBox(height: xxlGap),
MLogo("Care Soft"),
SizedBox(height: lGap),
MButton(
"Get Started",
submit: () {
Navigator.pop(context);
},
),
],
),
);
}
}
유효성 검사
책 167P
Design
간격 통일 위한 size 파일 만들기
size
double sGap = 5;
double mGap = 10;
double lGap = 20;
double xxlGap = 100;
MTextField Design
InputDecoration의 기본 디자인 4가지
- enabledBorder
- enabledBorder은 클릭하면 focusedBorder로 바뀌면서 디자인이 달라진다
- focusedBorder
- focusedBorder를 enabledBorder 디자인이랑 똑같이 하면 클릭해도 디자인 변경 안됨
- errorBorder
- focusedErrorBorder
→ 이 4가지가 TextFormField의 기본 구조
import 'package:flutter/material.dart';
class MTextField extends StatelessWidget {
String name;
bool isPassword;
TextEditingController controller;
MTextField(this.name, this.controller, {this.isPassword = false});
@override
Widget build(BuildContext context) {
return TextFormField(
controller: controller,
obscureText: isPassword,
decoration: InputDecoration(
hintText: "Enter $name", // 1. 기본 디자인 - enabledBorder은 클릭하면 focusedBorder로 바뀌면서 디자인이 달라진다
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
// 2. 포커스 디자인 - focusedBorder를 enabledBorder 디자인이랑 똑같이 하면 enabledBorder 클릭해도 디자인 변경 안됨
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
// 3. 기본 에러 디자인
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
// 4. 기본 에러 포커스 디자인
focusedErrorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
),
);
}
}

MForm Design
화면 맨 오른쪽으로 정렬하기
import 'package:flutter/material.dart';
import 'package:flutter_login_2/component/m_button.dart';
import 'package:flutter_login_2/component/m_text_field.dart';
class MForm extends StatelessWidget {
TextEditingController _email = TextEditingController();
TextEditingController _password = TextEditingController();
void submit(BuildContext context) {
var requestBody = {"emaii": _email.text, "password": _password.text};
print("전송할 데이터 : $requestBody");
}
@override
Widget build(BuildContext context) {
return Form(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text("Email"),
MTextField("Email", _email),
Text("Password"),
MTextField("Password", _password, isPassword: true),
MButton("Login", submit: () => submit(context)),
],
),
);
}
}
- crossAxisAlignment: CrossAxisAlignment.stretch
- 자식의 넓이가 최대로 늘어난다
- double.infinity랑 같다
- Column을 ListView로 바꾸면 높이가 없어서 부모만큼 늘어나버리는데 shrinkWrap: true를 사용하면 높이가 잡히면서 안에 있는 자식 갯수만큼 크기가 줄어든다
- 버튼을 Row로 감싸는 건 해결불가
- 넓이가 최대로 늘어나는 속성이 없다
- Container로 감싸면 넓이를 double.infinity 주면 됨
- 버튼을 확장(Expanded)하는 것도 안됨 → 버튼을 Row로 감싼 다음 확장하는 건 가능함

MButton Design
import 'package:flutter/material.dart';
class MButton extends StatelessWidget {
String name;
var submit;
MButton(this.name, {this.submit});
@override
Widget build(BuildContext context) {
return ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.black,
fixedSize: Size(double.infinity, 40),
),
onPressed: submit,
child: Text(
"$name",
style: TextStyle(color: Colors.white),
),
);
}
}

MLogo Design
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
class MLogo extends StatelessWidget {
String title;
MLogo(this.title);
@override
Widget build(BuildContext context) {
return Column(
children: [
SvgPicture.asset(
"assets/logo.svg",
width: 70,
height: 70,
),
Text(
"$title",
style: TextStyle(fontSize: 40, fontWeight: FontWeight.bold),
),
],
);
}
}

간격 주기
LoginPage
import 'package:flutter/material.dart';
import 'package:flutter_login_2/component/m_form.dart';
import 'package:flutter_login_2/component/m_logo.dart';
import 'package:flutter_login_2/size.dart';
class LoginPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: ListView(
children: [
SizedBox(height: xxlGap),
MLogo("Login"),
SizedBox(height: lGap),
MForm(),
],
),
);
}
}

MForm
import 'package:flutter/material.dart';
import 'package:flutter_login_2/component/m_button.dart';
import 'package:flutter_login_2/component/m_text_field.dart';
import 'package:flutter_login_2/size.dart';
class MForm extends StatelessWidget {
TextEditingController _email = TextEditingController();
TextEditingController _password = TextEditingController();
void submit(BuildContext context) {
var requestBody = {"emaii": _email.text, "password": _password.text};
print("전송할 데이터 : $requestBody");
}
@override
Widget build(BuildContext context) {
return Form(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text("Email"),
MTextField("Email", _email),
SizedBox(height: sGap),
Text("Password"),
MTextField("Password", _password, isPassword: true),
SizedBox(height: mGap),
MButton("Login", submit: () => submit(context)),
],
),
);
}
}

HomePage
import 'package:flutter/material.dart';
import 'package:flutter_login_2/component/m_button.dart';
import 'package:flutter_login_2/component/m_logo.dart';
import 'package:flutter_login_2/size.dart';
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: ListView(
children: [
SizedBox(height: xxlGap),
MLogo("Care Soft"),
SizedBox(height: lGap),
MButton(
"Get Started",
submit: () {
Navigator.pop(context);
},
),
],
),
);
}
}
Padding 주기
LoginPage

import 'package:flutter/material.dart';
import 'package:flutter_login_2/component/m_form.dart';
import 'package:flutter_login_2/component/m_logo.dart';
import 'package:flutter_login_2/size.dart';
class LoginPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Padding(
padding: EdgeInsets.symmetric(horizontal: 16),
child: ListView(
children: [
SizedBox(height: xxlGap),
MLogo("Login"),
SizedBox(height: lGap),
MForm(),
],
),
),
);
}
}

HomePage

import 'package:flutter/material.dart';
import 'package:flutter_login_2/component/m_button.dart';
import 'package:flutter_login_2/component/m_logo.dart';
import 'package:flutter_login_2/size.dart';
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Padding(
padding: EdgeInsets.symmetric(horizontal: 16),
child: ListView(
children: [
SizedBox(height: xxlGap),
MLogo("Care Soft"),
SizedBox(height: lGap),
MButton(
"Get Started",
submit: () {
Navigator.pop(context);
},
),
],
),
),
);
}
}

완성

Field값 입력 후 Login 버튼을 누르면

HomePage로 이동되고

HomePage에서 버튼을 누르면

LoginPage로 되돌아간다

Share article