Chapter 07. Flutter_login

김미숙's avatar
Jun 11, 2025
Chapter 07. Flutter_login

출처

만들면서 배우는 플러터 앱 프로그래밍 7가지 모바일 앱 UI 제작 & RiverPod 상태 관리
 

Preview

  • From 위젯
  • TextFromField 위젯
  • Navigator 위젯
  • 위젯을 위한 Route
  • Svg 위젯
  • 앱 전체 디자인을 위한 Theme 사용법 - 나중에 한방에 알려주신다 함
 

화면 구조

notion image
notion image
 

기본 코드

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로 만들어서 넣을 폴더 만들어두기

notion image
 

라이브러리 사용해서 logo.svg 가져오기

logo.svg → 벡터로 된 이미지
pub.dev에서 svg 검색
notion image
notion image

사진 경로 설정

notion image

라이브러리 연결

notion image
notion image

assets 폴더에 logo.svg 붙여넣기

notion image
notion image

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"), ], ), ); } }
notion image

이미지를 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"), ], ), ], ), ); } }
notion image

Column을 Component로 만들기

notion image

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(), ], ), ); } }
notion image
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", ), ), ], ), ); } }
notion image

TextFormField를 Component로 만들기

notion image
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"), ); } }
notion image

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), ], ), ); } }
 
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")), ], ), ); } }
notion image

ElevatedButton을 Component로 만들기

notion image

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"), ], ), ), ], ), ); } }
Column 때문에 로그인 버튼의 넓이가 안 맞는다 → 부모(Column)의 제약조건 때문에
Column 때문에 로그인 버튼의 넓이가 안 맞는다 → 부모(Column)의 제약조건 때문에

Form을 Component로 만들기

notion image
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(), ], ), ); } }
notion image
 

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), ), ), ); } }
notion image
 

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로 감싼 다음 확장하는 건 가능함
notion image

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), ), ); } }
notion image
 

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), ), ], ); } }
notion image

간격 주기

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(), ], ), ); } }
notion image

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)), ], ), ); } }
notion image

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

notion image
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(), ], ), ), ); } }
notion image

HomePage

notion image
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); }, ), ], ), ), ); } }
notion image
 
 
 
 

완성

notion image
Field값 입력 후 Login 버튼을 누르면
notion image
HomePage로 이동되고
notion image
HomePage에서 버튼을 누르면
notion image
LoginPage로 되돌아간다
notion image
 
 
 
Share article

parangdajavous