강 27. 채팅 메시지를 보냅니다.
1. new_message.dart 및 message.dart 파일을 생성하여 채팅 메시지 화면을 구성하고 message.dart에 stateless 메시지 위젯을 생성합니다.
2. Firestore 데이터베이스에 채팅 모음을 추가하고 테스트 데이터를 추가합니다.
3. chat_screen.dart 스캐폴드 본문을 삭제한 후 새로 생성된 메시지 위젯으로 이동합니다.
body: Container(
child: Column(
children: (
Expanded(child: Messages()) // 리스트뷰가 무조건 화면 내의 모든 공간을 확보해버리기 때문에 expanded 로 감싸주었따.
),
),
),
메시지 다트
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
class Messages extends StatelessWidget {
const Messages({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return StreamBuilder(
stream: FirebaseFirestore.instance.collection('chat').snapshots(),
builder: (context, AsyncSnapshot<QuerySnapshot<Map<String, dynamic>>> snapshot){
if(!
snapshot.hasData){
return Text('no data');
}
if(snapshot.connectionState==ConnectionState.waiting){
return Center(
child: CircularProgressIndicator(),
);
}
final chatDocs = snapshot.data!
.docs;
return ListView.builder(
itemCount: chatDocs.length,
itemBuilder: (context, index){
return Text(chatDocs(index)('text'));
});
}
);
}
}
이제 new_message.dart에서 새 메시지를 입력하는 부분을 구현합니다.
import 'package:flutter/material.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
class NewMessage extends StatefulWidget {
const NewMessage({Key? key}) : super(key: key);
@override
State<NewMessage> createState() => _NewMessageState();
}
class _NewMessageState extends State<NewMessage> {
var _userEnterMessage="";
void _sendMessage(){
FocusScope.of(context).unfocus();
FirebaseFirestore.instance.collection('chat').add({
'text' : _userEnterMessage,
});
}
@override
Widget build(BuildContext context) {
return Container(
margin: EdgeInsets.only(top:8),
padding: EdgeInsets.all(8.0),
child: Row(
children: (
Expanded(
child: TextField(
decoration: InputDecoration(
labelText: 'Send a message',
),
onChanged: (value){
setState(() {
_userEnterMessage = value;
//모든 키 입력에서 setState 불러옴.
});
},
),
),
IconButton(
onPressed: _userEnterMessage.trim().isEmpty?null:_sendMessage
, icon: Icon(Icons.send), color: Colors.blue,)
),
),
);
}
}
채팅 앱처럼 최근에 보낸 메시지를 아래에 보관하세요.
message.dart의 ListView.builder에서
reverse: true,
void _sendMessage(){
FocusScope.of(context).unfocus();
FirebaseFirestore.instance.collection('chat').add({
'text' : _userEnterMessage,
'time' : Timestamp.now()
});
시간 키를 추가합니다.
message.dart의 스냅샷 부분을 다음과 같이 수정합니다.
stream: FirebaseFirestore.instance.collection('chat').orderBy('time', descending: true).snapshots()
메시지 전송 후 TextField 삭제: 컨트롤러 추가
final _controller = TextEditingController();
마지막 변수를 추가한 후,
TextField(
controller: _controller,
sendMessage 메서드에서 TextFiled에 바인딩한 후
_controller.clear();
16강 채팅(메시지) 말풍선 만들기 및 메시지 넣기.
부울 isMe를 추가합니다.
userID를 추가하여 접근자의 uid와 메시지의 userID가 같으면 오른쪽에, 그렇지 않으면 왼쪽에 파란색으로 배치됩니다.
new_message.dart
void _sendMessage(){
FocusScope.of(context).unfocus();
final user = FirebaseAuth.instance.currentUser;
FirebaseFirestore.instance.collection('chat').add({
'text' : _userEnterMessage,
'time' : Timestamp.now(),
'userID' : user!
.uid,
});
_controller.clear();
}
chat_bubble.dart
import 'package:flutter/material.dart';
class ChatBubble extends StatelessWidget {
const ChatBubble(this.message, this.isMe, {Key? key}) : super(key: key);
final String message;
final bool isMe;
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: isMe?MainAxisAlignment.end:MainAxisAlignment.start,
children: (
Container(
decoration: BoxDecoration(
color: isMe? Colors.grey(300):Colors.blue,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(12),
topRight: Radius.circular(12),
bottomRight: isMe?Radius.circular(0):Radius.circular(12),
bottomLeft: isMe?Radius.circular(12,):Radius.circular(0)
),
),
width: 145,
padding: EdgeInsets.symmetric(vertical: 10, horizontal: 16),
margin: EdgeInsets.symmetric(vertical:4, horizontal: 8),
child: Text(message,
style: TextStyle(
color: Colors.white
),
),
),
),
);
}
}
메시지 다트
return ListView.builder(
reverse: true,
itemCount: chatDocs.length,
itemBuilder: (context, index){
return ChatBubble(chatDocs(index)('text'),
chatDocs(index)('userID').toString()==user!
.uid);
});
29 강. 채팅 메시지에 사용자 이름과 아바타를 표시합니다.
chat_bubble 패키지 설치
flatter_chat_bubble | Flutter 패키지(pub.dev)
chat_bubble.dart로 가져오기
import 'package:flutter_chat_bubble/bubble_type.dart';
import 'package:flutter_chat_bubble/chat_bubble.dart';
import 'package:flutter_chat_bubble/clippers/chat_bubble_clipper_8.dart';
메시지 위에 userName을 추가합니다.
메시지 다트
import 'package:chatting_app/chat_bubble.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
class Messages extends StatelessWidget {
const Messages({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final user = FirebaseAuth.instance.currentUser;
return StreamBuilder(
stream: FirebaseFirestore.instance.collection('chat').orderBy('time', descending: true).snapshots(),
builder: (context, AsyncSnapshot<QuerySnapshot<Map<String, dynamic>>> snapshot){
if(!
snapshot.hasData){
return Text('no data');
}
if(snapshot.connectionState==ConnectionState.waiting){
return Center(
child: CircularProgressIndicator(),
);
}
final chatDocs = snapshot.data!
.docs;
return ListView.builder(
reverse: true,
itemCount: chatDocs.length,
itemBuilder: (context, index){
return ChatBubbles(chatDocs(index)('text'),
chatDocs(index)('userID').toString()==user!
.uid,
chatDocs(index)('userName')
);
});
}
);
}
}
chat_bubble.dart
import 'package:flutter/material.dart';
import 'package:flutter_chat_bubble/bubble_type.dart';
import 'package:flutter_chat_bubble/chat_bubble.dart';
import 'package:flutter_chat_bubble/clippers/chat_bubble_clipper_8.dart';
class ChatBubbles extends StatelessWidget {
const ChatBubbles(this.message,this.isMe, this.userName, {Key? key}) : super(key: key);
final String message;
final bool isMe;
final String userName;
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: isMe?MainAxisAlignment.end:MainAxisAlignment.start,
children: (
if(isMe)
Padding(
padding: EdgeInsets.fromLTRB(0, 0, 5, 0),
child: ChatBubble(
clipper: ChatBubbleClipper8(type: BubbleType.sendBubble),
alignment: Alignment.topRight,
margin: EdgeInsets.only(top: 20),
backGroundColor: Colors.blue,
child: Container(
constraints: BoxConstraints(
maxWidth: MediaQuery.of(context).size.width * 0.7,
),
child: Column(
crossAxisAlignment: isMe?CrossAxisAlignment.end:CrossAxisAlignment.start,
children: (
Text(userName,
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold
),
),
Text(message,
style: TextStyle(color: Colors.white),
),
),
),
),
),
)
else if(!
isMe)
Padding(
padding: EdgeInsets.fromLTRB(5, 0, 0, 0),
child: ChatBubble(
clipper: ChatBubbleClipper1(type: BubbleType.receiverBubble),
backGroundColor: Color(0xffE7E7ED),
margin: EdgeInsets.only(top: 20),
child: Container(
constraints: BoxConstraints(
maxWidth: MediaQuery.of(context).size.width * 0.7,
),
child: Column(
crossAxisAlignment: isMe?CrossAxisAlignment.end:CrossAxisAlignment.start,
children: (
Text(userName, style: TextStyle(
fontWeight: FontWeight.bold,
color: Colors.black
),),
Text(
message,
style: TextStyle(color: Colors.black),
),
),
),
),
),
)
),
);
}
}
이제 회원가입 시 프로필 사진을 등록하는 기능을 구현하였습니다.
Main_screen.dart에 Icons.image를 추가합니다.
add_image.dart
import 'package:flutter/material.dart';
class AddImage extends StatefulWidget {
const AddImage({Key? key}) : super(key: key);
@override
State<AddImage> createState() => _AddImageState();
}
class _AddImageState extends State<AddImage> {
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.only(top:30),
width: 150,
height: 300,
child: Column(
children: (
CircleAvatar(
radius: 40,
backgroundColor: Colors.blue,
),
SizedBox(height: 10,),
OutlinedButton.icon(
onPressed: (){},
icon:Icon(Icons.image),
label: Text('Add icon'),),
SizedBox(height: 80,),
TextButton.icon(
onPressed: (){
Navigator.pop(context);
},
icon:Icon(Icons.close),
label:Text('close'))
),
),
);
}
}
메인 화면 다트
import 'package:chatting_app/add_image.dart';
import 'package:chatting_app/palette.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:modal_progress_hud_nsn/modal_progress_hud_nsn.dart';
import 'chat_screen.dart';
class LoginSignupScreen extends StatefulWidget {
const LoginSignupScreen({Key? key}) : super(key: key);
@override
State<LoginSignupScreen> createState() => _LoginSignupScreen();
}
class _LoginSignupScreen extends State<LoginSignupScreen> {
final _authentication = FirebaseAuth.instance;
bool isSignupScreen = true;
bool showSpinner = false;
final _formKey = GlobalKey<FormState>();
String userName="";
String userEmail="";
String userPassword = '';
void showAlert(BuildContext context){
showDialog(context: context, builder: (context){
return Dialog(
backgroundColor: Colors.white,
child: AddImage(),
);
});
}
void _tryValidation() {
final isValid = _formKey.currentState!
.validate(); // form의 유효성 체크
if (isValid) {
_formKey.currentState!
.save();
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Palette.backgroundColor,
body: ModalProgressHUD(
inAsyncCall: showSpinner,
child: GestureDetector(
onTap: () {
FocusScope.of(context).unfocus();
},
child: Stack(
children: (
Positioned(
top: 0,
right: 0,
left: 0,
child: Container(
height: 420,
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage('image/t2.png'),
opacity: 0.5,
fit: BoxFit.fill)),
child: Container(
padding: const EdgeInsets.only(
top: 50,
left: 20,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: (
RichText(
text: TextSpan(
text: 'Welcome',
style: TextStyle(
letterSpacing: 1.0,
fontSize: 25,
color: Colors.black),
children: (
TextSpan(
text: isSignupScreen
? ' to Timothee chat!
'
: ' back',
style: TextStyle(
letterSpacing: 1.0,
fontSize: 25,
color: Colors.black,
fontWeight: FontWeight.bold))
)),
),
SizedBox(
height: 15,
),
Text(
isSignupScreen
? 'Sign up to continue'
: 'Signin to continue',
style: TextStyle(
fontSize: 10,
letterSpacing: 1.0,
color: Colors.black,
),
),
),
),
),
)),
// 배경
AnimatedPositioned(
duration: Duration(milliseconds: 500),
curve: Curves.easeIn,
top: 220,
left: 20,
child: AnimatedContainer(
duration: Duration(milliseconds: 500),
curve: Curves.easeIn,
padding: EdgeInsets.all(20),
height: isSignupScreen ? 310 : 250,
width: MediaQuery.of(context).size.width - 40,
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: 10,
color: !
isSignupScreen
? Palette.activeColor
: Palette.textColor1,
fontWeight: FontWeight.bold),
),
if (!
isSignupScreen)
Container(
height: 2,
width: 55,
color: Colors.black87,
)
),
),
),
GestureDetector(
onTap: () {
setState(() {
isSignupScreen = true;
});
},
child: Column(
children: (
Row(
children: (
Text(
'SIGNUP',
style: TextStyle(
fontSize: 10,
color: isSignupScreen
? Palette.activeColor
: Palette.textColor1,
fontWeight: FontWeight.bold),
),
SizedBox(width: 15,),
GestureDetector(
onTap: (){
showAlert(context);
},
child: Icon(Icons.image,
color: isSignupScreen? Colors.cyan:Colors.grey(300) ,),
)
),
),
if (isSignupScreen)
Container(
margin: EdgeInsets.fromLTRB(0, 3, 35, 0),
height: 2,
width: 55,
color: Colors.black87,
)
),
),
)
),
),
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 charactors';
}
return null;
},
onSaved: (value) {
userName = value!
;
},
onChanged: (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)),
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Palette.textColor1),
borderRadius: BorderRadius.all(
Radius.circular(35)),
),
hintText: 'User name',
hintStyle: TextStyle(
fontSize: 12,
color: Palette.textColor1),
contentPadding: EdgeInsets.all(5)),
),
SizedBox(
height: 8,
),
TextFormField(
key: ValueKey(2),
keyboardType: TextInputType.emailAddress,
validator: (value) {
if (value!
.isEmpty ||
!
value!
.contains('@')) {
return 'Please enter a valid email address.';
}
return null;
},
onSaved: (value) {
userEmail = value!
;
},
onChanged: (value) {
userEmail = value;
},
decoration: InputDecoration(
prefixIcon: Icon(
Icons.email,
color: Palette.iconColor,
),
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Palette.textColor1),
borderRadius: BorderRadius.all(
Radius.circular(35)),
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Palette.textColor1),
borderRadius: BorderRadius.all(
Radius.circular(35)),
),
hintText: 'email',
hintStyle: TextStyle(
fontSize: 12,
color: Palette.textColor1),
contentPadding: EdgeInsets.all(5)),
),
SizedBox(
height: 8,
),
TextFormField(
key: ValueKey(3),
obscureText: true,
onSaved: (value) {
userPassword = value!
;
},
onChanged: (value) {
userPassword = value;
},
validator: (value) {
if (value!
.isEmpty || value!
.length < 6) {
return 'Password must be at least 7 characters long.';
}
return null;
},
decoration: InputDecoration(
prefixIcon: Icon(
Icons.lock,
color: Palette.iconColor,
),
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Palette.textColor1),
borderRadius: BorderRadius.all(
Radius.circular(35)),
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Palette.textColor1),
borderRadius: BorderRadius.all(
Radius.circular(35)),
),
hintText: 'password',
hintStyle: TextStyle(
fontSize: 12,
color: Palette.textColor1),
contentPadding: EdgeInsets.all(5)),
)
),
),
),
),
if (!
isSignupScreen)
Container(
margin: EdgeInsets.only(top: 20),
child: Form(
key: _formKey,
child: Column(
children: (
TextFormField(
key: ValueKey(4),
onSaved: (value) {
userEmail = value!
;
},
onChanged: (value) {
userEmail = value;
},
keyboardType: TextInputType.emailAddress,
validator: (value) {
if (value!
.isEmpty || value!
.length < 4) {
return 'Please enter a least 4 character';
}
return null;
},
decoration: InputDecoration(
prefixIcon: Icon(
Icons.account_circle,
color: Palette.iconColor,
),
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Palette.textColor1),
borderRadius: BorderRadius.all(
Radius.circular(35)),
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Palette.textColor1),
borderRadius: BorderRadius.all(
Radius.circular(35)),
),
hintText: 'User Email',
hintStyle: TextStyle(
fontSize: 12,
color: Palette.textColor1),
contentPadding: EdgeInsets.all(5)),
),
SizedBox(
height: 8,
),
TextFormField(
key: ValueKey(5),
obscureText: true,
onSaved: (value) {
userPassword = value!
;
},
onChanged: (value) {
userPassword = value;
},
validator: (value) {
if (value!
.isEmpty || value!
.length < 6) {
return 'Password must be at least 7 characters';
}
return null;
},
decoration: InputDecoration(
prefixIcon: Icon(
Icons.lock,
color: Palette.iconColor,
),
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Palette.textColor1),
borderRadius: BorderRadius.all(
Radius.circular(35)),
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Palette.textColor1),
borderRadius: BorderRadius.all(
Radius.circular(35)),
),
hintText: 'password',
hintStyle: TextStyle(
fontSize: 12,
color: Palette.textColor1),
contentPadding: EdgeInsets.all(5)),
)
),
),
),
)
),
),
),
),
),
// 텍스트 폼 필드
AnimatedPositioned(
duration: Duration(milliseconds: 500),
curve: Curves.easeIn,
top: isSignupScreen ? 500 : 440,
left: 0,
right: 0,
child: Center(
child: Container(
padding: EdgeInsets.all(6),
height: 65,
width: 65,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(80)),
child: GestureDetector(
onTap: () async {
setState(() {
showSpinner=true;
});
if (isSignupScreen) {
_tryValidation();
try {
final newUser = await _authentication
.createUserWithEmailAndPassword(
email: userEmail, password: userPassword);
print(newUser.user);
print(newUser.user!
.uid);
print(userName);
await FirebaseFirestore.instance.collection('user').doc(newUser.user!
.uid).set(
{'userName':userName,
'email':userEmail});
if (newUser.user !
= null) {
//등록 성공
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ChatScreen()));
setState(() {
showSpinner=false;
});
}
} catch (e) {
print(e);
setState(() {
showSpinner=false;
});
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content:
Text('Please check your email and password'),
backgroundColor: Colors.blue,
));
}
}
if (!
isSignupScreen) {
try {
_tryValidation();
final newUser =
await _authentication.signInWithEmailAndPassword(
email: userEmail, password: userPassword);
if (newUser.user !
= null) {
// 등록 성공
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ChatScreen())
);
}
} catch (e) {
print(e);
}
}
},
child: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: (Colors.grey, Colors.blue),
begin: Alignment.topLeft,
end: Alignment.bottomRight),
borderRadius: BorderRadius.circular(30),
boxShadow: (
BoxShadow(
color: Colors.black.withOpacity(0.3),
spreadRadius: 1,
blurRadius: 1,
offset: Offset(1, 0),
)
),
),
child: Icon(
Icons.arrow_forward,
color: Colors.white,
),
),
),
),
),
),
// 전송버튼
AnimatedPositioned(
duration: Duration(milliseconds: 500),
curve: Curves.easeIn,
top: isSignupScreen
? MediaQuery.of(context).size.height - 125
: MediaQuery.of(context).size.height - 150,
right: 0,
left: 0,
child: Column(
children: (
Text(
isSignupScreen ? 'or Signup with' : 'or Signin with',
style: TextStyle(
fontSize: 10,
),
),
SizedBox(
height: 10,
),
TextButton.icon(
style: TextButton.styleFrom(
primary: Colors.white,
minimumSize: Size(155, 40),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20)),
backgroundColor: Colors.blueAccent,
),
onPressed: () {},
icon: Icon(Icons.add),
label: Text('Google'))
),
))
//구글
),
),
),
),
);
}
}
30 강. 채팅 풍선에 아바타를 표시합니다.
image_picker 패키지 설치
이미지 선택기 | Flutter 패키지(pub.dev)
데이터베이스에 찍은 사진을 업로드하려면 firebase_storage를 설치하세요.
firebase_storage | Flutter 패키지(pub.dev)
Firebase 저장소 시작
규칙 수정
rules_version = ‘2’; 서비스 파이어베이스. 저장 { 일치 /b/{버킷}/o { 일치 /{allPaths=**} { 읽기 허용, 생성: if request.auth ! = null; } } } |
이미지 추가로 이미지 추가 후 로그인을 하시면,
파일이 저장소에 업로드되었는지 확인할 수 있습니다.
이제 이 이미지를 채팅 화면에 출력하세요.
강의만 따라가는게 너무 벅차서 설명을 쓸 수가 없고,
전체 코드를 작성하면서 마무리하고 다음에 리뷰를 해야 할 것 같습니다.
메인 다트
import 'package:flutter/material.dart';
import 'main_screen.dart';
import 'package:firebase_core/firebase_core.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Chatting app',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: LoginSignupScreen()
);
}
}
메인 화면 다트
import 'dart:io';
import 'package:chatting_app/add_image.dart';
import 'package:chatting_app/palette.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:modal_progress_hud_nsn/modal_progress_hud_nsn.dart';
import 'chat_screen.dart';
import 'package:firebase_storage/firebase_storage.dart';
class LoginSignupScreen extends StatefulWidget {
const LoginSignupScreen({Key? key}) : super(key: key);
@override
State<LoginSignupScreen> createState() => _LoginSignupScreen();
}
class _LoginSignupScreen extends State<LoginSignupScreen> {
final _authentication = FirebaseAuth.instance;
bool isSignupScreen = true;
bool showSpinner = false;
final _formKey = GlobalKey<FormState>();
String userName="";
String userEmail="";
String userPassword = '';
File? userPickedImage;
void pickedImage(File image){
userPickedImage = image;
}
void showAlert(BuildContext context){
showDialog(context: context, builder: (context){
return Dialog(
backgroundColor: Colors.white,
child: AddImage(pickedImage),
// 왜 메소드 뒤에 괄호를 넣지 않는지? ->
// 메소드를 실행시키려는게 아니라, 포인터만을 전달.
);
});
}
void _tryValidation() {
final isValid = _formKey.currentState!
.validate(); // form의 유효성 체크
if (isValid) {
_formKey.currentState!
.save();
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Palette.backgroundColor,
body: ModalProgressHUD(
inAsyncCall: showSpinner,
child: GestureDetector(
onTap: () {
FocusScope.of(context).unfocus();
},
child: Stack(
children: (
Positioned(
top: 0,
right: 0,
left: 0,
child: Container(
height: 420,
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage('image/t2.png'),
opacity: 0.5,
fit: BoxFit.fill)),
child: Container(
padding: const EdgeInsets.only(
top: 50,
left: 20,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: (
RichText(
text: TextSpan(
text: 'Welcome',
style: TextStyle(
letterSpacing: 1.0,
fontSize: 25,
color: Colors.black),
children: (
TextSpan(
text: isSignupScreen
? ' to Timothee chat!
'
: ' back',
style: TextStyle(
letterSpacing: 1.0,
fontSize: 25,
color: Colors.black,
fontWeight: FontWeight.bold))
)),
),
SizedBox(
height: 15,
),
Text(
isSignupScreen
? 'Sign up to continue'
: 'Signin to continue',
style: TextStyle(
fontSize: 10,
letterSpacing: 1.0,
color: Colors.black,
),
),
),
),
),
)),
// 배경
AnimatedPositioned(
duration: Duration(milliseconds: 500),
curve: Curves.easeIn,
top: 220,
left: 20,
child: AnimatedContainer(
duration: Duration(milliseconds: 500),
curve: Curves.easeIn,
padding: EdgeInsets.all(20),
height: isSignupScreen ? 310 : 250,
width: MediaQuery.of(context).size.width - 40,
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: 10,
color: !
isSignupScreen
? Palette.activeColor
: Palette.textColor1,
fontWeight: FontWeight.bold),
),
if (!
isSignupScreen)
Container(
height: 2,
width: 55,
color: Colors.black87,
)
),
),
),
GestureDetector(
onTap: () {
setState(() {
isSignupScreen = true;
});
},
child: Column(
children: (
Row(
children: (
Text(
'SIGNUP',
style: TextStyle(
fontSize: 10,
color: isSignupScreen
? Palette.activeColor
: Palette.textColor1,
fontWeight: FontWeight.bold),
),
SizedBox(width: 15,),
if(isSignupScreen)
GestureDetector(
onTap: (){
showAlert(context);
},
child: Icon(Icons.image,
color: isSignupScreen? Colors.cyan:Colors.grey(300) ,),
)
),
),
if (isSignupScreen)
Container(
margin: EdgeInsets.fromLTRB(0, 3, 35, 0),
height: 2,
width: 55,
color: Colors.black87,
)
),
),
)
),
),
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 charactors';
}
return null;
},
onSaved: (value) {
userName = value!
;
},
onChanged: (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)),
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Palette.textColor1),
borderRadius: BorderRadius.all(
Radius.circular(35)),
),
hintText: 'User name',
hintStyle: TextStyle(
fontSize: 12,
color: Palette.textColor1),
contentPadding: EdgeInsets.all(5)),
),
SizedBox(
height: 8,
),
TextFormField(
key: ValueKey(2),
keyboardType: TextInputType.emailAddress,
validator: (value) {
if (value!
.isEmpty ||
!
value!
.contains('@')) {
return 'Please enter a valid email address.';
}
return null;
},
onSaved: (value) {
userEmail = value!
;
},
onChanged: (value) {
userEmail = value;
},
decoration: InputDecoration(
prefixIcon: Icon(
Icons.email,
color: Palette.iconColor,
),
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Palette.textColor1),
borderRadius: BorderRadius.all(
Radius.circular(35)),
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Palette.textColor1),
borderRadius: BorderRadius.all(
Radius.circular(35)),
),
hintText: 'email',
hintStyle: TextStyle(
fontSize: 12,
color: Palette.textColor1),
contentPadding: EdgeInsets.all(5)),
),
SizedBox(
height: 8,
),
TextFormField(
key: ValueKey(3),
obscureText: true,
onSaved: (value) {
userPassword = value!
;
},
onChanged: (value) {
userPassword = value;
},
validator: (value) {
if (value!
.isEmpty || value!
.length < 6) {
return 'Password must be at least 7 characters long.';
}
return null;
},
decoration: InputDecoration(
prefixIcon: Icon(
Icons.lock,
color: Palette.iconColor,
),
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Palette.textColor1),
borderRadius: BorderRadius.all(
Radius.circular(35)),
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Palette.textColor1),
borderRadius: BorderRadius.all(
Radius.circular(35)),
),
hintText: 'password',
hintStyle: TextStyle(
fontSize: 12,
color: Palette.textColor1),
contentPadding: EdgeInsets.all(5)),
)
),
),
),
),
if (!
isSignupScreen)
Container(
margin: EdgeInsets.only(top: 20),
child: Form(
key: _formKey,
child: Column(
children: (
TextFormField(
key: ValueKey(4),
onSaved: (value) {
userEmail = value!
;
},
onChanged: (value) {
userEmail = value;
},
keyboardType: TextInputType.emailAddress,
validator: (value) {
if (value!
.isEmpty || value!
.length < 4) {
return 'Please enter a least 4 character';
}
return null;
},
decoration: InputDecoration(
prefixIcon: Icon(
Icons.account_circle,
color: Palette.iconColor,
),
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Palette.textColor1),
borderRadius: BorderRadius.all(
Radius.circular(35)),
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Palette.textColor1),
borderRadius: BorderRadius.all(
Radius.circular(35)),
),
hintText: 'User Email',
hintStyle: TextStyle(
fontSize: 12,
color: Palette.textColor1),
contentPadding: EdgeInsets.all(5)),
),
SizedBox(
height: 8,
),
TextFormField(
key: ValueKey(5),
obscureText: true,
onSaved: (value) {
userPassword = value!
;
},
onChanged: (value) {
userPassword = value;
},
validator: (value) {
if (value!
.isEmpty || value!
.length < 6) {
return 'Password must be at least 7 characters';
}
return null;
},
decoration: InputDecoration(
prefixIcon: Icon(
Icons.lock,
color: Palette.iconColor,
),
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Palette.textColor1),
borderRadius: BorderRadius.all(
Radius.circular(35)),
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Palette.textColor1),
borderRadius: BorderRadius.all(
Radius.circular(35)),
),
hintText: 'password',
hintStyle: TextStyle(
fontSize: 12,
color: Palette.textColor1),
contentPadding: EdgeInsets.all(5)),
)
),
),
),
)
),
),
),
),
),
// 텍스트 폼 필드
AnimatedPositioned(
duration: Duration(milliseconds: 500),
curve: Curves.easeIn,
top: isSignupScreen ? 500 : 440,
left: 0,
right: 0,
child: Center(
child: Container(
padding: EdgeInsets.all(6),
height: 65,
width: 65,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(80)),
child: GestureDetector(
onTap: () async {
setState(() {
showSpinner=true;
});
if (isSignupScreen) {
if(userPickedImage == null){
setState(() {
showSpinner = false;
SnackBar(content: Text('Please pick your image'),);
});
}
_tryValidation();
try {
final newUser = await _authentication
.createUserWithEmailAndPassword(
email: userEmail, password: userPassword);
print(newUser.user);
print(newUser.user!
.uid);
print(userName);
final refImage = await FirebaseStorage.instance.ref().
child('picked_image').
child(newUser.user!
.uid+'.png');
// 이미지가 저장되는 버킷에 접근할 수 있도록.
await refImage.putFile(userPickedImage!
);
final url = await refImage.getDownloadURL();
await FirebaseFirestore.instance.collection('user')
.doc(newUser.user!
.uid).set(
{'userName':userName,
'email':userEmail,
'picked_image' : url
}
);
if (newUser.user !
= null) {
//등록 성공
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ChatScreen()));
setState(() {
showSpinner=false;
});
}
} catch (e) {
print(e);
if(mounted){
setState(() {
showSpinner=false;
});
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content:
Text('Please check your email and password'),
backgroundColor: Colors.blue,
));
}}
}
if (!
isSignupScreen) {
try {
_tryValidation();
final newUser =
await _authentication.signInWithEmailAndPassword(
email: userEmail, password: userPassword);
if (newUser.user !
= null) {
// 등록 성공
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ChatScreen())
);
}
} catch (e) {
print(e);
}
}
},
child: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: (Colors.grey, Colors.blue),
begin: Alignment.topLeft,
end: Alignment.bottomRight),
borderRadius: BorderRadius.circular(30),
boxShadow: (
BoxShadow(
color: Colors.black.withOpacity(0.3),
spreadRadius: 1,
blurRadius: 1,
offset: Offset(1, 0),
)
),
),
child: Icon(
Icons.arrow_forward,
color: Colors.white,
),
),
),
),
),
),
// 전송버튼
AnimatedPositioned(
duration: Duration(milliseconds: 500),
curve: Curves.easeIn,
top: isSignupScreen
? MediaQuery.of(context).size.height - 125
: MediaQuery.of(context).size.height - 150,
right: 0,
left: 0,
child: Column(
children: (
Text(
isSignupScreen ? 'or Signup with' : 'or Signin with',
style: TextStyle(
fontSize: 10,
),
),
SizedBox(
height: 10,
),
TextButton.icon(
style: TextButton.styleFrom(
primary: Colors.white,
minimumSize: Size(155, 40),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20)),
backgroundColor: Colors.blueAccent,
),
onPressed: () {},
icon: Icon(Icons.add),
label: Text('Google'))
),
))
//구글
),
),
),
),
);
}
}
chat_screen.dart
import 'package:chatting_app/message.dart';
import 'package:chatting_app/new_message.dart';
import 'package:flutter/material.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
class ChatScreen extends StatefulWidget {
const ChatScreen({Key? key}) : super(key: key);
@override
State<ChatScreen> createState() => _ChatScreenState();
}
class _ChatScreenState extends State<ChatScreen> {
final _authentication = FirebaseAuth.instance;
User? loggedUser;
@override
void initState() {
// TODO: implement initState
super.initState();
getCurrentUser();
}
void getCurrentUser() {
try {
final user = _authentication.currentUser;
if (user !
= null) {
loggedUser = user;
print(loggedUser!
.email);
}
} catch (e) {
print(e);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Chat screen'),
actions: (
IconButton(
onPressed: () {
_authentication.signOut();
//Navigator.pop(context);
},
icon: Icon(Icons.exit_to_app_sharp),
color: Colors.white,
)
),
),
body: Container(
child: Column(
children: (
Expanded(
child: Messages(),
),
NewMessage(), // 리스트뷰가 무조건 화면 내의 모든 공간을 확보해버리기 때문에 expanded 로 감싸주었따.
),
),
),
// body: StreamBuilder(
// stream: FirebaseFirestore.instance.collection('chats/PMNxP6mCoF32Fuz0xEth/message').snapshots(),
// builder: (BuildContext context, AsyncSnapshot<dynamic> snapshot) {
//
// if(snapshot.connectionState==ConnectionState.waiting){
// return Center(
// child: CircularProgressIndicator(),
// );}
// final docs = snapshot.data!
.docs;
// return ListView.builder(
// itemCount: docs.length,
// itemBuilder: (context, index){
// return Container(
// padding: EdgeInsets.all(8.0),
// child: Text(
// docs(index)('text'),
// style: TextStyle(fontSize: 20.0),),
// );
// },
// );
// },
// )
);
}
}
메시지 다트
import 'package:chatting_app/chat_bubble.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
class Messages extends StatelessWidget {
const Messages({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final user = FirebaseAuth.instance.currentUser;
return StreamBuilder(
stream: FirebaseFirestore.instance.collection('chat').orderBy('time', descending: true).snapshots(),
builder: (context, AsyncSnapshot<QuerySnapshot<Map<String, dynamic>>> snapshot){
if(!
snapshot.hasData){
return Text('no data');
}
if(snapshot.connectionState==ConnectionState.waiting){
return Center(
child: CircularProgressIndicator(),
);
}
final chatDocs = snapshot.data!
.docs;
return ListView.builder(
reverse: true,
itemCount: chatDocs.length,
itemBuilder: (context, index){
return ChatBubbles(chatDocs(index)('text'),
chatDocs(index)('userID').toString()==user!
.uid,
chatDocs(index)('userName'),
chatDocs(index)('userImage')
);
});
}
);
}
}
new_message.dart
import 'package:flutter/material.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
class NewMessage extends StatefulWidget {
const NewMessage({Key? key}) : super(key: key);
@override
State<NewMessage> createState() => _NewMessageState();
}
class _NewMessageState extends State<NewMessage> {
var _userEnterMessage="";
final _controller = TextEditingController();
Future<void> _sendMessage() async {
FocusScope.of(context).unfocus();
final user = FirebaseAuth.instance.currentUser;
final userData = await FirebaseFirestore.instance.collection('user').doc(user!
.uid).get();
FirebaseFirestore.instance.collection('chat').add({
'text' : _userEnterMessage,
'time' : Timestamp.now(),
'userID' : user!
.uid,
'userName' : userData.data()!
('userName'),
'userImage' : userData('picked_image')
});
_controller.clear();
}
@override
Widget build(BuildContext context) {
return Container(
margin: EdgeInsets.only(top:8),
padding: EdgeInsets.all(8.0),
child: Row(
children: (
Expanded(
child: TextField(
maxLines: null,
controller: _controller,
decoration: InputDecoration(
labelText: 'Send a message',
),
onChanged: (value){
setState(() {
_userEnterMessage = value;
//모든 키 입력에서 setState 불러옴.
});
},
),
),
IconButton(
onPressed: _userEnterMessage.trim().isEmpty?null:_sendMessage
, icon: Icon(Icons.send), color: Colors.blue,)
),
),
);
}
}
add_image.dart
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
class AddImage extends StatefulWidget {
const AddImage(this.addImageFunc, {Key? key}) : super(key: key);
final Function(File pickedImage) addImageFunc;
@override
_AddImageState createState() => _AddImageState();
}
class _AddImageState extends State<AddImage> {
File? pickedImage;
void _pickImage() async {
final imagePicker = ImagePicker();
final pickedImageFile = await imagePicker.pickImage(
source: ImageSource.camera,
imageQuality: 50,
maxHeight: 150
);
setState(() {
if(pickedImageFile !
=null) {
pickedImage = File(pickedImageFile.path);
}
});
widget.addImageFunc(pickedImage!
);
}
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.only(top: 10),
width: 150,
height: 300,
child: Column(
children: (
CircleAvatar(
radius: 40,
backgroundColor: Colors.blue,
backgroundImage: pickedImage !
=null ? FileImage(pickedImage!
) : null,
),
SizedBox(
height: 10,
),
OutlinedButton.icon(
onPressed: () {
_pickImage();
},
icon: Icon(Icons.image),
label: Text('Add image'),
),
SizedBox(
height: 80,
),
TextButton.icon(
onPressed: () {
Navigator.pop(context);
},
icon: Icon(Icons.close),
label: Text('Close'),
),
),
),
);
}
}
팔레트 다트
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);
}