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

이전에 소개한 ‘StatefulWidget’도 상태 관리를 수행하는 기능 중 하나입니다만, 여러 화면이나 기능을 갖는 앱을 구현하는 경우, 관리해야 할 상태도 증가하여 그 관리가 어려워집니다.

그런 상태 관리를 쉽게 할 수 있는 멋진 패키지가 Riverpod입니다. 이번에는 Riverpod에 대해 설명합니다.

여기에서 소개하는 Riverpod는 v2입니다.

Riverpod란

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

변수나 상수라면, 정의하는 스코프를 바꿈으로써, 비교적 쉽게 글로벌 변수를 정의할 수 있습니다.

그러나, 프론트 시스템에서 사용되는 상태(State)는 Widget과 관계가 깊으며,
상태의 변화에 따라 Widget도 재구성되므로, 그렇게 쉽지 않습니다.
더욱이 여러 Widget에서 같은 상태 정보를 참조하려고 하면 더욱 복잡해집니다.

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

Riverpod는 글로벌 변수에 사용하는 것만이 아니라, 도구이지만,
이번에는 기본적인 설명에 그치고 있습니다.

상태 관리에 대하여

Riverpod 설명에 앞서, 상태 관리에 대해 설명합니다.

상태(State)

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

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

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

상태 관리

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

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

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

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

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

Riverpod에 대하여

Riverpod는 Flutter에서의 상태 관리를 용이하게 하기 위해 설계된 오픈 소스 패키지입니다. 아래 기능을 이용하여 상태 관리를 수행합니다.

  • Notifier
    Notifier(알림자)는 대상이 되는 상태나 업데이트 삭제의 로직을 관리하는 곳입니다.
    상태를 업데이트하거나, 상태의 변경을 위젯에 알리는 역할이 있습니다.
  • Provider
    Widget은 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<Todo> 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<TodoNotifier, List<Todo>>.internal(
  TodoNotifier.new,
  name: r'todoNotifierProvider',
  debugGetCreateSourceHash:
      const bool.fromEnvironment('dart.vm.product') ? null : _$todoNotifierHash,
  dependencies: null,
  allTransitiveDependencies: null,
);

typedef _$TodoNotifier = AutoDisposeNotifier<List<Todo>>;
// 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를 사용한 Widget

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를 사용하는 Widget을 정의
class TodoItemListPage extends ConsumerWidget {
  @override
  // "WidgetRef ref"는 Provider를 사용하기 위한 컨텍스트
  Widget build(BuildContext context, WidgetRef ref) {
    // "ref.watch"는 Provider를 감시하고, 상태의 변경을 감지합니다.
    List<TodoItem> 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(참조)의 약자입니다.
Widget에서 상태로의 참조는, ref→Provider→Notifier→State입니다.

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

실행 화면

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

완성!!

마지막으로

이번에는 Riverpod의 기본적인 것에 대해 설명했습니다. Riverpod는 여러 기능이 있기 때문에, 앞으로 그 기능에 대해서도 소개할 수 있으면 생각합니다.

본 기사가 조금이라도 Riverpod의 이해를 돕는 데 도움이 되었다면 다행입니다. 끝까지 읽어주셔서 감사합니다!!


제목과 URL을 복사했습니다