이전에 프로젝트 구성에 관해서 다뤄봤고 이번엔 각 Tool에 대해 공부하고 직접 적용해보는 과정을 가져보자 한다. 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의 생성 시 필요한 참조를 제공한다.
👉 요약
StateProvider는 가장 기본적인 상태 관리 단위로, 단순한 상태 변경이 필요한 경우에 사용된다. StateProvider는 내부적으로 상태를 변경할 수 있는 기능을 제공하며, 기본적으로는 단일 값을 상태로 관리한다.
final exampleProvider = StateProvider<int>((ref) => 0);
이 코드는 exampleProvider라는 이름의 StateProvider를 정의하고 있으며, int 타입의 상태를 관리한다. 초기값은 0으로 설정되어 있다. StateProvider는 상태의 읽기와 쓰기를 모두 지원하므로, 상태를 쉽게 변경하고, 그 변경 사항을 UI에 반영할 수 있다.
StateProvider는 간단한 카운터나 토글 상태와 같이, 복잡한 로직이 필요 없는 경우에 유용하다. 상태가 변경될 때 UI를 자동으로 리빌드하여 최신 상태를 반영할 수 있다.
👉 요약
StateNotifier는 더 복잡한 상태 관리를 위한 클래스로, 단일 상태를 관리하며, 이 상태를 변경하는 로직을 포함할 수 있다. StateNotifier는 상태 관리 로직을 하나의 클래스에 캡슐화하여, 상태 변경의 일관성을 유지하고, 상태 관리와 관련된 모든 로직을 명확하게 구조화할 수 있게 해준다.
class CounterNotifier extends StateNotifier<int> {
CounterNotifier() : super(0);
void increment() => state++;
void decrement() => state--;
}
위 예제에서 CounterNotifier는 StateNotifier<int>
를 상속받아 int 타입의 상태를 관리한다. CounterNotifier는 increment와 decrement 메서드를 통해 상태를 변경하는 로직을 포함하고 있다.
StateNotifierProvider와 함께 사용하면, 상태 관리 로직을 UI에서 완전히 분리하고 재사용성을 높일 수 있다. 이는 복잡한 상태 관리가 필요한 애플리케이션에서 특히 유용하며, 코드의 유지보수성과 확장성을 크게 개선할 수 있다.
👉 요약
StateNotifierProvider는 StateNotifier를 감싸서 사용하는 Provider로, 상태와 상태 변경 로직을 UI에서 분리할 수 있게 한다. 이 패턴을 사용하면 상태 관리와 UI의 결합을 줄이고, 비즈니스 로직을 더욱 명확하게 분리할 수 있다. StateNotifierProvider는 복잡한 상태 관리나 여러 상태를 처리해야 할 때 매우 유용하다.
final counterProvider = StateNotifierProvider<CounterNotifier, int>((ref) => CounterNotifier());
이 코드는 counterProvider라는 이름의 StateNotifierProvider를 정의하고, CounterNotifier 클래스를 통해 상태를 관리한다. StateNotifierProvider는 CounterNotifier 인스턴스를 생성하고, 이 인스턴스가 관리하는 int 타입의 상태를 제공한다.
- CounterNotifier: 상태 관리 로직을 포함한 클래스다. 이 클래스는 StateNotifier<int>
를 상속받아 상태 변경 로직을 정의할 수 있다.
- 상태와 상태 변경 로직의 분리: StateNotifierProvider를 사용하면 비즈니스 로직과 UI 로직을 분리하여, 코드의 가독성과 유지보수성을 높일 수 있다.
👉 요약
FutureProvider는 비동기 작업의 결과를 제공하는 Provider다. 주로 API 호출이나 비동기 연산과 같은 작업을 수행할 때 사용된다. FutureProvider는 비동기 작업이 완료되면 그 결과값을 자동으로 상태로 반영하고, 이를 UI에 전달할 수 있게 해준다.
final dataProvider = FutureProvider<String>((ref) async {
return fetchData();
});
이 코드에서는 dataProvider라는 이름의 FutureProvider를 정의하고, fetchData()라는 비동기 함수를 통해 데이터를 가져온다. 이 Provider는 String 타입의 데이터를 반환하는 Future를 관리한다.
FutureProvider를 사용하면 비동기 작업의 진행 상태에 따라 로딩, 성공, 실패 상태를 UI에 쉽게 반영할 수 있다. 예를 들어, 데이터가 로드되는 동안 로딩 스피너를 표시하고, 데이터 로드가 완료되면 그 데이터를 화면에 보여주며, 오류가 발생할 경우 에러 메시지를 표시하는 등의 작업이 가능하다.
👉 요약
StreamProvider는 스트림을 통해 지속적으로 업데이트되는 데이터를 제공하는 Provider다. 주로 실시간 데이터 처리나 비동기적인 데이터 스트림을 UI에 반영할 때 사용된다.
final streamProvider = StreamProvider<int>((ref) => myStream());
이 코드는 streamProvider라는 이름의 StreamProvider를 정의하며, myStream()이라는 함수에서 반환된 Stream<int>
를 구독한다.
StreamProvider를 사용하면, 스트림에서 발생하는 데이터의 변화를 실시간으로 감지하여, 이를 UI에 반영할 수 있다. 이로 인해 스트림 데이터가 업데이트될 때마다 해당 데이터를 사용하는 위젯이 자동으로 리빌드되어 최신 상태를 반영하게 된다.
👉 요약
ProviderScope는 Riverpod에서 모든 Provider를 관리하는 최상위 위젯이다. 앱의 최상위에 위치하여, 모든 Provider와 그 상태를 중앙에서 관리한다.
모든 Provider는 ProviderScope 내에서 생성되고 관리된다. 이를 통해 앱 전역에서 상태를 쉽게 공유하고, 필요한 곳에서 상태를 구독하거나 변경할 수 있다. 또한, ProviderScope는 Provider의 상태 변경을 감지하고, 이를 반영하여 필요한 UI 요소만 리빌드할 수 있도록 한다.
ProviderScope를 사용하면 앱 내에서 상태 관리를 일관되게 수행할 수 있으며, 효율적인 상태 관리가 가능해진다. 모든 Provider가 하나의 ProviderScope 아래에 있기 때문에, 상태의 생성과 소멸을 중앙에서 제어할 수 있고, 상태 관리의 복잡성을 줄일 수 있다.
👉 요약
ConsumerWidget과 Consumer는 Riverpod에서 Provider의 상태를 구독하고, 상태가 변경될 때 이를 반영하는 역할을 한다. 이 두 가지는 Provider 상태에 따라 특정 위젯이나 화면의 부분을 리빌드하는 데 사용된다.
ConsumerWidget은 StatelessWidget을 대체하며, 간편하게 Provider의 상태를 구독할 수 있게 해준다. 이 위젯은 build 메서드에서 WidgetRef 객체를 통해 Provider의 상태를 읽고, 변경된 상태를 UI에 반영한다. 이렇게 함으로써 상태와 UI의 결합을 줄이고, 상태 변경에 따라 UI를 효율적으로 업데이트할 수 있다.
Consumer는 ConsumerWidget보다 더 세밀한 제어가 필요할 때 사용한다. 특정 위젯 트리의 일부만 상태를 구독하게 함으로써, 불필요한 리빌드를 방지하고 성능을 최적화할 수 있다. 예를 들어, 화면의 특정 부분만 상태 변화에 반응하도록 할 수 있다.
WidgetRef 객체를 사용하여 어떤 값이 변경되는지 확인하고, 변경된 값과 관련된 위젯만 리빌드한다. 이를 통해 앱의 성능이 최적화되며, 불필요한 리빌드로 인한 성능 저하를 방지할 수 있다. 결과적으로, Riverpod은 UI의 특정 부분만 선택적으로 업데이트할 수 있게 하여 효율적인 상태 관리를 가능하게 한다.
간단하게 테스트해본 코드다.
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(),
);
}
}
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";
});
import 'package:flutter_riverpod/flutter_riverpod.dart';
// StateNotifier를 사용해 상태 변경 로직을 캡슐화
class CounterViewModel extends StateNotifier<int> {
CounterViewModel() : super(0); // 초기 상태는 0으로 설정
void increment() {
state++;
}
void decrement() {
state--;
}
}
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'),
),
],
),
],
),
),
);
}
}