flutter / riverpod

rO_Or·2024년 4월 2일

Dart 그리고 Flutter

목록 보기
10/19

riverpod은 상태 관리 라이브러리 provider의 애너그램, 개발자도 같다고 함.

설치

flutter pub add flutter_riverpod을 콘솔에 입력하거나
pubspec.yaml 파일에 의존성을 추가하면 된다.

ProviderScope

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

ProviderScope는 모든 프로바이더의 상태를 저장하는 위젯이다.

프로바이더 생성 및 읽기

final helloWorldProvider = Provider<String>((ref) {
	return 'Hello, World';
});

helloWorldProvider 프로바이더의 상태를 읽는 데 사용할 전역 변수.
Provider<String> 어떤 타입의 프로바이더인지 선언.
{...} 상태를 생성하는 함수. 읽거나 해제 등, 다양한 로직을 실행하는 데 필요한 ref 파라미터가 제공된다.

사용

플러터에서 위젯은 트리 내부의 항목에 접근할 수 있는 BuildContext 객체를 가지고 있다.
하지만 riverpod 프로바이더는 위젯 트리의 바깥에 존재하기 때문에
내부 항목에 접근하기 위한 WidgetRef라는 참조 객체가 필요하다.

ConsumerWidget 사용

final helloWorldProvider = Provider<String>((ref) {
  return 'Hello, World';
});

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

class MyApp extends ConsumerWidget {
  const MyApp({super.key});

  
  
  Widget build(BuildContext context, WidgetRef ref) {
    final helloWorld = ref.watch(helloWorldProvider);
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: const Text('Example')),
        body: Center(
          child: Text(helloWorld),
        )
      ),
    );
  }
}

StatelessWidget 대신 ConsumerWidget을 상속받으면 프로바이더를 watch할 수 있는
WidgetRef 타입의 ref 객체가 추가로 전달된다.
가장 일반적인 방법으로 주로 사용된다.

Consumer 사용

// ... 코드 동일
class MyApp extends StatelessWidget {
  const MyApp({super.key});

  
  
  Widget build(BuildContext context) {

    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: const Text('Example')),
        body: Consumer(
          builder: (_, WidgetRef ref, __) {
            final helloWorld = ref.watch(helloWorldProvider);
            return Text(helloWorld);
          }
        ),
      ),
    );
  }
}

Consumer로 접근할 위젯을 감싸는 방식도 있다.

이 경우, ref 객체는 Consumer의 builder 메소드의 인자 중 하나가 된다.

위 예제는 Scaffold를 감싸지 않고 오로지 Text 위젯만 감싸고 있다.
이러면 프로바이더의 값이 변경될 때, Text 위젯만 리빌딩된다.

복잡한 레이아웃을 가진 큰 위젯 클래스에서 일부분만 리빌드되길 원할 경우 이런 방식을 쓰는 듯하다.

ConsumerStatefulWidget 및 ConsumerState 사용

class MyApp extends ConsumerStatefulWidget {
  const MyApp({super.key});

  
  ConsumerState<MyApp> createState() => _MyAppState();
}

class _MyAppState extends ConsumerState<MyApp> {

  
  void initState() {
    super.initState();

    final helloWorld = ref.read(helloWorldProvider);
    print(helloWorld);
  }

  
  Widget build(BuildContext context) {
    final helloWorld = ref.watch(helloWorldProvider);
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: const Text('Example')),
        body: Center(
          child: Text(helloWorld),
        )
      ),
    );
  }
}

ConsumerStateWidgetConsumerState를 상속받으면
ConsumerWidget처럼 build 메소드에 ref 객체가 전달되어서 ref.watch를 사용하여 프로바이더에 접근할 수 있게 된다.
다른 프로바이더를 읽어야한다면 ref.read를 사용하여 읽어올 수 있다.

State 클래스가 BuildContext 객체를 어디서든 접근할 수 있는 것과 마찬가지로
ConsumerState가 WidgetRef를 프로퍼티로 가지고 있기 때문에, ref에 어디서든 접근할 수 있다.

