Flutter - riverpod

MUNGI JO·2024년 8월 30일
0

Flutter

목록 보기
6/6

서론

이전에 프로젝트 구성에 관해서 다뤄봤고 이번엔 각 Tool에 대해 공부하고 직접 적용해보는 과정을 가져보자 한다. riverpod부터 시작해서 다른 툴까지 한번씩 사용해볼예정이다.

riverpod

Riverpod은 Flutter 애플리케이션에서 상태 관리를 위해 제공되는 최신 도구로, 기존의 Provider 패턴을 개선하여 더 안전하고 간결한 코드를 작성할 수 있게 한다. Riverpod의 가장 큰 장점은 BuildContext에 의존하지 않고 상태 관리와 의존성 주입을 처리할 수 있다는 점이다. 이를 통해 전역적인 상태 관리가 가능하며, Flutter 위젯 트리와 독립적으로 상태를 관리할 수 있다.

Provider
Provider는 Riverpod에서 가장 기본적인 상태 관리 단위다. Provider는 상태나 값을 제공하는 역할을 하며, 여러 위젯에서 이를 공유하고 사용할 수 있다. 다만, Provider는 데이터를 읽기만 할 수 있고, 상태를 변경할 수는 없다. 이는 주로 읽기 전용 데이터나 전역적으로 접근해야 하는 값에 대해 사용된다.

예를 들어, 다음 코드는 Provider를 사용하여 간단한 문자열 값을 제공한다:

final exampleProvider = Provider<String>((ref) => "Hello Riverpod");

exampleProvider: Provider<String> 타입으로, 문자열 값을 제공한다. 이 Provider는 "Hello Riverpod"이라는 문자열을 제공하는 역할을 한다.
ref: ref는 ProviderReference의 약자로, Provider 내부에서 다른 Provider나 상태에 접근할 수 있게 해주는 객체다. 여기서는 주로 Provider의 생성 시 필요한 참조를 제공한다.

👉 요약

  • Provider는 Riverpod에서 가장 기본적인 상태 관리 단위로, 데이터를 읽기만 할 수 있다.
  • 상태 변경이 필요 없는 값(예: 설정 값, 상수 데이터)을 여러 위젯에서 공유하고 사용할 때 유용하다.
  • Riverpod은 전역 상태 관리와 의존성 주입을 BuildContext에 의존하지 않고 처리할 수 있게 하며, 이를 통해 Flutter 애플리케이션의 상태 관리를 더욱 유연하고 강력하게 할 수 있다.

1. StateProvider:

StateProvider는 가장 기본적인 상태 관리 단위로, 단순한 상태 변경이 필요한 경우에 사용된다. StateProvider는 내부적으로 상태를 변경할 수 있는 기능을 제공하며, 기본적으로는 단일 값을 상태로 관리한다.

final exampleProvider = StateProvider<int>((ref) => 0);

이 코드는 exampleProvider라는 이름의 StateProvider를 정의하고 있으며, int 타입의 상태를 관리한다. 초기값은 0으로 설정되어 있다. StateProvider는 상태의 읽기와 쓰기를 모두 지원하므로, 상태를 쉽게 변경하고, 그 변경 사항을 UI에 반영할 수 있다.

  • 단순 상태 관리: StateProvider는 단일 값의 상태를 관리하는 데 적합하며, 상태가 변경될 때 이를 쉽게 관리할 수 있다.
  • 상태 변경 기능 제공: StateProvider를 사용하면 상태를 읽고 쓸 수 있는 메커니즘을 제공하여, 필요한 곳에서 상태를 변경할 수 있다.

StateProvider는 간단한 카운터나 토글 상태와 같이, 복잡한 로직이 필요 없는 경우에 유용하다. 상태가 변경될 때 UI를 자동으로 리빌드하여 최신 상태를 반영할 수 있다.

👉 요약

  • StateProvider는 가장 기본적인 상태 관리 단위로, 단순한 상태 변경에 사용된다.
  • 단일 값을 관리하며, 상태를 읽고 변경하는 기능을 제공한다.
  • 간단한 상태 관리가 필요한 상황에서 효과적으로 사용할 수 있다.

