카테고리 없음

[세미나] Flutter의 상태관리 라이브러리 비교

taby 2023. 4. 13. 10:28

프런트 개발은 데이터들의 상태관리가 매우 중요합니다. 상태란 사용자에게 보여지는 모든 ui에 영향을 줄 수 있는 모든 데이터들이라고 할 수 있습니다. 하드코딩으로 앱개발을 하지 않는 이상 상태관리는 꼭 필요한데요. 이런 상태관리를 용이하게 해주기 위해 flutter는 많은 상태관리 라이브러리들이 있습니다. 데이터의 복잡도와 크기에 따라서 또는 팀원들의 숙련도와 인원 수 에따라서 상태관리 라이브러리를 결정하곤 합니다. 
오늘은 저희가 사용하는 Flutter에서의 상태관리 라이브러리들의 특징을 알아보려고 합니다.

 

상태관리란 무엇일까요?

상태라고 하면 추상적으로 다가올 수 있지만 상태란 Data와 동일하다고 생각하면 상태관리라는 말이 좀 더 자연스럽게 다가옵니다.

flutter에 대입하여 보면 flutter는 앱을 구성하는 모든 것이 위젯이고 모든 위젯은 상태를 가지고 있습니다. 

StatelessWidget도 마찬가지로 상태가 변하지 않는 것일 뿐 State를 가지고 있습니다. flutter에서 위젯과 상태는 1:1 관계로 위젯은 항상 상태를 가지고 있다고 생각하면 됩니다.
상태관리란 즉, 변화되는 데이터에 맞게 필요한 화면에 표현해주고 유저의 반응에 대응해 주는 것으로 즉 상태가 변화됨에 맞춰서 필요한 모든 컴포넌트에서 데이터를 공유하는 것입니다. 

 

상태관리가 필요한 이유

첫번째 이유는, 앱이 커지면서 하나의 상태를 각각의 위젯에서 공유받아야할 경우 setState만으로는 원하는대로 구현하기 힘들고 복잡해질 수 있습니다. 코드를 보다 체계적이고 유지보수하기 쉽게 만들기 위해 상태관리가 필요합니다.

상태를 관리하는 파일과 UI파일을 구분하여 보다 관리하게 쉽게 만들어 줄 수 있습니다.

두번재 이유는 앱의 성능 향상입니다.

상태관리를 사용하면 UI의 일부만 업데이트 해야할 때 전체 UI를 다시 빌드하지 않아도 됩니다.
이런 방식으로 렌더링하는데 걸리는 시간을 단축시키고 앱의 전반적인 성능을 향상시킬 수 있습니다.

Flutter 프레임워크 자체에서 InheritedWidget과 FutureBuilder와 같은 기본적인 상태관리 도구를 제공합니다.
이를 통해 Todo앱등 간단한 앱에는 적용하기 적합할 수있지만 규모가 큰 프로젝트의 아키텍처를 관리 가능한 상태로 유지하려면 상태관리 프레임워크가 필요하다고 생각됩니다.

 

다시 본론으로 돌아가서 많은 상태관리 프레임워크들이 있지만 3개의 프레임워크의 특징을 비교해 보려고 합니다.

2023년 기준 제일 인기 있는 상태관리 프레임워크 탑 7 중 getX, Provider, Riverpod을 알아보겠습니다.

 

getX에 관하여

저희가 현재 사용하고 있는 상태관리 라이브러리로 pud.dev에 가장 많은 좋아요를 받고 있습니다.
구문이 단순하고 사용하지않는 리소스는 자동으로 제거해줘서 관리가 편하다는 장점이 있습니다.

getX의 상태관리에는 두가지 유형이 있습니다.

 

단순 상태관리자 : GetBuilder 기능을 사용하여 상태를 관리합니다. 

반응형 상태관리자 : GetX 및 obx를 사용하여 상태를 관리합니다. 

 

getX에서 상태관리를 위한 클래스를 생성할 때는 getXConrtroller를 상속받게 됩니다. 

getXController는 상태를 관리하고 뷰와 비즈니스 로직 사이의 인터페이스 역할을 수행합니다. 

 

먼저 GetBuilder를 이용한 단순 상태관리부터 살펴보겠습니다.

