로그인을 성공하여 토큰을 가지고 있다고 가정하고 토큰을 요청헤더에 넣어서 데이터를 받는다.
void main() async {
await fetchPostList_test();
}
Future<void> fetchPostList_test() async {
String jwt = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJjb3PthqDtgbAiLCJpZCI6MSwiZXhwIjoxNjgyNjQyNTQzfQ.sRajDXrS-wBg64kBlyU5ZtxtsMMSsr279ssHyn8VY95ICVFYSMBGYze9yEnfRggbtD7adxwMlMvOAELfUk276g";
ResponseDTO responseDTO = await PostRepository().fetchPostList(jwt);
List<Post> postList = responseDTO.data;
for (var element in postList) {
print(element.title);
}
}
헤더에 토큰을 넣고 get 요청을 한다.
Future<ResponseDTO> fetchPostList(String jwt) async {
try {
Response response = await dio.get('/post',
options: Options(headers: {"Authorization": jwt})); // 헤더에 토큰
ResponseDTO responseDTO =
ResponseDTO.fromJson(response.data); // dart 오브젝트로 파싱
List<dynamic> mapList = responseDTO.data; // 데어터에 리스트가 들어있음 // 묵시적 형변환
// List<dynamic> mapList = responseDTO.data as List<dynamic> // 명시적 형변환
List<Post> postList =
mapList.map((e) => Post.fromJson(e)).toList(); // dynamic 요소를 Post로 변환
responseDTO.data = postList; // 응답 데이터에 Post
return responseDTO;
} catch (e) {
return ResponseDTO(code: -1, msg: "실패 : $e");
}
}
응답 결과는
"code": 1,
"msg": "목록보기완료",
"data": [
{
"id": 5,
"title": "제목5",
"content": "내용5",
"user": {
"id": 2,
"username": "cos",
"password": "1234",
"email": "cos@nate.com",
"created": "2021-07-10T08:05:49.039117",
"updated": "2021-07-10T08:05:49.039117"
},
"created": "2021-07-10T08:05:49.069037",
"updated": "2021-07-10T08:05:49.069037"
},
{
"id": 4,
"title": "제목4",
"content": "내용4",
"user": {
"id": 2,
"username": "cos",
"password": "1234",
"email": "cos@nate.com",
"created": "2021-07-10T08:05:49.039117",
"updated": "2021-07-10T08:05:49.039117"
},
"created": "2021-07-10T08:05:49.068049",
"updated": "2021-07-10T08:05:49.068049"
},
{
"id": 3,
"title": "제목3",
"content": "내용3",
"user": {
"id": 1,
"username": "ssar",
"password": "1234",
"email": "ssar@nate.com",
"created": "2021-07-10T08:05:49.0082",
"updated": "2021-07-10T08:05:49.0082"
},
"created": "2021-07-10T08:05:49.062057",
"updated": "2021-07-10T08:05:49.062057"
},
{
"id": 2,
"title": "제목2",
"content": "내용2",
"user": {
"id": 1,
"username": "ssar",
"password": "1234",
"email": "ssar@nate.com",
"created": "2021-07-10T08:05:49.0082",
"updated": "2021-07-10T08:05:49.0082"
},
"created": "2021-07-10T08:05:49.060063",
"updated": "2021-07-10T08:05:49.060063"
},
{
"id": 1,
"title": "제목1",
"content": "내용1",
"user": {
"id": 1,
"username": "ssar",
"password": "1234",
"email": "ssar@nate.com",
"created": "2021-07-10T08:05:49.0082",
"updated": "2021-07-10T08:05:49.0082"
},
"created": "2021-07-10T08:05:49.057069",
"updated": "2021-07-10T08:05:49.057069"
}
]
}
플러터에서는 Provider
로 상태를 관리한다.
이를 위해서 실행되는 앱을 ProviderScope
로 감싸게 되어 앱의 어느 위치에서도 Provider
에 접근이 가능해진다.
여러개의 Provide
r에 효율적으로 관리하기 위해서는 개발자가 Provider
의 범위를 적절히 구분하여 사용해야 한다.
예를 들어 앱의 하나의 페이지를 관리하는 Provider
, 하나의 패키지를 관리하는 Provider
, 앱 전체를 관리하는 Provider
로 나뉘게 된다.
지난 포스팅에서 언급했던 세션을 관리하는 Provider
는 앱 전체를 관리한다고 볼 수 있다.
// 토큰으로 세션 관리
final sessionProvider = Provider<SessionUser>((ref) {
return SessionUser();
});
ProviderContainer
에는 이러한 여러 Provider
리스트를 관리한다.
Provider
의 상태과 변경되면 구독하고 있는 위젯에 변화를 알리고, Provider
끼리의 의존관계도 관리한다.
ProviderContainer
는 어디서든 Provider
에 접근할 수 있도록 ProviderReference
, 즉 Ref
를 생성하고 관리한다.
어디서든 Ref
를 통해 Provider
에 접근이 되고 생성할 수 있게 된다.
개인적인 생각으로 Provider
의 존재 이유는 어디서든 접근 가능하다는 이점이 가장 큰 것 같다.
Riverpod
은 Provider
를 기반으로 하는 플러터 상태관리 라이브러리로서 Provider
와 마찬가지로 의존성 주입 패턴을 사용한다.
Riverpod
를 이용해서 해당 페이지에 이용되는 데이터와 데이터를 관리하는 뷰모델(데이터 스토어) 뷰모델에 접근하도록하는 Provider
(스토어 관리자)를 알아보자
read
를 하면 뷰모델을 보고 watch
를 하면 뷰모델의 state
를 본다.
먼저 해당 페이지에서 사용되는 데이터를 담을 오브젝트를 만든다. -> 모델
class PostHomePageModel {
List<Post> posts;
PostHomePageModel({required this.posts});
}
해당 데이터를 저장하고 데이터를 조작하는 스토어를 만든다.(VM) -> 뷰모델(스토어)
이때 모델은 데이터가 null일수도 있으므로 null을 허용한다. -> ?
class PostHomePageViewModel extends StateNotifier<PostHomePageModel?> {
PostHomePageViewModel(super.state);
void notifyInit(String jwt) async {
// 위에서 테스트함
ResponseDTO responseDTO = await PostRepository().fetchPostList(jwt);
// state 는 StateNotifier가 관리하는 데이터 -> 모델
state = PostHomePageModel(posts: responseDTO.data);
}
}
어디서든 데이터를 가지고 있는 뷰모델에 접근하도록 Provider
를 만든다. -> 스토어 관리자
// <창고, 데이터?> 필요
final postHomePageProvider = StateNotifierProvider.autoDispose<
PostHomePageViewModel, PostHomePageModel?>((ref) {
SessionUser sessionUser = ref.read(sessionProvider);
return PostHomePageViewModel(null)
..notifyInit(sessionUser.jwt!); // 로그인되어 있을때만 호출해야함
});
autoDispose
로 해당 위젯이 메모리에서 제거되면 Provider
도 자동으로 제거되도록 설정한다.
인증이 필요한 기능일 경우 ref.read(sessionProvider)
를 넣어 다른 Provider
를 의존하도록 설정할 수 있다.
뷰모델은 상태를 변경하고 관리하는 기능을 가진다.
메소드를 추가해서 상태를 변경한다. -> 뷰모델에 메소드 추가
class PostHomePageViewModel extends StateNotifier<PostHomePageModel?> {
PostHomePageViewModel(super.state);
void notifyAdd(Post post) {
if (state == null) {
List<Post> posts = [post]; // 입력된 데이터 추가
state = PostHomePageModel(posts: posts);
} else {
List<Post> posts = state!.posts;
List<Post> newPosts = [...posts, post];
state = PostHomePageModel(posts: newPosts);
}
}
void notifyRemove(int id) {
List<Post> posts = state!.posts;
List<Post> newPosts = posts.where((e) => e.id != id).toList(); // true만 통과
state = PostHomePageModel(posts: newPosts);
}
void notifyUpdate(Post post) {
List<Post> posts = state!.posts;
List<Post> newPosts = posts.map((e) => e.id == post.id ? post : e).toList();
state = PostHomePageModel(posts: newPosts);
}
void notifyInit(String jwt) async {
ResponseDTO responseDTO = await PostRepository().fetchPostList(jwt);
state = PostHomePageModel(posts: responseDTO.data);
}
}
페이지의 특정한 폼이나 버튼등의 이벤트를 통해 서버로 데이터를 보내고 Provider
를 갱신하면 구독중인 위젯이 다시 build
된다.
마찬가지로 어디서든 컨트롤러에 접근할 수 있도록 컨트롤러를 Provider
에 등록한다.
final postControllerProvider = Provider<PostController>((ref) {
return PostController(ref);
});
class PostController {
final mContext = navigatorKey.currentContext;
final Ref ref;
PostController(this.ref);
Future<void> refresh() async {
SessionUser sessionUser = ref.read(sessionProvider);
ref.read(postHomePageProvider.notifier).notifyInit(sessionUser.jwt!);
}
Future<void> deletePost(int id) async {
SessionUser sessionUser = ref.read(sessionProvider);
// delete 요청 후 성공하면 Provider에서 상태 제거
await PostRepository().fetchDelete(id, sessionUser.jwt!);
ref.read(postHomePageProvider.notifier).notifyRemove(id);
Navigator.pop(mContext!);
}
Future<void> updatePost(int id, String title, String content) async {
PostUpdateReqDTO postUpdateReqDTO =
PostUpdateReqDTO(title: title, content: content);
SessionUser sessionUser = ref.read(sessionProvider);
// update 요청 -> Provider 상태 추가
ResponseDTO responseDTO = await PostRepository().fetchUpdate(id, postUpdateReqDTO, sessionUser.jwt!);
// 상세 데이터 수정
ref.read(postDetailPageProvider(id).notifier).notifyUpdate(responseDTO.data);
// 목록 데이터 수정
ref.read(postHomePageProvider.notifier).notifyUpdate(responseDTO.data);
Navigator.pop(mContext!);
}
Future<void> savePost(String title, String content) async {
PostSaveReqDTO postSaveReqDTO =
PostSaveReqDTO(title: title, content: content);
SessionUser sessionUser = ref.read(sessionProvider);
// save 요청 -> Provider에 추가
ResponseDTO responseDTO = await PostRepository().fetchSave(postSaveReqDTO, sessionUser.jwt!);
ref.read(postHomePageProvider.notifier).notifyAdd(responseDTO.data);
Navigator.pop(mContext!);
}
}
update
시에만 postDetailPageProvider
가 호출되는데 Provider
의 autoDispose
설정에 의한것이다.
설명은 아래에 나와 있다.
레파지토리는 데이터를 송수신하고 파싱한다.
레파지토리는 컨트롤러에서만 호출하므로 Provider
에 등록하지 않는다.
class PostRepository {
// 매번 호출 될때 마다 싱글톤을 반환한다.
static final PostRepository _instance = PostRepository._single();
factory PostRepository() {
return _instance;
}
PostRepository._single();
Future<ResponseDTO> fetchPost(int id, String jwt) async {
try {
Response response = await dio.get("/post/$id",
options: Options(headers: {"Authorization": "$jwt"}));
ResponseDTO responseDTO = ResponseDTO.fromJson(response.data);
responseDTO.data = Post.fromJson(responseDTO.data);
return responseDTO;
} catch (e) {
return ResponseDTO(code: -1, msg: "실패 : ${e}");
}
}
Future<ResponseDTO> fetchDelete(int id, String jwt) async {
try {
Response response = await dio.delete("/post/$id",
options: Options(headers: {"Authorization": "$jwt"}));
ResponseDTO responseDTO = ResponseDTO.fromJson(response.data);
return responseDTO;
} catch (e) {
return ResponseDTO(code: -1, msg: "실패 : ${e}");
}
}
Future<ResponseDTO> fetchUpdate(
int id, PostUpdateReqDTO postUpdateReqDTO, String jwt) async {
try {
Response response = await dio.put(
"/post/$id",
options: Options(headers: {"Authorization": "$jwt"}),
data: postUpdateReqDTO.toJson(),
);
ResponseDTO responseDTO = ResponseDTO.fromJson(response.data);
responseDTO.data = Post.fromJson(responseDTO.data);
return responseDTO;
} catch (e) {
return ResponseDTO(code: -1, msg: "실패 : ${e}");
}
}
Future<ResponseDTO> fetchSave(
PostSaveReqDTO postSaveReqDTO, String jwt) async {
try {
Response response = await dio.post(
"/post",
options: Options(headers: {"Authorization": "$jwt"}),
data: postSaveReqDTO.toJson()
);
ResponseDTO responseDTO = ResponseDTO.fromJson(response.data);
responseDTO.data = Post.fromJson(responseDTO.data);
return responseDTO;
} catch (e) {
return ResponseDTO(code: -1, msg: "실패 : ${e}");
}
}
}
하나의 게시글을 관리하려면 각 게시글을 관리하는 Provider
가 필요하므로 family
를 Provider
에 붙여서 동일한 타입의 여러 Provider
를 생성한다.
final postDetailPageProvider = StateNotifierProvider
.family.autoDispose<PostDetailPageViewModel, PostDetailPageModel?, int>((ref, postId) {
SessionUser sessionUser = ref.read(sessionProvider);
return PostDetailPageViewModel(null, ref)..notifyInit(postId, sessionUser.jwt!);
});
상세보기를 하면 처음에는 상태가 없는 뷰모델을 리턴하고 notifyInit
을 실행하여 데이터를 뷰모델에 넣게 된다.
그동안 페이지에서는 CircularProgressIndicator()
를 보여주다가 데이터가 변하면 다시 build
가 된다.
// 창고 데이터
class PostDetailPageModel{
Post post;
PostDetailPageModel({required this.post});
}
// 창고
class PostDetailPageViewModel extends StateNotifier<PostDetailPageModel?>{
Ref ref;
PostDetailPageViewModel(super.state, this.ref);
void notifyInit(int id, String jwt) async{
ResponseDTO responseDTO = await PostRepository().fetchPost(id, jwt);
state = PostDetailPageModel(post: responseDTO.data);
}
// autoDispose에 의해서 해당 메소드를 호출하지 않아도 Provider가 메모리를 제거한다.
void notifyRemove(int id){
Post post = state!.post;
if(post.id == id){
state = null;
}
}
// api 수정 요청 -> 수정된 Post를 돌려받음.
void notifyUpdate(Post updatePost){
state = PostDetailPageModel(post: updatePost);
}
}