17. 플러터 : 로그인

로그인이 뭐가 어려울까?
너무 쉽게 생각했고 코드를 구성하는 과정에서 많은 것을 배웠다.
우재, 너에게 다 알려주마!!

화면이 조금 이상해 보일 수도 있지만, 아빠가 의도한 화면이니까.. 이해하고.
아이디와 비밀번호를 입력 받아서 로그인 버튼을 눌렀을 때
일치하면 초기화를 시켜서 다시 입력을 받도록 했고
일치하지 않으면 스낵바를 통해 틀렸다고 알려주도록 했지.

계속 그랬던 것처럼
아빠 코드를 보지 않고 직접 구현할 수 있으면 좋겠는데..
어쩔 수 없어 코드를 보고 이해했다면
나중에라도 이번 화면을 직접 구현해 봐야 해. 알았지?



설명할 게 너무 많아서 코드에 주석을 달아야 했다.
중복되는 코드가 많아서 기본 함수인 makeText와 makeTextField를 만들었고
이들 함수를 사용하는 makeRowContainer 함수까지 총 3개의 함수를 사용했다.

import 'package:flutter/material.dart';

void main() {
runApp(MaterialApp(
title: '로그인',
home: Scaffold(
appBar: AppBar(title: Text('로그인')),
body: Login(),
),
));
}

class Login extends StatefulWidget {
@override
State createState() => LoginState();
}

class LoginState extends State<Login> {
String userName = '';
String password = '';

@override
Widget build(BuildContext context) {
return Center(
child: Column(
children: <Widget>[
makeRowContainer('아이디', true),
makeRowContainer('비밀번호', false),
Container(child: RaisedButton(
child: Text('로그인', style: TextStyle(fontSize: 21)),
onPressed: () {
// 사용자 이름과 비밀번호가 일치한다면!
if(userName == 'dart' && password == 'flutter') {
// 세터로 초기화를 했기 때문에 build 함수 자동 호출하면서
// 아이디와 비밀번호 텍스트필드가 빈 문자열로 초기화된다.
setState(() {
userName = '';
password = '';
});
}
else
Scaffold.of(context)
..removeCurrentSnackBar()
..showSnackBar(SnackBar(content: Text('일치하지 않습니다!!')));
}
),
margin: EdgeInsets.only(top: 12),
),
],
mainAxisAlignment: MainAxisAlignment.center,
),
);
}

Widget makeRowContainer(String title, bool isUserName) {
return Container(
child: Row(
children: <Widget>[
makeText(title),
makeTextField(isUserName),
],
mainAxisAlignment: MainAxisAlignment.spaceBetween,
),
padding: EdgeInsets.only(left: 60, right: 60, top: 8, bottom: 8),
);
}

// Cascade 문법 사용. 주석으로 막은 코드보다 ..을 사용한 한 줄 코드가 훨씬 낫다.
// Cascade 문법은 아래에서 따로 설명한다.
Widget makeText(String title) {
// var paint = Paint();
// paint.color = Colors.green;

return Text(
title,
style: TextStyle(
fontSize: 21,
background: Paint()..color = Colors.green,
// background: paint,
),
);
}

Widget makeTextField(bool isUserName) {
// TextField 위젯의 크기를 변경하고 padding을 주려면 Container 위젯 필요.
// TextField 독자적으로는 할 수 없음.
return Container(
child: TextField(
// TextField 클래스는 입력 내용을 갖고 있지 않고, TextEditingController 클래스에 위임.
// 입력 내용에 접근할 때는 controller.text라고 쓰면 된다.
// 여기서는 로그인에 성공했을 때 초기화를 위한 용도로만 사용한다. 아래처럼 초기값을 줄 수도 있다.
// controller: TextEditingController()..text = '플러터',
controller: TextEditingController(),
style: TextStyle(fontSize: 21, color: Colors.black),
textAlign: TextAlign.center,
// 테두리 출력. enabledBorder 옵션을 사용하지 않으면 변경 불가.
decoration: InputDecoration(
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Colors.red,
width: 2.0
),
),
contentPadding: EdgeInsets.all(12),
),
onChanged: (String str) {
// 입력이 변경될 때마다 갱신이 필요하지 않기 때문에 세터 사용 안함
// 아이디와 비밀번호 중에서 하나를 갱신한다.
if(isUserName)
userName = str;
else
password = str;
},
),
// TextField 위젯의 크기를 설정하려면 Container 위젯을 부모로 가져야 한다.
// 컨테이너의 크기가 텍스트필드의 크기가 된다.
width: 200,
padding: EdgeInsets.only(left: 16),
);
}
}