getXController 코드입니다. 

class Controller extends GetxController{
  int _count = 0;
  int get count => _count;

  void increment(){
      _count++;
      update();
  }

update()함수는 getXController에서 제공하는 메서드로 이 메서드가 호출이 되어야 외부에서 state가 변한 것을 알 수 있게 됩니다. 

기능적으로 statefulWidget의 setState와 같은 역할을 하는 메서드입니다.


뷰쪽 코드입니다.

class MyPage extends StatelessWidget {
  const MyPage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
  //get.put과 init 중 하나만 사용하면 됩니다.
  Controller controller = Get.put(Controller());

    return Scaffold(
      appBar: AppBar(
        title: const Text('GetX'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          crossAxisAlignment: CrossAxisAlignment.center,
          children: [
            GetBuilder<Controller>(
              // init: Controller(),
                builder: (_) {
              return Text(
                '${controller.count}',
                style: TextStyle(fontSize: 20, color: Colors.red),
              );
            }),
            SizedBox(
              height: 20,
            ),
            ElevatedButton(
              onPressed: () {
                controller.increment();
              },
              child: Text('Add'),
            ),
          ],
        ),
      ),
    );
  }
}

GetBuilder를 통해 상태를 변경해야하는 ui에 의존성 주입을 하여 사용합니다. GetBuilder 파라미터의 init을 사용하여 직접 Coutroller인스턴스를 생성해도 되고 Get.put을 통해 생성하여도 됩니다. 

단순상태관리자를 사용하면 statelessWidget 처럼 비용이 많이 들지 않기 때문에 메모리를 아낄 수 있지만 데이터를 업데이트 하기 위해서는 update()메소드를 일일이 호출해주어야 한다는 단점도 있습니다. 

 

두번째로 반응형 상태관리자가 무엇인지 살펴보겠습니다. 

 

반응형 상태관리자는 데이터 스트림과 데이터 변경에 대한 반응에 중점을 둔 프로그래밍 패러다임을 차용하고 있으며,
state가 지속적으로 변하는 경우에 자동으로 상태를 업데이트 시켜줄때 적합하게 사용할 수 있습니다. 

단순 상태관리자와는 달리 obx getx라는 stream 데이터를 다루어야하기 때문에   무거운 상태관리 방식입니다. 그렇지만 부담스러울 정도로 무겁거나 메모리를 많이 차지하지는 않는다고 합니다. 

반응형 상태관리는 단순 상태관리처럼 GetxController를 상속하지 않아도 됩니다. 대신 Rx라이브러리를 통해 상태를 관리합니다.  

 

Rx는 getX를 이용하다보면 자주 접하게 되는데 이 Rx는 무엇인지 잠시 살펴보겠습니다. 

 

Rx는 Reactive Extensions의 약자로, 데이터 스트림의 처리를 위한 다양한 연산자를 제공합니다. Rx의 핵심 개념은 옵저버블(Observable)과 옵저버(Observer)입니다. 옵저버블은 이벤트 스트림을 표현하며, 옵저버는 이벤트를 처리하는 객체입니다.

GetX에서는 Rx의 옵저버블과 옵저버를 사용하여 상태를 관리합니다.

 

obx를 사용하는 Controller 코드입니다.

class Controller extends GetxController{
  final count = 0.obs;

  void updateInfo(){
    count.value++;
  }
}

위의 obs는 observable의 약자입니다. obs는 다양한 데이터 타입으로 사용할 수 있으며, 데이터 타입에 대한 Rx 버전을 제공합니다. 예를 들어, obs를 사용하여 RxInt, RxDouble, RxString, RxList, RxMap, RxSet 등등을 만들 수 있습니다.

obs를 사용하여 생성한 Rx변수의 값이 변경될 때마다 UI가 자동으로 업데이트 됩니다. 

그럼 이 Rx변수를 view에서는 어떻게 불러와서 사용할 수 있는지 보겠습니다. 

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("GetX Counter"),
      ),
      body: Column(
      children : [
      Center(
        child: Obx(() => Text(
              'Count: ${count.value}',
              style: TextStyle(fontSize: 24.0),
            ),
           ),
           Center(
                  child: GetX<Controller>(
                  init : Controller(),
                builder: (_) => Text(
                  '${controller().count.value}',
                  style: TextStyle(fontSize: 20, color: Colors.white),
                ),
              )),
       	],
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: increment,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }
}

