Programming-[CrossPlatform]/Flutter

Flutter 기본-21. 채팅앱 - 채팅 UI 작성, 데이터 관리

컴퓨터 탐험가 찰리 2023. 2. 13. 21:14
728x90
반응형

Youtube 코딩셰프님의 강의를 요약 정리한 글이다. dart 언어나 이론 부분은 자바와 유사하여 대부분 제외하였고, flutter 기초 위주로 정리한다.

https://www.youtube.com/@codingchef

 

코딩셰프

향후 대세가 될 플러터를 단계별로 맛있게 학습하실 수 있습니다!

www.youtube.com

 

 

 

background image reference : https://wallpapercave.com/cartoon-chickens-wallpapers

 

 


 

1.  채팅 메시지 화면 새로 구성

 

기존 chat_screen에 있던 부분을 리팩토링한다. chatting/chat 폴더를 만들고 messages.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: (BuildContext context,
            AsyncSnapshot<QuerySnapshot<Map<String, dynamic>>> snapshot) {
          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']);
              });
        });
  }
}

 

그리고 'chat'이라는 컬렉션에서 정보를 불러올 것이므로 파이어베이스에서 chat이라는 컬렉션을 만들고 기존에 chats/ 로 되있던 규칙도 변경해준다.

 

그리고 chat_screen에는 기존에 body 인자에 있던 StreamBuilder를 제거하고 새로 생성한 Messages 위젯을 삽입한다. UI 조정을 위해 Container를, 이후에 삽입할 채팅메시지 입력창을 삽입하기 위해 Column 위젯을 넣었다. 그리고 단순히 Messages 위젯만 있으면 Column 위젯이 세로 방향으로 무한한 높이를 차지하게 되어 Messages()를 표현할 공간이 없어서 에러가 난다. 이를 막기 위해서 Expanded 위젯을 추가해주었다.

 

body: Container(
    child: Column(
      children: [
        Expanded(
            child: Messages()
        ),
      ],
    )
)

 

 

2. 메시지 보내기

 

메시지 입력창 구성

이제 메시지 입력창을 만들어본다. 사용자의 입력을 받는 부분이 있으므로 StatefulWidget으로 만들어준다. 그리고 상기 작성한 Message() 위젯과 마찬가지로 Container-Row-Expanded로 작성한다. 텍스트만 입력하고 Validation 등 다른 기능은 사용하지 않을 것이므로 Form을 사용할 필요없이 TextField 위젯을 사용한다.

 

TextField에서는 onChanged 메서드에 setState 메서드를 생성하고 _userEnterMessage라는 변수에 사용자의 입력값인 value를 할당한다. 아무런 조건없이 할당하기 때문에 사용자의 입력값이 즉시 변수에 계속 할당된다.

 

Icon은 삼항연산자를 사용하여 값을 검사 후 기능 및 색깔을 조절한다. 우선은 아무런 기능을 등록하지 않고 빈 메서드로 두었다.

import 'package:flutter/material.dart';

class NewMessage extends StatefulWidget {
  const NewMessage({Key? key}) : super(key: key);

  @override
  State<NewMessage> createState() => _NewMessageState();
}

class _NewMessageState extends State<NewMessage> {

  var _userEnterMessage = '';

  @override
  Widget build(BuildContext context) {
    return Container(
      margin: EdgeInsets.only(top: 8),
      padding: EdgeInsets.all(8),
      child: Row(
        children: [
          Expanded(
              child: TextField(
                decoration: InputDecoration(
                    labelText: 'Send a message...'
                ),
                onChanged: (value) {
                  setState(() {
                    _userEnterMessage = value;
                  });
                },
              )
          ),
          IconButton(
              onPressed: _userEnterMessage.trim().isEmpty? null : (){}
              ,
              icon: Icon(Icons.send,
              color: _userEnterMessage.trim().isEmpty? null: Colors.blue,)
          )
        ],
      ),
    );
  }
}

 

 

이제 완성된 위젯을 chat_screen.dart 파일에서 상기 등록한 Messages() 위젯 아래에 넣어주면 된다.

 

 

메시지 전송 및 데이터베이스 등록

실제 메시지를 보내기 위해서 _sendMessage라는 메서드를 만들고 IconButton의 onPressed에 적용한다. hot restart를 적용하고 확인해보면 정상작동한다. Firebase의 collection을 불러온다음 add 메서드를 통해 chat 컬렉션 내 docs(문서)를 추가해주는 원리이다.

 

void _sendMessage() {
  FocusScope.of(context).unfocus();
  FirebaseFirestore.instance.collection('chat').add({
    "text": _userEnterMessage,
  });
}

