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
이 반환됩니다. 만약 model
이 null
이면 CircularProgressIndicator
가 표시되어 데이터를 불러오는 중임을 나타냅니다.- 데이터 렌더링:
model
이null
이 아닐 경우,ListView.separated
를 사용하여 게시글 목록을 출력합니다. 각 게시글은ListTile
을 통해 표현되며,id
와title
이 표시됩니다.
- 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나 데이터베이스에서 해당 게시물의 데이터를 가져오는 로직을 포함하고 있을 것입니다.- 코드 수정해보기
- 생성자에서
final
키워드 사용:id
를 한 번만 설정하고 변경하지 않는 경우에는final
키워드를 사용하여 불변성을 보장하는 것이 좋습니다.
final int id;
PostDetailPage(this.id);
- 페이지 이동 시 데이터 유지:
페이지 간 이동 시
Navigator
를 통해 전달된id
를 제대로 유지하는지 확인이 필요합니다. 현재 코드는 이를 잘 처리하고 있으나, 만약 데이터를 추가적으로 받아와야 한다면FutureBuilder
나AsyncValue
를 이용해 비동기 데이터 처리가 필요할 수 있습니다.
- 최종코드
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();
}
model
이 null
일 경우, 로딩 상태를 나타내는 인디케이터를 반환합니다. 데이터를 성공적으로 받아오면 이후의 본문 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
값을 받아 데이터를 처리할 때는 WidgetRef
를 build
메서드에 받아 사용합니다. 창고(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을 사용하여
PostDetailVM
과 PostDetailModel
을 관리하는 방식으로, 게시물의 상세 데이터를 비동기적으로 불러와 상태를 관리하는 패턴을 구현하고 있습니다.창고 (View): PostDetailVM
- StateNotifier:
PostDetailVM
은StateNotifier
를 상속받아 상태를 관리하는 역할을 합니다. 여기서 상태는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
함수를 사용해createdAt
과updatedAt
을 포맷팅합니다.이 클래스를 통해 게시물의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;
}
/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;
}
/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