WidgetRef
WidgetRef를 참조하면 프로바이더의 값을 watch하거나 read할 수 있다.
즉, 위젯이 프로바이더와 상호작용할 수 있는 객체이다.
이 위젯을 사용하면 앱 내부의 모든 프로바이덩 접근할 수 있다.
모든 리버팟 프로바이더가 전역으로 설계되었기 때문이다.

프로바이더 종류

  • Provider
  • StateProvider (Legacy)
  • StateNotifierProvider (Legacy)
  • FutureProvider
  • StreamProvider
  • ChangeNotifierProvider (Legacy)
  • NotifierProvider (riverpod 2.0)
  • AsyncNotifierProvider (riverpod 2.0)

Provider

final helloWorldProvider = Provider<String>((ref) {
	return 'Hello, World';
});

Provider는 변경되지 않거나, 의존성이 있는 객체에 접근하는 데 유용하다.
Repository나 Logger, 변경되지 않는 상태를 갖는 다른 클래스에 접근할 때 사용된다.

StateProvider (Legacy)

final counterStateProvider = StateProvider<int>((ref) {
  return 0;
});

StateProvider는 카운트와 같이, 변경될 수 있는 간단한 상태 객체를 저장하는 용도로 적합하다.

build 메소드에서 StateProvider를 watch하고 있다면, 상태가 변경될 때 위젯이 리빌드된다.
상태를 업데이트하려면 WidgetRef 객체를 통하여 read를 호출하고
StateProvider의 notifier를 전달하면 state 값을 변경할 수 있다.

class CounterWidget extends ConsumerWidget {
  const CounterWidget({super.key});

  
  Widget build(BuildContext context, WidgetRef ref) {
    final counter = ref.watch(counterStateProvider);
    return ElevatedButton(
      onPressed: () => ref.read(counterStateProvider.notifier).state++,
      child: Text('Value: $counter')
    );
  }
}

enum, String, boolean, number와 같은 간단한 상태 변수를 저장하기 위해 만들어졌다고 한다.
NotifierProvider 역시 같은 용도로 사용할 수 있지만, 더 활용할 수 있다.
복잡하거나, 비동기 상태를 다루기 위해서는 AsyncNotifierProvider FutureProvider StreamProvider를 사용하면 좋다.

StateNotifierProvider (Legacy)

StateNotifier를 listen하거나 사용하는 용도의 프로바이더.
StateNotifierProvider와 StateNotifier는 사용자의 입력이나 이벤트에 의해 변경되는 상태를 관리하기에 적합하다.

class Clock extends StateNotifier<DateTime> {
  
  late final Timer _timer;
  
  Clock() : super(DateTime.now()) {
    _timer = Timer.periodic(const Duration(seconds: 1), (_) {
      state = DateTime.now();
    });
  }

  
  void dispose() {
    _timer.cancel();
    super.dispose();
  }
}

위 클래스는 생성자가 호출될 때, super(DateTime.now())에 의해서
state 프로퍼티가 DateTime.now()의 값으로 초기화된 후 Timer.periodic 메소드에 의해 1초마다 state가 변경된다.

final clockProvider = StateNotifierProvider<Clock, DateTime>((ref) {
  return Clock();
});

위 프로바이더는, Clock 객체를 생성하는 프로바이더이다.

class ClockWidget extends ConsumerWidget {
  const ClockWidget({super.key});

  
  Widget build(BuildContext context, WidgetRef ref) {
    final currentTime = ref.watch(clockProvider);
    final timeFormatted = DateFormat().add_Hms().format(currentTime);
    return Text(timeFormatted);
  }
}

ConsumerWidget의 build 메소드에서 ref.watch로 clockProvider를 호출하면
해당 위젯은 프로바이더가 제공하는 Clock의 state가 변경될 때마다 리빌드된다.
(이 경우에는 1초마다 리빌드된다.)

ref.watch()는 StateNotifierProvider가 반환하는 Clock 객체가 아닌, state를 반환한다.
Clock 객체에 접근하려면 ref.read()를 사용해야 한다.

2.0으로 업그레이드된 후 위 StateNotifierAsyncNotifier로 대체될 수 있다고 한다.

FutureProvider

