Dandy Now!
  • [Flutter] "모두가 할 수 있는 플러터 UI 입문" - 로그인 앱 만들기
    2022년 02월 03일 18시 31분 48초에 업로드 된 글입니다.
    작성자: DandyNow
    728x90
    반응형

    "최주호, 정호준, & 정동진. (2021). 모두가 할 수 있는 플러터 UI 입문. 앤써북"으로 플러터 공부를 하고 있다. 이번 챕터에서는 size.dart 파일에 값을 지정하여 활용, 외부 라이브러리 SvgPicture 설치 및 사용, Theme 적용한 TextButton,  Navigator로 화면 이동, 그리고 ListView와 TextFormField 위젯을 중요하게 다뤘다.

     

    책에서는 ListView 위젯을 사용해 화면 overflow 문제를 해결하였는데, 아래 코드에서는 SingleChildScrollView 위젯을 적용하였다. 그리고 배경 터치 시 키보드를 사라지게 하는 기능을 추가하였다. 기존에는 키보드 숨기기 버튼으로만 사라지게 할 수 있었다. GestureDetector 위젯을 사용하였는데, "FocusScope.of(context).unfocus();"를 적용하니 [그림 1]과 같이 키보드가 깜박이는 문제가 있었다. 구글링 끝에 "FocusScope.of(context).requestFocus(new FocusNode());"를 적용하였으며 [그림 2]와 같이 키보드 깜박임 문제가 해결되었다. [그림 3]은 완성된 로그인 앱이다.

     

    [그림 1] unfocus 적용한 경우 발생한 키보드 깜박임 문제

     

    [그림 2] requestFocus(new FocusNode()) 적용하여 키보드 깜박임 문제 해결

     

    [그림 3] 완성된 로그인 앱(왼쪽 LoginPage, 오른쪽 HomePage)

     


     

    // main.dart
    import 'package:flutter/material.dart';
    import 'package:flutter_login/pages/home_page.dart';
    import 'package:flutter_login/pages/login_page.dart';
    
    void main() {
      runApp(MyApp());
    }
    
    class MyApp extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          debugShowCheckedModeBanner: false,
          theme: ThemeData(
            textButtonTheme: TextButtonThemeData(
              // TextButton 재사용을 위해 components 패키지 생성하여 만들수 있지만,
              // 모든 TextButton 디자인이 동일하다면 테마를 지정하여 쉽게 재사용할 수 있다.
              style: TextButton.styleFrom(
                backgroundColor: Colors.black,
                primary: Colors.white,
                shape: RoundedRectangleBorder(
                  borderRadius: BorderRadius.circular(30),
                ),
                minimumSize: Size(400, 60),
              ),
            ),
          ),
          initialRoute: "/login", // LoginPage가 최초 실행된다.
          routes: {
            "/login": (context) => LoginPage(),
            "/home": (context) => HomePage(),
          },
        );
      }
    }

     

    // size.dart
    // Expanded, Spacer 위젯은 화면의 남은 공간만큼 확장한다. ListView는 스크롤 있는 위젯으로 높이가 무한하기 때문에 Expanded, Spacer 사용불가하다.
    // SizeBox 이용하기 위해 size.dart 만들어 값을 정해둔다.
    const double small_gap = 5.0;
    const double medium_gap = 10.0;
    const double large_gap = 20.0;
    const double xlarge_gap = 100.0;

     

    // pages/login_page.dart
    import 'package:flutter/material.dart';
    import 'package:flutter_login/components/custom_form.dart';
    import 'package:flutter_login/components/logo.dart';
    import 'package:flutter_login/size.dart';
    
    class LoginPage extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          // body: Container(
          //   color: Colors.yellow,
          // ),
          body: Builder(builder: (context) {
            // GestureDetector, FocusScope 이용해 배경을 터치하면 키패드가 사라지게 만든다.
            return GestureDetector(
              onTap: () {
                // FocusScope.of(context).unfocus(); // 코딩셰프 조금 매운맛 강좌 참조함
                FocusScope.of(context).requestFocus(
                    new FocusNode()); // unfocus 적용시 TextFormField 터치시 키패드가 순식간에 사라지는 문제있어 FousNode 적용하여 해결함
              },
              child: Padding(
                padding: const EdgeInsets.all(16.0),
                // ListView로 전체화면 구성 이유(TextFormField 위젯 사용한다면 전체화면에 스크롤 다는 것이 좋다)
                // 1) 세로 방향이기 때문에 Column 뿐만 아니라 ListView 사용 가능
                // 2) TextFormField를 터치하면 키보드가 올라옴. 이때 화면에 그림을 그릴 수 없는 inset 영역이 생기는데 이때 스크롤이 없으면 overflow 오류 발생
                child: SingleChildScrollView(
                  // SingleChildScrollView로 overflow 문제 해결 가능하다(코딩셰프 조금 매운맛 강좌 참조함).
                  // child: ListView(
                  child: Column(
                    children: <Widget>[
                      SizedBox(height: xlarge_gap),
                      Logo("Login"),
                      SizedBox(height: large_gap),
                      CustomForm(),
                    ],
                  ),
                ),
              ),
            );
            // );
          }),
        );
      }
    }

     

    // pages/home_page.dart
    import 'package:flutter/material.dart';
    import 'package:flutter_login/components/logo.dart';
    
    // 파일명에 대문자를 사용하지 않는 것이 flutter 규칙이다. 스네이크 표기법을 사용한다.
    class HomePage extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          // body: Container(
          //   color: Colors.red,
          body: Padding(
            padding: const EdgeInsets.all(16.0),
            child: Column(
              children: [
                SizedBox(height: 200),
                Logo("Care Soft"),
                SizedBox(height: 50),
                TextButton(
                  onPressed: () {
                    Navigator.pop(
                        context); // 스택의 가장 위에 쌓인 HomePage 위젯을 pop하여 LoginPage가 화면에 보여지게 된다.
                  },
                  child: Text("Get Started"),
                ),
              ],
            ),
          ),
        );
      }
    }

     

    // components/custom_form.dart
    import 'package:flutter/material.dart';
    import 'package:flutter_login/components/custom_text_form_field.dart';
    import 'package:flutter_login/size.dart';
    
    class CustomForm extends StatelessWidget {
      final _formKey = GlobalKey<FormState>(); // 글로벌 key
      @override
      Widget build(BuildContext context) {
        // Form 위젯은 데이터 전송 위해 여러 양식의 위젯을 그룹화하는 컨테이너이다.
        return Form(
          key: _formKey, // 글로벌 key를 Form 태그에 연결하여 Form 상태를 관리한다.
          child: Column(
            children: [
              CustomTextFormField("Email"),
              SizedBox(height: medium_gap),
              CustomTextFormField("Password"),
              SizedBox(height: large_gap),
              TextButton(
                onPressed: () {
                  // 유효성 검사, TextFormField가 비었는지 확인한다. 비었으면 false, 입력 값이 있으면 true 이다.
                  if (_formKey.currentState!.validate()) {
                    Navigator.pushNamed(context, "/home");
                  }
                },
                child: Text("Login"),
              ),
            ],
          ),
        );
      }
    }

     

    // components/custom_text_form_field.dart
    import 'package:flutter/material.dart';
    import 'package:flutter_login/size.dart';
    
    class CustomTextFormField extends StatelessWidget {
      final String text;
      const CustomTextFormField(this.text);
    
      @override
      Widget build(BuildContext context) {
        return Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: <Widget>[
            Text(text),
            SizedBox(height: small_gap),
            // TextFormField는 TextField와 유사하지만, validator 속성 활용해 유효성 검사를 가능하게 한다.
            TextFormField(
              // !는 null 이 절대 아님을 컴파일러에게 알려준다.
              validator: (value) =>
                  value!.isEmpty ? "Please enter some text" : null,
              // 비밀번호 입력 양식이면 * 처리한다.
              obscureText: text == "Password" ? true : false,
              decoration: InputDecoration(
                hintText: "Enter $text",
                enabledBorder: OutlineInputBorder(
                  // 기본 디자인
                  borderRadius: BorderRadius.circular(20),
                ),
                focusedBorder: OutlineInputBorder(
                  // 손가락 터치시 디자인
                  borderRadius: BorderRadius.circular(20),
                ),
                errorBorder: OutlineInputBorder(
                  // 에러 발생시 디자인
                  borderRadius: BorderRadius.circular(20),
                ),
                focusedErrorBorder: OutlineInputBorder(
                  // 에러 발생 후 손가락 터치시 디자인
                  borderRadius: BorderRadius.circular(20),
                ),
              ),
            ),
          ],
        );
      }
    }

     

    // components/logo.dart
    import 'package:flutter/material.dart';
    import 'package:flutter_svg/flutter_svg.dart';
    
    class Logo extends StatelessWidget {
      final String title;
      const Logo(this.title);
    
      @override
      Widget build(BuildContext context) {
        return Column(
          children: [
            SvgPicture.asset(
              // SvgPicture 외부 라이브러리 이므로 pub.dev 사이트 통해 flutter_svg 검색 null-safety 적용된 라이브러리 설치해야 한다(pubspec.yaml의 라이브러리를 추가해야 dependencies에 추가해야 함).
              "assets/logo_eco.svg",
              height: 70,
              width: 70,
            ),
            Text(
              title,
              style: TextStyle(fontSize: 40, fontWeight: FontWeight.bold),
            ),
          ],
        );
      }
    }

     

    728x90
    반응형
    댓글