[플러터] 23. MVVM 패턴 - (4) 게시글 상세 보기, 삭제

KangHo Lee's avatar
Jan 06, 2025
[플러터] 23. MVVM 패턴 - (4) 게시글 상세 보기, 삭제

1. post_repository.dart

class PostRepository { const PostRepository(); // 게시글 상세 보기 Future<Map<String, dynamic>> findById(int id) async { Response response = await dio.get("/api/post/${id}"); Map<String, dynamic> body = response.data; return body; } // 게시글 삭제 Future<Map<String, dynamic>> delete(int id) async { Response response = await dio.delete("/api/post/${id}"); Map<String, dynamic> body = response.data; return body; } }
  • 메서드 작성은 API 문서를 참고했습니다.
notion image
notion image

2. post_list_body.dart

onTap: () { Navigator.push( context, MaterialPageRoute( builder: (_) => PostDetailPage(model.posts[index].id!))); },
  • PostDetailPage에 postId( model.posts[index].id! )를 전달합니다.

3. PostDetailBody

class PostDetailBody extends ConsumerWidget { int postId; PostDetailBody(this.postId); @override Widget build(BuildContext context, WidgetRef ref) { PostDetailModel? model = ref.watch(postDetailProvider(postId)); if (model == null) { return Center(child: CircularProgressIndicator()); } return Padding( padding: const EdgeInsets.all(16.0), child: ListView( children: [ PostDetailTitle("${model.post.title}"), const SizedBox(height: largeGap), PostDetailProfile(model.post), // postId가 아니라 post 통으로 보낸 이유 -> update때문 PostDetailButtons(model.post), const Divider(), const SizedBox(height: largeGap), PostDetailContent("${model.post.content}"), ], ), ); } }
notion image
  • PostDetailButtons(model.post)
    • 삭제와 수정 아이콘이 포함된 위젯입니다.
    • 삭제만 있으면 postId만 있으면 되지만 수정에는 post 전체 내용이 필요하기 때문에 post를 전달합니다.

4. PostDetailButtons

class PostDetailButtons extends ConsumerWidget { Post post; PostDetailButtons(this.post); @override Widget build(BuildContext context, WidgetRef ref) { SessionUser sessionUser = ref.read(sessionProvider); PostDetailVM vm = ref.read(postDetailProvider(post.id!).notifier); // 로그인한 유저와 Post 작성 유저 비교 if (sessionUser.id == post.user!.id!) { return Row( mainAxisAlignment: MainAxisAlignment.end, children: [ IconButton( onPressed: () { vm.delete(post.id!); }, icon: const Icon(CupertinoIcons.delete), ), IconButton( onPressed: () { Navigator.push(context, MaterialPageRoute(builder: (_) => PostUpdatePage(post))); }, icon: const Icon(CupertinoIcons.pen), ), ], ); } else { // 로그인한 유저랑 Post 작성자랑 다를 경우 빈 박스 return SizedBox(); } } }
  • sessionProvider를 동해 로그인 한 유저의 정보를, postDetailProvider를 통해 post의 정보를 가져와서 로그인 한 유저와 post의 작성자가 같을 경우 삭제, 수정 버튼을 노출합니다.

5. PostDetailVM

class PostDetailModel { Post post; PostDetailModel.fromMap(Map<String, dynamic> map) : post = Post.fromMap(map); } // .autoDispose 를 쓸 경우 삭제 실행 시 VM의 PostDetail 데이터도 삭제 final postDetailProvider = NotifierProvider.family .autoDispose<PostDetailVM, PostDetailModel?, int>(() { return PostDetailVM(); }); class PostDetailVM extends AutoDisposeFamilyNotifier<PostDetailModel?, int> { final mContext = navigatorKey.currentContext!; PostRepository postRepository = const PostRepository(); @override PostDetailModel? build(id) { init(id); return null; } Future<void> init(int id) async { Map<String, dynamic> responseBody = await postRepository.findById(id); if (!responseBody["success"]) { ScaffoldMessenger.of(mContext!).showSnackBar( SnackBar( content: Text("게시글 상세보기 실패 : ${responseBody["errorMessage"]}")), ); return; } state = PostDetailModel.fromMap(responseBody["response"]); } Future<void> delete(int id) async { Map<String, dynamic> responseBody = await postRepository.delete(id); if (!responseBody["success"]) { ScaffoldMessenger.of(mContext!).showSnackBar( SnackBar(content: Text("게시글 삭제 실패 : ${responseBody["errorMessage"]}")), ); return; } // detail은 autoDispose니까 PostListVM의 상태를 변경 ref.read(postListProvider.notifier).remove(id); // 화면 파괴시 vm이 autoDispose 됨 Navigator.pop(mContext); } }
// 변경 전 final postDetailProvider = NotifierProvider<PostDetailVM, PostDetailModel?>(() // 변경 후 final postDetailProvider = NotifierProvider.family .autoDispose<PostDetailVM, PostDetailModel?, int>(() { return PostDetailVM(); });
  • postDetailProvider 에 postId를 추가하기 위해 family 추가
  • autoDispose를 추가해서 post를 삭제할 경우 VM의 post 데이터를 삭제합니다.
    • 삭제 통신을 보낼 경우 DB 내용은 바뀌지만 화면에 표시되는 VM의 데이터가 바뀌지 않기 때문에 autoDispose가 필요합니다.

6. PostListVM

class PostListModel { bool isFirst; bool isLast; int pageNumber; int size; int totalPage; List<Post> posts; // copyWith를 위한 기본 생성자 필요 PostListModel({ required this.isFirst, required this.isLast, required this.pageNumber, required this.size, required this.totalPage, required this.posts, }); // 변화한 데이터만 변경해서 복사하는 메서드 PostListModel copyWith({ bool? isFirst, bool? isLast, int? pageNumber, int? size, int? totalPage, List<Post>? posts, }) { return PostListModel( isFirst: isFirst ?? this.isFirst, isLast: isLast ?? this.isLast, pageNumber: pageNumber ?? this.pageNumber, size: size ?? this.size, totalPage: totalPage ?? this.totalPage, posts: posts ?? this.posts); } PostListModel.fromMap(Map<String, dynamic> map) : isFirst = map["isFirst"], isLast = map["isLast"], pageNumber = map["pageNumber"], size = map["size"], totalPage = map["totalPage"], posts = (map["posts"] as List<dynamic>) .map((e) => Post.fromMap(e)) .toList(); } final postListProvider = NotifierProvider<PostListVM, PostListModel?>(() { return PostListVM(); }); class PostListVM extends Notifier<PostListModel?> { final mContext = navigatorKey.currentContext!; PostRepository postRepository = const PostRepository(); @override PostListModel? build() { init(0); return null; } Future<void> init(int page) async { Map<String, dynamic> responseBody = await postRepository.findAll(page: page); if (!responseBody["success"]) { ScaffoldMessenger.of(mContext!).showSnackBar( SnackBar( content: Text("게시글 목록 보기 실패 : ${responseBody["errorMessage"]}")), ); return; } state = PostListModel.fromMap(responseBody["response"]); } // 통신없이 상태 갱신 void remove(int id) { PostListModel model = state!; model.posts = model.posts.where((p) => p.id != id).toList(); // copyWith는 삭제만 반영된 posts 만 넣는 것. 나머지는 그대로 쓰는 것 state = state!.copyWith(posts: model.posts); } }
// 통신없이 상태 갱신 void remove(int id) { PostListModel model = state!; // PostList에서 삭제된 post 제외 model.posts = model.posts.where((p) => p.id != id).toList(); // 상태 갱신 state = state!.copyWith(posts: model.posts); }
  • copyWith 메서드를 통해 변경된 사항만 반영합니다.
    • post를 삭제했으므로 List<Post>인 posts만 바뀌었습니다.
Share article

devleekangho