[Flutter] Riverpod 사용해보기 #1

leeeeeoy·2021년 10월 6일
10

이 글은 공식 문서, 유튜브 강의와 블로그를 참고하여 정리한 글입니다.

Riverpod

Flutter의 패키지 중 하나로, GetX, Provider, BLoC처럼 상태관리를 위한 패키지이다. 쉽게 생각하면 Provider의 확장판(?) 정도로 생각할 수 있는 것 같다. 실제 Provider와 Riverpod의 개발자는 같은 사람인데, 공식 문서에 따르면 Riverpod에서는 Provider에서 발생한 여러가지 문제점을 해결했다고 한다. 다만 Provider를 완전히 대체하는 것은 아직 아니며, 실제 프로덕션 단계에서의 사용은 조금 주의를 기울여야 한다고 적혀있다.

요즘 주로 개발을 하면서 GetX를 많이 사용했다. 아무래도 상태관리 뿐만 아니라 라우팅이나 의존성 관리에도 비교적 큰 어려움 없이 사용할 수 있다는 점 때문이지 않을까 싶다. 이번에 Riverpod를 공부하면서 Riverpod도 GetX와는 다른 느낌으로 상당히 매력적인 부분들이 많았던 것 같다. 차근차근 기본 개념들을 정리해보도록 하겠다.

Package 설정

pubspec.yaml


dependencies:
	# dart
	riverpod: 
    
    	# flutter
	flutter_riverpod: 
    
    	# flutter & flutter_hooks
	flutter_hooks: 
  	hooks_riverpod: 

riverpod는 여러가지 방법으로 사용할 수 있는데 dart에서도 사용이 가능하고, flutter에서도 사용이 가능하며, flutter_hooks와 결합하여 사용이 가능하다. 이 글에서는 flutter_riverpod를 설치하여 사용했다.

기본 개념

1. Providers

사용할 provider를 정의하는 부분이다. 즉 위젯에서 공통적으로 사용하고 싶은 데이터를 정의한다고 생각하면 된다. riverpod에서는 여러가지 provider를 제공하는데 자주 쓰이는 3가지 provider에 대해 정리해보았다.

1-1. Provider

final valueProvider = Provider<int>((ref) {
  return 0;
});

가장 간단한 기본 형태의 Provider이다. Provider는 읽기만 가능하며 값을 변경할 수 없다.

1-2. StateProvider

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

StateProvider는 상태를 변경할 수 있는 Provider이다. 내부 상태는 state로 접근이 가능한데, 사용하고자 하는 위젯에서 state 값을 직접 변경할 수 있다.

1-3. StateNotifierProvider

class Counter extends StateNotifier<int> {
  Counter() : super(0);

  void increment() => state++;
  void decrement() => state--;
}

final counterStateNotifierProvider = StateNotifierProvider<Counter, int>((ref) {
  return Counter();
});

StateNotifierProvider는 상태 뿐만 아니라 일부 로직을 함께 저장할 때 사용된다. 예를 들어 다른 Provider와 결합을 하거나, 내부에서 사용할 로직을 정의할 수 있다.

Reading a provider

WidgetRef??

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

Provider를 사용하려면 먼저 전체 앱을 ProviderScope로 감싸줘야 한다. 그리고 riverpod에서 정의한 WidgetRef를 이용해 접근이 가능한데, 이 WidgetRef는 Widget과 Provider사이에 상호 작용을 도와주는 역할을 한다. 즉 WidgetRef를 통해 특정 Widget에서 특정 Provider에 접근이 가능하다고 생각하면 된다.

// 사용할 Provider
final valueProvider = Provider<int>((ref) {
  return 0;
});

// Stateless --> ComsumerWidget
class MyHomePage extends ConsumerWidget {
  
  Widget build(BuildContext context, WidgetRef ref) {
    final value = ref.watch(valueProvider);
    return Scaffold(
      body: Center(
        child: Text(
          'Value: $value',
        ),
      ),
    );
  }
}

