조금 매운맛 20 채팅앱 UI 디자인 pt1,2
따로 이렇게 만들어주기
삼항조건연산자
!isSingupScreen ? Palette.activeColor : Palette.textColor1,
> 로그인 화면이 선택 됐을 때 activeColor 보여주고 아니면 textColor1 보여주기
inline if
- dart 2.3 이상 도입
- 한 컬렉션(column 위젯 내 list 요소) 안에서 어떤 예외적인 조건을 쉽고 명확하게 지정할 수 있는 기능
signup 스크린이 선택되지 않은 상태에서만 밑줄을 위한 컨테이너를 그림
UI 완성본 및 전체코드
//palette.dart
import 'package:flutter/painting.dart';
class Palette{
static const Color iconColor = Color(0xFFB6C7D1);
static const Color activeColor = Color(0xFF09126C);
static const Color textColor1 = Color(0XFFA7BCC7);
static const Color textColor2 = Color(0XFF9BB3C0);
static const Color facebookColor = Color(0xFF3B5999);
static const Color googleColor = Color(0xFFDE4B39);
static const Color backgroundColor = Color(0xFFECF3F9);
}
//main.dart
import 'package:flutter/material.dart';
import 'package:yami_chat/screens/main_screen.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
backgroundColor: Colors.white,
),
home: LoginSignupScreen(),
);
}
}
class MyHomePage extends StatelessWidget {
const MyHomePage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return const Placeholder();
}
}
//main_screen.dart
import 'package:flutter/material.dart';
import 'package:yami_chat/config/palette.dart';
class LoginSignupScreen extends StatefulWidget {
const LoginSignupScreen({Key? key}) : super(key: key);
@override
State<LoginSignupScreen> createState() => _LoginSignupScreenState();
}
class _LoginSignupScreenState extends State<LoginSignupScreen> {
bool isSignupScreen = true;// 로그인가입화면 state 관리를 위해서
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Palette.backgroundColor,
body: Stack(
children: [
//배경
Positioned(
top: 0,
left: 0, // 스크린 상단 시작점 맞추기위해서 좌우 0
right: 0,
child: Container(
height: 300,
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage('image/red.jpg'),
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: isSignupScreen ? ' To Yummy chat!' : ' back',
style: TextStyle(
letterSpacing: 1.0,
fontSize: 25,
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
],
),
),
SizedBox(
height: 5.0,
),
Text(
isSignupScreen ? 'Signup to continue' : 'Signin to continue',
style: TextStyle(
letterSpacing: 1.0,
color: Colors.white,
),
),
],
),
),
),),
//텍스트필드
AnimatedPositioned(//
duration: Duration(milliseconds: 500),
curve: Curves.easeIn,
top: 180.0,
child:
Container(
padding: EdgeInsets.all(20.0),
height: isSignupScreen ? 280.0 : 200.0,
width: MediaQuery.of(context).size.width-40, // MediaQuery 이용해서 디바이스의 크기에 맞춰 -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: [
GestureDetector(
onTap: () {
setState(() {
isSignupScreen = false;
});
},
child: Column(
children: [
Text(
'LOGIN',
style: TextStyle(
fontSize: 16,
color: !isSignupScreen
? Palette.activeColor
: Palette.textColor1,
fontWeight: FontWeight.bold),
),
if (!isSignupScreen)
Container(
height: 2,
width: 55,
color: Colors.orange,
)
],
),
),
GestureDetector(
onTap: () {
setState(() {
isSignupScreen = true;
});
},
child: Column(
children: [
Text(
'SIGNUP',
style: TextStyle(
fontSize: 16,
color: isSignupScreen
? Palette.activeColor
: Palette.textColor1,
fontWeight: FontWeight.bold),
),
if (isSignupScreen)
Container(
margin: EdgeInsets.only(top: 3),
height: 2,
width: 55,
color: Colors.orange,
)
],
),
)
],
),
if(isSignupScreen)
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( // textfield 선택 했을 때 보더라인 보여주는 기능
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.email_sharp,
color: Palette.iconColor,
),
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Palette.textColor1
),
borderRadius: BorderRadius.all(Radius.circular(35.0),
),
),
focusedBorder: OutlineInputBorder( // textfield 선택 했을 때 보더라인 보여주는 기능
borderSide: BorderSide(
color: Palette.textColor1
),
borderRadius: BorderRadius.all(Radius.circular(35.0),
),
),
hintText: 'email',
hintStyle: TextStyle(
fontSize: 14,
color: Palette.textColor1
),
contentPadding: EdgeInsets.all(10)
),
),
SizedBox(
height: 8,
),
TextFormField(
decoration: InputDecoration(
prefixIcon: Icon(Icons.lock,
color: Palette.iconColor,
),
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Palette.textColor1
),
borderRadius: BorderRadius.all(Radius.circular(35.0),
),
),
focusedBorder: OutlineInputBorder( // textfield 선택 했을 때 보더라인 보여주는 기능
borderSide: BorderSide(
color: Palette.textColor1
),
borderRadius: BorderRadius.all(Radius.circular(35.0),
),
),
hintText: 'password',
hintStyle: TextStyle(
fontSize: 14,
color: Palette.textColor1
),
contentPadding: EdgeInsets.all(10)
),
)
],
),
),
),
if(!isSignupScreen)
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( // textfield 선택 했을 때 보더라인 보여주는 기능
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.0,
),
TextFormField(
decoration: InputDecoration(
prefixIcon: Icon(Icons.lock,
color: Palette.iconColor,
),
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Palette.textColor1
),
borderRadius: BorderRadius.all(Radius.circular(35.0),
),
),
focusedBorder: OutlineInputBorder( // textfield 선택 했을 때 보더라인 보여주는 기능
borderSide: BorderSide(
color: Palette.textColor1
),
borderRadius: BorderRadius.all(Radius.circular(35.0),
),
),
hintText: 'Password',
hintStyle: TextStyle(
fontSize: 14,
color: Palette.textColor1
),
contentPadding: EdgeInsets.all(10)
),
),
],
),
),
)
],
),
),
),
//전송버튼
AnimatedPositioned(
duration: Duration(milliseconds: 150),
curve: Curves.easeIn,
top: isSignupScreen ? 430 : 350,
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,
),
),
),
)),
//signUp과 구글버튼
Positioned(
top: MediaQuery.of(context).size.height-125,
right: 0,
left: 0,
child: Column(
children: [
Text(isSignupScreen ? 'or Signup with' : 'or Sign with'),
SizedBox(
height: 10,
),
TextButton.icon(
style: TextButton.styleFrom(
primary: Colors.white,
minimumSize: Size(155, 40),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20)
),
backgroundColor: Palette.googleColor
),
icon: Icon(Icons.add),
onPressed: (){},
label: Text('Google'),
)
],
)
,),
],
),
);
}
}
조금 매운맛 21 텍스트폼필드 validation 구현
//텍스트폼필드
AnimatedPositioned(
.
.
.
TextFormField(
key: ValueKey(1),
> 모든 TextFormField에 ValuKey 1, 2, 3, 4, 5 할당
> state가 뒤엉키는 일은 해결~
Validation : 사용자가 입력한 내용들에 유효성을 검사
- 각 textformfield 마다 null 값이 아니고 필요한 글자수 등등을 지정해줌
- 사용자가 모든 textformfield를 입력한 후 submit 버튼을 눌러때 Validation이 진행되어야함. > 이를 위해서 Global Key를 만듦
- Global Key에 bind할 state는 form 위젯을 통해서 모든 textformfield의 Validation 기능을 작동 시킬 것이기에 Form위젯의 <FormState>
class _LoginSignupScreenState extends State<LoginSignupScreen> {
bool isSignupScreen = true;// 로그인가입화면 state 관리를 위해서
final _formKey = GlobalKey<FormState>();
.
.
.
TextFormField(
key: ValueKey(1),
validator: (value){
if(value!.isEmpty || value.length < 4) {
return 'Please enter at least 4 characters';
} return null;
},
.
.
.
TextFormField(
key: ValueKey(2),
validator: (value){
if(value!.isEmpty || value.contains('@')) {
return 'Please enter a valid email address';
} return null;
},
.
.
.
TextFormField(
key: ValueKey(3),
validator: (value){
if(value!.isEmpty || value.length < 6) {
return 'Password must be at least 7 characters long';
} return null;
},
.
.
.
TextFormField(
key: ValueKey(4),
validator: (value){
if(value!.isEmpty || value.length < 4) {
return 'Please enter at least 4 characters';
} return null;
},
.
.
.
TextFormField(
key: ValueKey(5),
validator: (value){
if(value!.isEmpty || value.length < 6) {
return 'Password must be at least 7 characters long';
} return null;
},
Container(
margin: EdgeInsets.only(top: 20),
child: Form(
key: _formKey,
> form 위젯에서 key argument를 불러온 후, formKey 전달
사용자가 submit 버튼 눌렀을때 실행되도록 메소드 생성. currentState 속성을 이용
void _tryValidation(){
final isValid = _formKey.currentState!.validate();
}
> Validate메소드를 통해서 모든 textformfield의 Validator를 작동시킬 수 있게 됨
currentState에 근거해서만 Validator 메소드가 호출될수 있으므로 currentState에 대한 null체크 필수
이를 바탕으로 form에 대한 유효성결과 리턴하면 final 변수에 담기
void _tryValidation(){
final isValid = _formKey.currentState!.validate();
if(isValid){
_formKey.currentState!.save();
}
}
사용자가 submit 버튼을 눌었을때 form이 유효하면 Validator들이 null 리턴한 후,
사용자의 value갑을 가져와 실제적인 Validation 기능 실행
=> Form값이 저장되는 과정 필요
Formstate 값이 유효한 경우에 한해서 formKey의 currentState에서 value값 가져온 후 save 메소드 이용해서 저장
onSaved: (value){
userName = value!;
},
textformfield의 onSaved 메서드 : 사용자가 입력한 value값을 저장하는 기능
채팅앱 UI 로그인 진짜 마지막 최종코드
import 'package:flutter/material.dart';
import 'package:yami_chat/config/palette.dart';
class LoginSignupScreen extends StatefulWidget {
const LoginSignupScreen({Key? key}) : super(key: key);
@override
State<LoginSignupScreen> createState() => _LoginSignupScreenState();
}
class _LoginSignupScreenState extends State<LoginSignupScreen> {
bool isSignupScreen = true;// 로그인가입화면 state 관리를 위해서
final _formKey = GlobalKey<FormState>();
String userName = '';
String userEmail = '';
String userPassword = '';
void _tryValidation(){
final isValid = _formKey.currentState!.validate();
if(isValid){
_formKey.currentState!.save();
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Palette.backgroundColor,
body: GestureDetector(
onTap: (){
FocusScope.of(context).unfocus();
},
child: Stack(
children: [
//배경
Positioned(
top: 0,
left: 0, // 스크린 상단 시작점 맞추기위해서 좌우 0
right: 0,
child: Container(
height: 300,
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage('image/red.jpg'),
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: isSignupScreen ? ' To Yummy chat!' : ' back',
style: TextStyle(
letterSpacing: 1.0,
fontSize: 25,
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
],
),
),
SizedBox(
height: 5.0,
),
Text(
isSignupScreen ? 'Signup to continue' : 'Signin to continue',
style: TextStyle(
letterSpacing: 1.0,
color: Colors.white,
),
),
],
),
),
),),
//텍스트폼필드
AnimatedPositioned(//
duration: Duration(milliseconds: 500),
curve: Curves.easeIn,
top: 180.0,
child:
Container(
padding: EdgeInsets.all(20.0),
height: isSignupScreen ? 280.0 : 200.0,
width: MediaQuery.of(context).size.width-40, // MediaQuery 이용해서 디바이스의 크기에 맞춰 -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: SingleChildScrollView(
padding: EdgeInsets.only(bottom: 20),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
GestureDetector(
onTap: () {
setState(() {
isSignupScreen = false;
});
},
child: Column(
children: [
Text(
'LOGIN',
style: TextStyle(
fontSize: 16,
color: !isSignupScreen
? Palette.activeColor
: Palette.textColor1,
fontWeight: FontWeight.bold),
),
if (!isSignupScreen)
Container(
height: 2,
width: 55,
color: Colors.orange,
)
],
),
),
GestureDetector(
onTap: () {
setState(() {
isSignupScreen = true;
});
},
child: Column(
children: [
Text(
'SIGNUP',
style: TextStyle(
fontSize: 16,
color: isSignupScreen
? Palette.activeColor
: Palette.textColor1,
fontWeight: FontWeight.bold),
),
if (isSignupScreen)
Container(
margin: EdgeInsets.only(top: 3),
height: 2,
width: 55,
color: Colors.orange,
)
],
),
)
],
),
if(isSignupScreen)
Container(
margin: EdgeInsets.only(top: 20),
child: Form(
key: _formKey,
child: Column(
children: [
TextFormField(
key: ValueKey(1),
validator: (value){
if(value!.isEmpty || value.length < 4) {
return 'Please enter at least 4 characters';
} return null;
},
onSaved: (value){
userName = value!;
},
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( // textfield 선택 했을 때 보더라인 보여주는 기능
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(
key: ValueKey(2),
validator: (value){
if(value!.isEmpty || value.contains('@')) {
return 'Please enter a valid email address';
} return null;
},
onSaved: (value){
userEmail = value!;
},
decoration: InputDecoration(
prefixIcon: Icon(Icons.email_sharp,
color: Palette.iconColor,
),
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Palette.textColor1
),
borderRadius: BorderRadius.all(Radius.circular(35.0),
),
),
focusedBorder: OutlineInputBorder( // textfield 선택 했을 때 보더라인 보여주는 기능
borderSide: BorderSide(
color: Palette.textColor1
),
borderRadius: BorderRadius.all(Radius.circular(35.0),
),
),
hintText: 'email',
hintStyle: TextStyle(
fontSize: 14,
color: Palette.textColor1
),
contentPadding: EdgeInsets.all(10)
),
),
SizedBox(
height: 8,
),
TextFormField(
key: ValueKey(3),
validator: (value){
if(value!.isEmpty || value.length < 6) {
return 'Password must be at least 7 characters long';
} return null;
},
onSaved: (value){
userPassword = value!;
},
decoration: InputDecoration(
prefixIcon: Icon(Icons.lock,
color: Palette.iconColor,
),
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Palette.textColor1
),
borderRadius: BorderRadius.all(Radius.circular(35.0),
),
),
focusedBorder: OutlineInputBorder( // textfield 선택 했을 때 보더라인 보여주는 기능
borderSide: BorderSide(
color: Palette.textColor1
),
borderRadius: BorderRadius.all(Radius.circular(35.0),
),
),
hintText: 'password',
hintStyle: TextStyle(
fontSize: 14,
color: Palette.textColor1
),
contentPadding: EdgeInsets.all(10)
),
)
],
),
),
),
if(!isSignupScreen)
Container(
margin: EdgeInsets.only(top: 20),
child: Form(
key: _formKey,
child: Column(
children: [
TextFormField(
key: ValueKey(4),
validator: (value){
if(value!.isEmpty || value.length < 4) {
return 'Please enter at least 4 characters';
} return null;
},
onSaved: (value){
userName = value!;
},
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( // textfield 선택 했을 때 보더라인 보여주는 기능
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.0,
),
TextFormField(
key: ValueKey(5),
validator: (value){
if(value!.isEmpty || value.length < 6) {
return 'Password must be at least 7 characters long';
} return null;
},
onSaved: (value){
userPassword = value!;
},
decoration: InputDecoration(
prefixIcon: Icon(Icons.lock,
color: Palette.iconColor,
),
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Palette.textColor1
),
borderRadius: BorderRadius.all(Radius.circular(35.0),
),
),
focusedBorder: OutlineInputBorder( // textfield 선택 했을 때 보더라인 보여주는 기능
borderSide: BorderSide(
color: Palette.textColor1
),
borderRadius: BorderRadius.all(Radius.circular(35.0),
),
),
hintText: 'Password',
hintStyle: TextStyle(
fontSize: 14,
color: Palette.textColor1
),
contentPadding: EdgeInsets.all(10)
),
),
],
),
),
)
],
),
),
),
),
//전송버튼
AnimatedPositioned(
duration: Duration(milliseconds: 150),
curve: Curves.easeIn,
top: isSignupScreen ? 430 : 350,
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: GestureDetector(
onTap: (){
_tryValidation();
},
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,
),
),
),
),
)),
//signUp과 구글버튼
AnimatedPositioned(
duration: Duration(milliseconds: 200),
top: isSignupScreen ? MediaQuery.of(context).size.height-125 : MediaQuery.of(context).size.height-185,
right: 0,
left: 0,
child: Column(
children: [
Text(isSignupScreen ? 'or Signup with' : 'or Sign with'),
SizedBox(
height: 10,
),
TextButton.icon(
style: TextButton.styleFrom(
primary: Colors.white,
minimumSize: Size(155, 40),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20)
),
backgroundColor: Palette.googleColor
),
icon: Icon(Icons.add),
onPressed: (){},
label: Text('Google'),
)
],
)
,),
],
),
),
);
}
}
'공부 > Flutter' 카테고리의 다른 글
Flutter 스터디 27 Stream, Firebase Rule 및 로그아웃 기능 수정 (0) | 2023.02.23 |
---|---|
Flutter 스터디 26 Firebase 연동 및 SignUp/SignIn 기능 구현 (0) | 2023.02.23 |
Flutter 스터디 24 Flutter Key 이해하기(Value Key, Global Key) (0) | 2023.02.20 |
Flutter 오류 Entrypoint isn't within the current project (0) | 2023.02.16 |
Flutter 스터디 23 날씨 앱 만들기 json 데이터 전달하기, UI 디자인, 데이터 연동 (0) | 2023.02.16 |