[Flutter] Riverpod 상태관리

merci·2023년 4월 3일
0

Flutter

목록 보기
13/24

지금까지는 위젯이 상태를 관리했는데 -> setState
위젯과 상태관리를 분리시키자.

플러터의 상태관리를 하는 여러 라이브러리가 있는데 그 중에서 Riverpod을 이용해서 상태관리를 해보자

의존성 추가

https://riverpod.dev/docs/getting_started 로 가서 의존성을 추가한다.

dependencies:
  flutter:
    sdk: flutter
  flutter_riverpod: ^2.3.2   // 추가

Provider

Provider패턴은 객체지향 디자인패턴의 하나로 상태를 캡슐화하고 상태 객체를 컨슈머에게 제공해서 재사용성과 유지보수성을 높이기 위한 패턴이다.

Provider를 이용하면 여러 위치에서 해당 상태에 쉽게 액세스할 수 있다.
Provider는 필요한 위젯만 렌더링 하므로 최적화에 좋다.
비용이 많이 드는 상태 계산을 캐싱하기에도 좋다.

Riverpod 스니펫 추가

일반적으로 아래와 같은 방법으로 생성한다.

final myProvider = Provider((ref) { // 반드시 fianl
  return MyValue();
});

Providerfinal 이므로 불변의 특징을 가진다.

공식 예제로 연습해보기

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

// We create a "provider", which will store a value (here "Hello world").
// By using a provider, this allows us to mock/override the value exposed.
final helloWorldProvider = Provider((_) => 'Hello world');

void main() {
  runApp(
    // For widgets to be able to read providers, we need to wrap the entire
    // application in a "ProviderScope" widget.
    // This is where the state of our providers will be stored.
    ProviderScope(
      child: MyApp(),
    ),
  );
}

// Extend ConsumerWidget instead of StatelessWidget, which is exposed by Riverpod
class MyApp extends ConsumerWidget {
  
  Widget build(BuildContext context, WidgetRef ref) {
    final String value = ref.watch(helloWorldProvider);

    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: const Text('Example')),
        body: Center(
          child: Text(value),
        ),
      ),
    );
  }
}

예제를 보면 먼저 Provider를 생성한다.

final helloWorldProvider = Provider((_) => 'Hello world');
// 원래 코드에서 제네릭이 생략되어 있다.
final helloWorldProvider = Provider<String>((_) => 'Hello world');
// dart에서는 매개변수에 들어온 변수를 사용하지 않을경우 _를 넣을 수 있다.

Provider가 가지고 있는 매개변수를 스토어라고 한다 -> ((_) => 'Hello world')
스토어에는 상태가 저장되고 이러한 스토어를 Provider로 관리하는 패턴을 Provider패턴이라고 한다.


ProviderScope

다음으로 Provider를 제공할 범위를 지정한다.

  ProviderScope(
    child: MyApp(),
  ),

일반적으로 루트 위젯을 ProviderScope로 감싸서 하위 위젯트리에서 Provider객체에 접근할 수 있도록 한다.
이를 통해서 Provider객체들의 상태를 전역적으로 관리하고 생성 또한 담당한다.


ConsumerWidget

상태를 구독하기 위해 ConsumerWidget을 상속한 클래스를 만든다.

class MyApp extends ConsumerWidget

ConsumerWidgetProvider 객체의 상태를 구독하고, 상태 변경이 있을 때마다 build를 호출하여 화면을 다시 그린다.

WidgetRef

Widget build(BuildContext context, WidgetRef ref)

WidgetRef는 위젯 트리 내의 Provider 객체에 대한 참조를 제공하며, 이를 통해 하위 위젯에서 Provider 객체를 생성하거나 액세스 할 수 있다.
WidgetRefProvider객체에 액세스하여 상태변경을 감지하면 현재 위젯에 다시 빌드를 요청한다.

또한 WidgetRefcontext와 같은 역할을 해서 다른위젯에서도 상태를 공유하는 역할을 한다.
이를 통해 상태 관리를 보다 효율적으로 할 수 있다.

WidgetRef 의 메소드

// Provider를 초기에 한 번만 읽음 - 값이 변경되더라도 build를 요청하지 않음
final String value = ref.read(helloWorldProvider);
// Provider를 반복해서 관찰하고 상태가 변화면 build를 호출함
final String value = ref.watch(helloWorldProvider);

이를 통해 상태에 변화가 생기면 build가 호출되어 화면의 데이터가 변하게 된다.

또한 Provider는 다른 Provider를 포함할 수 있다.

final byeWorldProvider = Provider<String>((ref) => 'bye');

final helloWorldProvider = Provider<String>((ref) {
  final String value = ref.read(byeWorldProvider); // 상태 조합이 가능하다
  return 'Hello world: '+value;
});


Provider의 종류

FutureProvider

컨슈머가 데이터를 요청하면 공급자는 서버에 데이터를 요청하면서 I/O가 발생하는데 이와 동시에 공급자는 데이터를 가지고 있지 않지만 컨슈머에게 future를 반환한다. ( Promise와 유사 )
공급자가 데이터를 얻는 즉시 데이터를 주입해준다.