WidgetRef를 사용하려면 Consumer, ConsumerWidget, ConsumerStatefulWidget을 통해 사용이 가능하다 (마치 GetX에서 GetBuilder, Obx, GetView 등을 사용하는 것과 비슷하다). 이 위젯들은 riverpod에서 정의한 위젯들로 WidgetRef를 포함하고 있다. 즉 StatelessWidget, StatefulWidget을 대신해서 사용된다. 아직 많이 사용은 안해봤지만 보편적으로 ConsumerWidget를 많이 사용하게 되는 것 같다.


WidgetRef를 이용해 읽기

WidgetRef를 이용해서 Provider에 접근이 가능한데 크게 3가지 방식을 접근이 가능하다. watch, listen, read를 제공하는데 문서에서는 대부분의 기능에서는 watch 사용을 권장하고 있다. 3가지 메서드 모두 다 값을 읽는 것은 동일하지만, 읽고 난 후에 동작이 조금씩 다르다.

ref.watch

  • 반응형으로 Provider의 값이 변경되면 자체적으로 다시 build 된다.
  • 비동기적으로 호출하거나, onTab, initState 등의 생명주기에서는 사용을 하면 안된다.
  • 다른 Provider와 결합할 때 아주 유용하게 쓰인다!

ref.listen

  • Provider의 값이 변경되면 값을 읽는 것이 아니라 정의한 함수를 실행한다.
  • ref.watch와 마찬가지로 build 안이나 Provider 안에서 사용되어야 한다.
  • SnackBar나 Dialog를 처리하는데 유용하다!

ref.read

  • Provider의 값을 읽어오기만 한다. 값이 변경되어도 별다른 동작을 하지 않는다.
  • 공식 문서에 따르면 특별한 경우가 아니면 사용을 하지 않는 것 같다

참고사항

공식 문서에 따르면 ref.read의 사용은 피해야 한다고 한다. 가능하면 ref.watch를 사용하는 것을 권장하며, ref.read를 build 메소드 내에서 사용하지 말라고 권장하고 있다. 또한 build 수 감소를 위해 ref.read를 사용하는 경우, ref.watch를 사용해도 똑같은 효과를 얻을 수 있다고 한다.


코드 예시

class MyCounter extends StateNotifier<int> {
  MyCounter() : super(0);

  void increment() => state++;
  void decrement() => state--;
  void initCount() => state = 0;
}

final myCounterStateNotifierProvider =
    StateNotifierProvider<MyCounter, int>((ref) {
  return MyCounter();
});

사용할 Provider를 정의했다. StateNotifierProvider를 이용했으며 증가, 감소, 초기화 로직을 추가해주었다.


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

  
  Widget build(BuildContext context, WidgetRef ref) {
    final counterRead = ref.read(myCounterStateNotifierProvider.notifier);
    final counterState = ref.watch(myCounterStateNotifierProvider);

    ref.listen(
      myCounterStateNotifierProvider,
      ((int num) {
        print('바뀔때마다 동작');
        print('ref.listen: $num');
      }),
    );

    return Scaffold(
      appBar: AppBar(
        title: const Text('Riverpod Practice'),
      ),
      body: Center(
        child: Text(
          'Value: $counterState',
          style: const TextStyle(
            fontSize: 48,
            fontWeight: FontWeight.bold,
          ),
        ),
      ),
      floatingActionButton: Align(
        alignment: Alignment.bottomRight,
        child: Row(
          mainAxisAlignment: MainAxisAlignment.end,
          children: [
            FloatingActionButton(
              heroTag: '111',
              onPressed: () => counterRead.increment(),
              child: const Icon(
                Icons.add,
              ),
            ),
            const SizedBox(width: 10.0),
            FloatingActionButton(
              heroTag: '222',
              onPressed: () => counterRead.decrement(),
              child: const Icon(
                Icons.remove,
              ),
            ),
            const SizedBox(width: 10.0),
            FloatingActionButton(
              heroTag: '333',
              onPressed: () => counterRead.initCount(),
              child: const Icon(
                Icons.refresh,
              ),
            ),
          ],
        ),
      ),
    );
  }
}

각각 ref.watch, ref.listen, ref.read의 사용예시이다. ref.watch는 값이 변경되면 Widget을 다시 build 하기 때문에 보여지는 값에서 사용을 했고, 버튼에서는 ref.read를 사용했다. 그리고 listen함수의 동작을 보기 위해 값이 변경될 때마다 변경된 값을 출력하도록 했다. 테스트를 해보면서 알았는데 초기화 버튼을 계속 누르면 ref.listen 함수가 동작하지 않는다. 즉 마치 GetX의 Obx처럼 이전 상태와 달라지지 않으면 build를 하지 않았다.

