flutter pub add riverpod
위 코드로 라이브러리를 설치하면 된다.
또한 위 라이브러리는 provider를 개발한 개발자가 만든 라이브러리이며 provider보다 조금 더 편리하고 유연한 상태관리를 위한 라이브러리로 업그레이드 됐다.
이때 riverpod(다트), flutter_riverpod가 따로 있으니 반드시 flutter_riverpod로 설치해줘야한다.
이와 또 다른 유명한 라이브러리는 getX정도가 있겠고 마치 vue.js의 상태관리 라이브러리가 vuex와 pinia가 매우 유망하고 pinia로 넘어가는 시대처럼 RiverPod를 많이 쓰지만 최근에는 getX를 더 많이 사용하는 것 같다. (뇌피셜)
일단 기본적인 Riverpod를 공부하고 추후 getX를 공부하여 포스팅하겠다.
공급자라는 객체를 생성할 수 있다.
리버포드 패키지가 제공하는 클래스에 기반하여 객체를 생성하는 것이다.
이제 이 공급자는 동적 값를 제공할 수 있고 결국 그 값을 바꿀 수 있는 메서드도 제공한다.
그리고 우리 앱에서 어떤 위젯에서든 공급자에 연결된 소비자를 설정할 수 있다.
이는 리버포드 패키지가 자동으로 연결하는 것이다.
이 소비자 위젯에서 해당 공급자 값에 대한 변화를 listen할 수 있다.
혹은 공급자가 제공하는 메서드를 호출해 그러한 변화를 트리거할 수도 있다.
어떤 프레임웤이든 state를 관리하는 라이브러릴 사용할 땐 항상 app에 해당되는 부분에 warp을 하였다.
react에서는 <Provider>로 <APP>을 감싸고 vue.js에서는 app.use()로 넣어줬다. 마찬가지이다.
ProviderScope 위젯엔 child 속성이 있는데 여기에 우리가 사용하는 App 위젯을 넣어주면 App wide하게 사용이 가능하다. 즉, ProviderScope는 프로바이더 상태 저장소 역할을 하며 앱 어디서든 동일 Container를 공유한다.
Provider | 목적 | 상태 | 변경 |
---|---|---|---|
Provider<T> | 불변 의존성 주입 | ❌ | DB·API 클라이언트 등 싱글턴 |
StateProvider<T> | 가벼운 가변 값 | state = | 내부적으로 StateController<T> 반환 |
StateNotifierProvider<Notifier,T> | 로직·이벤트 분리 | state =, Notifier 메서드 | UI-로직 분리 권장 |
FutureProvider<T> | Future 기반 비동기 | 자동 재빌드 | 반환형 AsyncValue<T> |
StreamProvider<T> | 스트림 구독 | 자동 재빌드 | 마지막 값 캐시·테스트 Override 용이 |
NotifierProvider/AsyncNotifierProvider | 코드 생성 기반 동기·비동기 Notifier | build()·update() | 최신 권장 API |
import 'package:riverpod/riverpod.dart';
final configProvider = Provider<Config>((ref) {
return Config(baseUrl: 'https://api.example.com');
});
위 패키지에서 제공하는 공급자 객체를 인스턴스화 하고 이를 변수에 저장해야한다.
값은 읽기 전용. 다른 Provider에서 ref.watch(configProvider)로 주입.
내부적으로 캐시되어 같은 값을 여러 번 요청해도 새로 생성되지 않는다.
단순 값이나 객체를 제공하며, 외부에서 값을 직접 변경할 수 없다.
그리고 이 공금자 객체는 반드시 최소 1개의 함수를 매개변수로 갖는다.
그리고 또 그 함수는 ProviderRef를 매개변수로 받는다. 이때 이름 그대로 변수의 타입은 ProviderRef 타입이다.
그리고 return 값은 우리가 전역 state로 사용하고 싶은 값을 return하면 된다.
final counterProvider = StateProvider<int>((ref) => 0);
UI
ConsumerWidget build(...) {
final count = ref.watch(counterProvider);
return IconButton(
onPressed: () => ref.read(counterProvider.notifier).state++,
// 또는 간결하게 ref.read(counterProvider.notifier).update((state) => state + 1);
icon: Text('$count'),
);
}
int, bool, String, enum 등과 같은 단순하고 변경 가능한 상태를 관리할 때 사용한다.
예를 들어, 카운터 값, 체크박스 상태, 라디오 버튼 선택, 간단한 필터링 기준 등 직접적으로 값을 변경해야 할 때 유용하다.
notifier
속성을 통해 StateController
에 접근하여 .state
를 직접 변경함으로써 상태를 업데이트할 수 있다.
상태가 변경되면 이를 구독하는 위젯이 자동으로 리빌드된다.
class Todo {
final String id;
final String description;
final bool completed;
Todo({required this.id, required this.description, this.completed = false});
// 새로운 상태를 생성하는 copyWith 메서드 (불변성 유지)
Todo copyWith({String? id, String? description, bool? completed}) {
return Todo(
id: id ?? this.id,
description: description ?? this.description,
completed: completed ?? this.completed,
);
}
}
Todo 목록을 관리하는 로직
class TodoListNotifier extends StateNotifier<List<Todo>> {
TodoListNotifier() : super([]); // 초기 상태는 빈 리스트
void addTodo(String description) {
state = [...state, Todo(id: DateTime.now().toString(), description: description)];
}
void toggleTodo(String id) {
state = [
for (final todo in state)
if (todo.id == id)
todo.copyWith(completed: !todo.completed)
else
todo,
];
}
void removeTodo(String id) {
state = state.where((todo) => todo.id != id).toList();
}
}
final todoListProvider = StateNotifierProvider<TodoListNotifier, List<Todo>>((ref) {
return TodoListNotifier();
});
class TodoPage extends ConsumerWidget {
const TodoPage({super.key});
Widget build(BuildContext context, WidgetRef ref) {
// Todo 목록의 상태를 구독
final todos = ref.watch(todoListProvider);
// Notifier 인스턴스를 읽어와 메서드 호출
final todoListNotifier = ref.read(todoListProvider.notifier);
return Scaffold(
appBar: AppBar(title: const Text('Todo List with StateNotifierProvider')),
body: ListView.builder(
itemCount: todos.length,
itemBuilder: (context, index) {
final todo = todos[index];
return ListTile(
title: Text(
todo.description,
style: TextStyle(
decoration: todo.completed ? TextDecoration.lineThrough : null,
),
),
trailing: Checkbox(
value: todo.completed,
onChanged: (_) => todoListNotifier.toggleTodo(todo.id),
),
onLongPress: () => todoListNotifier.removeTodo(todo.id),
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: () {
todoListNotifier.addTodo('새로운 할 일 ${todos.length + 1}');
},
child: const Icon(Icons.add),
),
);
}
}
상태 업데이트는 StateNotifier 클래스 내부의 메서드를 통해 이루어진다.
상태는 불변 객체로 다루어지므로, 상태를 변경할 때는 항상 새로운 상태 객체를 생성하여 state = newState
형태로 할당해야 한다 (Dart의 Collection-if
, Spread Operator
, copyWith
활용).
StateProvider
보다 복잡한 상태와 비즈니스 로직을 효율적으로 관리할 수 있다.
복잡한 비즈니스 로직을 포함하는 변경 가능한 상태를 관리할 때 사용.
상태는 불변(immutable) 객체로 관리하고, 상태 변경은 StateNotifier
클래스 내부의 메서드를 통해서만 이루어지는 패턴을 따른다.
예를 들어 Todo 리스트 관리, 사용자 인증 상태, 장바구니 관리, 복잡한 폼 상태 등 여러 동작을 통해 상태가 변경되고, 상태 변경 로직이 비교적 복잡할 때 적합하다.
class CounterNotifier extends StateNotifier<int> {
CounterNotifier() : super(0);
void increment() => state++;
}
final counterNotifierProvider =
StateNotifierProvider<CounterNotifier, int>(CounterNotifier.new);
로직 캡슐화와 테스트 용이성 향상.
비동기 작업(Future)으로 값을 가져올 때 사용한다.
주로 네트워크 요청, 데이터베이스 조회 등 시간이 걸리는 작업을 처리하고 그 결과를 UI에 표시해야 할 때 사용한다.
final jokeProvider = FutureProvider<Joke>((ref) async {
return JokeService().getRandomJoke();
});
UI에서 AsyncValue 처리
final jokeAsync = ref.watch(jokeProvider);
return jokeAsync.when(
loading: () => CircularProgressIndicator(),
error: (e, _) => Text('오류: $e'),
data: (j) => Text(j.setup),
);
AsyncValue 한 번에 세 가지 상태 표현.
시간에 따라 여러 개의 값을 지속적으로 방출하는 비동기 작업(Stream)을 관리할 때 사용한다.
실시간 데이터 스트림, 채팅 메시지, 타이머, 센서 데이터 등 연속적인 데이터 업데이트가 필요한 경우에 적합하다.
FutureProvider와 마찬가지로 로딩 중, 데이터 준비 완료, 에러 발생 상태를 자동으로 관리한다.
final chatStreamProvider = StreamProvider<List<String>>((ref) {
return ChatSocket().messagesStream; // Stream<List<String>>
});
final countdownProvider = StreamProvider<int>((ref) {
return Stream.periodic(const Duration(seconds: 1), (count) => count)
.take(10); // 0부터 9까지 10번만 방출
});
Firebase/웹소켓 실시간 데이터에 적합.
final countAsyncValue = ref.watch(countdownProvider);
child: countAsyncValue.when(
loading: () => const Text('카운트다운 시작 중...'), // 로딩 중 (스트림 시작 전)
error: (err, stack) => Text('에러 발생: $err'), // 에러 발생 시
data: (count) => Text(
'현재 카운트: $count', // 데이터 수신 시
style: Theme.of(context).textTheme.headlineLarge,
),
),
지속적으로 새로운 값을 받을 때마다 이를 구독하는 위젯이 업데이된다.
class TodoList extends _$TodoList {
List<Todo> build() => [];
void add(String desc) {
state = [...state, Todo(desc)];
}
}
flutter pub run build_runner build
실행 후 todo_list.g.dart
에 todoListProvider
생성.
AsyncNotifier
를 상속하면 build를 Future로 정의해 로딩·에러를 자동 관리.
Riverpod 2.0
버전 이상에서 도입된 최신 Provider들이다.
StateNotifierProvider
와 FutureProvider/StreamProvider
의 기능을 통합하고 개선하여, 더욱 강력하고 안전하며, 타입 추론 및 의존성 주입이 용이하도록 설계되었다.
NotifierProvider
: 동기적으로 변경되는 복잡한 상태를 관리하며, StateNotifierProvider
의 더 현대적인 대안이다.
AsyncNotifierProvider
: 비동기적으로 변경되는 복잡한 상태 (AsyncValue
)를 관리하며, FutureProvider
나 StreamProvider
보다 더 복잡한 비동기 로직(예: 캐싱, 동시 요청 처리, 실시간 데이터 업데이트와 사용자 액션을 결합)을 처리할 때 적합하다.
state는 AsyncValue<String>
타입이 된다.
class UserNotifier extends AsyncNotifier<String> {
// AsyncNotifier 초기화. Future를 반환하여 초기 비동기 상태를 설정.
Future<String> build() async {
await Future.delayed(const Duration(seconds: 1));
return '기본 사용자';
}
// 사용자 이름을 비동기적으로 변경하는 메서드
Future<void> updateUserName(String newName) async {
state = const AsyncLoading(); // 로딩 상태로 변경
try {
await Future.delayed(const Duration(seconds: 1)); // 비동기 작업 시뮬레이션
state = AsyncData(newName); // 성공적으로 데이터 업데이트
} catch (e) {
state = AsyncError(e, StackTrace.current); // 에러 발생 시
}
}
}
final userNotifierProvider = AsyncNotifierProvider<UserNotifier, String>(
() => UserNotifier(),
);
class UserProfileNotifierPage extends ConsumerWidget {
const UserProfileNotifierPage({super.key});
Widget build(BuildContext context, WidgetRef ref) {
// Notifier의 상태를 구독 (AsyncValue<String>)
final userAsyncValue = ref.watch(userNotifierProvider);
// Notifier 인스턴스를 읽어와 메서드 호출
final userNotifier = ref.read(userNotifierProvider.notifier);
return Scaffold(
appBar: AppBar(title: const Text('AsyncNotifierProvider Example')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
userAsyncValue.when(
loading: () => const CircularProgressIndicator(),
error: (err, stack) => Text('에러 발생: $err'),
data: (user) => Text(
'현재 사용자: $user',
style: Theme.of(context).textTheme.headlineSmall,
),
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: () {
userNotifier.updateUserName('새로운 사용자 (${DateTime.now().second})');
},
child: const Text('사용자 이름 변경'),
),
],
),
),
);
}
}
build
메서드를 통해 Provider
가 초기화될 때의 상태를 정의한다.
state
속성에 직접 AsyncData
, AsyncLoading
, AsyncError
를 할당하여 상태를 수동으로 제어할 수 있다.
NotifierProvider
는 AsyncNotifierProvider
와 유사하지만 build
메서드가 Future
를 반환하지 않고, state
도 AsyncValue
가 아닌 일반 값을 직접 관리한다.
앞서 사용했던 그냥 Provider는 state 값이 변하지 않을 때 사용하는 값이다. 만약 state 값이 변한다면 StateNotifierProvider객체를 사용해야한다.
그리고 이 StateNotifierProvider는 항상 StateNotifier와 함께 사용된다. 마치 StatefulWidget이 State class와 함께 사용되던 것 처럼 말이다.
그래서 StateNofifier를 상속받는 클래스는 사실 아무렇게나 이름을 지어도 되지만 전통적으로 Notifier라는 접미어는 반드시 붙어있어야한다.
그리고 StateNofifier에 들어갈 데이터 타입은 Riverpod에게 어떤 종류의 데이터가 StateNofifier에 의해 관리되고 StateNotifierProvider에 의해 최종적으로 반환되는 값이어야한다.
:
으로 initializing을 해줬다. 이때 StateNotifier에 StateNotifierProvider에서 반환할 타입을 넣주는데 이때 초기화도 마찬가지로 해당 타입에 맞춰서 초기화를 해주면 된다.StateNotifier가 갖는 데이터 타입은 앞으로 해당 state에 다룰 데이터 타입을 넣어주면 된다.
또한 StateNotifier는 기존의 컴포넌트 클래스의 생성자 함수 선언 방식과는 조금 다르기 때문에 유의깊게 살펴봐야한다.
그리고 위 사진에서는 빼먹은 것이 있는데 바로 super 메서드 내부에 초기화해주는 값에 const
키워드를 빼먹었다. 이를 추가해줘야하는 이유는 river pod로 관리되는 state값들은 mutate 되면 안 되기 때문이다. 즉, 직접적으로 수정하면 안 되고 새로운 state 객체를 생성한 후 대체해야한다.
따라서 혹시나 실수로 직접적으로 수정하려고 할 때 막아줄 수 있는 보험역할로서 const
키워드가 들어가는 것이다.
이 문장은 밑에 setFilter 메서드가 state를 설정하는 방식을 보면 쉽게 이해할 수 있다.
위 state 객체는 마치 widget과 ref와 비슷하며 StateNotifier가 제공하는 객체이다.
그리고 state객체는 StateNotifier가 들어있는 데이터 타입의 값을 갖는다.
그리고 이 state를 절대 직접 변환하면 안 된다. 오직 할당 =
연산만 가능하다.
그래서 위 코드는 state에 .contains 메서드를 사용하여 매개변수로 들어온 값이 있다면 빼고 없다면 추가해주는 메서드이다.
앞서 언급 했듯 state객체를 절대 직접변환하면 안 되기에 그래서 remove 메서드 대신 where 메서드를 사용한다.
where 메서드는 항상 새 배열을 생성한다. => 기존 데이터를 건들지 않는다.
참고로 where 메서드로 remove 함수와 같은 효과를 내는 메서드는 위 코드와 같다. where 함수는 주어진 배열에서 주어진 조건이 참인 것 만을 반환하기 때문이다.
또한 ...
spread 연산자로 기존에 값을 모두 가져오고 추가적으로 매개변수로 들어온 값을 넣어주기 위함이다.
이때 타입이 ary가 아닌 Map이라면?!
마찬가지로 spread 연산자를 하고 그 밑에 매개변수로 들어오는 값을 추가해주면 된다.
이렇게 하면 비로소 edit class가 완성 되었다. 이제 실제 Provier 클래스와 연결하면 된다.
여기 StateNotifierProvider 공급자도 마찬가지로 함수를 매개변수로 받고 그 함수의 매개변수는 ref이다.
그리고 반환 값은 당연 위에서 data를 edit 한 ~~Notifier 인스턴스가 들어와한다. 위의 Provider와 같은 원리이다. 실제 공급자가 데이터를 들고갈 데이터가 와야한다.
그리고 반환값이 있다면 반환값에 대한 type을 지정해줘야한다.
첫번째는 인스턴스의 타입이고 두번째는 인스턴스가 반환하는 데이터 타입을 넣어줘야한다.
그리고 이렇게 지정해두면 다른 consumer에서 사용했을 때 보다 나은 경험을 제공할 것이다.
그리고 실행은 .read() 메서드를 사용하여 가져온다. watch는 여기서 적절하지 않다. 메서드를 사용하는데 메서드는 객체의 메서드이고 이를 watch 주시하는 것은 메모리 낭비이기 때문에 read를 공급자를 불러들인 다음 해당 메서드를 사용하면 된다.
또한 initState와 같은 1번 실행되는 함수에서도 watch 함수를 사용할 필요가 없다. 왜? => 코드 자체가 1번만 실행되기 때문에
.autoDispose
: 리스너가 0개가 되면 상태 파기
.family
: 외부 파라미터로 다중 인스턴스 생성
.keepAlive()
: autoDispose 환경에서 특정 Provider 오래 유지
.overrideWith
: 테스트/하위 Scope에서 Provider 교체