[Flutter] Riverpod 상태관리

Noah·2024년 4월 14일
0

Flutter

목록 보기
4/11

패키지 설치

flutter pub add flutter_riverpod
flutter pub add riverpod_annotation
flutter pub add dev:riverpod_generator
flutter pub add dev:build_runner
flutter pub add dev:custom_lint
flutter pub add dev:riverpod_lint

pubspec.yaml

name: my_app_name
environment:
  sdk: ">=3.0.0 <4.0.0"
  flutter: ">=3.0.0"

dependencies:
  flutter:
    sdk: flutter
  flutter_riverpod: ^2.5.1
  riverpod_annotation: ^2.3.5

dev_dependencies:
  build_runner:
  custom_lint:
  riverpod_generator: ^2.4.0
  riverpod_lint: ^2.3.10

Code Generation

작성한 Riverpod을 Generation 해준다. provider.g.dart 파일이 생긴다.

dart run build_runner watch

종속성 주입 (ProviderScope)

main.dart

void main() {
  runApp(
    // Riverpod을 설치하려면 다른 모든 위젯 위에 이 위젯을 추가해야 합니다.
    // 이 위젯은 "MyApp" 내부가 아니라 "runApp"에 직접 파라미터로 추가해야 합니다.
    ProviderScope(
      child: MyApp(),
    ),
  );
}

Annotation

모든 프로바이더는 @riverpod 또는 @Riverpod()로 어노테이션해야 한다. 이 어노테이션은 전역 함수나 클래스에 배치할 수 있습니다.

예를 들어, @Riverpod(keepAlive: true)를 작성하여 "auto-dispose"를 비활성화할 수 있다.

@riverpod
Result myFunction(MyFunctionRef ref) {
  <your logic here>
}

Ref

다른 providers와 상호작용하는 데 사용되는 객체다.
모든 providers에는 provider 함수의 매개변수(parameter) 또는 Notifier의 속성(property)으로 하나씩 가지고 있다. 이 객체의 타입은 함수/클래스의 이름에 의해 결정된다.

Http Get 요청

provider.dart

import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'activity.dart';

// 코드 생성이 작동하는 데 필요합니다.
part 'provider.g.dart';

/// 그러면 `activityProvider`라는 이름의 provider가 생성됩니다.
/// 이 함수의 결과를 캐시하는 공급자를 생성합니다.
@riverpod
Future<Activity> activity(ActivityRef ref) async {
  // package:http를 사용하여 Bored API에서 임의의 Activity를 가져옵니다.
  final response = await http.get(Uri.https('boredapi.com', '/api/activity'));
  // 그런 다음 dart:convert를 사용하여 JSON 페이로드를 맵 데이터 구조로 디코딩합니다.
  final json = jsonDecode(response.body) as Map<String, dynamic>;
  // 마지막으로 맵을 Activity 인스턴스로 변환합니다.
  return Activity.fromJson(json);
}
  • 네트워크 요청은 UI가 provider를 한 번 이상 읽을 때까지 실행되지 않는다. (lazy)
  • 이후 읽기는 네트워크 요청을 다시 실행하지 않고 이전에 가져온 활동을 반환한다.
  • UI가 이 공급자의 사용을 중단하면 캐시가 삭제된다. 그런 다음 UI가 이 공급자를 다시 사용하면 새로운 네트워크 요청이 이루어진다.

데이터 랜더링

consumer.dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

import 'activity.dart';
import 'provider.dart';

/// The homepage of our application
class Home extends StatelessWidget {
  const Home({super.key});

  @override
  Widget build(BuildContext context) {
    return Consumer(
      builder: (context, ref, child) {
        // Read the activityProvider. This will start the network request
        // if it wasn't already started.
        // By using ref.watch, this widget will rebuild whenever the
        // the activityProvider updates. This can happen when:
        // - The response goes from "loading" to "data/error"
        // - The request was refreshed
        // - The result was modified locally (such as when performing side-effects)
        // ...
        final AsyncValue<Activity> activity = ref.watch(activityProvider);

        return Center(
          /// Since network-requests are asynchronous and can fail, we need to
          /// handle both error and loading states. We can use pattern matching for this.
          /// We could alternatively use `if (activity.isLoading) { ... } else if (...)`
          child: switch (activity) {
            AsyncData(:final value) => Text('Activity: ${value.activity}'),
            AsyncError() => const Text('Oops, something unexpected happened'),
            _ => const CircularProgressIndicator(),
          },
        );
      },
    );
  }
}
  • Consumer의 ref를 통해 provider를 읽는다.
  • Stateless 대신 CunsumerWidget을 extends 하는 방법도 있다.
  • ref.watch를 통해 데이터를 구독하고 상태가 바뀔 때마다 Consumer Widget이 리빌드 된다.

