[Flutter] 실제 데이터로 RiverPod CRUD 구현

merci·2023년 4월 22일
0

Flutter

목록 보기
22/24

테스트 코드

로그인을 성공하여 토큰을 가지고 있다고 가정하고 토큰을 요청헤더에 넣어서 데이터를 받는다.

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 상태관리

플러터에서는 Provider로 상태를 관리한다.
이를 위해서 실행되는 앱을 ProviderScope로 감싸게 되어 앱의 어느 위치에서도 Provider에 접근이 가능해진다.
여러개의 Provider에 효율적으로 관리하기 위해서는 개발자가 Provider의 범위를 적절히 구분하여 사용해야 한다.
예를 들어 앱의 하나의 페이지를 관리하는 Provider, 하나의 패키지를 관리하는 Provider, 앱 전체를 관리하는 Provider로 나뉘게 된다.

지난 포스팅에서 언급했던 세션을 관리하는 Provider는 앱 전체를 관리한다고 볼 수 있다.

// 토큰으로 세션 관리
final sessionProvider = Provider<SessionUser>((ref) {
  return SessionUser();
});

ProviderContainer

ProviderContainer에는 이러한 여러 Provider리스트를 관리한다.
Provider의 상태과 변경되면 구독하고 있는 위젯에 변화를 알리고, Provider끼리의 의존관계도 관리한다.
ProviderContainer는 어디서든 Provider에 접근할 수 있도록 ProviderReference, 즉 Ref를 생성하고 관리한다.
어디서든 Ref를 통해 Provider에 접근이 되고 생성할 수 있게 된다.
개인적인 생각으로 Provider의 존재 이유는 어디서든 접근 가능하다는 이점이 가장 큰 것 같다.

Riverpod

RiverpodProvider를 기반으로 하는 플러터 상태관리 라이브러리로서 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);
  }
}

Controller

페이지의 특정한 폼이나 버튼등의 이벤트를 통해 서버로 데이터를 보내고 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가 호출되는데 ProviderautoDispose설정에 의한것이다.
설명은 아래에 나와 있다.

Repository

레파지토리는 데이터를 송수신하고 파싱한다.
레파지토리는 컨트롤러에서만 호출하므로 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

하나의 게시글을 관리하려면 각 게시글을 관리하는 Provider가 필요하므로 familyProvider에 붙여서 동일한 타입의 여러 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);
  }
}
profile
작은것부터

0개의 댓글