20. Flutter -게시글 상세보기, 데이터 바인딩

데이터 바인딩
홍윤's avatar
Oct 11, 2024
20. Flutter -게시글 상세보기, 데이터 바인딩
 

1. 상세보기 위해 id값 전달하기

1. post_list_body.dart

import 'package:blog/ui/detail/post_detail_page.dart'; import 'package:blog/ui/list/post_list_vm.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; class PostListBody extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { //1. 페이지 들어오면서 ViewModel이 만들어져야함 watch로 보기 PostListModel? model = ref.watch(postListProvider); if (model == null) { return CircularProgressIndicator(); } else { return Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: ListView.separated( itemBuilder: (context, index) { return ListTile( leading: Text("${model.posts[index].id}"), title: Text("${model.posts[index].title}"), trailing: IconButton( icon: Icon(Icons.arrow_forward_ios), onPressed: () { //context가 없으면 아무것도 할 수가 없다. //스택이 하나 더 쌓인다. 매개변수 전달이 가능하다. Navigator.push( context, MaterialPageRoute( builder: (context) => PostDetailPage(model.posts[index].id))); }, ), ); }, separatorBuilder: (context, index) => Divider(), itemCount: model.posts.length), ); } } }
💡

코드설명

이 코드에서 PostListBody 클래스는 ConsumerWidget을 상속하여 Riverpod의 상태를 구독하는 역할을 합니다. build 메서드에서 ref.watch를 통해 postListProvider를 구독하여 PostListModel을 가져옵니다.
  • ViewModel 구독:
    • PostListModel? model = ref.watch(postListProvider);
      이 부분에서 postListProvider를 구독하고, PostListModel이 반환됩니다. 만약 modelnull이면 CircularProgressIndicator가 표시되어 데이터를 불러오는 중임을 나타냅니다.
  • 데이터 렌더링: modelnull이 아닐 경우, ListView.separated를 사용하여 게시글 목록을 출력합니다. 각 게시글은 ListTile을 통해 표현되며, idtitle이 표시됩니다.
  • Navigator를 통한 페이지 전환:
    • Navigator.push( context, MaterialPageRoute( builder: (context) => PostDetailPage(model.posts[index].id)));
      여기서는 IconButton을 클릭했을 때 PostDetailPage로 이동하며, 게시글의 id 값을 전달합니다. Navigator.push로 새로운 페이지를 스택에 추가하여 화면 전환을 처리합니다.
 

2. post_detail_page.dart