Notifier 정의

Method 형식 (Notifier X)

@riverpod
Future<List<Todo>> todoList(TodoListRef ref) async {
  // 네트워크 요청을 시뮬레이션합니다. 이는 일반적으로 실제 API로부터 수신됩니다.
  return [
    Todo(description: 'Learn Flutter', completed: true),
    Todo(description: 'Learn Riverpod'),
  ];
}

Class 형식 (Notifier)

@riverpod
class MyNotifier extends _$MyNotifier {
  @override
  Result build() {
    <your logic here>
    state = AsyncValue.data(42);
  }
  <your methods here>
}

notifier

  • Notifiers는 providers의 "상태저장 위젯(stateful widget)"이다.
  • @riverpod 어노테이션이 클래스에 배치되면 해당 클래스를 "Notifier"라고 부른다.
  • 클래스는 _$NotifierName을 확장해야 하며, 여기서 NotifierName은 클래스 이름입니다.
  • Notifiers는 provider의 상태(state)를 수정하는 메서드를 노출할 책임이 있다.
  • 이 클래스의 공개 메서드는 ref.read(yourProvider.notifier).yourMethod()를 사용하여 consumer가 액세스할 수 있다.

build method

  • 모든 notifiers는 build 메서드를 재정의(override)해야 한다.
  • 이 메서드는 일반적으로 notifier가 아닌 provider(non-notifier provider)에서 로직을 넣는 위치에 해당한다.
  • 이 메서드는 직접 호출해서는 안 된다.

ref Method

ref.watch

  • 일반적으로 유지 관리가 더 쉽기 때문에 일반적으로 다른 옵션보다 ref.watch를 사용할 수 있도록 코드를 설계하는 것이 좋습니다.
  • ref.watch 메서드는 provider를 받아 현재 상태를 반환합니다. 그러면 리스닝된 provider가 변경될 때마다 provider가 무효화(invalidated)되고 다음 프레임 또는 다음 읽기(read) 시 다시 빌드됩니다.
  • ref.watch를 사용하면 로직이 "reactive"이면서 "declarative"이게 됩니다.
    즉, 필요할 때 로직이 자동으로 다시 계산(recompute)된다는 뜻입니다. 그리고 업데이트 메커니즘이 'on change'와 같은 부작용(side-effects)에 의존하지 않습니다. 이는 StatelessWidgets의 작동 방식과 유사합니다.
  • 예를 들어 사용자의 위치를 수신하는 provider를 정의할 수 있습니다. 그런 다음 이 위치를 사용하여 사용자 근처의 레스토랑 목록을 가져올 수 있습니다.

ref.read

  • 이 옵션은 provider의 현재 상태를 반환한다는 점에서 ref.watch와 유사합니다. 하지만 ref.watch와 달리 공급자를 수신(listen)하지 않습니다.
  • 따라서 ref.read는 Notifier의 메서드 내부와 같이 ref.watch를 사용할 수 없는 곳에서만 사용해야 합니다.

ref.listen / listenSelf

  • ref.listen 메서드는 ref.watch의 대안입니다.
    이 메서드는 기존의 "listen"/"addListener" 메서드와 유사합니다. 이 메서드는 provider와 callback을 받으며, provider의 콘텐츠가 변경될 때마다 해당 callback을 호출합니다.
  • ref.listen 대신 ref.watch를 사용할 수 있도록 코드를 리팩토링하는 것이 일반적으로 권장되는데, 전자는 명령형으로 인해 오류가 발생하기 쉽기 때문입니다.
    하지만 ref.listen는 큰 리팩토링을 하지 않고도 빠른 로직을 추가하는 데 유용할 수 있습니다.

