ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Flutter - 코딩마스터하기|코멘트 레이아웃
    개발/Flutter 2021. 11. 23. 00:23
    반응형

    create comment model

    코멘트 레이아웃을 만들기 위해 코멘트 모델을 만들자. models - firestore 에 comment_model.dart를 생성해주고 다른 model을 복사해온다. 그리고 class명을 CommentModel로 바꿔준다. comment에 들어간 부분들은 String username; String userKey; String comment; DateTime commentTime; String commentKey; DocumentReference reference; 로 생성해주면 된다. 그리고 fromMap에는 username, userKey, comment, commentTime로 만들어 준다. fromSnapshot는 똑같이 두면 된다. 그리고 가장 하단의 메소드는 getMapForNewComment로 변경해주고 userKey, username, comment,  commentTime을 넣어주면 된다. 이렇게 comment model 을 생성하고 firestore와 연결하는 부분까지 만들었다.


    class CommentModel {
      final String username;
      final String userKey;
      final String comment;
      final DateTime commentTime;
      final String commentKey;
      final DocumentReference reference;
    
      CommentModel.fromMap(Map<String, dynamic> map, this.commentKey,
          {this.reference}) //fromMap은 Dart 내장 함수
          : username = map[KEY_USERNAME],
            userKey = map[KEY_USERSKEY],
            comment = map[KEY_COMMENT],
            commentTime = map[KEY_COMMENTTIME] == null
                ? DateTime.now().toUtc()
                : (map[KEY_COMMENTTIME] as Timestamp).toDate();
    
      CommentModel.fromSnapshot(DocumentSnapshot snapshot) // cloud firestore 함수
          : this.fromMap(snapshot.data, snapshot.documentID,
                reference: snapshot.reference);
    
      static Map<String, dynamic> getMapForNewComment(
          String userKey, String username, String comment) {
        Map<String, dynamic> map = Map();
        map[KEY_USERSKEY] = userKey;
        map[KEY_USERNAME] = username;
        map[KEY_COMMENT] = comment;
        map[KEY_COMMENTTIME] = DateTime.now().toUtc();
        return map;
      }
    }

    create new comment into firestore

    우선 repo 폴더에 comment_network_repository.dart를 만들어 주고 class명을 CommentNetworkRepository를 해주고 with Transformers 까지 해준다. 다른 네트워크 연결 부분과 마찬가지로 post ref를 연결해주고 snapshot를 가져온다. 그리고 comment ref까지 가져온다. 다음으로 runTransaction을 해줘서 set으로 코멘트를 만들어 주고, update로 코멘트 갯수 +1, 마지막 코멘트 내용, 마지막 코멘트 작성자, 마지막 코멘트 시간을 업데이트 해주면 된다.


     

     

    class CommentNetworkRepository with Transformers{
      Future<void> createNewComment(String postKey, Map<String, dynamic> commentData) async {
        final DocumentReference postRef = Firestore.instance.collection(COLLECTION_POSTS).document(postKey);
        final DocumentSnapshot postSnapshot = await postRef.get();
        final DocumentReference commentRef = postRef.collection(COLLECTION_COMMENTS).document();
    
        return Firestore.instance.runTransaction((tx) async {
          if(postSnapshot.exists){
            await tx.set(commentRef, commentData);
    
            int numOfComments = postSnapshot.data[KEY_NUMOFCOMMENTS];
            await tx.update(postRef, {
              KEY_NUMOFCOMMENTS: numOfComments+1,
              KEY_LASTCOMMENT: commentData[KEY_COMMENT],
              KEY_LASTCOMMENTTIME: commentData[KEY_COMMENTTIME],
              KEY_LASTCOMMENTOR: commentData[KEY_LASTCOMMENTOR],
            });
          }
        });
      }
    }​

    fetch all the comments for a post

    해당 포스터에 대한 모든 comment 가져와 보자. comment_network_repository에 가서 Stream<List<CommentModel>> fetchAllcomments(String postKey)를 만들어 준다. postKey를 받아오는 이유는 포스트 글 아래에 하위 콜렉션을 만들어서 코멘트를 담기 때문에 postKey 하나만 있으면 된다. return으로 Firestore.instance.collection(COLLECTION_POSTS).document(postKey).collection(COLLECTION_COMMENTS)를 해줘서 comment 콜렉션까지 접속해준다. 그 뒤에 .orderby로 정렬해준다. KEY_COMMENTTIME 로 정렬 해줄꺼고 기본값이 descending가 false라 최근게 밑으로 간다. 그리고 .snapshots()로 가져와 준뒤 transform(toComments)로 변경 시켜준다. transformers로 이동해서 toComments를 추가해준다. toPosts를 똑같이 복사 한 후 PostModel을 CommentModel로 변경 시켜주고 posts를 comments로 변경시켜주기만 하면 된다.


      Stream<List<CommentModel>> fetchAllComments(String postKey){
        //기본 설정이 asc이므로 최근게 가장 밑으로 감.기본값 false
        return Firestore.instance.collection(COLLECTION_POSTS).document(postKey).collection(COLLECTION_COMMENTS).orderBy(KEY_COMMENTTIME,descending: false,).snapshots().transform(toComments);
      }
      final toComments =
      StreamTransformer<QuerySnapshot, List<CommentModel>>.fromHandlers(
        //StreamTransformer<도착한 모델, 보낼 모델>
          handleData: (snapshot, sink) async {
            List<CommentModel> comments = [];
    
            snapshot.documents.forEach((documentSnapshot) {
              comments.add(CommentModel.fromSnapshot(documentSnapshot));
            });
    
            sink.add(comments);
          });

    navigate to comments screen page

    피드페이지에서 코멘트를 누르면 전체 코멘트가 보이고 코멘트를 달 수 있는 페이지를 만들어 보자. screen 폴더에 comments_screen.dart를 만들어 주고  StatefulWidget로 CommentsScreen을 만들어 준다. 우선 아무 컬러나 넣어준뒤 피드페이지에서 코멘트 아이콘을 누르면 이동하게해주자. post.dart로 와서 comment 아이콘이 있는 버튼의 onPressed에 Navigator로 push해주자. 그리고 이를 위해서 Buildcontext 를 받아 오도록 해주자.


      Row _postActions(BuildContext context) {
        return Row(
          children: [
            IconButton(
              onPressed: () {},
              icon: ImageIcon(
                AssetImage('assets/images/bookmark.png'),
              ),
              color: Colors.black87,
            ),
            IconButton(
              onPressed: () {
                Navigator.of(context).push(MaterialPageRoute(builder: (BuildContext context){return CommentsScreen();}));
              },
              icon: ImageIcon(
                AssetImage('assets/images/comment.png'),
              ),
              color: Colors.black87,
            ),
            IconButton(
              onPressed: () {},
              icon: ImageIcon(
                AssetImage('assets/images/direct_message.png'),
              ),
              color: Colors.black87,
            ),
            Spacer(),
            IconButton(
              onPressed: () {},
              icon: ImageIcon(
                AssetImage('assets/images/heart_selected.png'),
              ),
              color: Colors.black87,
            ),
          ],
        );
      }
    class CommentsScreen extends StatefulWidget {
      const CommentsScreen({Key key}) : super(key: key);
    
      @override
      _CommentsScreenState createState() => _CommentsScreenState();
    }
    
    class _CommentsScreenState extends State<CommentsScreen> {
      @override
      Widget build(BuildContext context) {
        return Container(color: Colors.amberAccent,);
      }
    }

    comments page input layout

    댓글이 보이는 부분과 댓글을 입력하고 게시하는 버튼이 있는 부분을 만들어 줄 것이다.

    Scaffold로 만들어 주고 Form으로 만들어 준다. form을 위해서 state class 상단에 TextEditingController과 GlobalKey<FromState>를 만들어 준다. key에 _formKey를 넣어주고 child 에 위에서 말한 2가지를 열로 해주기 위해서 Column을 해준다. 그리고 children에 댓글이 보이는 부분은 우선 Expanded로 Container 에 색을 줘서 임시로 보이게 해준다. 그리고 아래 버튼 부분은 Row로 만들어 준다. children에 Enpanded로 TextFormField를 만들어 주고 controller에 _textEditingController을 넣어준다 cursorColor에는 black54를 주고 decoration은 InputDecoration으로 hintText와 border은 InputBorder.none을 주면 된다. validator는 (String value){}로 if(value,isEmpty)일때 return으로 댓글을 입력하라는 메시지를 리턴시켜주고 else일때는 null을 return해준다. 그리고 children에 FlatButton을 만들어서 child에는 Text("Post")를 넣어주고 onPressed 에는 if(_formKey.currentState.validate())로 이 안에 댓글을 firestore에 등록해주는 부분을 만들면 된다.


    class _CommentsScreenState extends State<CommentsScreen> {
      TextEditingController _textEditingController = TextEditingController();
      GlobalKey<FormState> _formKey = GlobalKey<FormState>();
    
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          body: Form(
            key: _formKey,
            child: Column(
              children: [
                Expanded(child: Container(color: Colors.amberAccent,),),
                Row(
                  children: [
                    Expanded(child: Padding(
                      padding: const EdgeInsets.symmetric(horizontal: common_gap,),
                      child: TextFormField(
                        controller: _textEditingController,
                        cursorColor: Colors.black54,
                        decoration: InputDecoration(hintText: "Add a Comment...", border: InputBorder.none,),
                        validator: (String value){
                          if(value.isEmpty){
                            return 'Input someThing';
                          }
                          else{
                            return null;//null을 해줘야 validator를 통과함
                          }
                        },
                      ),
                    ),),
                    FlatButton(
                      onPressed: (){
                        if(_formKey.currentState.validate()){
    
                        }
    
                      },
                      child: Text("POST"),
                    ),
    
                  ],
                )
              ],
            ),
          ),
        );
      }
    }

    connect comments screen to firestore

    이제 댓글이 보이는 부분을 만들어 보자. Container였던 부분을 StreamProvider로 변경해준다. 이때 List<CommentModel> 을 받아오고 댓글이 생길때마다 변해야 하므로 StreamProvider<List<CommentModel>>.value로 해준다. value에는 commentNetworkRepository.fetchAllComments()를 해주는데 ()에 postKey가 필요하므로 class상단에 final String postKey;를 가져오고 필수로 받아오도록 key부분에 {}밖으로 this.postKey를 해준다. 그리고 fetchAllComments()안에 widget.postKey를 넣어주면 된다. 그리고 child에 Consumer<List<CommentModel>>();을 생성하고 안에 builder에 (BuildContext context, List<CommentModel> commentModel, Widget child){}을 생성 해준다. 이 안의 return에 ListView.separated()를 생성해준다. itemBuilder에는 아이템의 부분이 들어가는데 여기에는 (context, index){}의 형태로 {}d안에 Comment()를 생성해준다. index: index, username: commentModel[index].username, text: commentModel[index].comment,dateTime: commentModel[index].commentTime, showImage: true, 를 넣어주면 된다. 너무 붙어있어서 Padding로 감싸서 xxs의 갭만큼 주면 된다. separatorBuilder에도 (context, index){}의 형태인에 return에는 SizedBox(height: common_xxs_gap);를 주면 된다. itemCount에는 commentModel이 null이면 0을 주고 아니면 commentModel.length 만큼 주면 된다. 그리고 작성부분과 댓글을 보여주는 부분이 경계가 안보이므로 Divider(height: 1,thickness: 1,color: Colors.grey[300],),를 사이에 추가해주면 된다. 게시를 하는 버튼에 if문 안에는 userModel을 Provider로 listen: false로 하여 한번만 불러온다. 그리고 Map<String, dynamic>으로  CommentModel.getMapForNewComment로 하여 데이터를 담아서  commentNetworkRepository.createNewComment로 파이어스토어에 넣어준다. 그리고 _textEditingController을 clear해주면 되는데 이때 getMapForNewComment부분에 await를 해준다. async도 넣어주면 된다. 그리고 코멘트 아이콘에서 넘겨주는 부분에서도 CommentScreen() 에 postModel.postKey를 넣어주면 정상 작동 한다.


    class CommentsScreen extends StatefulWidget {
      final String postKey;
    
      const CommentsScreen(
        this.postKey, {
        Key key,
      }) : super(key: key);
    
      @override
      _CommentsScreenState createState() => _CommentsScreenState();
    }
    
    class _CommentsScreenState extends State<CommentsScreen> {
      TextEditingController _textEditingController = TextEditingController();
      GlobalKey<FormState> _formKey = GlobalKey<FormState>();
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: Text("Comments"),
          ),
          body: Form(
            key: _formKey,
            child: Column(
              children: [
                Expanded(
                  child: StreamProvider<List<CommentModel>>.value(
                    value: commentNetworkRepository.fetchAllComments(widget.postKey),
                      child: Consumer<List<CommentModel>>(
                        builder: (BuildContext context, List<CommentModel> commentModel, Widget child){
                          return  ListView.separated(
                              itemBuilder: (context, index) {
                                return Padding(
                                  padding: const EdgeInsets.all(common_xxs_gap),
                                  child: Comment(index: index, username: commentModel[index].username, text: commentModel[index].comment,dateTime: commentModel[index].commentTime, showImage: true,),
                                );
                              },
                              separatorBuilder: (context, index) {
                                return SizedBox(height: common_xxs_gap,);
                              },
                              itemCount: commentModel == null? 0 : commentModel.length);
                        },
                      )),
                ),
                Divider(height: 1,thickness: 1,color: Colors.grey[300],),
                Row(
                  children: [
                    Expanded(
                      child: Padding(
                        padding: const EdgeInsets.symmetric(
                          horizontal: common_gap,
                        ),
                        child: TextFormField(
                          controller: _textEditingController,
                          cursorColor: Colors.black54,
                          decoration: InputDecoration(
                            hintText: "Add a Comment...",
                            border: InputBorder.none,
                          ),
                          validator: (String value) {
                            if (value.isEmpty) {
                              return 'Input someThing';
                            } else {
                              return null; //null을 해줘야 validator를 통과함
                            }
                          },
                        ),
                      ),
                    ),
                    FlatButton(
                      onPressed: () async {
                        if (_formKey.currentState.validate()) {
                          UserModel userModel = Provider.of<UserModelState>(context, listen: false,).userModel;
                          Map<String,dynamic> newComment = CommentModel.getMapForNewComment(userModel.userKey, userModel.username, _textEditingController.text);
                          await commentNetworkRepository.createNewComment(widget.postKey, newComment);
                          _textEditingController.clear();
                        }
                      },
                      child: Text("POST"),
                    ),
                  ],
                )
              ],
            ),
          ),
        );
      }
    }

    fix last comment update bug

    파이어베이스의 last comment관련해서 에러가 있음을 알 수 있다. 처음 만들때 comment_network_repository에서 update에서 데이터를 받아올때 comment_model에서 username과 commenttime으로 받아왔는데 여기서는 last를 써서 받아와서 데이터가 다르게 들어간 것이다. 그래서 해당 부분을 똑같이 맞게 변경 시켜주면 된다.


      Future<void> createNewComment(String postKey, Map<String, dynamic> commentData) async {
        final DocumentReference postRef = Firestore.instance.collection(COLLECTION_POSTS).document(postKey);
        final DocumentSnapshot postSnapshot = await postRef.get();
        final DocumentReference commentRef = postRef.collection(COLLECTION_COMMENTS).document();
    
        return Firestore.instance.runTransaction((tx) async {
          if(postSnapshot.exists){
            await tx.set(commentRef, commentData);
    
            int numOfComments = postSnapshot.data[KEY_NUMOFCOMMENTS];
            await tx.update(postRef, {
              KEY_NUMOFCOMMENTS: numOfComments+1,
              KEY_LASTCOMMENT: commentData[KEY_COMMENT],
              KEY_LASTCOMMENTTIME: commentData[KEY_COMMENTTIME],
              KEY_LASTCOMMENTOR: commentData[KEY_USERNAME],
            });
          }
        });
      }

    show more comments label

    댓글 아래에 몇개의 댓글이 더 있는지, 그리고 그 부분을 누르면 댓글창으로 가게 해주는 부분을 만들 것이다. 우선 _moreComments()라는 부분을 _lastComment(), 밑에 만들어 주고 메소드를 생성한다. 그리고 Text로 ${post.Model.numOfComments -1} more comments...로 만들어 준다. 그런데 이때 댓글수가 2 이하이면 보여줄 필요가 없으니 if문으로 감싸줘도 된다. 하지만 여기서는 Visibility로 한번 해보자. Text를 Visibility로 감싸주고 visible 옵션에 (postModel.numOfComments != null && postModel.numOfComments >=2) 를 줘서 null이 아니고 갯수가 2개 이상일때 보여주게 하면 된다. Text는 Padding으로 감싸줘서 symmetric에서 horizontal로 common_gap만큼 주면 된다. 여기서 버튼을 누르면 이동하게 해줘야 하므로 코멘트 아이콘에서 썼던 부분이 Navigator부분을 메소드로 빼주자. _goToComments(BuildContext context){}로 해주면 된다. Padding를 GestureDetector로 감싸주고 onTap에 _goToComments(context)를 해주는데 (){}의 안에 넣어줘야 한다. 그냥 하면 바로 실행된다. 기존 코멘트 아이콘 부분도 수정해준다. lastcomment도 padding부분을 조금 수정해준다. 마지막으로 코멘트창을 들어갈때 바로 댓글을 쓰도록 나오지 않는데 이부분은 comments_screen으로 가서 TextFormField에서 autofocus를 true로 해주면 된다.


      Widget _moreComments(BuildContext context) {
        return Visibility(
          visible:
              (postModel.numOfComments != null && postModel.numOfComments >= 2),
          child: GestureDetector(
            onTap: (){_goToComments(context);},
            child: Padding(
              padding: const EdgeInsets.symmetric(horizontal: common_gap),
              child: Text("${postModel.numOfComments - 1} more comments..."),
            ),
          ),
        );
      }
    }
    
      _goToComments(BuildContext context) {
        Navigator.of(context)
            .push(MaterialPageRoute(builder: (BuildContext context) {
          return CommentsScreen(postModel.postKey);
        }));
      }

    toggle like method

    좋아요 버튼의 toggle method를 만들어 보자. post_network_repository에 가서 하단에 Future<void>toggleList(String postKey, String userKey) async {}를 만들어 주자. 어떤 게시글에 좋아요를 했는지 알기 위해 postKey와 현재 좋아요를 한 사람(나)의 userKey를 받아온다. 우선 DocumentReference 로 postRef를 받아온다. Firestore.instance.collection(COLLECTION_POSTS).document(postKey); 를 해주면 된다. 그리고 DocumnetSnapshot postSnapshot 도 만들어 주면 된다. await postRef.get(); 해서 가져오면 된다. 우선 if문에 postSnapshot.exists로 존재 하는지 확인 후 존재하면 그 안에 if문을 하나 더 만들어 준다. 내 userKey가 존재 하는지 확인해주면 된다. postSnapshot.data[KEY_NUMOFLIKES].contains(userKey) 이렇게 확인 해 주면 된다. 그래서 있다면 update로 arrayRemove 해주면 되고 없으면 arrayUnion을 해주면 된다.


      Future<void> toggleLike(String postKey, String userKey) async{
        final DocumentReference postRef =
        Firestore.instance.collection(COLLECTION_POSTS).document(postKey);
        final DocumentSnapshot postSnapshot = await postRef.get();
        if(postSnapshot.exists){
          if(postSnapshot.data[KEY_NUMOFLIKES].contains(userKey)){
            postRef.updateData({KEY_NUMOFLIKES: FieldValue.arrayRemove([userKey])});
          }else{
            postRef.updateData({KEY_NUMOFLIKES: FieldValue.arrayUnion([userKey])});
          }
        }
      }

    connect like button to firestore

    좋아요 버튼을 연동해보자. IconButton에서 onPressed에서쓰려면 userModelState가 필요한데 Provider로 불러오는 것보다 IconButton을 Consumer로 감싸는게 더 낫기 때문에 감싸면 된다. Consumer<UserModelState>로 감싸주고 build는 (BuildContext context, UserModelState userModelState, Widget child){} 를 넣어준다. 기존 child는 잘라내서 {}에 return에 넣어준다. 그리고 toggleLike에 postModel.postKey, userModelState.userModel.userKey로 해를 해주면 된다. 또한 아이콘도 꽉찬 하트와 빈하트로 변경 해줘야 하는데 AssetImage 안에 postModel.numOfLikes.contains로 안에 포함되어있는지 아닌지로 찬으면 된다. postModel.numOfLikes.contains(userModelState.userModel.userKey)?'assets/images/heart_selected.png':'assets/images/heart.png' 이렇게 해주면 좋아요 누른상태면 꽉찬 하트 아니면 빈 하트가 된다.


            Consumer<UserModelState>(
              builder: (BuildContext context, UserModelState userModelState, Widget child) {
                return IconButton(
                  onPressed: () {
                    postNetworkRepository.toggleLike(postModel.postKey, userModelState.userModel.userKey);
                  },
                  icon: ImageIcon(
                    AssetImage(postModel.numOfLikes.contains(userModelState.userModel.userKey)?'assets/images/heart_selected.png':'assets/images/heart.png'),
                    color: Colors.redAccent,
                  ),
                  color: Colors.black87,
                );
              }
            ),

    이상으로 모든것을 마친다.

    반응형

    댓글

Designed by Tistory.