Riverpod

샤워실의 바보·2024년 2월 16일
0
post-thumbnail

이 코드 예제는 Flutter에서 Riverpod를 사용하여 상태 관리를 구현하는 방법을 보여줍니다. Riverpod는 Provider 패키지의 기능을 확장하고 개선하여 더 유연하고 강력한 상태 관리 솔루션을 제공합니다. 이 예제에서는 비디오 재생 설정(음소거 및 자동 재생 여부)을 관리하는 데 Riverpod를 사용합니다.

주요 구성 요소와 개념:

  • Model (PlaybackConfigModel): 비디오 재생 설정의 데이터 구조를 정의합니다. 여기에는 음소거 및 자동 재생 여부가 포함됩니다.

  • Repository (PlaybackConfigRepository): SharedPreferences를 통해 비디오 재생 설정을 로컬 저장소에 저장하고 불러오는 로직을 캡슐화합니다. 이를 통해 데이터 소스의 구체적인 구현 세부 사항을 ViewModel로부터 분리합니다.

  • ViewModel (PlaybackConfigViewModel): 비디오 재생 설정의 비즈니스 로직을 처리합니다. Notifier를 상속받아 상태 변경을 감지하고, 해당 변경을 구독하는 위젯에 알립니다.

  • Provider (playbackConfigProvider): 앱 전역에서 접근 가능한 PlaybackConfigViewModel 인스턴스를 제공합니다. ProviderScope를 사용하여 오버라이드된 프로바이더를 앱에 적용합니다.

코드 내 주석 처리 및 설명:

// PlaybackConfigViewModel: 비디오 재생 설정의 상태를 관리하는 ViewModel.
// Notifier<PlaybackConfigModel>를 상속받아 상태 관리 기능을 구현합니다.
class PlaybackConfigViewModel extends Notifier<PlaybackConfigModel> {
  final PlaybackConfigRepository _repository; // 데이터 관리를 위한 레포지토리 인스턴스

  PlaybackConfigViewModel(this._repository);

  // 음소거 설정을 변경하고 상태를 업데이트합니다.
  void setMuted(bool value) {
    _repository.setMuted(value);
    state = PlaybackConfigModel(
      muted: value,
      autoplay: state.autoplay,
    );
  }

  // 자동 재생 설정을 변경하고 상태를 업데이트합니다.
  void setAutoplay(bool value) {
    _repository.setAutoplay(value);
    state = PlaybackConfigModel(
      muted: state.muted,
      autoplay: value,
    );
  }
}

// playbackConfigProvider: PlaybackConfigViewModel을 제공하는 프로바이더.
// 앱 전역에서 ViewModel에 접근할 수 있도록 합니다.
final playbackConfigProvider =
    NotifierProvider<PlaybackConfigViewModel, PlaybackConfigModel>(
  () => throw UnimplementedError(), // 실제 인스턴스 생성 로직은 runApp에서 ProviderScope를 통해 제공됩니다.
);

// main: 앱의 진입점. ProviderScope를 사용하여 프로바이더 오버라이드를 적용합니다.
void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  final preferences = await SharedPreferences.getInstance();
  final repository = PlaybackConfigRepository(preferences);

  runApp(
    ProviderScope(
      overrides: [
        playbackConfigProvider.overrideWith(() => PlaybackConfigViewModel(repository)),
      ],
      child: const TikTokApp(),
    ),
  );
}

이 예제에서는 ProviderScope를 사용하여 playbackConfigProvider의 구현을 오버라이드함으로써, SharedPreferences 인스턴스에 의존하는 PlaybackConfigRepositoryPlaybackConfigViewModel에 주입합니다. 이렇게 함으로써, 앱 전역에서 동일한 PlaybackConfigViewModel 인스턴스에 접근할 수 있으며, 상태 변경 시 리스너들에게 자동으로 알릴 수 있습니다.

Riverpod는 상태 관리를 위한 선언적 접근 방식을 제공하며, 컴파일 타임 안전성, 테스트 용이성, 그리고 모듈화된 아키텍

처를 가능하게 합니다. 이러한 특성은 크고 복잡한 앱을 개발할 때 특히 유용합니다.

