[Flutter 스터디] 12. 채팅

강 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)

이미지 선택기 | 플러터 팩

Android 및 iOS 이미지 라이브러리에서 이미지를 선택하고 카메라로 새 이미지를 캡처하는 Flutter 플러그인.

pub.dev

데이터베이스에 찍은 사진을 업로드하려면 firebase_storage를 설치하세요.

firebase_storage | Flutter 패키지(pub.dev)

firebase_storage | 플러터 팩

Firebase Cloud Storage용 Flutter 플러그인은 Android 및 iOS용 강력하고 단순하며 경제적인 개체 스토리지 서비스입니다.

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);
}