[Flutter] Riverpod으로 CRUD 구현

merci·2023년 4월 4일
0

Flutter

목록 보기
17/24


위젯의 상태관리를 위젯 외부의 store에서 하는 여러 방법중에서 Riverpod을 이용해서 CRUD를 구현해보자

MVCS 아키텍쳐

먼저 아키텍처를 구상한다.
스프링을 배워 MVC패턴에 익숙해져 있기 때문에 MVC를 변형한 MVCS아키텍처를 구상했다.
일반적인 MVC아키텍쳐에서 Store를 추가한 아키텍처인데 뷰에서 특정 이벤트를 발생시키면 컨트롤러와 레파지토리를 통해 Store를 갱신한다.
뷰는 Provider를 구독중이므로 변경감지를 통해 화면을 새롭게 렌더링한다.

각각의 역할은 ?

뷰는 로직을 알지 못하고 화면을 렌더링하고 컨트롤러를 호출하는 코드만 존재한다.
레파지토리는 데이터를 송수신하고 Dart오브젝트로 파싱하는 역할만 한다.
컨트롤러는 레파지토리를 호출하고 리턴된 Dart오브젝트를 이용해 Provider의 상태를 제어 한다.
Provider(Store)는 관리중인 state(클래스)를 CRUD로 제어하는 로직을 구현한다.


의존성 추가 및 폴더 구조

dependencies:
  flutter:
    sdk: flutter
  dio: ^5.1.1
  flutter_riverpod: ^2.3.2

ProviderScope 지정

void main() {
  runApp(
    const ProviderScope(
      child: MyApp(),
    ),
  );
}


View 작성

ConsumerWidget를 상속해서 Provider를 구독하는 클래스를 만든다.

class PostHomePage extends ConsumerWidget {
  const PostHomePage({Key? key}) : super(key: key);

  
  Widget build(BuildContext context, WidgetRef ref) {
    PostController pc = ref.read(postController);  
    PostHomePageModel? pm = ref.watch(postHomePageProvider); // Provider를 지켜본다.

    return Scaffold(
      body: Column(
        children: [
          Expanded(
              child: pm != null ? buildListView(pm.posts) : buildListView([])), // 리스트
          buildButton(pc), // 버튼
        ],
      ),
    );
  }

Riverpod에서 정의된 추상클래스 WidgetRef를 통해서 Provider에 접근 가능하고 ref.watch를 통해서 변경을 감지하면 해당 위젯을 다시 build한다.

postController는 컨트롤러의 Provider 레퍼런스다.
뷰에서는 Provider 레퍼런스를 이용하여 의존성을 주입받은 것처럼 컨트롤러의 메소드를 호출할 수 있게 된다.

PostController pc = ref.read(postController);  


Controller 작성

컨트롤러는 레파지토리를 호출하고 Provider를 갱신한다.

// PostController 위에 작성
final postController = Provider<PostController>((ref) {
  return PostController(ref);
});

Provider에서 관리할 상태(클래스)의 타입으로 컨트롤러를 등록한다.
매개변수 refProviderContainer의 레퍼런스를 의미하고 내부의 Provider에 접근이 가능해진다.
Provider는 다른 Provider를 포함하거나 조합하여 반환할 수 있는데 이때 ref를 이용하여 다른 Provider에 접근한다.


class PostController {

  Ref ref;
  PostController(this.ref);
  // 밑과 이어짐

생성자 인자로 ProviderContainer의 레퍼런스를 주입해서 컨트롤러가 의존하도록 한다.
이를 통해 컨트롤러는 내부 Provider의 상태를 관리하게 된다.
PostController의 상태를 관리 -> 게시글들의 상태를 crud로 관리할 수 있다.


Future 클래스

I/O같이 시간이 많이 걸리는 작업은 비동기적으로 처리하고 결과를 반환한다.
비동기적으로 연속된 데이터를 처리하는 클래스로 Stream도 있다.

Future클래스는 제네릭을 통해 반환받을 타입을 지정한다.
Future클래스는 비동기 작업을 하므로 async/await와 함께 사용된다.

아래의 코드에서는 Repository를 호출하고 반환받은 결과를 Provider에 갱신하고 있다.