캐시 지우기 및 상태 폐기 (disposal)

  • 코드 생성(code-generation)을 사용할 때 기본적으로 provider가 수신이 중지되면 상태가 파괴됩니다.
    이는 리스너에 전체 프레임에 대한 활성 리스너가 없을 때 발생합니다. 이 경우 상태가 소멸됩니다.

  • 이 동작은 keepAlive: true를 사용하여 해제(opted out)할 수 있습니다.
    이렇게 하면 모든 리스너가 제거될 때 상태가 소멸되는 것을 방지할 수 있습니다.

// We can specify "keepAlive" in the annotation to disable
// the automatic state destruction
@Riverpod(keepAlive: true)
int example(ExampleRef ref) {
  return 0;
}

상태 폐기될 때 로직 실행 방법

@riverpod
Stream<int> example(ExampleRef ref) {
  final controller = StreamController<int>();

  // When the state is destroyed, we close the StreamController.
  ref.onDispose(controller.close);

  // TO-DO: Push some values in the StreamController
  return controller.stream;
}

생명주기 (Life Cycles)

  • 상태가 폐기될 때 호출되는 ref.onDispose
  • provider의 마지막 리스너가 제거될 때 호출되는 ref.onCancel.
  • onCancel이 호출된 후 새 리스너가 추가될 때 호출되는 ref.onResume.

수동으로 강제 삭제

  • ref.invalidate를 사용하면 현재 provider 상태가 파괴됩니다. 그러면 두 가지 결과가 발생할 수 있습니다:
  • provider가 청취되고 있으면 새 상태가 생성됩니다.
    provider를 청취되고 있지 않으면 provider가 완전히 소멸됩니다.
class MyWidget extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return ElevatedButton(
      onPressed: () {
        // On click, destroy the provider.
        ref.invalidate(someProvider);
      },
      child: const Text('dispose a provider'),
    );
  }
}

ref.keepAlive를 사용하여 폐기(disposal)를 조정하기

  • 위에서 언급했듯이 자동 폐기를 사용하도록 설정하면 provider에 전체 프레임에 대한 리스너가 없는 경우 상태가 삭제됩니다.
  • 하지만 이 동작을 보다 세밀하게 제어하고 싶을 수도 있습니다. 예를 들어, 성공한 네트워크 요청의 상태는 유지하되 실패한 요청은 캐시하지 않으려 할 수 있습니다.
  • 이는 자동 폐기를 활성화한 후 ref.keepAlive를 사용하면 가능합니다. 이 함수를 사용하면 상태의 자동 폐기를 중지하는 시점을 결정할 수 있습니다.
@riverpod
Future<String> example(ExampleRef ref) async {
  final response = await http.get(Uri.parse('https://example.com'));
  // We keep the provider alive only after the request has successfully completed.
  // If the request failed (and threw), then when the provider stops being
  // listened, the state will be destroyed.
  ref.keepAlive();

  // We can use the `link` to restore the auto-dispose behavior with:
  // link.close();

  return response.body;
}

특정 시간 동안 상태를 살아있게 유지하기

  • 현재 Riverpod은 특정 시간 동안 상태를 유지하는 내장된 방법을 제공하지 않습니다.
  • 하지만 지금까지 살펴본 도구를 사용하면 이러한 기능을 쉽게 구현하고 재사용할 수 있습니다.
  • Timer + ref.keepAlive를 사용하면 특정 시간 동안 상태를 유지할 수 있습니다. 이 로직을 재사용할 수 있게 하려면 확장 메서드(extension method)로 구현하면 됩니다.
extension CacheForExtension on AutoDisposeRef<Object?> {
  /// Keeps the provider alive for [duration].
  void cacheFor(Duration duration) {
    // Immediately prevent the state from getting destroyed.
    final link = keepAlive();
    // After duration has elapsed, we re-enable automatic disposal.
    final timer = Timer(duration, link.close);

    // Optional: when the provider is recomputed (such as with ref.watch),
    // we cancel the pending timer.
    onDispose(timer.cancel);
  }
}
@riverpod
Future<Object> example(ExampleRef ref) async {
  /// Keeps the state alive for 5 minutes
  ref.cacheFor(const Duration(minutes: 5));

  return http.get(Uri.https('example.com'));
}
profile
Flutter Specialist

0개의 댓글