미래에 데이터를 제공해주기 때문에 비동기 특성을 가진다.
이때 데이터를 모두 다운받게 되면 공급자는 컨슈머에게 콜백을 날리고 컨슈머는 read를 하게 된다.

FutureProvider가 아닌 Provider를 이용하면 비동기 통신을 지원하지 않아 컨슈머는 null 데이터를 받게 된다.
간단하게 구현하는 경우에는 StateNotifierProviderwatch를 이용하는데 자원을 철저하게 관리하는 경우에 FutureProvider는를 사용한다.

StateNotifierProvider

StateNotifier는 상태를 가지고 있으며 상태가 변경되면 리스너들에게 알린다.

하나의 위젯에서 상태변경

 class Counter extends StateNotifier<int>{ // int를 관리하는 스토어
   Counter(int num) : super(num);

   void increment(){
     state++; // state의 타입은 StateNotifier의 제네릭 타입이다.
     // super 의 파라미터 자리의 데이터를 바꿀 수 있다.
     // 제네릭에 User 클래스 만들어 넣었다면 state.name 처럼 접근가능함
   }
 }

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

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

class MyApp extends ConsumerWidget {
  
  Widget build(BuildContext context, WidgetRef ref) {
    final int value = ref.watch(counterProvider);
    // ref.read를 하면 한번만 읽으므로 초기화면에서 바뀌지 않는다.

    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: const Text('Example')),
        body: Center(
          child: Text(value.toString()),
        ),
        floatingActionButton: FloatingActionButton(
          onPressed: (){
            ref.read(counterProvider.notifier).increment(); 
            // notifier는 변경 불가능한 일반적인 Provider와 달리 상태를 변경하는 Provider를 생성한다.
          },
          child: Icon(Icons.add),
        ),
      ),
    );
  }
}

notifierStateNotifier를 구현하는 객체로 버튼을 클릭하면 생성된 StateNotifierProvider
버튼을 클릭하면 increment가 호출되어 Provider의 상태가 변경되고 구독하는 위젯의 화면이 갱신된다.

버튼을 누르면 숫자가 변한다.


다른 위젯에서 상태 변경

마찬가지로 StateNotifier을 상속한 클래스를 만들어서 상태를 저장한다.

class Counter extends StateNotifier<int> {
  Counter(super.state);
  void add() {
    state++;
  }
  void down() {
    state--;
  }
}

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

Consumer

변경하고 싶은 부분만 특정하고 싶을때 Consumer를 사용한다.

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

  
  Widget build(BuildContext context) {
    return Container(
      color: Colors.red[300],
      child: Align(
        child: ProviderScope( // 스코프지정
          child: Consumer(   // 해당 위젯만 변경하고자 할때
            builder: (context, ref, child) {
              final int number = ref.watch(countProvider);
              return Text(
                "$number",
                style: TextStyle(
                    color: Colors.white,
                    fontSize: 100,
                    fontWeight: FontWeight.bold),
              );
            },
          ),
        )
       ),
     ); 
   }
 }

다른 위젯의 버튼으로 상태를 변경해서 데이터를 바꾼다.
WidgetRef을 이용한다.

  
  Widget build(BuildContext context, WidgetRef ref) {
    return Container(
      color: Colors.blue[300],
      child: Align(
        child: ElevatedButton(
          style: ElevatedButton.styleFrom(
              backgroundColor: Colors.green[200], minimumSize: Size(300, 200)),
          onPressed: () {
            text == "증가" ? (ref.read(countProvider.notifier).add())
            : (ref.read(countProvider.notifier).down());
          },
          child: Text(
            text,
            style: TextStyle(fontSize: 100, fontWeight: FontWeight.bold),
          ),
        ),
      ),
    );
  }

A 위젯의 버튼을 눌러서 스토어의 상태를 변경하고 Provider를 구독중인 B의 위젯의 데이터가 변경된다.


ChangeNotifierProvider

상태가 변경될 때 UI를 업데이트해야하는 경우에 특히 유용합니다.

StreamProvider

컨슈머가 데이터를 요청하면 공급자는 데이터를 서버에 요청해 I/O가 발생하는 동안 컨슈머에게 stream을 반환한다.
서버에서 계속해서 데이터를 받기때문에 StreamProvider는 비동기를 지원한다.

다음과 같이 순차적으로 스트림을 보낸다면

final streamProvider = StreamProvider<int>((ref) async* {
  for (var i = 1; i <= 10; i++) {
    await Future.delayed(Duration(seconds: 1));
    yield i;
  }
});

컨슈머는 watch로 스트림을 읽을 수 있다.
또한 사용자가 원하는때에 Provider 구독을 취소할 수 있는 autoDispose 매개변수도 제공한다.

Consumer(
  builder: (context, ref, child) {
    final streamData = ref.watch(streamProvider);
    return Text(
      '${streamData.data}',
      style: TextStyle(fontSize: 24),
    );
  },
),
profile
작은것부터

0개의 댓글