overrideWith 메서드를 사용하는 이유는 주로 개발 과정에서 특정 Provider의 구현을 다른 구현으로 대체하기 위함입니다. 이는 테스트 환경에서 모의 객체(mock objects)를 사용하거나, 애플리케이션의 다른 부분에서 다른 구현을 제공해야 할 때 유용합니다. overrideWith를 사용하면, 애플리케이션의 나머지 부분에 영향을 주지 않고 특정 Provider에 대한 의존성을 재정의할 수 있습니다.

Riverpod에서 ProviderScope와 함께 overrideWith 메서드를 사용하는 주된 시나리오는 다음과 같습니다:

1. 테스트 환경 설정:

테스트를 실행할 때, 실제 데이터를 불러오는 대신 모의 객체나 가짜 데이터를 사용하여 Provider를 오버라이드할 수 있습니다. 이를 통해 테스트의 신뢰성을 높이고, 외부 시스템에 의존하지 않으며, 테스트 실행 속도를 향상시킬 수 있습니다.

2. 개발 중 의존성 주입:

애플리케이션의 특정 부분에서 다른 구현이 필요할 경우, ProviderScope 내에서 overrideWith를 사용하여 의존성을 주입할 수 있습니다. 예를 들어, 개발 초기 단계에서 백엔드 시스템이 준비되지 않았을 때 가짜 데이터를 제공하는 Provider로 오버라이드하여 UI 개발을 계속 진행할 수 있습니다.

3. 구성 가능한 애플리케이션:

애플리케이션의 다양한 실행 환경(개발, 스테이징, 프로덕션 등)에 맞춰 다른 설정이나 서비스를 제공해야 할 때, overrideWith를 사용하여 실행 환경에 맞는 구현을 Provider에 제공할 수 있습니다. 이를 통해 애플리케이션의 구성을 더 유연하게 관리할 수 있습니다.

예시:

ProviderScope(
  overrides: [
    playbackConfigProvider.overrideWith(
      () => PlaybackConfigViewModel(repository)
    ),
  ],
  child: const MyApp(),
);

이 코드는 playbackConfigProvider에 대한 기본 구현을 PlaybackConfigViewModel(repository)로 오버라이드합니다. 이런 방식으로, 애플리케이션 전반에 걸쳐 특정 Provider의 구현을 교체할 수 있어, 의존성 관리가 더욱 유연해집니다.

overrideWith 메서드는 Riverpod의 강력한 기능 중 하나로, 애플리케이션의 다양한 상황에 맞춰 의존성을 쉽게 관리하고, 테스트 및 개발 과정을 용이하게 만들어 줍니다.

throw UnimplementedError()를 사용하는 부분은 종종 Flutter Riverpod에서 초기 프로바이더 설정 시 사용되는데, 특히 프로바이더의 실제 인스턴스 생성을 나중에, 예를 들어 애플리케이션의 진입점에서 수행할 때 유용합니다. 코드의 이 부분은 프로바이더가 어떻게 초기화되어야 하는지를 명시적으로 나타내지 않으며, 대신 ProviderScope 내에서 오버라이드될 것임을 암시합니다.

final playbackConfigProvider =
    NotifierProvider<PlaybackConfigViewModel, PlaybackConfigModel>(
  () => throw UnimplementedError(),
);

이 코드 조각에서 throw UnimplementedError()는 기본적으로 "이 프로바이더의 기본 생성자는 구현되지 않았으며, 이를 사용하기 전에 오버라이드해야 한다"는 의미입니다. SharedPreferences 인스턴스와 같은 외부 의존성이 필요한 경우, 해당 의존성이 준비되는 시점(예: 앱이 시작하고 필요한 비동기 작업이 완료된 후)에 프로바이더를 적절히 초기화할 수 있도록 합니다.