// 중략
IconButton(
              onPressed: _userEnterMessage.trim().isEmpty ? null : _sendMessage,

 

 

 

데이터 출력 방식, UI 개선

 

전송 후 입력창 초기화

이 부분은 sendMessages()내에서 TextEditingController를 눌러와서 clear() 메서드를 호출하면 된다. 코드는 아래 항목과 함께 살펴본다.

 

출력 순서 변경

기본적인 채팅앱과 같이 채팅 메시지가 밑에서부터 위로 올라가고, 가장 최신의 메시지가 가장 아래에 위치하도록 변경한다. 먼저 messages.dart 파일에서 ListView.builder에 reverse 인자값을 true로 주면 ListView를 반대로 보이게해서 맨 아래쪽에 새로운 메시지가 등록될 수 있도록 할 수 있다. 좀 더 근본적으로는 Firebase의 stream에 orderBy 옵션을 특정 키값으로 설정해주는 것이다. 여기서는 'time'으로 설정하였다.

return ListView.builder(
    reverse: true,

 

Widget build(BuildContext context) {
  return StreamBuilder(
      stream: FirebaseFirestore.instance
          .collection('chat')
          .orderBy('time', descending: true)
          .snapshots(),

 

 

그리고 새로운 메시지가 추가될 때마다 'time' 값이 입력되어야 하므로 new_messages.dart 파일에서 "time" 값을 삽입한다. unfocus()는 메시지 입력 후 키보드를 제거하기 위해 적용하였다. 그리고 Timestamp는 cloud_firestore에서 제공하는 객체이다.

void _sendMessage() {
  FocusScope.of(context).unfocus();
  FirebaseFirestore.instance.collection('chat').add({
    "text": _userEnterMessage,
    "time": Timestamp.now()
  });
  _controller.clear();
}

 

 

기존에는 time 필드가 없었으므로 정상적으로 데이터들이 안보인다. 파이어베이스의 데이터베이스에 있는 데이터를 모두 삭제 후 새롭게 메시지를 입력해줘야한다. 그럼 시각값이 잘 저장된다.

 

 

 

 

 

3. 메시지 UI 개선

 

이제 각 메시지의 UI를 개선한다.

 

 

chat_bubble.dart

텍스트(Message)마다 버블 형태의 UI를 입혀주는 클래스를 만든다. 단순히 UI용 이므로 Stateless 위젯으로 만들면 된다.

 

class ChatBubble extends StatelessWidget {
  const ChatBubble(this.message, {Key? key}) : super(key: key);

  final String message;

  @override
  Widget build(BuildContext context) {
    return Row(
      children: [
        Container(
          decoration: BoxDecoration(
            color: Colors.blue,
            borderRadius: BorderRadius.circular(12),
          ),
          width: 145,
          padding: EdgeInsets.symmetric(vertical: 10, horizontal: 16),
          margin: EdgeInsets.symmetric(vertical: 4, horizontal: 8),
          child: Text(
            message,
            style: TextStyle(
              color: Colors.white
            ),
          ),
        ),
      ],
    );
  }
}

 

외부에서 message를 받아와서 그것을 Text 위젯에 삽입하는 형태이다. Container를 Row로 감싸서 UI가 화면 가로의 전체를 차지하는게 아니라 자식인 Container 만큼만 차지하도록 설정해준다.

 

그리고 message.dart에서 마지막 Text로 리턴하던 부분을 ChatBubble로 변경해준다.

return ListView.builder(
    reverse: true,
    itemCount: chatDocs.length,
    itemBuilder: (context, index) {
      return ChatBubble(chatDocs[index]['text']);

 

 

 

사용자 표시: 데이터 준비

각 메시지의 사용자를 표시하고, 본인의 메시지인지를 구분한다. 그럴려면 각 메시지에 사용자의 id값을 포함시켜줘야할 것이다. 새 메시지가 입력되었을 때 'userID' 값을 추가해준다. 새로운 필드값을 추가하였으므로 위에서 했던 것처럼 데이터베이스의 기존 정보들을 날려주고 새로 만든다.

final user = FirebaseAuth.instance.currentUser;
FirebaseFirestore.instance.collection('chat').add({
  "text": _userEnterMessage,
  "time": Timestamp.now(),
  "userID": user!.uid,
});

 

그리고 본인의 메시지임을 구분하기 위한 isMe 변수값을 ChatBubble에 추가한다.

class ChatBubble extends StatelessWidget {
  const ChatBubble(this.message, this.isMe, {Key? key}) : super(key: key);

  final String message;
  final bool isMe;
  
  //중략
  
  //BoxDecoration color
  color: isMe? Colors.grey[300] : Colors.blue,
  
  //Text Color
  color: isMe? Colors.black : Colors.white

 

 

 

 

사용자 표시: 렌더링

버블의 위치와 BorderRadius를 수정해준다. isMe에 따라 Radius의 디자인을 달리한다.

decoration: BoxDecoration(
  color: isMe? Colors.grey[300] : Colors.blue,
  borderRadius: BorderRadius.only(
    topRight: Radius.circular(12),
    topLeft: Radius.circular(12),
    bottomRight: isMe? Radius.circular(0) : Radius.circular(12),
    bottomLeft: isMe? Radius.circular(12) : Radius.circular(0)
  ),
),

 

Bubble의 위치는 isMe에 따라 Row 아래 MainAxisAlignment 속성을 조절해주면 된다.

return Row(
  mainAxisAlignment: isMe? MainAxisAlignment.end : MainAxisAlignment.start,
  children: [

 

 

테스트

에뮬레이터 2대를 띄워서 메시지 전송 테스트를 해본다. create device를 통해 Andriod 계열의 에뮬레이터를 띄우고, 실행한 다음 해당 에뮬레이터에서 프로젝트를 실행시키면 된다.

 

 

 

텍스트 여러 줄로 보이게 하기

추가로 TextField에 긴 글을 입력했을 때 여러 줄로 보이도록 하는 옵션으로 설정한다. TextField의 maxLines 인자값은 디폴트가 1이라서 이것을 null로 바꿔주면 된다.

child: TextField(
  maxLines: null,

 

728x90
반응형