본문 바로가기

개발/플러터(Flutter)

[Riverpod] Provider 인듯 provider 아닌 provider 같은 너

참고: riverpod.dev/

 

Provider, but different | Riverpod

A boilerplate-free and safe way to share state

riverpod.dev

 

정의

어디서든(widget 등) 변경을 감지(listen)할 수 있는 상태 관리 객체

 

왜 사용하는가?

  1. 어디서든 상태값에 접근 가능
  2. 다른 상태값과 결합하여 사용 용이
  3. 상태 변화에 영향을 받는 부분에 대해서만 부분 렌더링 (성능 최적화)
  4. 로깅 등 다른 feature와 결합하여 사용 가능

종류

1. Provider

가장 기본적인 provider 형태
상태값 자체를 리턴한다.

pub.dev/documentation/riverpod/latest/all/Provider-class.html

 

Provider class - all library - Dart API

Provider class A provider that exposes a read-only value. What is a provider Providers are the most important components of Riverpod. In short, you can think of providers as an access point to a shared state. Providers solve the following problems: Provide

pub.dev

 

사용 케이스

  • 단순히 특정 값을 읽기만 하고 싶은 경우.
  • 비동기 처리가 필요 없는 경우.

 

예제

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

// provider 객체 생성.
// 상태값을 리턴하는 콜백함수를 인자로 넣어준다.
final helloWorldProvider = Provider((_) => 'Hello world');

void main() {
  runApp(
    // riverpod provider를 사용하기 위해서는 전체 위젯을 ProviderScope 위젯으로 감싸야 함.
    ProviderScope(
      child: MyApp(),
    ),
  );
}

// Riverpod에서 관리하는 Consumerwidger을 상속.
class MyApp extends ConsumerWidget {
  @override
  Widget build(BuildContext context, ScopedReader watch) {
    final String value = watch(helloWorldProvider);

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

 

다른 provider와의 결합

final weatherProvider = FutureProvider((ref) async {
  // We use `ref.watch` to watch another provider, and we pass it the provider
  // that we want to consume. Here: cityProvider
  final city = ref.watch(cityProvider);

  // We can then use the result to do something based on the value of `cityProvider`.
  return fetchWeather(city: city);
});

2. StateProvider

Provider 스펙 + state 프로퍼티에 직접 직접 값 변경이 가능함

 

사용 케이스

  • 상태값 변경이 로직이 단순한 경우

 

예제

final selectedProductIdProvider = StateProvider<String>((ref) => null);
final productsProvider = StateNotifierProvider<ProductsNotifier>((ref) => ProductsNotifier());

Widget build(BuildContext context, ScopedReader watch) {
  final List<Product> products = watch(productsProvider.state);
  final selectedProductId = watch(selectedProductIdProvider);

  return ListView(
    children: [
      for (final product in products)
        GestureDetector(
          onTap: () => selectedProductId.state = product.id,
          child: ProductItem(
            product: product,
            isSelected: selectedProductId.state == product.id,
          ),
        ),
    ],
  );
}

3. StateNotifierProvider

StateNotifier를 리턴하는 provider

 

사용 케이스

  • 좀더 복잡한 상태값 변경 로직을 작성하고 싶은 경우

 

예제

3.1. StateNotifier 정의

기본적으로 상속해서 정의하도록 디자인되어 있음.
StateNotifierBuilder & StateNotifierProvider에서 listen 가능.
class TodosNotifier extends StateNotifier<List<Todo>> {
  // 생성자의 인자로 초기 상태값을 입력.
  TodosNotifier(): super([]);

  void add(Todo todo) {
    state = [...state, todo];
  }

  void remove(String todoId) {
    state = [
      for (final todo in state)
        if (todo.id != todoId) todo,
    ];
  }

  void toggle(String todoId) {
    state = [
      for (final todo in state)
        if (todo.id == todoId) todo.copyWith(completed: !todo.completed),
    ];
  }
}

 

3.2. StateNotifierProvider 정의

final todosProvider = StateNotifierProvider((ref) => TodosNotifier());

 

3.3. 결합

Widget build(BuildContext context, ScopedReader watch) {
  // 상태값 구독.
  List<Todo> todos = watch(todosProvider.state);

  return ListView(
    children: [
      for (final todo in todos)
        CheckboxListTile(
           value: todo.completed,
           // StateNotifier의 메서드를 사용해서 상태값 변경.
           onChanged: (value) => todosProvider.read(context).toggle(todo.id),
           title: Text(todo.description),
        ),
    ],
  );
}

4. FutureProvider

pub.dev/documentation/riverpod/latest/all/FutureProvider-class.html

 

FutureProvider class - all library - Dart API

FutureProvider class A provider that asynchronously creates a single value. FutureProvider can be considered as a combination of Provider and FutureBuilder. By using FutureProvider, the UI will be able to read the state of the future synchronously, handle

pub.dev

 

사용 케이스

  • HTTP 통신처럼 비동기 처리가 필요한 경우

 

예제

 

4.1. FutureProvider 생성

final configProvider = FutureProvider<Configuration>((ref) async {
  final content = json.decode(
    await rootBundle.loadString('assets/configurations.json'),
  ) as Map<String, dynamic>;

  return Configuration.fromJson(content);
});

 

4.2. 위젯에서의 사용

Widget build(BuildContext, ScopedReader watch) {
  AsyncValue<Configuration> config = watch(configProvider);

  return config.when(
    loading: () => const CircularProgressIndicator(),
    error: (err, stack) => Text('Error: $err'),
    data: (config) {
      return Text(config.host);
    },
  );
}

5. StreamProvider

리턴값이 Stream인 것을 제외하고는 FutureProvider과 동일

pub.dev/documentation/riverpod/latest/all/StreamProvider-class.html

 

StreamProvider class - all library - Dart API

StreamProvider class Creates a stream and expose its latest event. StreamProvider is identical in behavior/usage to FutureProvider, modulo the fact that the value created is a Stream instead of a Future. It can be used to express a value asynchronously loa

pub.dev

 

사용 케이스

  • 웹 소켓 처럼 실시간 양방향 통신이 필요한 경우
  • 비동기 처리가 필요한 경우

 

예제

5.1 StreamProvider 생성

final messageProvider = StreamProvider.autoDispose<String>((ref) async* {
  // Open the connection
  final channel = IOWebSocketChannel.connect('ws://echo.websocket.org');

  // Close the connection when the stream is destroyed
  ref.onDispose(() => channel.sink.close());

  // Parse the value received and emit a Message instance
  await for (final value in channel.stream) {
    yield value.toString();
  }
});

 

5.2. 위젯에서의 사용 

Widget build(BuildContext, ScopedReader watch) {
  AsyncValue<String> message = watch(messageProvider);

  return message.when(
    loading: () => const CircularProgressIndicator(),
    error: (err, stack) => Text('Error: $err'),
    data: (message) {
      return Text(message);
    },
  );
}

정리

기능 Provider StateProvider StateNotifierProvider FutureProvider StreamProvider
상태값 읽기 O O O O O
상태값 쓰기 X O O O O
자체 메서드 X X O X X
비동기 처리(단방향) X X X O O
비동기 처리(양방향) X X X X O