Riverpod란?Flutter의 가장 주요한 상태 관리를 소개합니다!

프론트 시스템에서는 “상태 관리”가 매우 중요합니다.

이전에 소개한 「StatefulWidget」도 상태 관리를 위한 기능이지만,
여러 화면이나 기능을 가진 앱을 구현할 경우, 관리할 상태가 늘어나고 그 관리가 어려워집니다.

그럴 때 상태를 쉽게 관리할 수 있는 멋진 패키지가 Riverpod입니다.
이번에는 Riverpod에 대해 개요와 기본적인 사용법을 소개하겠습니다.
여기서 소개하는 Riverpod은 v2입니다.

Riverpod란

Riverpod란, 글로벌한상태(State)를 정의하기 위한 도구입니다.

변수나 상수라면, 정의하는 범위를 바꾸는 것으로,
비교적 쉽게 글로벌한 변수를 정의할 수 있습니다.

그러나, 프론트 시스템에서 사용되는상태(State)는 위젯과 밀접한 관계가 있어,
상태 변화에 따라 위젯도 재구축되기 때문에 그렇게 간단하지 않습니다.
게다가 여러 위젯에서 동일한 상태 정보를 참조하려고 하면 더욱 복잡해집니다.

그것을 쉽게 실현할 수 있는 도구가 Riverpod입니다.

Riverpod은 글로벌한 변수에 사용하는 것뿐만 아니라,
이번에는 기본적인 설명에 그칩니다.

상태 관리에 대하여

Riverpod의 설명에 앞서, 상태 관리에 대해 설명하겠습니다.

상태(State)

간단히 말해서, 상태(State)란 화면상의 정보에 영향을 미치는 변수를 의미합니다.

예를 들어, 로그인한 사용자 정보, ToDo 리스트 정보,
체크박스의 선택 여부 등, 이러한 화면 표시에 영향을 미치는 데이터가상태(State)입니다.

위젯은 이상태를 감시하고, 변화가 있을 때마다 화면을 재구축합니다.

상태 관리

그러한 상태를 적절히 업데이트하거나 삭제하고, 화면에 표현하는 것을 상태 관리라고 합니다.
상태에는 로컬 상태와 글로벌 상태가 있습니다.

  • 로컬 상태
    하나의 화면이나 하나의 위젯에만 사용됩니다.
    그 화면에서만 사용하는 텍스트 필드나 카운터 값 등.
  • 글로벌 상태
    앱 전체에서 공유되는 상태.
    홈 화면이나 사용자 정보 화면에 표시되는 사용자 정보 등.

Flutter에는 상태 관리를 위한 “StatefulWidget”이 준비되어 있습니다.
하지만, “StatefulWidget”으로 글로벌 상태를 관리하려면 복잡해집니다.

다른 화면에 상태를 전달하거나, 다른 화면에서 업데이트될 가능성이 있는 경우,
그 상태를 사용하기 전에 동기화 등의 기능을 구현해야 하기 때문입니다.

Riverpod에서는 Provider가 상태의 감시를 수행하며, 자동으로 영향을 미치는 위젯에 전달되기 때문에
위와 같은 동기화 기능을 구현할 필요가 없습니다.

Riverpod에 대하여

Riverpod은 Flutter에서 상태 관리를 쉽게 하기 위해 설계된 오픈 소스 패키지입니다.
다음 기능을 사용하여 상태 관리를 수행합니다.

  • Notifier
    Notifier(알림자)는 대상이 되는 상태나 업데이트 삭제의 로직을 관리하는 곳입니다.
    상태를 업데이트하거나 상태의 변화를 위젯에 알리는 역할이 있습니다.
  • Provider
    위젯은 Provider(제공자)를 사용하여 Notifier(상태)의 값을 참조하거나 업데이트합니다.

Riverpod을 사용하면 위의 이미지처럼 WidgetA의 업데이트가 WidgetB에도 전달됩니다!