2. StateNotifier:

StateNotifier는 더 복잡한 상태 관리를 위한 클래스로, 단일 상태를 관리하며, 이 상태를 변경하는 로직을 포함할 수 있다. StateNotifier는 상태 관리 로직을 하나의 클래스에 캡슐화하여, 상태 변경의 일관성을 유지하고, 상태 관리와 관련된 모든 로직을 명확하게 구조화할 수 있게 해준다.

class CounterNotifier extends StateNotifier<int> {
  CounterNotifier() : super(0);

  void increment() => state++;
  void decrement() => state--;
}

위 예제에서 CounterNotifier는 StateNotifier<int>를 상속받아 int 타입의 상태를 관리한다. CounterNotifier는 increment와 decrement 메서드를 통해 상태를 변경하는 로직을 포함하고 있다.

  • 단일 상태 관리: StateNotifier는 하나의 상태를 관리하며, 상태 변경 로직을 StateNotifier 클래스 내에 정의할 수 있다.
  • 상태 변경 로직 포함: StateNotifier 클래스는 상태 변경 로직을 포함하여, 상태가 변경될 때마다 이를 일관되게 처리할 수 있다.

StateNotifierProvider와 함께 사용하면, 상태 관리 로직을 UI에서 완전히 분리하고 재사용성을 높일 수 있다. 이는 복잡한 상태 관리가 필요한 애플리케이션에서 특히 유용하며, 코드의 유지보수성과 확장성을 크게 개선할 수 있다.

👉 요약

  • StateNotifier는 복잡한 상태 관리와 상태 변경 로직을 캡슐화하는 클래스다.
  • 단일 상태를 관리하며, 상태를 변경하는 메서드를 포함할 수 있다.
  • StateNotifierProvider와 함께 사용하여 상태 관리 로직을 UI와 분리하고, 재사용성을 높일 수 있다.

3. StateNotifierProvider:

StateNotifierProvider는 StateNotifier를 감싸서 사용하는 Provider로, 상태와 상태 변경 로직을 UI에서 분리할 수 있게 한다. 이 패턴을 사용하면 상태 관리와 UI의 결합을 줄이고, 비즈니스 로직을 더욱 명확하게 분리할 수 있다. StateNotifierProvider는 복잡한 상태 관리나 여러 상태를 처리해야 할 때 매우 유용하다.

final counterProvider = StateNotifierProvider<CounterNotifier, int>((ref) => CounterNotifier());

이 코드는 counterProvider라는 이름의 StateNotifierProvider를 정의하고, CounterNotifier 클래스를 통해 상태를 관리한다. StateNotifierProvider는 CounterNotifier 인스턴스를 생성하고, 이 인스턴스가 관리하는 int 타입의 상태를 제공한다.

- CounterNotifier: 상태 관리 로직을 포함한 클래스다. 이 클래스는 StateNotifier<int>를 상속받아 상태 변경 로직을 정의할 수 있다.
- 상태와 상태 변경 로직의 분리: StateNotifierProvider를 사용하면 비즈니스 로직과 UI 로직을 분리하여, 코드의 가독성과 유지보수성을 높일 수 있다.

👉 요약

  • StateNotifierProvider는 상태와 상태 변경 로직을 UI와 분리하기 위해 사용된다.
  • 상태 관리 로직을 StateNotifier 클래스에 캡슐화하고, 이를 StateNotifierProvider를 통해 제공함으로써, 비즈니스 로직을 명확하게 분리할 수 있다.
  • 복잡한 상태 관리가 필요할 때 유용하며, 코드의 구조를 명확하게 하는 데 도움이 된다.

4. FutureProvider

FutureProvider는 비동기 작업의 결과를 제공하는 Provider다. 주로 API 호출이나 비동기 연산과 같은 작업을 수행할 때 사용된다. FutureProvider는 비동기 작업이 완료되면 그 결과값을 자동으로 상태로 반영하고, 이를 UI에 전달할 수 있게 해준다.

