Programming-[CrossPlatform]/Flutter

Flutter 기본-23. 채팅앱 - 이미지 등록 및 조회, firebase storage

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

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

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

 

코딩셰프

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

www.youtube.com

 

 

 

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

 

 


1. Image picker 설치, XFile

 

Image Picker

사용자가 이미지를 업로드하는 기능을 image_picker라는 라이브러리를 통해 구현한다. 

https://pub.dev/packages/image_picker

 

다른 라이브러리들과 똑같이 설치하면 되지만, 해당 라이브러리의 readme 부분에 보면 IOS의 경우 핸드폰 갤러리 접근, 카메라 접근 등을 위해 ios/info.plist에 image picker 관련 key를 추가해주라고 되어있으니 참고한다.

 

XFile

이제 이전에 작성했던 다이얼로그용 add_image.dart 파일로 돌아와서 아래 메서드를 작성해준다. imagePicker는 Future<XFile?>을 리턴하는데 XFile은 파일의 위치에 대한 정보만 담고 있는 파일이다. 카메라로 찍는 사진은 임시 저장이 되고, XFile로 해당 경로를 호출하면 그 이미지를 표시할 수 있게 된다.

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

 

참고로 File 타입은 다트에서 제공하는 것이며, dart.io에서 불러와야한다.

 

 

 

그리고 ImageSource.camera를 선택하여 바로 카메라로 사진을 찍을 수 있게 해주었다. galleryvalues에 접근할 수도 있다.

 

 

State에 선언한 pickedImage 변수값과 메서드를 각각 CircleAvatar와 OutlinedButton에 적용한다.

child: Column(
  children: [
    CircleAvatar(
      radius: 40,
      backgroundColor: Colors.blue,
      backgroundImage: pickedImage != null? FileImage(pickedImage!) : null,
    ),
    SizedBox(
      height: 10,
    ),
    OutlinedButton.icon(
      onPressed: (){
        _pickImage();
      },

 

 

그럼 이제 사진을 촬영하고 XFile을 통해 미리 보기로 볼 수 있게 된다.

 

 

2. Image 데이터 바인딩

 

 

(중요) Image 데이터 바인딩

유저가 이미지를 업로드하고 submit 버튼을 눌렀을 때 데이터베이스에 이미지가 저장되도록 하는 것이므로 add_image -> main_screen 파일로 값을 전달해주어야 한다. 이 개념은 함수를 불러와서 데이터를 바인딩하는 개념이라 살짝 헷갈린다. 중요한 부분인 것 같다.

 

Function

우선 선택된 이미지를 AddImage 위젯에서 저장하고 있기 위해서 File 타입의 파라미터를 갖는 Function을 선언하고 pickedImage 값을 저장한다.

 

class AddImage extends StatefulWidget {
  const AddImage(this.addImageFunc, {Key? key}) : super(key: key);

  final Function(File pickedImage) addImageFunc;

  @override
  State<AddImage> createState() => _AddImageState();
}

class _AddImageState extends State<AddImage> {

  File? pickedImage;

  void _pickImage() async {

    //중략
    widget.addImageFunc(pickedImage!);
  }

 

widget에 함수 형태로 이미지 데이터를 저장하되 인자값으로 받는 pickedImage 값을 AddImage 위젯을 호출하는 곳에서 바인딩을 하는 개념이다. main_screen 파일에서 다음과 같이 선언한다.

 

class _LoginSignupScreenState extends State<LoginSignupScreen> {
  //중략
  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),
        );
      },
    );
  }

 

AddImage 위젯에 다시 함수 pickedImage의 참조값을 전달한다. 이러면 상기 add_image 파일에서 전달된 pickedImage가 해당 클래스의 userPickedImage로 바인딩 되는 것 같다! 데이터를 바인딩하고 싶으면 Function 타입을 써야된다 정도로만 기억하고 복습 및 다양한 경험을 통해 더 잘 이해해야되지 않을까 싶다.

 

 

이미지가 있을 때만 Signup

