Study/Dart,Flutter

22. [Flutter] 상태 관리(Riverpod)를 이용하여 비동기 관리하기

코딩 잘 할거얌:) 2024. 6. 20. 17:33
반응형

이번에는 Flutter의 riverpod을 이용하여 비동기관리에 대해서 설명하도록 하겠다.

내 상태


https://riverpod.dev/ko/

 

Riverpod

어디서나 공유 상태 선언하기 더 이상 main.dart과 UI 파일 사이를 오갈 필요가 없습니다. 공유 상태의 코드를 별도의 패키지에 넣든, 필요한 위젯 바로 옆에 넣든, 테스트 가능성을 잃지 않고 적절

riverpod.dev

우선 공식자료는 다음과 같다.

 

Riverpod이란?

우선 들어가기에 앞서 riverpod이 무엇인지 알아보도록 하자.

https://github.com/rrousselGit

 

rrousselGit - Overview

Flutter enthusiast. You'll find me on stackoverflow. Or as a speaker in Flutter meetups - rrousselGit

github.com

Flutter의 Provider과 Rivepod의 창시자이다. 우선 이전에 포스팅한 내용이 있지만 다시 설명하도록 하겠다.

https://pcseob.tistory.com/32

 

8. Dart, Flutter 상태관리 그리고 Riverpod (1)

이번 포스팅은 상태 관리법 중 하나인 Riverpod 알아보도록 하자. 이론적인 내용이므로 만약 코드에 바로 사용할 방법을 찾는다면 다음 포스팅을 읽으면 된다. 목차 Flutter의 상태관리 Flutter의 상태

monocsp.dev

이전 포스팅 내용.


우선 Riverpod의 큰 특징을 설명하도록 하겠다.

  • Provider의 제한사항이 없다.
  • 어디서든 명시된 상태를 공유한다.
  • Flutter에 종속적이지 않다.

크게 이렇게 3가지의 특징을 가지고 있다. 간단하게 설명하자면,

Provider의 제한사항이 없다.

개인적으로 provider로 개발했을 때 가장 불편했던점은 다른 provider에서 또 다른 provider의 종속이 생길 때 가장 불편했다. provider의 경우에는 Widget Tree를 기준으로 상태관리가 된다. 그래서 상위 widget을 업데이트하는 경우에는 Proxy Provider를 사용하여 꽤나 불편했다. 하지만 riverpod의 경우에는 상태관리가 전역으로 이루어지므로 Widget tree의 영향을 받지 않는다.

어디서든 명시된 상태를 공유한다.

위에서 이어지는 내용으로 provider의 경우에는 Widget tree의 순서를 따르게 되므로 Flutter의 Widget tree에 익숙하다면 편리할 수 있겠지만, 대부분의 경우는 불편할 때가 많다. 그래서 상태가 전역적으로 관리되는 riverpod이 조금 더 편리한 경우가 많다.

Flutter에 종속적이지 않다.

Provider는 내부적으로 InheritedWidget을 베이스로 두고 있다. 그래서 반드시 Flutter를 사용해야만 사용할 수 있다. dart로만 쓸 수 없다는 뜻이다. riverpod은 flutter 없이 순수 dart언어에서도 사용할 수 있는 특징이 있다. 그래서 Widget tree에 관계없이 전역적으로 처리할 수 있다.


Riverpod으로 비동기 처리하기

이제 본격적으로 riverpod으로 비동기 처리하는 법을 알아보도록 하자.

AsyncValue를 이용하여 비동기 데이터를 처리하였다. 

AsyncValue

https://pub.dev/documentation/riverpod/latest/riverpod/AsyncValue-class.html

 

AsyncValue class - riverpod library - Dart API

A utility for safely manipulating asynchronous data. By using AsyncValue, you are guaranteed that you cannot forget to handle the loading/error state of an asynchronous operation. It also exposes some utilities to nicely convert an AsyncValue to a differen

pub.dev

AsyncValue를 이용하여 data, loading, error를 잡을 수 있다. 이거를 riverpod과 같이 사용하면 조금 더 편리하게 작업을 진행할 수 있어서 개인적으로 선호하는 편이다.

아래는 riverpod과 AsyncValue를 사용한 코드이다.

// 이 파일의 이름을 'upload_provider.g.dart'로 지정합니다.
// 이 파일은 `build_runner`를 통해 생성될 것입니다.
part 'upload_provider.g.dart';

@riverpod
class UploadProvider extends _$UploadProvider {
  // 초기 상태로 빈 UploadFileEntity를 담은 AsyncData를 반환합니다.
  @override
  AsyncValue<UploadFileEntity> build() => AsyncData(UploadFileEntity.empty());