이 패턴은 특히 다음과 같은 이유로 유용합니다:

  1. 의존성 주입: 앱의 시작 시점에 SharedPreferences와 같은 외부 의존성을 준비하고, 이를 필요로 하는 ViewModel이나 다른 객체에 주입할 수 있습니다. 이는 의존성 주입 원칙을 따르며, 테스트 용이성과 코드의 유지보수성을 높여줍니다.

  2. 비동기 작업: SharedPreferences.getInstance()와 같은 비동기 작업을 완료한 후에야 프로바이더를 올바르게 초기화할 수 있습니다. throw UnimplementedError()를 사용하면, 프로바이더의 사용을 지연시킬 수 있으며, 필요한 의존성이 준비될 때까지 기다릴 수 있습니다.

  3. 구성 가능성: 개발, 테스트, 프로덕션 등 다양한 환경에서 다른 구성이나 구현을 쉽게 제공할 수 있습니다. 예를 들어, 테스트 환경에서는 모의 객체를 사용하여 프로바이더를 오버라이드할 수 있습니다.

결론적으로, throw UnimplementedError()를 사용하는 것은 프로바이더의 초기화와 구성을 더 유연하게 관리하기 위한 목적입니다. 이 방식을 통해, 애플리케이션의 다양한 요구사항과 환경에 맞춰 의존성을 효과적으로 관리할 수 있습니다.

Flutter에서 Riverpod를 사용하여 설정 화면을 구성하는 방법을 보여주는 예제입니다. 이 코드는 Riverpod의 ConsumerWidget을 사용하여 비디오 재생 설정(음소거, 자동 재생)을 관리하는 설정 화면을 구현합니다.

import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:tiktok_clone/features/videos/view_models/playback_config_vm.dart';

// StatefulWidget을 ConsumerWidget으로 변경하여 Riverpod와 함께 사용
class SettingsScreen extends ConsumerWidget {
  const SettingsScreen({super.key});

  
  // State<SettingsScreen> createState() => _SettingsScreenState(); 
  // ConsumerWidget은 build 메서드에 WidgetRef 파라미터를 추가합니다.
  Widget build(BuildContext context, WidgetRef ref) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('설정'),
      ),
      body: ListView(
        children: [
          // 음소거 설정 스위치
          SwitchListTile.adaptive(
            // value: false, // 기본 값을 지정하는 대신 Riverpod의 상태를 참조합니다.
            // onChanged: (value) => {}, // 비어 있는 콜백 대신 실제 동작을 구현합니다.
            value: ref.watch(playbackConfigProvider).muted,
            onChanged: (value) => ref.read(playbackConfigProvider.notifier).setMuted(value),
            title: const Text("음소거"),
            subtitle: const Text("기본적으로 비디오를 음소거합니다."),
          ),
          // 자동 재생 설정 스위치
          SwitchListTile.adaptive(
            // value: false,
            // onChanged: (value) => {},
            value: ref.watch(playbackConfigProvider).autoplay,
            onChanged: (value) => ref.read(playbackConfigProvider.notifier).setAutoplay(value),
            title: const Text("자동 재생"),
            subtitle: const Text("비디오가 자동으로 재생됩니다."),
          ),
          // 기타 설정 예시 (여기서는 기능 구현이 생략되어 있음)
          SwitchListTile.adaptive(
            value: false, // 이 부분은 실제 앱 상태와 연동되지 않음
            onChanged: (value) {},
            title: const Text("알림 활성화"),
            subtitle: const Text("알림이 귀엽게 보일 겁니다."),
          ),
          CheckboxListTile(
            activeColor: Colors.black,
            value: false, // 이 부분도 실제 앱 상태와 연동되지 않음
            onChanged: (value) {},
            title: const Text("마케팅 이메일"),
            subtitle: const Text("우리는 스팸을 보내지 않습니다."),
          ),
        ],
      ),
    );
  }
}

이 코드는 ConsumerWidget을 사용하여 설정 화면을 구성합니다. ConsumerWidgetWidgetRef 파라미터를 통해 Riverpod 상태를 읽거나 쓸 수 있게 해주어, 각 설정의 현재 값을 가져오고 변경할 때 사용됩니다. 예제에서는 playbackConfigProvider를 통해 음소거 및 자동 재생 설정의 상태를 관리하며, 사용자가 설정을 변경할 때마다 해당 값을 업데이트합니다.