  Future<void> findPosts() async {
    List<Post> homePagePostDto = await PostRepository().findAll();
    ref.read(postHomePageProvider.notifier).state = PostHomePageModel(posts: homePagePostDto);
  }

ref.read를 이용해서 ProviderContainer 내부의 Provider객체를 가져온다.
notifier를 이용해서 StateNotifier객체의 상태를 변경하거나, 다른 위젯에서 상태를 읽을 수 있게 된다.
stateStateNotifier의 기본 메소드로서 상태를 저장한다.

  Future<void> addPost(String title) async {
    Post post = await PostRepository().save(title);
    ref.read(postHomePageProvider.notifier).add(post);
  }

  Future<void> removePost(int id) async {
    PostRepository().deleteById(id); 
    ref.read(postHomePageProvider.notifier).remove(id); 
  }

  Future<void> updatePost(Post post) async {
    Post postPS = await PostRepository().update(post);
    ref.read(postHomePageProvider.notifier).update(postPS);
  }
}

데이터를 CRUD로 관리하기 위해 add, remove, update 메소드를 이용한다.
위 메소드들은 StateNotifier의 메소드가 아니므로 직접 구현해야 한다.

컨트롤러의 메소드를 뷰에서 다음처럼 호출하게 된다.

	// 이벤트 - 각각의 버튼
    onPressed: () {       
       pc.findPosts();									// read
       // pc.addPost("제목추가"); 						// create
       // pc.removePost(1); 							// delete
       // pc.updatePost(Post(id: 2, title: "제목수정")); // update
    },


Repository 작성

레파지토리는 데이터를 송수신하고 json과 Dart오브젝트를 서로 파싱해주는 역할만 한다.
I/O 과정은 시간이 소요되므로 Future클래스를 이용해서 비동기 통신을 진행한다.

class PostRepository {
  // 싱글톤 생성 ( IoC 컨테이너에 등록된 것 같은 효과 )
  static PostRepository _instance = PostRepository._single();

  PostRepository._single(); // 기본 생성자
  
  factory PostRepository(){ // factory가 메모리에 주소가 없다면 새로 만들고 있다면 재사용한다.
    return _instance;
  }

  // DB는 생략함, 더미데이터를 만들어 데이터를 송수신한다고 가정 !!
  Future<List<Post>> findAll() {
    return Future.delayed(Duration(seconds: 1), () { // I/O에 1초가 걸렸다고 가정
      return [
        Post(id: 1, title: "제목1"),
        Post(id: 2, title: "제목2"),
        Post(id: 3, title: "제목3"),
      ];
    });
  }

  // save할때 데이터 영속화 -> REST API를 통해 저장된 데이터를 반환받음
  Future<Post> save(String title) {
    return Future.delayed(Duration(seconds: 1), () {
      return Post(id: 4, title: title); // id는 임의 - 자동생성키로 가정
    });
  }

  Future<void> deleteById(int id) {
    return Future.delayed(Duration(seconds: 1));
  }
  
  // 마찬가지로 save -> 영속화 객체 존재(더티체킹) or update -> 데이터 반환
  Future<Post> update(Post post) {
    return Future.delayed(Duration(seconds: 1), () {
      return post;
    });
  }
}


StateNotifierProvider

MVC+S아키텍처에서 상태를 관리할 Provider를 생성한다.

Store에서 관리할 상태 데이터를 등록한다.
json을 파싱한 여러 Dart오브젝트가 들어갈 수 있다.

class PostHomePageModel {
  List<Post> posts;
  PostHomePageModel({required this.posts});
}



상태 데이터 PostHomePageModel를 관리할 Store를 생성한다.
상태를 관리하기 위한 클래스인 StateNotifier를 상속하여 상태를 관리하는 메소드를 정의할 수 있게 되고 상태 변경 시점을 제어할 수 있게 된다.

class PostHomePageViewModel extends StateNotifier<PostHomePageModel?>{
  PostHomePageViewModel(super.state);
}



스토어를 관리할 StateNotifierProvider 를 등록한다.
StateNotifierProviderStateNotifierProvider로 제공해준다.
즉, 어느 위치에서도 Provider를 통해 상태를 제어할 수 있게 된다.

final postHomePageProvider = StateNotifierProvider<PostHomePageViewModel, PostHomePageModel?>((ref) {
  return PostHomePageViewModel(null);
});

첫번째 인자는 관리하려는 스토어를 넣는다. -> StateNotifier타입
두번째 인자는 스토어에서 관리할 상태를 넣는다. (스토어가 제어하는 상태 클래스)
상태데이터가 처음에는 없을수도 있기 때문에 null을 이용해서 초기화를 시켰다.

스토어에 들어갈 타입을 구체적으로 지정한다면 매번 코드를 수정해야 하는 불편함이 생길수도 있어 상태클래스를 만들고 상태클래스의 필드를 변화를 준다.

특정한 페이지에서 여러 테이블을 조합한 데이터를 필요로 한다면 해당 데이터를 받을 수 있는 상태클래스를 새로 만들고 위 과정으로 새로운 Provider를 생성해서 관리하면 된다.

구조적인 Dto를 받아서 오브젝트 변환

레파지토리에서 Dart오브젝트와 dio라이브러리를 이용해서 파싱을 한다.

class Post {
  int? userId;
  int? id;
  String? title;
  String? body;
  User? user; // User 오브젝트를 포함

