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 문서를 참고했습니다.


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

- 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