Riverpod의 사용법(개요)

Riverpod을 사용할 때는 다음을 준비합니다.

  1. 상태 클래스
  2. Notifier
  3. Provider

「3. Provider」에 대해서는 v2.0 이후라면, “Riverpod Generator”가 자동으로 생성해 줍니다.

Riverpod의 기본적인 사용법

ToDo 앱을 예로 들어 기본적인 사용법을 소개하겠습니다.

설정 파일

pubspec.yaml에 다음 패키지를 추가합니다.

dependencies:
  flutter_riverpod:
  riverpod_annotation:
  freezed_annotation:

dev_dependencies:
  build_runner:
  riverpod_generator:
  

Riverpod 관련 패키지
flutter_riverpod: Flutter에서 Riverpod을 사용하기 위한 패키지
riverpod_generator: 자동으로 Riverpod의 Provider를 정의해주는 패키지
riverpod_annotation: riverpod_generator에서 사용하는 어노테이션을 판정하는 패키지

기타 패키지
build_runner: 실제로 코드를 자동 생성하는 패키지
freezed: 불변 클래스를 만들기 위한 패키지
freezed_annotation: freezed에서 사용하는 어노테이션을 판정하는 패키지

상태 클래스 정의

상태로 사용하는 클래스는 불변(Immutable) 클래스여야 합니다.
다음과 같이 freezed 등을 사용하여 불변 클래스를 정의합니다.

import 'package:freezed_annotation/freezed_annotation.dart';

part 'todo.freezed.dart';

@freezed // freezed를 사용하기 위한 어노테이션
class Todo with _$Todo {
  factory Todo({
    required String id,
    required String description,
    required bool completed,
  }) = _Todo;
}

불변 클래스의 이점은?

불변 클래스는 값을 업데이트할 수 없는 클래스입니다.
즉, 한 번 생성하면 값을 변경할 수 없습니다. 언뜻 보기에는 불편해 보입니다.

비불변 클래스의 인스턴스 데이터 전달은 “참조 전달”입니다.
다른 클래스나 함수에 인스턴스를 전달할 경우, 값 그 자체가 아니라,
값이 들어 있는 주소 값을 전달합니다.

클래스나 함수에 인수로 전달된 인스턴스의 값을 업데이트하면,
그 주소를 참조하는 호출 원의 인스턴스 값도 변경됩니다.

이것만으로는 상태 관리에 사용할 수 있을 것 같지만,
참조 전달의 까다로운 점은, 읽기 전용으로 전달된 인스턴스라도,
주소 값의 값을 업데이트할 수 있어, 의도치 않은 업데이트가 발생할 가능성이 있다는 것입니다.
또한, 이 업데이트는 오류도 발생하지 않기 때문에, 감지할 수 없습니다.

이러한 의도치 않은 업데이트는, 예를 들어 다음과 같이 복사한 인스턴스를 정렬한 경우,
복사 원본도 동일하게 정렬됩니다.

Riverpod에서는, 위와 같은 의도치 않은 변경을 방지하기 위해서도,
의도한 시점에만 업데이트할 수 있는 불변 클래스로 상태를 관리하고자 합니다.

Notifier 정의

위에서 정의한 클래스를 기반으로 Notifier를 정의합니다.
여기에서는, 상태 업데이트와 신규 추가 처리를 기재합니다.

import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'todo.dart';

// 다음 절차에 따라, build runner를 실행하면 이 파일이 생성됩니다.
part 'todo_notifier.g.dart';

@riverpod
class TodoNotifier extends _$TodoNotifier {
  @override
  List build() {
    return [];
  }

  // 신규 추가
  void addTodo(Todo todo) {
    state = [...state, todo];
  }

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

  // "completed" 판정 전환
  void toggle(String todoId) {
    state = [
      for (final todo in state)
        if (todo.id == todoId)
          todo.copyWith(completed: !todo.completed)
        else
          todo,
    ];
  }
}

