inblog logo
|
parangdajavous
    Flutter

    Chapter 07. Flutter_login

    김미숙's avatar
    김미숙
    Jun 11, 2025
    Chapter 07. Flutter_login
    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 사용법 - 나중에 한방에 알려주신다 함
     

    화면 구조

    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 → 벡터로 된 이미지
    The official repository for Dart and Flutter packages.
    Pub is the package manager for the Dart programming language, containing reusable libraries & packages for Flutter and general Dart programs.
    The official repository for Dart and Flutter packages.
    https://pub.dev/
    The official repository for Dart and Flutter packages.
    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), ], ), ); } }
     

    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")), ], ), ); } }
    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
    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 주기완성

    parangdajavous

    RSS·Powered by Inblog