final dataProvider = FutureProvider<String>((ref) async { 
  return fetchData(); 
});

이 코드에서는 dataProvider라는 이름의 FutureProvider를 정의하고, fetchData()라는 비동기 함수를 통해 데이터를 가져온다. 이 Provider는 String 타입의 데이터를 반환하는 Future를 관리한다.

FutureProvider를 사용하면 비동기 작업의 진행 상태에 따라 로딩, 성공, 실패 상태를 UI에 쉽게 반영할 수 있다. 예를 들어, 데이터가 로드되는 동안 로딩 스피너를 표시하고, 데이터 로드가 완료되면 그 데이터를 화면에 보여주며, 오류가 발생할 경우 에러 메시지를 표시하는 등의 작업이 가능하다.

👉 요약

  • FutureProvider는 비동기 작업의 결과를 상태로 관리하고, 이를 UI에 반영하는 데 사용된다.
  • 비동기 작업의 상태에 따라 로딩, 성공, 실패 상태를 UI에 쉽게 반영할 수 있다.
  • 주로 API 호출이나 복잡한 비동기 연산을 처리할 때 유용하다.

5. StreamProvider

StreamProvider는 스트림을 통해 지속적으로 업데이트되는 데이터를 제공하는 Provider다. 주로 실시간 데이터 처리나 비동기적인 데이터 스트림을 UI에 반영할 때 사용된다.

final streamProvider = StreamProvider<int>((ref) => myStream());

이 코드는 streamProvider라는 이름의 StreamProvider를 정의하며, myStream()이라는 함수에서 반환된 Stream<int>를 구독한다.

StreamProvider를 사용하면, 스트림에서 발생하는 데이터의 변화를 실시간으로 감지하여, 이를 UI에 반영할 수 있다. 이로 인해 스트림 데이터가 업데이트될 때마다 해당 데이터를 사용하는 위젯이 자동으로 리빌드되어 최신 상태를 반영하게 된다.

👉 요약

  • StreamProvider는 스트림 기반의 데이터를 실시간으로 UI에 반영하기 위해 사용되는 Provider다.
  • 스트림의 데이터를 구독하여, 데이터가 변경될 때마다 자동으로 UI를 업데이트할 수 있다.

6. ProviderScope

ProviderScope는 Riverpod에서 모든 Provider를 관리하는 최상위 위젯이다. 앱의 최상위에 위치하여, 모든 Provider와 그 상태를 중앙에서 관리한다.

모든 Provider는 ProviderScope 내에서 생성되고 관리된다. 이를 통해 앱 전역에서 상태를 쉽게 공유하고, 필요한 곳에서 상태를 구독하거나 변경할 수 있다. 또한, ProviderScope는 Provider의 상태 변경을 감지하고, 이를 반영하여 필요한 UI 요소만 리빌드할 수 있도록 한다.

ProviderScope를 사용하면 앱 내에서 상태 관리를 일관되게 수행할 수 있으며, 효율적인 상태 관리가 가능해진다. 모든 Provider가 하나의 ProviderScope 아래에 있기 때문에, 상태의 생성과 소멸을 중앙에서 제어할 수 있고, 상태 관리의 복잡성을 줄일 수 있다.

👉 요약

  • ProviderScope는 앱의 최상위에서 모든 Provider를 관리하는 컨테이너 역할을 한다.
  • 모든 Provider는 ProviderScope 내에서 생성, 관리되며, 상태 변경을 감지하고 이를 반영한다.
  • 앱 전역에서 상태를 효율적으로 관리하고, 필요한 부분만 리빌드하여 성능을 최적화할 수 있다.

7. ConsumerWidget과 Consumer

ConsumerWidget과 Consumer는 Riverpod에서 Provider의 상태를 구독하고, 상태가 변경될 때 이를 반영하는 역할을 한다. 이 두 가지는 Provider 상태에 따라 특정 위젯이나 화면의 부분을 리빌드하는 데 사용된다.