텍스트필드에 테두리를 넣는 코드도 힘들었고
아무리 해도 테두리 색상이 바뀌지 않는 것도 힘들었고.
모든 시행착오를 알려주고 싶은데.. 그건 우재가 직접 해야겠다.
너무 길어져서 알려줘도 알려주지 않은 것만 못하다.

앞에서 사용한 double dot(..) 문법은 다트에서 Cascade notation이라고 불러.
아래 두 개의 코드는 같은 코드이고, 그냥 봐서는 double dot의 장점이 없어 보이겠지?

// 1. 일반적인 코드
var button = querySelector('#confirm');
button.text = 'Confirm';
button.classes.add('important');

// 2. double dot을 사용해서 축약한 코드
querySelector('#confirm')
..text = 'Confirm'
..classes.add('important')


1번 코드는 별도의 변수를 선언해야 하고, 2번 코드는 선언이 필요없다.
변수를 선언한다는 것은 기존 코드와 유기적으로 연동할 수 없다는 것을 뜻해.
앞에 나온 메인 코드에서도 추가 변수를 사용한 불편한 코드를 주석으로 막아놓았다. 봤지?


왜 굳이 다른 방법을 찾는 것일까?
앞에서 보여준 것이 정석이라고 생각되지만, 가끔은 정석만으로 코딩할 수 없으니까.
플러터가 구성하는 위젯 트리에서 직접 텍스트필드를 찾으면 더 쉽지 않을까?

그래서 해봤는데.. 번거로운 것들이 너무 많아서 비추.
다만 아래 코드는 프로젝트 규모가 커졌을 때 활용할 수 있기 때문에 보여주는 걸로.

웃긴데.. 아래 코드 만드는데 4시간 정도 걸렸네.
다트에 대한 자료를 봐도.. 뭐가 뭔지 구분도 안 되고..
아직까지는 범용적으로 사용하는 언어가 아니라는 건 분명히 배웠다.

참, 아래 코드는 버튼의 onPressed 함수에 대해서만 보여준다.
나머지 코드에서는 세터 변수로 사용하는 userName과 password만 삭제하면 끝.

onPressed: () {
// 위젯 검색. TextField 위젯이 많을 경우 추가 검색 필요.
// byType 함수로 찾고, evaluate 함수로 찾은 위젯 반환.
// byType 함수의 반환값 자료형은 Finder 클래스. evaluate 함수는 Iterable<> 반환.
// 모든 위젯의 루트 클래스에 해당하는 Element 클래스가 요소의 타입이기 때문에 변환 필요.
var finds_1 = find.byType(TextField).evaluate();
var finds_2 = finds_1.cast<StatefulElement>();
// 런타임 객체가 위젯을 감싸고 있는 형태라서 widget 속성을 사용해서 실제 위젯을 가져옴
var finds_3 = finds_2.map((w) => w.widget);
// Iterable<> 자료형을 리스트로 변환해서 [0]과 같은 정수 인덱스 사용함
var finds_4 = finds_3.cast<TextField>().toList();

// 상위 클래스를 하위 클래스로 변환하는 다운캐스팅(downcasting)이기 때문에 에러. 업캐스팅만 가능.
// var finds_2 = finds_1 as List<StatefulElement>;
// var finds_4 = finds_3 as List<TextField>;

// 이전 코드에서는 userName이 문자열이었지만 여기서는 TextField 위젯
var userName = finds_4[0];
var password = finds_4[1];

// 텍스트필드의 입력 데이터에 접근하려면 controller 속성 사용
if(userName.controller.text == 'dart' && password.controller.text == 'flutter') {
// 입력 데이터를 직접 바꾸면 화면에서도 변경되기 때문에 세터(setState) 사용하지 않음
userName.controller.text = '';
password.controller.text = '';
}
else
Scaffold.of(context)
..removeCurrentSnackBar()
..showSnackBar(SnackBar(content: Text('일치하지 않습니다!!')));
}


find 객체는 플러터에서 제공하는 최상위 상수 객체로 미리 정의되어 있다.
사용하려면 아래처럼 import 추가할 것.
목적은 해당 객체가 위젯 트리에 잘 들어갔는지 검사(test)하기 위한 용도로 제작된 것처럼 보인다.
아마도 겸사겸사 만들었겠지..

import 'package:flutter_test/flutter_test.dart';

'플러터' 카테고리의 다른 글

19. 플러터 : 탭바 기본  (0) 2019.02.11
18. 플러터 : 화면 이동(네비게이션)  (0) 2019.02.10
16. 플러터 : 텍스트 입력  (0) 2019.02.08
15. 플러터 : 버튼 종류  (0) 2019.02.08
14. 플러터 : 버튼 + 사진  (0) 2019.02.07