앞서 말했던대로 Obx와 getX를 사용하여 위젯에 컨트롤러를 전달해줄 수 있습니다.

Obx를 사용하는 경우는 Observable(obs)의 변화를 listen하고 있고 반드시 Controller의 인스턴스가 다른 곳에 미리 initalize 되어 있야 합니다.  

GetX의 경우도 마찬가지로 obs의 변화를 listen하고 있지만 자체적으로 Controller인스턴스를 initialize 할 수 있다는 차이점이 있습니다. GetX를 사용하는 경우 자체적으로 다양한 기능들을 가지고 있는 만큼 obx를 사용할 때 보다 무겁다고 합니다.

 

getX라이브러리는 위와 같이 사용성이 간단하고 BuildContext를 참조하지 않아도 되어 편리하다는 장점이 있습니다. 

그러나 BuildContext를 사용하지 않는 객체 참조 방식은 동일한 클래스의 인스턴스를 두개  이상 등록을 해야할 때 일일이 tag값을 따로 전달하는 방식으로 진행할 수 밖에 없어 getX를 사용하는 유저들이 뽑은 불편한 점이라고도 합니다. 

 

 

Provider에 관하여

provider는 flutter 공식 Google IO에서 추천하는 상태관리로, 내부적으로는 ChangeNotifier라는 관찰자 패턴과 InheritedWidget 매커니즘을 사용하여 상태를 업데이트합니다. 또한 Buildcontext를 통해 아래에 전파되는 위젯을 관리하여 찾아서 변경하기 때문에 context를 꼭 필요로 합니다. Provider는 inheritedWidget을 단순화하여 구현한 패키지입니다. 

 

inheritedWidget이란

inheritedWiddget은 앞서 말씀드린대로 flutter에서 기본적으로 제공하는 상태를 전달하기 위한 방법입니다. 위젯트리의 상위에서 하위로 데이터를 전달하는 방식이며 데이터를 공유하기 위해 inheritedWidget을 구현하고 데이터 변경을 감지하여 데이터가 변경될 경우 child 위젯들에게 알려줍니다.  InheritedWidget은 자체 build() 함수를 가지지 않으므로 위젯 계층 구조에서 자신의 하위에 위치할 위젯을 생성자의 매개변수로 받아 상위 생성자에 전달합니다. 결국 super()에 매개변수로 지정하는 위젯이 자신의 하위 위젯이며, 이 위젯부터 그 하위에 있는 모든 위젯이 InheritedWidget의 상태를 이용할 수 있습니다.

InheritedWidget의 하위 위젯이 InheritedWidget을 이용하려면 InheritedWidget에서 제공하는 of() 메서드를 호출하면 됩니다. 
of()메서드를 사용하면 위젯 계층 구조에 있는 InheritedWidget 객체가 전달되므로 이 객체를 이용해 필요한 데이터나 함수를 이용하면 됩니다.

. of() 메서드는 하위 위젯에서 이용할 InheritedWidget을 반환해야 하는데 이때 dependOnInheritedWidgetOfExactType()이라는 함수를 사용했습니다. 이 함수는 위젯 계층 구조에서 of() 함수를 호출한 위젯과 가장 가까운 InheritedWidget을 반환해 줍니다.

 

ChangeNotifier는 무엇일까요?

flutter 기본 라이브러리인 foundation 라이브러리에서 제공하는 유틸리티 클래스로 관찰자패턴을 제공합니다. ChangeNotifier를 mixin하여 사용하게 되면 ChangeNotifier클래스를 listen하고 있는 모든 위젯들에게 데이터가 변경될때마다 내부에 있는 notifyLister()를 호출하여 변경된 사실을 알려줍니다.

위젯들이 ChangeNotifier를 listen할 수 있는 이유는 내부적으로 addListener()라는 메서드를 보유하고 있고, 이 메서드를 통해서 콜백메서드를 등록할 수 있기 때문입니다.

