[플러터] 22. MVVM 패턴 - (3) 게시글 목록 보기

KangHo Lee's avatar
Jan 06, 2025
[플러터] 22. MVVM 패턴 - (3) 게시글 목록 보기

패키지 구조

notion image

1. 게시글 목록 보기

1) PostListBody

post_list_body.dart
class PostListBody extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { PostListModel? model = ref.watch(postListProvider); if (model == null) { return Center(child: CircularProgressIndicator()); } else { print("body 로드 완료"); return ListView.separated( itemCount: model.posts.length, itemBuilder: (context, index) { return InkWell( onTap: () { Navigator.push( context, MaterialPageRoute(builder: (_) => PostDetailPage())); }, child: PostListItem(post: model.posts[index]), ); }, separatorBuilder: (context, index) { return const Divider(); }, ); } } }
  • ListView.separated
    • separatorBuilder를 통해서 item 사이 구분 선을 넣을 수 있습니다.
  • InkWell
    • 터치 가능한 영역을 만들어주는 위젯입니다.

2) PostListModel

post_list_model.dart
class PostListModel { bool isFirst; bool isLast; int pageNumber; int size; int totalPage; List<Post> posts; PostListModel.fromMap(Map<String, dynamic> map) : isFirst = map["isFirst"], isLast = map["isLast"], pageNumber = map["pageNumber"], size = map["size"], totalPage = map["totalPage"], // Map<String, dynamic> 이니까 dynamic으로 인식 중이라 묵시적 형변환 후 .map 사용 가능 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; // watch로 볼 예정이라 반환값 필요 x } 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"]); } }
  • class PostListModel
    • PostListBody에만 쓰이는 데이터를 모아둔 객체입니다.
    • int 타입의 경우 double값이 들어오지 않는지 체크해야 합니다.
    • List<Post> posts;
      • 자바와 달리 inner class 문법이 없기 때문에 따로 class를 만들어야 합니다.
  • PostListModel.fromMap(Map<String, dynamic> map)
    • posts = (map["posts"] as List<dynamic>).map
      • map["posts"] 는 dynamic 타입이라 .map 메서드를 쓰기 위해 명시적으로 캐스팅함으로써, Dart 컴파일러가 이 값이 실제로는 리스트라는 것을 알 수 있게 합니다.
    • : → 이니셜라이저는 클래스의 생성자를 의미하며, 객체를 생성할 때 초기화하는 방법을 정의합니다.
      • PostListModel 클래스의 이니셜라이저는 fromMap 팩토리 생성자입니다.
  • init(0);
    • 시작 페이지는 0페이지라서 0을 파라미터로 넣습니다.

post.dart

class Post { int? id; String? title; String? content; DateTime? createdAt; DateTime? updatedAt; int? bookmarkCount; User? user; bool? isBookmark; // post detail에서 쓰기 위해 추가 Post.fromMap(Map<String, dynamic> map) // 필드 이름 겹치는 거 없으니까 this. 생략 가능 : id = map["id"], title = map["title"], content = map["content"], createdAt = DateFormat("yyyy-mm-dd").parse(map["createdAt"]), updatedAt = DateFormat("yyyy-mm-dd").parse(map["updatedAt"]), bookmarkCount = map["bookmarkCount"], isBookmark = map["isBookmark"], user = User.fromMap(map["user"]); }

user.dart

class User { int? id; String? username; String? imgUrl; User.fromMap(Map<String, dynamic> map) : this.id = map["id"], this.username = map["username"], this.imgUrl = map["imgUrl"]; }

3) PostRepository

post_repository.dart
class PostRepository { const PostRepository(); Future<Map<String, dynamic>> findAll({int page = 0}) async { Response response = await dio.get("/api/post", queryParameters: {"page": page}); Map<String, dynamic> body = response.data; return body; } }
  • dio.get 요청에서 쿼리 파라미터는 /api/post?page=0 로 적지 말고 queryParameters 속성에 추가하는 것이 좋습니다.
Share article

devleekangho