본 기사는 Riverpod의 개요를 설명하기 위한 것이므로, 자세한 처리는 생략합니다.

Riverpod Generator 실행

다음 명령을 터미널에서 build_runner 실행합니다.

flutter pub run build_runner build --delete-conflicting-outputs

그러면Riverpod Generator가 실행되어, ~.d.dart 파일이 생성됩니다.
위 명령으로 freezed도 실행되므로, ~.freezed.dart 파일도 생성됩니다.

Riverpod Generator에 의해, Provider가 생성됩니다.
생성되는 ~.d.dart를 보면 Provider가 정의되어 있는 것을 알 수 있을 것입니다.

// GENERATED CODE - DO NOT MODIFY BY HAND

part of 'todo_notifier.dart';

// **************************************************************************
// RiverpodGenerator
// **************************************************************************

String _$todoNotifierHash() => r'1330ee68f148d9ff18b1e0370e3f3541ab67c621';

@ProviderFor(TodoNotifier)
final todoNotifierProvider =
    AutoDisposeNotifierProvider>.internal(
  TodoNotifier.new,
  name: r'todoNotifierProvider',
  debugGetCreateSourceHash:
      const bool.fromEnvironment('dart.vm.product') ? null : _$todoNotifierHash,
  dependencies: null,
  allTransitiveDependencies: null,
);

typedef _$TodoNotifier = AutoDisposeNotifier>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member

Riverpod을 사용한 위젯

Riverpod을 사용하려면, 사용하는 범위를 선언해야 합니다.

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

void main() {
  // Riverpod을 사용하는 범위를 "ProviderScope"로 선언
  runApp(ProviderScope(child: MyApp()));
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: TodoItemListPage(),
    );
  }
}

// "ConsumerWidget"으로 Riverpod을 사용하는 위젯을 정의
class TodoItemListPage extends ConsumerWidget {
  @override
  // "WidgetRef ref"는 Provider를 사용하기 위한 컨텍스트
  Widget build(BuildContext context, WidgetRef ref) {
    // "ref.watch"는 Provider를 감시하고, 상태의 변경을 감지합니다.
    List todoItems = ref.watch(todoNotifierProvider);

    return Scaffold(
      appBar: AppBar(title: Text('todoItem List')),
      body: ListView.builder(
        itemCount: todoItems.length,
        itemBuilder: (context, index) {
          final todoItem = todoItems[index];
          return ListTile(
            title: Text(todoItem.title),
            leading: Checkbox(
              value: todoItem.completed,
              onChanged: (bool? newValue) {
                // "ref.read"는 Provider에서 Notifier의 메서드를 호출합니다.
                ref.read(todoNotifierProvider.notifier).toggle(todoItem.id);
              },
            ),
            onLongPress: () => ref.read(todoNotifierProvider.notifier).removeTodo(todoItem.id),
          );
        },
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          // addTodo
        },
        child: Icon(Icons.add),
      ),
    );
  }
}

“ref”는 reference(참조)의 약자입니다.
위젯에서 상태로의 참조는 ref→Provider→Notifier→State입니다.

※위의 처리에서는, Notifier의 “addTodo”는 사용하지 않았습니다.

실행 화면

실행하면 다음과 같이, 초기 데이터가 표시됩니다.
체크박스를 탭하거나, 리스트 아이템을 길게 누르면, 상태가 변경됩니다.

완성!!

마지막으로

이번에는 Riverpod의 기본적인 것에 대해 설명했습니다.
Riverpod은 다양한 기능이 있으므로, 앞으로는 그 기능에 대해서도 소개할 수 있으면 좋겠습니다.

본 기사가 조금이라도 Riverpod의 이해에 도움이 되었으면 좋겠습니다.
끝까지 읽어주셔서 감사합니다!!

제목과 URL을 복사했습니다