StateNotifierProvier 사용

final provider = StateNotifierProvider<MyStateNotifier, MyModel>((ref) {
  return MyStateNotifier();
});
Widget build(BuildContext context, ScopedReader watch) {
  // Provider 객체에 접근
  MyStateNotifier notifier = watch(provider.notifier); 
  // Provider의 상태(값) 반환
  MyModel state = watch(provider);
}

StateNorifierProvider를 사용할 경우, 다음과 같이 2가지 방법으로 호출이 가능하다. 하나는 객체에 접근하는 것이고, 하나는 상태(값)에 접근한다. 객체에 접근하는 경우는 해당 Provider에 필요한 로직을 호출할 때 사용하고, 상태에 접근하는 경우는 값을 반환받을 때 사용한다.


그 외에 기능들

Future & Stream Provider

이 부분은 아직 복잡한 예제를 작성해보지 못해서 간단하게 예제 코드로만 정리했다

final futureProvider = FutureProvider<int>((ref) {
  return Future.delayed(const Duration(seconds: 3), () => 5);
});

final streamProvider = StreamProvider<int>((ref) {
  int count = 0;
  return Stream.periodic(const Duration(seconds: 2), (_) => count++);
});

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

  
  Widget build(BuildContext context, WidgetRef ref) {
    final streamAsyncValue = ref.watch(streamProvider);

    final futureAsyncValue = ref.watch(futureProvider);

    return Scaffold(
      appBar: AppBar(
        title: Text('HomePage'),
      ),
      body: Column(
        mainAxisAlignment: MainAxisAlignment.spaceEvenly,
        children: [
          Center(
            child: streamAsyncValue.when(
              data: (data) => Text('Value: $data'),
              loading: (_) => const CircularProgressIndicator(),
              error: (e, st, _) => Text('Error: $e'),
            ),
          ),
          Center(
            child: futureAsyncValue.when(
              data: (data) => Text('Value: $data'),
              loading: (_) => const CircularProgressIndicator(),
              error: (e, st, _) => Text('Error: $e'),
            ),
          ),
        ],
      ),
    );
  }
}

FutureProvider는 3초 후에 값을 반환하고, StreamProvider는 2초 간격으로 값을 계속 받도록 작성했다. 이 두 Provider의 사용법은 유사한데, Freezed에서 지원하는 when을 이용해서 작성이 가능하다. 각 상태에 따른 로직을 위의 예시처럼 간단하게 작성할 수 있다.


Combining providers

Provider 끼리의 결합 역시 손쉽게 가능한데, Provider 안에서 다른 Provider의 값을 읽기만 하면 쉽게 결합할 수 있다.

final cityProvider = Provider((ref) => 'London');

final weatherProvider = FutureProvider((ref) async {
  final city = ref.watch(cityProvider);

  return fetchWeather(city: city);
});

이 때, ref.watch를 이용하여 참고하고 있는 Provider의 값이 변경되면 상태를 읽고 있는 Provider에도 변경된다.

정리

간단하게 Riverpod를 정리해봤다. 원래 사용하던 GetX와 비교해보면 장단점이 있는 것 같다. 사실 어떤걸 사용하든 결국 상태관리를 목적으로 하기 때문에 목적에 맞게 적절한 것을 택하면 될 것 같다. 개인적으로 사용해본 느낌으로는 GetX도 사용하기 상당히 쉬운 편이었다고 생각했는데, Riverpod 역시 크게 어렵지 않게 사용이 가능해보인다. 물론 아직 공부를 다 한게 아니기 때문에 그럴수도 있다... 다음에는 유튜브 강의를 참고해서 작성한 간단한 날씨 앱을 정리해보려고 한다. 강의가 1년전 영상이라 최근 Migration되면서 부분적으로 바뀐 것들이 있어서 처음에 고생좀 했다...


참고자료

profile
100년 후엔 풀스택

2개의 댓글

comment-user-thumbnail
2023년 6월 19일

잘봤습니다

답글 달기