그렇지만 addListener 메서드는 자동으로 dispose가 되지 않기 때문에 매번 dispose를 시켜주어야 한다는 번거로움이 있습니다. 또한 listener가 데이터가 변경되었다는 사실을 addListener를 통해 알 수는 있지만 자동으로 UI를 업데이트 시켜주지는 않습니다. 

그래서 Provider는 ChangeNotifierProvider를 사용하여 ChangeNotifier의 불편성을 보완합니다.

ChangeNotifierProvider를 사용하면 얻을 수 있는 장점은 5가지가 있습니다.

1. 모든 위젯들이 listen할 수 있는 ChangeNotifier 인스턴스를 생성합니다.

2. 자동으로 필요 없는 ChangeNotifier를 제거합니다.

3. Provier.of를 통해서 위젯들이 쉽게 ChangeNotifier 인스턴스에 접근할 수 있게 해줍니다.

4. 필요시 UI를 자동으로 리빌드 시켜줄 수 있습니다. 

5. UI를 리빌드할 필요가 없는 위젯을 위해서 listen:false 기능을 제공하여 줍니다. 

 

그렇다면 provider 어떤방식으로 상태관리를 제공하는지 알아보겠습니다. 

provider는 데이터를 생산하고 소비하는 2가지로 구분됩니다. 

 

provider를 생산하는 쪽의 코드입니다. 

class MyModel extends ChangeNotifier {
  int _count = 0;

  int get count => _count;

  void incrementCount() {
    _count++;
    notifyListeners();
  }
}

 소비하는 쪽의 코드도 살펴보겠습니다. 

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (_) => MyModel(),
      child: MaterialApp(
        title: 'Flutter Provider Example',
        home: MyHomePage(),
      ),
    );
  }
}

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Flutter Provider Example'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'You have pushed the button this many times:',
            ),
            Consumer<MyModel>(
              builder: (context, myModel, child) {
                return Text(
                  '${myModel.count}',
                  style: Theme.of(context).textTheme.headline4,
                );
              },
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          Provider.of<MyModel>(context, listen: false).incrementCount();
        },
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }
}

Button 위젯에서 함수가 호출되면 Provider에서 해당 변수를 변경 한 후 notifyListeners() 함수를 호출하여 값이 변경되었음을 알려줍니다. 값이 변경되면 위젯들은 값의 변경에 따라 리빌드가 발생하고 위젯이 새로운 값과 함께 다시표시됩니다.

데이터를 소비하는 방식에는 Provider.of(context) Consumer() 두가지로 사용할  있습니다.

두 방식의 성능은 동일하며 경우에 따라서는 Consumer를 더 효율적으로 사용할 수 있습니다.

 

Provider.of

provider의 상태가 변경될때마다 listen하여 UI를 업데이트해주는 역할을 합니다.

provider의 of 메소드는 주어진 context를 거슬러 올라가면서 가장 가까이 있는 원하는 인스턴스를 찾아서 반환하라는 의미입니다. p

of.context를 사용하고 notifiListeners()가 실행되면 of.context를 사용하는 위젯의 전체가 rebuild되며 listen:false를 통해

변경되지 않아도 되는 위젯의 rebuiler를 제한할 수 있습니다.

 

Cosumer

Consumer 역시 상태가 변경될떄마다 UI를 자동으로 업데이트 하는데 사용됩니다.

꼭 ChangeNotifierProvider의 하위 트리에 위치하여야 작동하며  builder와 child 두가지 인수를 가지는 형태로 사용할 수 있습니다.

builder 메소드는 UI를 작성하기 위해 사용되고 상태를 수신하여 UI를 빌드합니다. Provider의 특성상 BuildContext를 받아와서 사용하여야 합니다. 

child는 Consumer 위젯의 하위 트리 중 변경되지 않는 부분을 지정합니다.
이 child위젯은 builder 메서드가 호출될때마다 다시 빌드되지 않습니다.

 

Provider는 비교적 사용하기 쉽고 단일 어플리케이션에서 여러 상태 및 종속성을 관리할 수 있다는 장점이 있지만, 메모리에 상태를 지속적으로 유지하여 제대로 관리하지 않으면 메모리 소비가 커져 성능문제로 이어질 수 있어 전반적으로 세세한 관리가 필요하다는 단점이 존재하고 Provider의 창시자인 Remi가 곧 deprecated 될 수 있다고 선언하여 대체할 수 있는 상태관리 툴을 많이 찾는 추세라고 합니다. 

 

