앱을 만들 때 서버가 있어야 로그인도 할 수 있고 이것저것 정보 저장을 할 수 있기 때문에 서버를 연동하는 법을 알아볼 것이다.
파이어베이스 서버를 이용해서 채팅 앱 만드는 강의가 유튜브에 있길래 이번 글에서는 일단 그대로 따라해볼 것이다.
먼저 image 폴더를 만들어주고 앱에서 사용될 배경 이미지를 넣어준다.
그리고 pubspec.yaml 파일로 가서
flutter의 assets 부분에 저렇게 추가한 이미지를 넣어줘야 한다.
이번에는 lib 폴더에 config 폴더를 만들고 palette.dart라는 파일을 만들어준다.
flutter의 painting 패키지를 import하고 Palette라는 클래스를 만들어서 사용할 색들을 지정해준다.
화면 구성을 위한 스크립트를 모아두기 위해 lib 폴더에 screens 폴더를 만들고 안에 main_screen.dart 파일을 만든다.
vscode라면 st만 쳐도 자동완성으로 stateful위젯 클래스를 만들 수 있다.
클래스 이름만 LoginSignupScreen 바꿔주었다.
import 'package:chat/screens/main_screen.dart';
import 'package:flutter/material.dart';
void main() {
runApp(const MainApp());
}
class MainApp extends StatelessWidget {
const MainApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Chatting App',
theme: ThemeData(primarySwatch: Colors.blue),
home: LoginSignupScreen(),
);
}
}
main.dart 파일은 간단하게 home을 LoginSignupScreen으로 지정해주는 코드로 구성하였다.
다시 돌아와서 main_screen.dart 파일에서 본격적으로 화면을 꾸며볼 것이다.
class _LoginSignupScreenState extends State<LoginSignupScreen> {
bool isSignupScreen = true;
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Palette.backgroundColor,
body: Stack(children: [
Positioned(
top: 0,
right: 0,
left: 0,
child: Container(
height: 300,
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage('image/background.png'),
fit: BoxFit.fill),
),
child: Container(
padding: EdgeInsets.only(top: 90, left: 20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
RichText(
text: TextSpan(
text: 'Welcome',
style: TextStyle(
letterSpacing: 1.0,
fontSize: 25,
color: Colors.white),
children: [
TextSpan(
text: ' to yummy chat',
style: TextStyle(
letterSpacing: 1.0,
fontSize: 25,
color: Colors.white,
fontWeight: FontWeight.bold))
])),
SizedBox(
height: 5.0,
),
Text(
'Sign up to continue',
style: TextStyle(
letterSpacing: 1.0,
color: Colors.white,
),
)
],
),
),
),
),
]));
}
}
새로운게 매우매우 많은데 하나씩 설명하도록 하겠다.
일단 제일 위에 isSignupScreen이라는 boolean 변수는 signup을 해야 하는지 login을 해야 하는지 구분하기 위한 변수이다.
나중에 사용할 것이니 일단은 생각 안해도 된다.
Scaffold 위젯은 주요 요소 ui를 편하게 배치하기 위한 위젯이다.
backgoundcolor는 이전에 만든 palette 클래스에서 색을 가져왔다.
body의 Stack은 여러 위젯들을 겹쳐서 배치할 수 있는 레이아웃 위젯이며, 각 자식 위젯들의 위치를 Positioned를 사용해 지정할 수 있다. Positioned에서 top, left, right를 0으로 설정해서 좌우 전체에 걸쳐서 상단에 위치하도록 하였다.
그 다음에 Container를 만들어서 위아래 크기를 300으로 지정하고 배경이미지를 Asset에서 가져왔다.
Boxfit.fill은 이미지가 컨테이너 크기에 맞춰져서 여백이 남지 않게 한다.
배경 이미지를 담은 컨테이너 안에 또다른 내부 컨테이너를 만들어서 텍스트를 담았다.
텍스트 컨테이너의 padding은 top 90, left 20으로 설정했고, Column을 만들었다.
Column은 자식 위젯을 수직으로 나열한다. 여러 줄의 텍스트를 담기 위해 사용하였다.
crossAxisAlignment를 start로 설정해서 자식 위젯을 수평 방향에서 왼쪽으로 정렬한다. 텍스트를 왼쪽 정렬시킨 것이다.
그 다음에 텍스트의 위젯으로는 RichText를 사용하였는데 다양한 스타일의 텍스트를 한 줄에 쓸 수 있는 위젯이다.
TextSpan은 RichText 안에서 쓰이며 텍스트와 스타일을 정의한다.
TextSpan을 이용해 to yummy chat은 bold 처리를 했다.
RichText 바깥에 그리고 Column 안에 새로운 텍스트 'Sign up to continue'를 만들었다.
그리고 두 텍스트 사이에 간격을 주기 위해서 SizedBox를 만들고 height를 5.0으로 설정하였다.
여기까지 진행한 결과이다.
처음에는 헷갈리겠지만 어떤 요소가 어떤 요소 안에 들어가 있는지 포함 관계를 이해하는 것이 중요하고, 각 요소들의 역할을 이해하면 될 것 같다. 그리고 하나씩 해보면서 해당 요소를 추가했을 때 화면 상에서 어떻게 달라지는지 보면서 하는 것이 도움이 된다.
배경 ui를 만들었으니 이제 로그인, 가입 창을 만들 차례이다.
새로운 Positioned로 만들건데 그 전에 코드가 보기 너무 복잡하니까 에디터에서 몇번째 줄인지 표시된 숫자 살짝 오른쪽을 누르면
이렇게 깔끔하게 접혀서 정리된다.
Positioned(
top:180,
child: Container(
padding: EdgeInsets.all(20.0),
height: 280.0,
width: MediaQuery.of(context).size.width-40,
margin: EdgeInsets.symmetric(horizontal:20.0),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(15.0),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.3),
blurRadius: 15,
spreadRadius: 5
)
]
),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Column(
children: [
Text(
'LOGIN',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Palette.textColor1
),
),
Container(
margin: EdgeInsets.only(top: 3),
height: 2,
width: 55,
color: Colors.orange,
)
],
),
Column(
children: [
Text(
'SIGNUP',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Palette.textColor1
),
),
Container(
margin: EdgeInsets.only(top: 3),
height: 2,
width: 55,
color: Colors.orange,
)
],
)
],
)
],
),
)
)
다음 Positioned 코드이다.
우선 해당 위젯을 앞서 만든 배경 Positioned와 겹치지 않게 하기 위해 top을 180으로 설정해 내려준다.
그리고 Container를 만드는데 padding을 EdgeInsets.all(20)으로 한 이유는 컨테이너 안에 들어갈 글자들이 너무 가장자리에 붙지 않게 하기 위해서이다. EdgeInsets.all은 모든 가장자리 즉, 테두리 부분에서 20만큼 거리가 띄어지게 한 것이다.
height은 280으로 설정하였고, width가 좀 이상한데 웹이든 스마트폰이든, 아이패드든 디바이스에 따라 화면의 비율은 달라질 텐데 그때 UI가 깨지면 안되기 때문에 MediaQuery.of(context).size.width를 이용해서 이용중인 디바이스의 가로 사이즈를 가져온다.
margin 설정은 컨테이너 좌우에 20만큼의 여백을 준다. symmetric은 대칭이니까 좌우 똑같이 여백을 줬다고 이해하면 된다.
BoxDecoration 부분에서는 박스의 디자인을 설정하는데 BorderRadius는 컨테이너의 모서리를 둥글게 만든다.
그리고 BoxShadow를 이용해 그림자 효과를 사용한다.
그림자의 색상은 검정이고, Opacity를 이용해 불투명도를 30%로 설정하였다.
blurRadius는 흐림 정도, spreadRadius는 퍼지는 정도이다.
이제 글자를 채우기 위해서 Column을 만들어 준다.
login과 signup 글자는 가로로 나란히 배치되기 때문에 Row를 만들어주는데, 그 안에 또 Column이 있다.
이 Column은 글자 밑에 언더바 느낌으로 선을 그리기 위해 만들었다.
Column 안에 텍스트를 채우고, Container로 오렌지색 줄을 그렸다.
Signup부분도 Login 부분과 글자만 바꾸고 똑같이 만든다.
그리고 Row에서 login과 signup이 중앙에서부터 대칭되게 배치하기 위해 mainAxisAlignment를 설정해줬다.
여기까지 진행한 결과이다.
이제 login글자를 누르면 로그인 양식이, signup 글자를 누르면 회원가입 양식이 뜨게 하는 기능을 만들어야 한다.
우선 signup의 Column 부분에서 왼쪽에 전구를 눌러서 Wrap with widget을 누른다.
이건 vscode의 경우고 안드로이드 스튜디오도 비슷한게 있을 것이다. (안보이면 우클릭도 해봐라)
widget은 GestureDetector로 바꿔주고,
이렇게 onTap 안에 아까 만든 isSignupScreen 변수를 true로 설정하게 한다.
login 부분도 똑같이 GestureDetector로 감싸는데 onTap 안에 isSignupScreen 변수는 false로 설정하게 한다.
login 글자를 누르면 색이 진해지고 밑에 오렌지색 줄이 나타나게 하기 위해 2가지를 바꿔야 한다.
color: !isSignupScreen
? Palette.activeColor
: Palette.textColor1
먼저 색깔이다.
삼항연산자라고 불리는 방식인데 그냥 if else를 줄여서 한줄로 나타낸거라고 생각하면 된다.
!isSignupScreen 즉 로그인을 눌렀을 때는 activeColor로 설정해주고 그게 아닐 때는 textColor1으로 설정해준다.
조건 ? 해당 : 해당X 이런 구조라고 보면 된다.
오렌지 선은 이렇게 if문으로 처리하면 된다.
신기한게 dart는 if문을 저렇게만 해두고 중괄호 같은걸로 안 감싸도 잘 돌아간다.
signup 부분도 마찬가지고 조건만 바꿔서 설정하면 된다.
이렇게 잘 된다.
이제 textfield 들을 배치해서 로그인과 회원가입 양식을 배치할 것이다.
Container(
margin: EdgeInsets.only(top: 20),
child: Form(
child: Column(
children: [
TextFormField(
decoration: InputDecoration(
prefixIcon: Icon(
Icons.account_circle,
color: Palette.iconColor,
),
enabledBorder: OutlineInputBorder(
borderSide:
BorderSide(color: Palette.textColor1),
borderRadius: BorderRadius.all(
Radius.circular(35.0))),
focusedBorder: OutlineInputBorder(
borderSide:
BorderSide(color: Palette.textColor1),
borderRadius: BorderRadius.all(
Radius.circular(35.0))),
hintText: 'User name',
hintStyle: TextStyle(
fontSize: 14, color: Palette.textColor1),
contentPadding: EdgeInsets.all(10)),
),
SizedBox(
height: 8,
),
TextFormField(
decoration: InputDecoration(
prefixIcon: Icon(
Icons.account_circle,
color: Palette.iconColor,
),
enabledBorder: OutlineInputBorder(
borderSide:
BorderSide(color: Palette.textColor1),
borderRadius: BorderRadius.all(
Radius.circular(35.0))),
focusedBorder: OutlineInputBorder(
borderSide:
BorderSide(color: Palette.textColor1),
borderRadius: BorderRadius.all(
Radius.circular(35.0))),
hintText: 'User name',
hintStyle: TextStyle(
fontSize: 14, color: Palette.textColor1),
contentPadding: EdgeInsets.all(10)),
),
SizedBox(
height: 8,
),
TextFormField(
decoration: InputDecoration(
prefixIcon: Icon(
Icons.account_circle,
color: Palette.iconColor,
),
enabledBorder: OutlineInputBorder(
borderSide:
BorderSide(color: Palette.textColor1),
borderRadius: BorderRadius.all(
Radius.circular(35.0))),
focusedBorder: OutlineInputBorder(
borderSide:
BorderSide(color: Palette.textColor1),
borderRadius: BorderRadius.all(
Radius.circular(35.0))),
hintText: 'User name',
hintStyle: TextStyle(
fontSize: 14, color: Palette.textColor1),
contentPadding: EdgeInsets.all(10)),
)
],
),
),
)
코드가 좀 많이 긴데 하나를 세번 복붙한거라 금방 할 수 있다.
Row 위젯이 login과 signup 부분을 가로로 배치하기 위해 사용됐었으니 Row 바깥에 새로운 Container 위젯을 만든다.
margin을 설정한 이유는 위에 row 컨테이너의 오렌지 선과 딱 붙는 것을 방지하기 위해서이다.
그 다음에 textfield를 넣기 위해 Form 위젯을 만들고 Column을 만든다.
TextFormField에 decoration으로 꾸민다.
먼저 텍스트필드 앞에 올 아이콘을 설정해주고, enabledBorder와 focusedBorder를 설정해주는데
enabled는 그냥 상태이고, focused는 입력하기 위해서 텍스트필드를 눌렀을 때 키보드가 활성화된 상태이다.
눌렀을 때나 안 눌렀을 때나 똑같이 할거라서 내용은 똑같고 둥근 테두리를 borderRadius로 설정했다.
hint text는 입력하기 전 써있는 문자열이고 hint text의 스타일도 지정해주었다.
마지막으로 contentPadding을 이용해서 글작 텍스트 필드의 테두리와 너무 달라붙지 않게 했다.
이제 만든 TextFormField 부분을 복사해서 2번 붙여넣어 주면 된다.
근데 이렇게 하면 TextFormField끼리 너무 딱 달라 붙어 있어서 중간중간에 SizedBox를 넣어줬다.
여기까지 한 결과이다.
코드랑 결과물과 설명을 잘 비교해보면서 어떤 코드가 어떤 기능을 하는지 이해하고 넘어가길 바란다.
이번에는 다음으로 넘어가는 오렌지색 버튼을 만들 것이다.
Positioned(
top: 430,
right: 0,
left: 0,
child: Center(
child: Container(
padding: EdgeInsets.all(15),
height: 90,
width: 90,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(50)),
child: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Colors.orange, Colors.red],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(30),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.3),
spreadRadius: 1,
blurRadius: 1,
offset: Offset(0, 1))
]),
child: Icon(
Icons.arrow_forward,
color: Colors.white,
),
),
),
),
)
새로운 Positioned 위젯을 만들고 안에 Container까지 하나 만든 뒤, 위치 설정 등을 조작한다.
ui를 꾸미는 코드는 아까랑 크게 다른 것이 없으니 설명은 굳이 안하고 넘어가겠다.
실행하면 이렇게 나온다.
이번에는 오렌지색 버튼 아래에 sign up google 버튼을 만들 것이다.
Positioned(
top: MediaQuery.of(context).size.height - 125,
right: 0,
left: 0,
child: Column(
children: [
Text('or Signup with'),
SizedBox(
height: 10,
),
TextButton.icon(
onPressed: () {},
style: TextButton.styleFrom(
foregroundColor: Colors.white,
minimumSize: Size(155, 40),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20)),
backgroundColor: Palette.googleColor),
icon: Icon(Icons.add),
label: Text('Google'),
)
],
))
이번에도 마찬가지로 코드만 올리고 넘어가겠다.
실행 결과이다.
이번에는 login과 signup을 눌렀을 때 각각 텍스트 필드가 달라지고, 배경 위에 써있던 문구도 달라지게 해볼 것이다.
먼저 문구부터 고치기 위해 배경 Positioned 위젯을 열어주고 yummy chat 부분을 삼항연산자로 고친다.
마찬가지고 signup to continue 부분도 삼항연산자로 고친다.
이번엔 텍스트 필드 Positioned 위젯을 열어서 텍스트 필드 3개가 담긴 Container 위에다 if문을 넣어준다.
이렇게 하면 저 텍스트 필드 3개는 signup일때만 보이게 된다.
이제 login일 때 보여질 텍스트 필드를 만들어야 한다.
원래 텍스트 필드 3개가 담긴 Container는 접고, 똑같은 Container를 복붙으로 만들어 준 뒤, 텍스트 필드는 2개로 줄인다.
그리고 if문을 위에 추가해준다.
로그인 창에서는 텍스트 필드가 2개만 필요하기 때문에 컨테이너 박스의 크기가 줄어들어야 한다.
위에 메인 Container에서 height 부분을 삼항연산자로 고쳐준다.
컨테이터의 크기가 바뀌면 밑에 오렌지색 제출 버튼의 위치도 좀 올라와야 한다.
이렇게 삼항연산자로 top을 바꿔서 위치를 조정해주면 된다.
그리고 오렌지색 제출 버튼 다음으로 만들었던 signup with google 부분도 문구가 sign in with로 바뀌어야 한다.
이렇게 바꿔주면 된다.
그리고 각각의 텍스트필드의 아이콘과 hint text들을 바꿔주면 된다.
아이콘은 Icons 뒤에 .을 찍으면 이것저것 많이 나오니까 원하는걸 고르면 된다. (내가 사용한건 email과 lock이다)
실행 결과이다.
지금은 login과 signup을 왔다갔다 할 때 그냥 누르면 바로 바뀌는데 애니메이션 효과를 추가해보겠다.
텍스트 필드가 들어있는 Positioned를 Animated로 감싸고 duration을 설정해주고, curve로 애니메이션 효과를 골라준다.
Container도 마찬가지로 Animated로 바꿔주고 똑같이 효과를 설정한다.
텍스트 필드 말고 오렌지색 제출 버튼도 똑같이 Animated로 만들고 효과를 설정해준다.
지금 상태에서 텍스트 필드에 입력값을 넣어보고 login과 signup을 왔다 갔다 하면 값들이 남아있고 꼬이게 되는데 이 문제를 해결할 것이다.
해결 자체는 간단하게 할 수 있다.
텍스트 필드 위젯 안에 이렇게 key를 설정해주면 된다.
5개의 텍스트 필드가 있으니 1부터 5까지 설정해주면, 더 이상 값이 꼬이지 않는다.
key는 위젯의 state를 보존하는 것이다.
위젯이나 요소들을 식별하게 해주는 역할을 한다.
이번에는 입력값을 검증하는 validation을 해보겠다.
텍스트 필드 코드로 이동해서 validator를 추가한다.
validator: (value) {
if (value!.isEmpty || value.length < 4) {
return 'Please enter at least 4 characters';
}
return null;
},
validator: (value) {
if (value!.isEmpty || value.contains('@')) {
return 'Please enter a valid email address';
}
return null;
},
validator: (value) {
if (value!.isEmpty || value.length < 6) {
return 'Password must be at least 7 characters long';
}
return null;
},
각각의 코드들은 username, email, password를 입력할 때의 validator 코드이다.
소괄호 안에 있는 value는 사용자의 입력값이고, 중괄호 안은 검증 과정의 함수 코드이다.
value값이 비었거나 4보다 작으면 최소한 4글자를 입력하라고 띄우고, 문제가 없으면 null을 return 한다.
if문을 돌릴 때 value가 null이면 에러가 생기기 때문에 value 앞에 !을 붙였다.
email에서는 @ 문자를 검사했고, password는 6글자 이상 입력하도록 하였다.
login 부분도 email과 password는 똑같이 하면 된다.
validator는 구현을 했지만 사용자가 입력값을 다 입력하고 제출 버튼을 눌렀을 때 기능이 작동하도록 해야 한다.
이를 위해 우선, 맨 위 class에 formkey를 생성할 것이다.
Form 위젯을 통해서 작동이 되어야 하기 때문에 FormState의 키라고 지정을 해줬다.
final은 const랑 비슷한데 한번 값이 할당되면 변하지 않고 계속 같은 값을 가질 때 사용한다.
그리고 Form 위젯에 가서 key를 전달해주면 된다.
login 부분에도 똑같이 전달해준다.
키를 만들어 놨으니 해당 키가 포함된 Form 위젯에서는 validator가 작동하게 만들어야 한다.
아까랑 똑같이 맨 위 클래스로 올라가서 메소드를 만든다.
void _tryValidation() {
final isValid = _formKey.currentState!.validate();
if (isValid) {
_formKey.currentState!.save();
}
}
_formKey의 currentState에서 validate 메소드를 실행시키면 모든 텍스트 필드들이 validator를 사용하게 되고 그 결과가 isValid로 들어온다.
검사가 제대로 통과되면 save를 하는데, 이 때 텍스트 필드 위젯이 기본적으로 가지고 있는 onSaved라는 메소드가 실행이 된다.
그래서 텍스트 필드 코드로 가서 onSaved를 구현해야 한다.
그 전에 입력된 username, email, password를 담기 위한 변수 3개를 선언한다.
이제 텍스트 필드 코드로 가서 onSaved 메소드를 만들고
이렇게 변수를 입력값 value로 업데이트 해준다.
5개의 텍스트 필드에 모두 onSaved를 만들어준다.
이제 마지막으로, 전송 버튼에서 눌렀을 때 _tryValidation이 실행되며 onSaved까지 실행되게 해야 한다.
일단 누를 수 있게 만들기 위해 이전에 login과 signup 글자를 누를 때 했던 것처럼 Container를 GestureDetector 위젯으로 감싸준다.
이렇게 onTap까지 설정했으면 끝이다.
테스트해보기 위해서 텍스트 필드에 아무것도 입력하지 않고 제출 버튼을 누르면 경고 문구는 잘 뜨는데 에러가 생긴다.
경고 문구가 뜨면서 기존 컨테이너와 겹치게 되어서 발생하는 오버플로우이다.
해결하기 위해서 텍스트 필드가 들어있는 위젯 안에 있는 Column 위젯을 SingleChildScrollView라는 것으로 감싸면 된다.
이 위젯을 이용하면 경고 문구가 뜰 때 위젯의 모든 요소들이 스크롤 될 수 있게 되면서 아래로 밀려난다.
설명하기 좀 애매하니까 사진으로 보여주자면
이렇게 된다.
이번에는 텍스트를 입력할 때 올라와있는 키보드가 화면 빈 곳을 터치하면 자동으로 내려가게 하는 기능을 추가할 것이다.
화면 어디를 터치해도 입력할 때 사용하는 키보드가 사라지게 하기 위해 body의 Stack 위젯 코드로 이동한다.
터치를 인식해야 하기 때문에 GesturDector로 Stack 위젯을 감싸고, onTap에서 focusScope를 unscope로 바꿔준다.
이제 마지막으로, login과 signup을 왔다갔다 할때 텍스트 필드 부분은 애니메이션으로 처리했지만 아래에 or sign with google
부분은 그대로라 애니메이션 처리를 해주겠다.
AnimatedPositioned로 바꿔주고 duration과 curve를 지정해준다.
그리고 삼항연산자로 top위치를 바꿔준다.
드디어 ui 구성과 입력값 처리까지 끝이 났다.
다음 글에서는 실제로 로그인 기능을 구현하기 위해 파이어베이스 서버와 연동을 해볼 것이다.
'앱개발' 카테고리의 다른 글
앱 개발 - flutter (4) 파이어베이스 서버 이용한 채팅 앱 2 (0) | 2024.09.01 |
---|---|
앱 개발 - flutter (2) 기본 코드 해석 (0) | 2024.08.29 |
앱 개발 - flutter (1) 설치 및 개발 환경 구축 (5) | 2024.08.28 |
앱 개발 - flet (창의플랫폼) (2) (0) | 2024.08.21 |
앱 개발 - flet (창의플랫폼) (1) (0) | 2024.08.15 |