유저가 이미지를 올렸을 때만 Signup이 되도록 코드를 추가한다. inline if문으로 userPickedImage 값을 검사하고 null인 경우 Spinner를 보여주지 않고 SnackBar를 띄운다. 마지막으로 return문을 추가하여 onTap 메서드를 종료시킨다.

child: GestureDetector(
  onTap: () async {
    if (isSignupScreen) {
      if(userPickedImage == null) {
        setState(() {
          showSpinner = false;
        });

        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(
              content: Text('Please pick your image'),
            backgroundColor: Colors.blue,
          )
        );
        return;
      }
      _tryValidation();

 

 

 

 

3. 파이어베이스에 이미지 저장

 

firebase_storage

사용자가 설정한 프로필 이미지를 데이터베이스에 저장해놓아야 계속해서 데이터를 유지할 수 있다. 이를 위해서 firebase_storage 패키지를 설치한다.

https://pub.dev/packages/firebase_storage

 

import 'package:firebase_storage/firebase_storage.dart';

 

 

우선 Firestore의 doc을 만들었을 때와 마찬가지로 파이어베이스의 Storage로 가서 ref를 생성한다. 똑같이 테스트모드로 시작하되, 모든 유저가 접근하여 write 기능은 수행할 수 없도록 create로 보안 규칙 업데이트만 해준다.

 

 

 

그리고 Submit 버튼 부분에서 이를 참조하여 파일을 업로드할 수 있도록 만들어준다. picked_image 라는 폴더를 만들고 그 하위에 user의 uid값을 이용해서 파일 이름을 지정해주었다. putFile 메서드로 실제 FirebaseStorage에 업로드를 할 수 있다.

 

try {
  //중략

  final refImage = FirebaseStorage.instance.ref()
  .child('picked_image')
  .child(newUser.user!.uid);

  await refImage.putFile(userPickedImage!);

 

이제 getDownloadURL() 메서드로 해당 이미지의 저장 위치 url을 불러오고, 유저 정보 저장 시에 pickedImage라는 키값으로 같이 저장되도록 해주면 된다.

await refImage.putFile(userPickedImage!);
final url = await refImage.getDownloadURL();

if (newUser.user != null) {
  await FirebaseFirestore.instance
      .collection('user')
      .doc(newUser.user!.uid)
      .set({
    'userName': userName,
    'email': userEmail,
    'pickedImage': url
  });

 

 

4. 채팅창에 이미지 출력

 

이제 이미지를 채팅창에 추가해준다. 우선 메시지를 저장할 때마다 chat 컬렉션에 userImage를 저장해주기 위해 new_message 파일에서 'userImage' 필드를 추가한다.

class _NewMessageState extends State<NewMessage> {
  final _controller = TextEditingController();
  var _userEnterMessage = '';

  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['pickedImage']
    });

 

그리고 message 파일에서 리스트로 불러올 때 userImage 정보도 함께 불러온다.

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'],
      );
    });

 

마지막으로 chat_bubble에 기존 채팅 버블 위에 이미지가 표시될 수 있도록 한다. userImage 멤버 속성을 추가하고,

 

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

  final String userName;
  final String message;
  final bool isMe;
  final String userImage;

 

 

기존 Row위젯만 있던 부분을 Stack으로 감싼다. 메시지와 이미지를 같이 쌓아가며 표시한다고 생각하면 된다.

@override
Widget build(BuildContext context) {
  return Stack(children: [
    Row(

 

그리고 Image의 위치를 조절하기 위해서 Positioned로 Stack의 children에 추가해준다. userImage 값은 url 이므로 NetworkImage 위젯을 사용하였다. Row에서 패딩값도 EdgeInsets를 적절히 조절하여 마무리하였다.

  ),
  Positioned(
    top: 0,
    right: isMe? 5 : null,
    left: isMe? null : 5,
    child: CircleAvatar(
      backgroundImage: NetworkImage(userImage),
    ),
  ),
]);

 

728x90
반응형