StateNotifierProvider로 비동기를 처리하지 않고, FutureProvier로 하는 게 좋다고 한다.

FutureProvider는 autoDispose라는 Modifier와 자주 사용된다.

반환되는 유형은 AsyncValue이다.
AsyncValue는 플러터에서 비동기 데이터를 처리하기 위한 유틸리티 클래스이다.

  • 비동기 연산과 수행과 캐싱
  • 비동기 작업의 에러와 로딩 상태 처리
  • 여러 비동기 값 결합
  • 비동기 데이터 다시 가져오기 및 새로고침

StreamProvider

StreamProvider는 실시간 API 결과인 Stream을 watch하여 반응형으로 위젯을 리빌드시키는 프로바이더이다.

역시, AsyncValue가 반환되는데, when을 통해서 data, loading, error 상태 처리를 할 수 있다.

ChangeNotifierProvider (Legacy)

ChangeNotifier 클래스는 Flutter SDK의 일부이다.
이를 이요하여 상태를 저장하거나, ChangeNotifier를 구독하는 객체에게 알려줄 수 있다.

ref.watch 와 ref.read

프로바이더의 값을 가져오기 위해서는 ref.watch를 사용하는데, 프로바이더의 값에 의존하고 있는 위젯이 다시 리빌드된다.
ref.watch를 사용해서는 안 되는 경우도 있는데 버튼의 콜백 내부에서 프로바이더 값에 접근할 때 ref.read를 사용해야 한다.

class CounterWidget extends ConsumerWidget {
  const CounterWidget({super.key});

  
  Widget build(BuildContext context, WidgetRef ref) {
    final counter = ref.watch(counterStateProvider);
    return ElevatedButton(
      onPressed: () => ref.read(counterStateProvider.notifier).state++,
      child: Text('Value: $counter')
    );
  }
}
  • ref.watch를 빌드 메서드에서 호출하면, 프로바이더의 상태를 관찰하고, 변경되면 다시 리빌드된다.
  • ref.read를 호출하여 프로바이더의 상태를 한 번만 읽는다.
    initState나 build가 아닌 위젯의 다른 라이프 사이클 메서드에서 사용할 수 있다.

ref.read는 provider.notifier를 호출하는데, 상태를 변경하기 위해 호출했다.
provider.notifier는 StateProvider 및 StateNotifierProvider에서만 사용할 수 있다.

위 둘은 위젯 내부에서 사용할 수도 있고, 프로바이더 내부에서도 사용할 수 있다.

프로바이더 상태 변경 감지

ref.listen는 프로바이더의 상태가 변경될 때, Dialog나 Snackbar를 이용해
변경된 상태를 표시하고 싶은 경우 이 메소드를 사용하면 된다.

ref.listen의 콜백으로 이전 상태와 현재 상태가 전달된다.
이 상태를 Snackbar나 Dialog로 표시하면 된다.
빌드 메소드가 호출될 떄가 아니라, 프로바이더 값이 변경될 때 콜백을 통해 전달된다.

autoDispose

FutureProvider나 StreamProvider 사용 시, 프로바이더가 더 이상 필요 없어 제거하고 싶을 때가 있는데
이때, autuDispose Modifier를 추가하면 된다.

이 메소드를 사용하면 프로바이더를 watch하고 있는 페이지가 사라지면, 따라서 제거가 된다.

autoDispose로 생성된 프로바이더를 watch, listen 하고 있는 모든 위젯이 제거되면
프로바이더가 제공하는 상태도 함께 제거가 된다.

캐싱 타임아웃

ref.keepAlive를 사용하면 사용자가 페이지를 나갔다가 다시 돌아왔을 때
요청이 다시 실행되지 않도록 할 수 있다.

keppAlive에 의해 유지되는 상태는 새로고침이나 무효화된 상태가 필요한 경우 다시 요청하게 된다.

이 메소드가 반환하는 KeepAliveLink를 사용하면 타이머와 연동하여 특정 시간 이후
close를 호출해 프로바이더가 제공하는 상태를 제거할 수도 있다.

family

family는 프로바이더에 값을 전달할 때 사용한다.

profile
즐거워지고 싶다.

0개의 댓글