Riverpod을 살펴보겠습니다. 

RiverPod은 Provider를 제작한 개발자가 일부 제한 사항(예를 들어 런타임시 발생하는 에러,타입에 대한 제한사항 등)을 극복하도록 수정하여 배포한 Provider의 업그레이드 버전이라고 합니다. Provider가 inheritedWdiget을 단순화하여 제작한 것이라면  Riverpod은 inheritedWidget을 처음부터 다시 구현하여 만들었고 Provider보다 사용이 쉬우며 상태관리를 위한 빠르고 가벼운 패키지라고 소개하고 있습니다. provider의 개선된 버전인 만큼 inheritedWidget 및 inheritedNotifier 매커니즘을 사용하고 있는데 상태가 위젯트리를 통해 효율적으로 전파되고 위젯이 필요할 때만 다시 빌드되도록 보장합니다. 또한 각각의 Provider끼리 큰 제약없이 참조하고 사용할 수 있다고 합니다.  

 

Riverpod으로 개선된 사항들

- 런타임이 아닌 컴파일 타임에 프로그래밍 오류 포착

- Flutter SDK에 직접적으로 의존하지 않음(테스트가 쉽다)

- loading/errror 케이스를 깔끔하게 다룰 수 있음

- 더 이상 사용되지 않는 Provider의 상태를 폐기함

- 전역 변수를 사용하는 것처럼 쉽게 상태에 접근이 가능함. 등등..

 

Riverpod은 Provider처럼 단일 패키지만 있는 것이 아니라 여러 종류의 패키지가 존재합니다. 각 패키지마다 사용 목적이 다르고 어떤 패키지를 설치할 지는 생성할 앱의 형태에 따라 다르다고 합니다.

- flutter_riverpod : Flutter 앱에 Riverpod을 사용할 경우의 가장 기본적인 패키지

- Riverpod : Flutter에 관련된 모든 클래스가 완전히 제거된 Riverpod 패키지

- hooks_riverpod : flutter_hooks와 Riverpod을 함께 변용한 패키지

 

Riverpod은 기본적으로 ProviderScope 위젯을 포함해야 합니다. ProviderScope에서 프로바이더의 상태가 저장되기 때문입니다. 상위 위젯을 ProviderScope로 래핑해야 하위 위젯이 상태를 전달받을 수 있으며 위젯이 값을 요청하면 Riverpod은 가장 가까운 ProviderScope 위젯에서 해당 제공자의 상태를 조회합니다. 즉 scope 내에서 이루어진 모든 상태 변경은 해당 scope내의 위젯에만 영향을 미칩니다. ProviderScope를 사용하면 전역 종속성에서 발생할 수 있는 성능 문제를 방지할 수 있습니다.

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

 

ProviderScope를 선언해 주지 않으면 Bad state: No ProviderScope found 라는 에러가 발생하니 꼭 선언해 주어야 합니다.

 

Riverpod은 Provider처럼 데이터를 생성하는 코드와 소비하는 코드가 있습니다.

ProviderScope를 완료했다면 데이터를 생성하는 코드를 작성해줍니다.

final counterProvider = StateProvider((ref) => 0);

StateProvider는 Riverpod 라이브러리의 provider 유형으로 보유하고 있는 상태의 유형을 나타내는 일반 클래스입니다. 매개 변수 ref는 provider 자체에 대한 참조이며 앱의 다른 provider 및 객체와 상호작용을 하는 데 사용할 수 있습니다. 

그럼 counterProvider를 어떻게 사용하는지 살펴보겠습니다. 먼저 provider와 같이 데이터를 소비하는 코드를 작성해야 합니다.

Riverpod도 데이터를 소비하는 방식이 두가지가 있습니다.

 

Consumer를 사용하는 방법입니다.

class HomePage extends StatelessWidget {
  const HomePage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Consumer(
  	builder: (context, ref, child) {
    	final count = ref.watch(counterProvider).state;
    	return Text('Counter: $count');
  		},
    );
  }
}

Consumer에서의 builder 함수는 context, ref(아래에서 자세히 설명하겠습니다.), child 세가지 인수를 사용합니다. 