ConsumerWidget은 StatelessWidget을 대체하며, 간편하게 Provider의 상태를 구독할 수 있게 해준다. 이 위젯은 build 메서드에서 WidgetRef 객체를 통해 Provider의 상태를 읽고, 변경된 상태를 UI에 반영한다. 이렇게 함으로써 상태와 UI의 결합을 줄이고, 상태 변경에 따라 UI를 효율적으로 업데이트할 수 있다.

Consumer는 ConsumerWidget보다 더 세밀한 제어가 필요할 때 사용한다. 특정 위젯 트리의 일부만 상태를 구독하게 함으로써, 불필요한 리빌드를 방지하고 성능을 최적화할 수 있다. 예를 들어, 화면의 특정 부분만 상태 변화에 반응하도록 할 수 있다.

WidgetRef 객체를 사용하여 어떤 값이 변경되는지 확인하고, 변경된 값과 관련된 위젯만 리빌드한다. 이를 통해 앱의 성능이 최적화되며, 불필요한 리빌드로 인한 성능 저하를 방지할 수 있다. 결과적으로, Riverpod은 UI의 특정 부분만 선택적으로 업데이트할 수 있게 하여 효율적인 상태 관리를 가능하게 한다.

전체 코드

간단하게 테스트해본 코드다.

main.dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:learn_riverpod/presentation/home_screen.dart';

// test 시 terminal에 입력
/// 전부 테스트
// flutter test
/// 개별 테스트
// flutter test test/providers_test.dart
// flutter test test/home_screen_test.dart


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

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: HomePage(),
    );
  }
}

providers.dart

import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:learn_riverpod/presentation/viewmodels/counter_viewmodel.dart';

// StateProvider로 기본적인 상태 관리
final counterProvider = StateProvider<int>((ref) => 0);

// 예시용 Provider: 간단한 문자열 제공
final greetingProvider = Provider<String>((ref) => "Hello Riverpod");

// StateNotifierProvider로 더 복잡한 상태 관리
final counterViewModelProvider = StateNotifierProvider<CounterViewModel, int>(
      (ref) => CounterViewModel(),
);

// FutureProvider를 사용해 비동기 데이터를 가져오는 예제
final futureDataProvider = FutureProvider<String>((ref) async {
  await Future.delayed(const Duration(seconds: 2)); // 비동기 작업 시뮬레이션
  return "Fetched Data";
});

counter_viewmodel.dart

import 'package:flutter_riverpod/flutter_riverpod.dart';

// StateNotifier를 사용해 상태 변경 로직을 캡슐화
class CounterViewModel extends StateNotifier<int> {
  CounterViewModel() : super(0); // 초기 상태는 0으로 설정

  void increment() {
    state++;
  }

  void decrement() {
    state--;
  }
}

home_screen.dart

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

class HomePage extends ConsumerWidget {
  
  Widget build(BuildContext context, WidgetRef ref) {
    final greeting = ref.watch(greetingProvider);
    final count = ref.watch(counterViewModelProvider);
    final viewModel = ref.read(counterViewModelProvider.notifier);

    // FutureProvider의 상태를 구독하여 처리
    final asyncValue = ref.watch(futureDataProvider);

    return Scaffold(
      appBar: AppBar(title: const Text('Riverpod Counter')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text(greeting, style: const TextStyle(fontSize: 20)),
            const SizedBox(height: 20),
            Text('Count: $count', style: const TextStyle(fontSize: 40)),
            const SizedBox(height: 20),
            asyncValue.when(
              data: (data) => Text(data, style: const TextStyle(fontSize: 20)),
              loading: () => const CircularProgressIndicator(),
              error: (error, stack) => Text('Error: $error'),
            ),
            const SizedBox(height: 20),
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                ElevatedButton(
                  onPressed: viewModel.increment,
                  child: const Text('Increment'),
                ),
                const SizedBox(width: 20),
                ElevatedButton(
                  onPressed: viewModel.decrement,
                  child: const Text('Decrement'),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}
profile
안녕하세요. 개발에 이제 막 뛰어든 신입 개발자 입니다.

0개의 댓글