Riverpod를 사용하면 앱의 상태 관리 로직을 위젯 트리에서 분리할 수 있어, 코드의 가독성과 유지보수성이 향상됩니다. 또한, ConsumerWidget을 사용하면 상태 변경에 따라 자동으로 위젯을 재빌드할 수 있어, Flutter의 선언적 UI 패러다임을 더욱 효과적으로 활용할 수 있습니다.

이 코드 예제에서 사용된 ref.watchref.read는 Flutter Riverpod 상태 관리 라이브러리의 핵심 메서드입니다. ConsumerWidget 내부에서, 이 메서드들은 WidgetRef 객체를 통해 접근되며, Riverpod 프로바이더를 통해 관리되는 상태를 읽거나, 액션을 수행하기 위해 사용됩니다.

ref.watch

ref.watch 메서드는 주어진 프로바이더의 현재 상태를 읽고, 해당 프로바이더의 상태가 변경될 때마다 위젯을 다시 빌드하도록 위젯에 알립니다. 즉, ref.watch를 사용하여 프로바이더를 구독하고, 프로바이더의 상태가 업데이트될 때마다 이를 반영하여 UI를 자동으로 갱신합니다.

value: ref.watch(playbackConfigProvider).muted,

위 코드에서 ref.watch(playbackConfigProvider).mutedplaybackConfigProvider 프로바이더가 관리하는 muted 상태(음소거 여부)를 구독하고 있습니다. muted 값이 변경될 때마다, 해당 SwitchListTile 위젯이 포함된 UI 부분이 자동으로 업데이트되어 사용자에게 최신 상태를 반영합니다.

ref.read

ref.read 메서드는 주어진 프로바이더의 현재 상태를 읽지만, ref.watch와 달리 위젯을 다시 빌드하도록 트리거하지 않습니다. ref.read는 상태를 읽거나, 프로바이더의 액션(예: 메서드 호출)을 수행할 때 사용됩니다. 이 메서드는 주로 사용자의 입력과 같은 이벤트 핸들러 내에서 상태를 변경하거나, 부작용을 일으키는 액션을 수행할 때 사용됩니다.

onChanged: (value) => ref.read(playbackConfigProvider.notifier).setMuted(value),

위 코드에서 ref.read(playbackConfigProvider.notifier).setMuted(value)는 사용자가 스위치를 토글할 때 setMuted 메서드를 호출하여 playbackConfigProvider의 상태를 업데이트합니다. 여기서는 ref.read를 사용하여 상태 변경을 수행하지만, 이 코드 라인이 실행된다고 해서 바로 위젯이 다시 빌드되지는 않습니다. 대신, 상태 변경으로 인해 watch를 통해 해당 상태를 구독하는 위젯들이 자동으로 업데이트됩니다.

요약하면, ref.watch는 상태 변화를 구독하고 상태가 변경될 때마다 위젯을 재빌드하기 위해 사용되며, ref.read는 상태를 읽거나 액션을 수행하기 위해 사용되지만 위젯의 재빌드를 트리거하지 않습니다.

BuildContext vs WidgetRef

Riverpod는 기존의 Flutter의 BuildContext를 사용하는 방식과는 다르게 동작합니다. 대신, Riverpod는 상태 관리를 위해 WidgetRef 객체를 제공합니다. 이는 Riverpod가 컨텍스트에 의존하지 않고, 직접적으로 상태를 관리하고 액세스할 수 있는 독립적인 방법을 제공하기 때문입니다.

Flutter에서 Provider와 같은 상태 관리 라이브러리를 사용할 때, 상태에 접근하기 위해 BuildContext를 사용해야 했습니다. 예를 들어, Provider.of<T>(context) 또는 context.watch<T>()와 같은 방식으로 상태를 조회하고 구독했습니다. 이 방식은 Flutter 위젯 트리의 특정 위치에 의존적이며, 때로는 BuildContext가 올바른 범위에 있지 않아 접근할 수 없는 경우가 발생하기도 합니다.