Cousumer의 builder내에서 사용된 provider의 값이 변경되면 Consumer로 래핑된 위젯만 다시 빌드됩니다. 

래핑된 위젯만 재빌드 되기 때문에 앱성능을 빠르게 만드는 경우 유용하게 사용할 수 있습니다.

 

두번째는 ConsumerWidget을 사용하는 방법입니다. 

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

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final count = ref.watch(myValueProvider);
    return Scaffold(
      appBar: AppBar(
        title: const Text('Riverpod'),
      ),
      body: Center(
        child: return Text('$count');
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => ref.read(count.state).state++,
        child: const Icon(Icons.add),
      ),
    );
  }
}

Cousumer와의 차이점은 provider의 값이 변경되면 전체 위젯트리가 리빌드 된다는 점입니다.

위에서 사용된 ref에 대해서 살펴보겠습니다.

Riverpod에서의 ref 는 Provider 값을 가져오거나 Provider 값을 변경하는데 사용되는 객체이며 Provider의 내부에서 생성됩니다. 

이 객체는 Provider 값을 가져오는데 사용할 수 있는 ProviderReference와 함께 제공됩니다. ref는 context와 같은 역할을 해서 다른 위젯에서도 상태를 공유하는 역할을 합니다.  즉, Provider의 단점인 BuildContext 의존을 ref를 사용해서 상태접근할 수 있도록 해준다고 합니다. 

 

ref는 3가지로 사용할 수 있습니다. 

1. ref.read

- 일반적으로 유저의 상호 작용에 의해 트리거되는 함수 내에서 사용하며 다른 Provider로 부터 값을 받는 등의 경우에 사용합니다.

- ref.read는 리액티브하지 않기 때문에 가급적 사용을 자제해야 한다고 합니다.

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

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Riverpod'),
      ),
      body: Center(
        child: GestureDetector(
          onTap: () {
            ref.read(repositoryProvider);
          },
          child: Text(
            value.toString(),
            style: Theme.of(context).textTheme.headline2,
          ),
        ),
      ),
    );
  }
}

2. ref.watch

- Provider의 값을 얻고 값의 변화를 감지합니다.

- 값의 변화를 감지하면 wdiget을 재빌드하거나 해당값을 구독하고 있는 곳에 변경된 상태값을 전달합니다.

final helloStringProvider = StateProvider<String>((ref) {
  return 'Hello';
});

final worldStringProvider = StateProvider<String>((ref) {
  return 'World';
});

final helloWorldStringProvider = Provider<String>((ref) {
 final hello = ref.watch(helloStringProvider); 
 final world= ref.watch(worldStringProvider);
});

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

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final value = ref.watch(helloWorldStringProvider);
    return Scaffold(
      appBar: AppBar(
        title: const Text('Riverpod'),
      ),
      body: Center(
        child: Text(
          value,
          style: Theme.of(context).textTheme.headline2,
        ),
      ),
    );
  }
}

3. ref.listen

- ref.watch와 비슷하게 동작합니다.

- ref.watch는 프로바이더의 상태값이 변경되면 다시 빌드하지만 ref.listen은 함수를 호출한다는 차이점이 있습니다.

- 일반적으로 상태가 변경될 때 스낵바를 호출하거나 다른 화면으로 이동하는 등 무언가를 수행하려는 경우 사용합니다.

 

final counterProvider = StateNotifierProvider<Counter, int>((ref) => Counter(ref));

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

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    ref.listen<int>(counterProvider, (int? previousCount, int newCount) {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('Value is ${current.state}')),
      );
    });
    
    return Scaffold(...);
  }
}

Riverpod은 Provider보다 사용이 간편하다고 소개되어 있지만 Provider보다 사용법이 까다롭다고 생각합니다. RiverPod을 제대로 사용해 보려면 hook과 StateNotifier, Riverpod 내에 있는 다양한 Provider들을 익혀야 한다는 점이 단점이 될 거 같습니다.

 

flutter에는 더 다양한 상태관리 라이브러리들이 있지만 일단은 여기까지 찾아서 소개해보았습니다. 

요새는 Get-it이라는 라이브러리도 핫하다고 하는데 다음에 기회가 되면 소개해보겠습니다.