  Post({
    this.userId,
    this.id,
    this.title,
    this.body,
    this.user,
  });

  factory Post.fromJson(Map<String, dynamic> json) => Post(
        userId: json["userId"],
        id: json["id"],
        title: json["title"],
        body: json["body"],
        user: User.fromJson(json["user"]), // 오브젝트를 넣어줌
      );

  Map<String, dynamic> toJson() => {
        "userId": userId,
        "id": id,
        "title": title,
        "body": body,
        "user": user?.toJson(),  // null 이 아닐때 메소드 접근
      };
}

StateNotifier 상태 관리

StateNotifier에서는 상태를 제어할 메소드를 등록할 수 있다.

class PostHomePageViewModel extends StateNotifier<PostHomePageModel?>{
  PostHomePageViewModel(super.state);
  
  // 초기화 ( 화면 새로 고침 한다면 )
  void init(List<Post> postDtoList){ 
    state = PostHomePageModel(posts: postDtoList);
  }

  // save 기능 추가
  void add(Post post) {
    List<Post> posts = state!.posts;
    // 스프레드 연산자로 새로운 컬렉션 생성 - 깊은 복사
    List<Post> newPosts = [...posts, post]; // 복사 + 추가
    state = PostHomePageModel(posts: newPosts);
  }

state를 이용해서 상태클래스 PostHomePageModel에 접근한다.

변경감지는 레퍼런스 주소가 달려져야 작동한다.
스프레드 연산자를 이용해서 새로운 컬렉션을 생성하면 레퍼런스 주소가 달라진다.

레퍼런스 주소를 다르게 하여 변경감지를 하는 이점을 예로들면
10만개의 리스트가 있는 컬렉션의 데이터를 하나만 변경했을때 변경감지를 한다면 리스트를 모두 돌면서 데이터를 비교하는해야 해야하지만 컬렉션의 레퍼런스를 비교하면 훨씬 효율적으로 변경감지를 하게 된다.
또한 새로운 컬렉션을 만들게 되면 개발자의 실수도 줄여준다.
하지만 레퍼런스가 변경되더라도 내부 데이터가 그대로라면 플러터는 다시 렌더링 하지 않는다


  // delete
  void remove(int id){
    List<Post> posts = state!.posts;
    List<Post> newPosts = posts.where((e) => e.id != id).toList(); 
    state = PostHomePageModel(posts: newPosts);
  }

스트림의 where는 지정한 조건을 만족하는 요소를 추출하여 새로운 스트림을 생성한다.
따라서 where조건을 이용해서 제거를 하거나 검색된 오브젝트를 반환받을 수 있다.

  // update
  void update(Post post){
    List<Post> posts = state!.posts;
    List<Post> newPosts = posts.map((e) => e.id == post.id ? post : e).toList();
    state = PostHomePageModel(posts: newPosts);
  }
}

스트림의 map은 값을 변환하여 새로운 스트림을 반환한다.
특정 게시글만 수정하고 나머지는 그대로 복사한뒤 상태에 저장한다.

구현하기

이러한 과정을 통해서 MVC+S아키텍처에서는 어디서든 원하는 모델의 상태를 제어하고 변화가 필요한 위젯만 build하게 되어 쓸데 없이 많은 위젯을 렌더링 하지 않아 효율적인 메모리 관리와 자원을 사용하게 된다.

profile
작은것부터

0개의 댓글