반면, Riverpod는 WidgetRef를 사용하여 이러한 문제를 해결합니다. WidgetRefConsumerWidget 또는 ConsumerStatefulWidget과 같은 Riverpod의 위젯 내에서 사용할 수 있는 객체로, 어떤 위치에서든지 동일한 방식으로 상태를 조회하고, 구독하고, 변경할 수 있게 해줍니다. WidgetRef를 통해, 상태 관리 로직을 위젯 트리의 구조로부터 독립적으로 만들어 줍니다. 이로 인해 코드의 재사용성과 테스트 용이성이 향상되며, 상태 관리 로직을 더 명확하고 일관된 방식으로 구현할 수 있게 됩니다.

final exampleProvider = Provider((ref) => "Hello, Riverpod!");

class ExampleWidget extends ConsumerWidget {
  
  Widget build(BuildContext context, WidgetRef ref) {
    final String value = ref.watch(exampleProvider);
    return Text(value);
  }
}

위의 예시처럼, ConsumerWidgetbuild 메서드에서 BuildContext 대신 WidgetRef를 사용하여 exampleProvider의 상태를 조회합니다. 이 방식은 Riverpod를 사용하는 모든 곳에서 일관되게 적용됩니다.

결론적으로, Riverpod는 BuildContext를 사용하는 대신 WidgetRef를 통해 보다 유연하고, 강력하며, 예측 가능한 상태 관리를 가능하게 하는 현대적인 접근 방식을 제공합니다.

ref.readref.watch를 사용할 때 .notifier 접근자의 유무는 Riverpod의 StateNotifierProvider 또는 비슷한 프로바이더와 상호 작용하는 방식과 관련이 있습니다. 이 차이는 특히 StateNotifierProvider를 사용할 때 두드러지는데, 이는 상태(state)와 상태를 변경하는 로직(notifier) 사이의 구분 때문입니다.

ref.watch(.notifier 불필요)

  • ref.watch는 주로 위젯이 프로바이더가 제공하는 상태를 구독할 때 사용됩니다. 이를 통해 상태가 변경될 때마다 위젯이 자동으로 다시 빌드되도록 할 수 있습니다.
  • StateNotifierProvider의 경우, ref.watch를 사용하면 상태의 현재 값을 가져옵니다. 여기서 .notifier 접근자는 필요하지 않습니다. 왜냐하면 watch는 상태 변화에 반응하여 UI를 업데이트하는 데 목적이 있기 때문입니다.
final String value = ref.watch(myStateNotifierProvider); // 상태 값에 접근

ref.read(.notifier 필요)

  • ref.read는 주로 한 번만 상태를 읽거나, StateNotifier와 같은 객체의 메서드를 호출할 때 사용됩니다. ref.read를 사용하면 상태가 변경되어도 위젯이 자동으로 다시 빌드되지 않습니다.
  • StateNotifierProvider를 사용할 경우, .notifier 접근자를 통해 StateNotifier 인스턴스에 접근하고, 그 메서드(예: 상태를 변경하는 로직)를 호출할 수 있습니다. 이 경우, .notifier 접근자가 필요한 이유는 StateNotifier의 메서드를 호출하여 상태를 변경하기 위함입니다.
ref.read(myStateNotifierProvider.notifier).someMethod(); // StateNotifier의 메서드 호출

요약

  • ref.watch는 상태 자체를 구독하고, 상태가 변경될 때마다 위젯을 다시 빌드하는 데 사용됩니다. 여기서는 상태의 현재 값을 직접 관찰하는 것이 목적이므로 .notifier가 필요 없습니다.
  • ref.read는 상태를 변경하는 로직을 호출할 때 사용되며, 이 경우 .notifier 접근자를 통해 StateNotifier와 같은 상태 변경자에 접근합니다. 상태 변경 메서드를 호출할 때는 위젯을 자동으로 다시 빌드할 필요가 없기 때문에 read를 사용합니다.

이러한 구분은 Riverpod가 상태 자체와 상태를 변경하는 로직을 명확히 분리하도록 설계되었기 때문에 생깁니다. 이는 코드의 가독성과 유지보수성을 높이는 데 도움이 됩니다.

profile
공부하는 개발자

0개의 댓글