  // 파일 업로드를 처리하는 비동기 메서드입니다.
  Future uploadFile({
    required String filePath, // 업로드할 파일의 경로
    required UploadType type, // 업로드 타입 (파일 또는 이미지)
    required UploadPartType uploadPartType, // 업로드 파트 타입
  }) async {
    // 현재 상태가 로딩 중인 경우, 업로드를 중단합니다.
    if (state.isLoading) return;

    // 상태를 로딩 중으로 설정합니다.
    state = const AsyncLoading();

    try {
      // 업로드 결과를 담을 변수입니다.
      Either<ErrorModel, UploadFileEntity> result;
      
      // 업로드 타입에 따라 다른 업로드 유즈케이스를 실행합니다.
      if (type == UploadType.file) {
        result = await UploadFileUsecase()
            .excute(filePath: filePath, uploadPartType: uploadPartType);
      } else {
        result = await UploadImageUsecase()
            .excute(filePath: filePath, uploadPartType: uploadPartType);
      }

      // 업로드가 성공한 경우, 상태를 성공 데이터로 설정합니다.
      if (result.isRight()) {
        state = AsyncData(result.asRight());
      } else {
        // 업로드가 실패한 경우, 상태를 에러로 설정합니다.
        state = AsyncError(result.asLeft(), StackTrace.current);
      }
    } catch (error) {
      // 예외가 발생한 경우, 상태를 커스텀 에러 모델을 담은 에러로 설정합니다.
      state = AsyncError(
        left(
          CustomErrorModel.errorModel(
            errorCode: '700',
            errorDescription: 'Upload provider error $error',
            errorMessage: '업로드에 실패했습니다.',
          ),
        ),
        StackTrace.current,
      );
    }
  }
}

개인적으로 Either를 사용하여 두 가지 타입반환까지 하였다. 이 부분까지 따라 할 필요는 없다.

https://pub.dev/packages/either_dart

 

either_dart | Dart package

Error handler library for type-safe and easy work with errors on Dart and Flutter. Either is an alternative to Nullable value and Exceptions.

pub.dev

이렇게 사용하고 riverpod generate를 실행하면. g파일이 생성이 된다.

비동기 실행부.

// `uploadProviderProvider`의 상태 변경을 구독하고, 업로드 작업을 실행합니다.
ref.watch(uploadProviderProvider.notifier).uploadFile(
    // 업로드할 파일의 경로를 설정합니다.
    filePath: (result).path,
    // 업로드 타입을 이미지로 설정합니다.
    type: UploadType.image,
    // 업로드 파트 타입을 사용자 사진으로 설정합니다.
    uploadPartType: UploadPartType.userPicture,
);

선언한 provider를 다음과 같이 ref.watch로 사용하면 된다.

UI 부

// 업로드 타입을 정의하는 열거형입니다.
enum UploadType { image, file }

// 업로드 위젯을 정의하는 클래스입니다.
class UploadWidget extends ConsumerWidget {
  // 업로드 타입 (image 또는 file)을 나타내는 필드입니다.
  final UploadType type;

  // 업로드가 성공했을 때 표시할 위젯을 반환하는 함수입니다.
  final Widget Function(UploadFileEntity) child;

  // 업로드 중 에러가 발생했을 때 표시할 위젯을 반환하는 함수입니다.
  final Widget Function(Object, StackTrace) errorWidget;

  // 업로드가 진행 중일 때 표시할 위젯을 반환하는 함수입니다.
  final Widget Function() loadingWidget;

  // 생성자입니다. 필요한 매개변수들을 받아 초기화합니다.
  const UploadWidget({
    super.key,
    required this.type,
    required this.child,
    required this.errorWidget,
    required this.loadingWidget,
  });

  // 위젯 빌드 메서드입니다.
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // uploadProviderProvider를 구독하고 상태에 따라 다른 위젯을 표시합니다.
    return ref
        .watch(uploadProviderProvider) // 상태를 구독합니다.
        .when(
          // 데이터가 로드된 경우 child 함수를 호출하여 위젯을 반환합니다.
          data: child,
          // 에러가 발생한 경우 errorWidget 함수를 호출하여 위젯을 반환합니다.
          error: errorWidget,
          // 로딩 중인 경우 loadingWidget 함수를 호출하여 위젯을 반환합니다.
          loading: loadingWidget,
        );
  }
}

ConsumerWidget을 받아와서 ref.watch.when을 통해 data, error, loading을 처리할 수 있다.

개인적으로 error의 경우에는 errorWidget을 따로 페이지를 만들어 통일감 있게 해 주는걸 조금 더 선호하지만, 이건 기획마다 다르므로 알맞게 적용하면 된다.


궁금한 게 있거나 수정사항 있다면 댓글 부탁드립니다.

728x90