import 'package:blog/ui/detail/components/post_detail_body.dart'; import 'package:flutter/material.dart'; import '../components/custom_appbar.dart'; class PostDetailPage extends StatelessWidget { int id; PostDetailPage(this.id); @override Widget build(BuildContext context) { return Scaffold( appBar: CustomAppBar(title: "Post Detail Page"), //body에 넣어서 만들기 body: PostDetailBody(id), ); } }
💡

코드설명

이 코드는 PostDetailPage라는 페이지를 구성하는 데 사용되며, 주어진 id를 통해 특정 게시물의 세부 정보를 표시하는 페이지입니다.
  • 생성자에서 id 전달:
    • int id; PostDetailPage(this.id);
      PostDetailPage는 게시물의 id를 인자로 받아서 해당 페이지에서 해당 id에 대한 게시물 세부 정보를 보여줍니다. id는 이 페이지에서 중요한 역할을 하며, 상세 정보를 불러오는 데 사용됩니다.
  • 앱바(CustomAppBar 사용):
    • appBar: CustomAppBar(title: "Post Detail Page"),
      CustomAppBar는 재사용 가능한 커스텀 앱바를 사용하여 PostDetailPage 상단에 표시됩니다. 앱바의 타이틀은 "Post Detail Page"로 설정됩니다.
  • 본문(PostDetailBody 사용):
    • body: PostDetailBody(id),
      PostDetailBody는 게시물의 상세 내용을 표시하는 위젯으로, 생성자에 전달된 id를 이용해 해당 게시물의 데이터를 불러옵니다. PostDetailBody는 아마도 API나 데이터베이스에서 해당 게시물의 데이터를 가져오는 로직을 포함하고 있을 것입니다.
  • 코드 수정해보기
  1. 생성자에서 final 키워드 사용: id를 한 번만 설정하고 변경하지 않는 경우에는 final 키워드를 사용하여 불변성을 보장하는 것이 좋습니다.
    1. final int id; PostDetailPage(this.id);
  1. 페이지 이동 시 데이터 유지: 페이지 간 이동 시 Navigator를 통해 전달된 id를 제대로 유지하는지 확인이 필요합니다. 현재 코드는 이를 잘 처리하고 있으나, 만약 데이터를 추가적으로 받아와야 한다면 FutureBuilderAsyncValue를 이용해 비동기 데이터 처리가 필요할 수 있습니다.
  1. 최종코드
import 'package:blog/ui/detail/components/post_detail_body.dart'; import 'package:flutter/material.dart'; import '../components/custom_appbar.dart'; class PostDetailPage extends StatelessWidget { final int id; PostDetailPage(this.id); @override Widget build(BuildContext context) { return Scaffold( appBar: CustomAppBar(title: "Post Detail Page"), body: PostDetailBody(id), ); }
 

3. post_detail_body.dart

import 'package:blog/ui/detail/post_detail_vm.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; class PostDetailBody extends ConsumerWidget { int id; PostDetailBody(this.id); @override Widget build(BuildContext context, WidgetRef ref) { PostDetailModel? model = ref.watch(postDetailProvider(id)); if (model == null) { return CircularProgressIndicator(); } return Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Column( children: [ Align( alignment: Alignment.centerRight, child: ElevatedButton( child: Icon(CupertinoIcons.trash_fill), onPressed: () {}, ), ), SizedBox(height: 10), Text("id : ${model.id}", style: TextStyle(fontSize: 20)), Text("title : ${model.title}"), Text("content : ${model.content}"), Text("createdAt : ${model.createdAt}"), Text("updatedAt: ${model.updatedAt}") ], ), ); } }
💡

코드설명

이 코드에서는 PostDetailBody 클래스가 ConsumerWidget을 상속받아, postDetailProvider를 통해 게시물 세부 정보를 구독하여 화면에 표시하는 기능을 제공합니다.
  • ConsumerWidget 사용:
    • ConsumerWidget을 사용하면 build 메서드에서 WidgetRef를 받을 수 있어, 이를 통해 Riverpod의 상태를 구독할 수 있습니다.
  • WidgetRef 사용:
    • build 메서드에 WidgetRef ref를 추가하여, ref 객체를 통해 창고(Provider)를 구독합니다. ref.watch로 필요한 데이터를 가져옵니다.
  • 창고(Provider) 생성 및 구독:
    • Provider(창고)를 먼저 정의하고, ref.watch를 통해 상태를 구독합니다. 이미 Provider가 생성된 상태라면 새로운 인스턴스를 생성하지 않고 기존의 상태를 그대로 사용하게 됩니다.
  • id를 통한 데이터 구독:
    • PostDetailModel? model = ref.watch(postDetailProvider(id));
      ref.watch를 통해 postDetailProvider에서 게시물의 id에 해당하는 데이터를 구독하고, PostDetailModel을 반환받습니다. 데이터를 불러오는 중이거나 아직 데이터가 없을 때는 null일 수 있으므로, 로딩 중에는 CircularProgressIndicator를 표시합니다.
  • 로딩 상태 처리:
    • if (model == null) { return CircularProgressIndicator(); }
      modelnull일 경우, 로딩 상태를 나타내는 인디케이터를 반환합니다. 데이터를 성공적으로 받아오면 이후의 본문 UI를 렌더링합니다.
  • 게시물 세부 정보 표시:
    • Text("id : ${model.id}", style: TextStyle(fontSize: 20)), Text("title : ${model.title}"), Text("content : ${model.content}"), Text("createdAt : ${model.createdAt}"), Text("updatedAt : ${model.updatedAt}
      게시물의 ID, 제목, 내용, 생성일, 수정일 등의 세부 정보가 Text 위젯을 통해 표시됩니다. 각각의 데이터는 PostDetailModel 객체에서 받아옵니다.
  • 삭제 버튼:
    • Align( alignment: Alignment.centerRight, child: ElevatedButton( child: Icon(CupertinoIcons.trash_fill), onPressed: () {}, ), ),
      화면의 오른쪽 상단에 쓰레기통 아이콘이 있는 삭제 버튼을 배치했습니다. onPressed 콜백 함수는 아직 구현되지 않았으며, 삭제 동작이 필요할 경우 이곳에 추가 로직을 넣으면 됩니다.

      요약:

      ConsumerWidget을 사용하여 id 값을 받아 데이터를 처리할 때는 WidgetRefbuild 메서드에 받아 사용합니다. 창고(Provider)를 생성하고, ref.watch를 통해 데이터를 구독하며, 이미 창고가 생성되어 있으면 기존의 창고를 호출하여 상태를 재사용합니다.
 

4. post_detail_vm.dart

1. post_detail_vm.dart 기존 코드

// 1. 창고(ViewModel) class PostDetailVM extends StateNotifier<PostDetailModel?> { PostDetailVM(super.state); // async를 쓰려고 Future타입으로 리턴 Future<void> notifyInit() async { } } //..중략.. // 3. 창고 관리자 (Provider) // 물음표를 넣은 이유는 일단 null을 넣어놓고 갱신시켜줄 것이므로 final postDetailProvider = StateNotifierProvider<PostDetailVM, PostDetailModel?>((ref) { return PostDetailVM(null)..notifyInit(); });
 

2. ViewModel 생성시 초기 파라미터 전달하는 문법 추가 코드(StateNotifierProvider에 .family를 붙이고 제네릭과 파라미터를 추가해준다.)

  • 수정하면 창고 생성(post_detail_body) 코드를 수정해야 한다.
//1. 창고(View) import 'package:blog/core/utils.dart'; import 'package:blog/data/post_repository.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; class PostDetailVM extends StateNotifier<PostDetailModel?> { PostDetailVM(super.state); //notifyinit 해서 데이터 받고? family데이터 받기 Future<void> notifyInit(int id) async { Map<String, dynamic> one = await PostRepository().findById(id); //객체 만들어서 상태를 new해서 다시 만들어준다. state = PostDetailModel.fromMap(one); } } //2. 창고 데이터(State) DTO 같은거다! //내부 데이터 필요하면 _클래스 만들다. //통신해서 받아야 할 데이터 class PostDetailModel { int id; String title; String content; String createdAt; String updatedAt; PostDetailModel.fromMap(map) : this.id = map["id"], this.title = map["title"], this.content = map["content"], this.createdAt = formatDate(map["createdAt"]), this.updatedAt = formatDate(map["updatedAt"]); } //3. 창고 관리자 (Provider) final postDetailProvider = //이름이 있는 생성자(family) ,타입을 적고 id를 넣으면 된다. StateNotifierProvider.autoDispose .family<PostDetailVM, PostDetailModel?, int>((ref, id) { print("나 만들어져? $id"); //? 붙이 이유는 아직 데이터를 넣지않기때문에 나중에 바꿔준다. return PostDetailVM(null)..notifyInit(id); });
💡

코드설명

이 코드는 Riverpod을 사용하여 PostDetailVMPostDetailModel을 관리하는 방식으로, 게시물의 상세 데이터를 비동기적으로 불러와 상태를 관리하는 패턴을 구현하고 있습니다.

    창고 (View): PostDetailVM

    • StateNotifier: PostDetailVMStateNotifier를 상속받아 상태를 관리하는 역할을 합니다. 여기서 상태는 PostDetailModel이며, 게시물 상세 데이터를 저장하고 처리합니다.
    • notifyInit 메서드: 이 메서드는 id 값을 받아 해당 게시물을 PostRepository에서 가져온 후, 받아온 데이터를 PostDetailModel로 변환하여 상태(state)를 업데이트합니다.여기서는 id를 통해 PostRepository에서 데이터를 가져오고, 그 데이터를 PostDetailModel의 인스턴스로 만들어 상태를 업데이트합니다.
      • Future<void> notifyInit(int id) async { Map<String, dynamic> one = await PostRepository().findById(id); state = PostDetailModel.fromMap(one); }

    2. 창고 데이터 (State): PostDetailModel

    • PostDetailModel: 이 클래스는 DTO(Data Transfer Object)와 유사하며, API나 데이터베이스에서 받아온 데이터를 관리하는 역할을 합니다. 생성자는 fromMap을 통해 데이터를 받아오고, formatDate 함수를 사용해 createdAtupdatedAt을 포맷팅합니다.이 클래스를 통해 게시물의 id, title, content, createdAt, updatedAt 등의 데이터를 관리합니다.
      • PostDetailModel.fromMap(map) : this.id = map["id"], this.title = map["title"], this.content = map["content"], this.createdAt = formatDate(map["createdAt"]), this.updatedAt = formatDate(map["updatedAt"])

    3. 창고 관리자 (Provider): postDetailProvider

    • StateNotifierProvider.autoDispose.family: postDetailProvider는 게시물의 id를 받아 해당 게시물의 상세 정보를 관리하는 PostDetailVM 인스턴스를 생성합니다. family는 파라미터를 받아 특정한 데이터를 처리하는 기능을 제공합니다.
      • final postDetailProvider = StateNotifierProvider.autoDispose.family<PostDetailVM, PostDetailModel?, int>((ref, id) { print("나 만들어져? $id"); return PostDetailVM(null)..notifyInit(id); });
      • autoDispose: 이 옵션을 통해 사용하지 않는 Provider는 자동으로 해제되어 메모리 관리를 돕습니다.
      • family: id라는 파라미터를 받아 특정 게시물의 데이터를 처리할 수 있게 해줍니다.
      • notifyInit(id)는 생성자에서 호출되어, id에 해당하는 게시물 데이터를 비동기적으로 가져옵니다.
     

    5. post_repository.dart

    import 'package:blog/core/utils.dart'; import 'package:dio/dio.dart'; class PostRepository { // 싱클톤으로 데이터 바인딩 하는법 , get 만들거면 instnace에 _ 붙이고 //static PostRepository instnace = PostRepository._single(); //PostRepository._single(); // 하는일: 통신 후 body 데이터만 응답 // List<dynamic> or Map<String,dynamic> Future<List<dynamic>> findAll() async { //1. 통신 -> response [header, body] Response response = await dio.get("/api/post"); //2. body 부분 리턴 //body 부분이 컬렉션이면 파싱 할 때 List<dynamic>으로 받아드린다. //body 부분이 json이면 Map<String,dynamic>으로 받아드린다. List<dynamic> resposebody = response.data["body"]; //list의 map타입 return resposebody; } Future<Map<String, dynamic>> findById(int id) async { //1. 통신 -> response [header, body] Response response = await dio.get("/api/post/$id"); //2. body 부분 리턴 //body 부분이 컬렉션이면 파싱 할 때 List<dynamic>으로 받아드린다. //body 부분이 json이면 Map<String,dynamic>으로 받아드린다. Map<String, dynamic> resposebody = response.data["body"]; //list의 map타입 return resposebody; } }
    💡

    코드설명

    이 코드는 PostRepository라는 클래스를 통해 API와 통신하고, 게시물 데이터를 가져오는 역할을 합니다. Dio 라이브러리를 사용해 HTTP 요청을 보내고, 응답 데이터에서 필요한 부분만 추출해 반환합니다.

    1. 클래스 설명:

    PostRepository는 API와 통신하는 리포지토리 역할을 합니다. 주로 게시물 데이터를 가져오는 메서드들이 포함되어 있습니다.

    2. 싱글톤 패턴 (주석 처리된 부분):

    • 주석 처리된 부분은 PostRepository를 싱글톤 패턴으로 구현하려는 부분입니다. 싱글톤 패턴은 객체를 하나만 생성해 재사용할 수 있도록 하는 패턴입니다.
    • 주석을 기준으로:이렇게 하면, PostRepository 인스턴스는 애플리케이션 전체에서 하나만 존재하고 이를 계속 사용할 수 있습니다. 그러나 현재는 주석으로 되어 있어서 싱글톤으로 사용되지 않고 있습니다.
      • static PostRepository instnace = PostRepository._single(); PostRepository._single();

    3. findAll 메서드:

    • 이 메서드는 서버로부터 모든 게시물 데이터를 가져오는 역할을 합니다.
      • Future<List<dynamic>> findAll() async { Response response = await dio.get("/api/post"); List<dynamic> resposebody = response.data["body"]; return resposebody; }
      • Dio를 사용한 HTTP GET 요청: /api/post 엔드포인트로 GET 요청을 보내 서버에서 데이터를 가져옵니다.
      • 응답 데이터에서 body 부분 추출: 서버 응답(response)에서 data["body"]를 추출하여 반환합니다. body는 여러 개의 게시물이 포함된 리스트(List<dynamic>)입니다.
      • 반환되는 데이터는 List<dynamic> 형태로, 여러 게시물 정보를 담고 있습니다.

    4. findById 메서드:

    • 이 메서드는 특정 ID의 게시물 데이터를 가져오는 역할을 합니다.
      • Future<Map<String, dynamic>> findById(int id) async { Response response = await dio.get("/api/post/$id"); Map<String, dynamic> resposebody = response.data["body"]; return resposebody; }
      • HTTP GET 요청: /api/post/{id}로 GET 요청을 보내 해당 ID의 게시물을 가져옵니다.
      • 응답 데이터에서 body 부분 추출: 응답 데이터에서 body를 추출해 반환합니다. 이 body는 해당 게시물의 정보를 담고 있는 Map<String, dynamic>입니다.
      • 반환되는 데이터는 특정 게시물의 세부 정보를 포함하는 Map<String, dynamic> 형태입니다.

    코드 요약:

    • PostRepository는 서버와 통신하는 기능을 담당합니다.
      • findAll: 모든 게시물을 List<dynamic> 형태로 가져옵니다.
      • findById: 특정 ID의 게시물 정보를 Map<String, dynamic>으로 가져옵니다.
    • 응답 데이터는 항상 response.data["body"] 부분에서 추출됩니다. 이는 서버에서 보내는 응답의 구조가 { "body": ... }로 되어 있음을 가정합니다.
    • Dio 라이브러리는 HTTP 통신을 간편하게 처리해주며, 비동기적으로 요청을 처리합니다.
    Share article

    Uni