RiverPod
상태관리 패키지 중 하나로, GetX, Provider, BLoC등 플러터에서 주로사용하는 상태관리를 위한 패키지중 하나이다. Riverpod는 실제 Provider의 개발자가 개발 하였고, Provider 패턴을 기반으로 하며, Provider의 문제점을 개선하고 상태 관리를 보다 쉽고 간단하게 구현할 수 있도록 도와준다.
dependencies:
flutter_riverpod:
플러터에서 riverpod를 사용하기 위해 pubspec.yaml에 flutter_riverpod를 추가해준다.
우선 RiverPod를 구성하는 provider의 종류들을 코드를 통해 알아보겠다.
단순한 데이터 , UI 내에서 직접 변경을할때 사용하며 복잡한로직 보다는 비교적 간단한로직에서 사용이 적합하다.
import 'package:flutter_riverpod/flutter_riverpod.dart';
final numberProvider = StateProvider<int>((ref) => 0);
전역변수로 StateProvider를 생성한다.
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_riverpod_playground/layout/default_layout.dart';
import 'package:flutter_riverpod_playground/riverpod/state_provider.dart';
class StateProviderScreen extends ConsumerWidget {
const StateProviderScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final provider = ref.watch(numberProvider);
return Scaffold(
appBar: AppBar(
title: const Text(
'StateProviderScreen',
),
),
body: SizedBox(
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
ElevatedButton(
onPressed: () {
ref.read(numberProvider.notifier).update((state) => state + 1);
},
child: const Text('Up'),
),
const SizedBox(
width: 16,
),
Text(provider.toString()),
const SizedBox(
width: 16,
),
ElevatedButton(
onPressed: () {
ref.read(numberProvider.notifier).state =
ref.read(numberProvider.notifier).state - 1;
},
child: const Text('Down'),
),
],
),
),
);
}
}
기본적인 RiverPod 구조이다. 위젯은 ConsumerWidget을 상속받고, WidgetRef ref를 사용하여 provider에 접근 할수 있다. 보통 업데이트가 있을때 지속적으로 build가 필요한 경우에는 ref.watch()를 이용하여 provider의 변화를 감지할때 사용한다. 반면, read를 사용하는 경우는 실행되는 순간 한번 provider 가져오는데 , onPressed 콜백 처럼 특정 액션 뒤 read를 사용한다. read는 보통 2가지 방법으로 사용한다.
방법 1. ref.read(numberProvider.notifier).update((state) => state + 1);
방법 2. ref.read(numberProvider.notifier).state = ref.read(numberProvider.notifier).state - 1;
클래스 메소드를 이용한 상태관리로 StateProvider보다는 복잡한 데이터 관리에 적합. StateNotifier를 상속한클래스를 반환한다.
class WorkoutModel {
final String name;
final int weight;
final bool isSuccess;
WorkoutModel({
required this.name,
required this.weight,
required this.isSuccess,
});
WorkoutModel copyWith({
String? name,
int? weight,
bool? isSuccess,
}) {
return WorkoutModel(
name: name ?? this.name,
weight: weight ?? this.weight,
isSuccess: isSuccess ?? this.isSuccess);
}
}
StateNotifierProvider에서 사용할 모델이다.
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_riverpod_playground/model/workout_model.dart';
final workoutListProvider =
StateNotifierProvider<WorkOutListNotfier, List<WorkoutModel>>(
(ref) => WorkOutListNotfier());
class WorkOutListNotfier extends StateNotifier<List<WorkoutModel>> {
WorkOutListNotfier()
: super(
[
WorkoutModel(name: '벤치프레스', weight: 105, isSuccess: true),
WorkoutModel(name: '데드리프트', weight: 200, isSuccess: true),
WorkoutModel(name: '스쿼트', weight: 170, isSuccess: false),
],
);
void toggleIsSuccess(String name) {
state
.map((e) => e.name == name
? WorkoutModel(
name: e.name, weight: e.weight, isSuccess: !e.isSuccess)
: e)
.toList();
}
}
StateNotifier를 상속한 WorkOutListNotifier 클래스를 StateNotifierProvider로 반환한다.
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_riverpod_playground/layout/default_layout.dart';
import 'package:flutter_riverpod_playground/riverpod/state_notifier_provider_2.dart';
class StateNotifierProviderScreen2 extends ConsumerWidget {
const StateNotifierProviderScreen2({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final state = ref.watch(workoutListProvider);
return DefaultLayout(
title: 'StateNotifierProvider2',
body:ListView(
children: state
.map((e) => CheckboxListTile(
title: Text(e.name),
value: e.isSuccess,
onChanged: (bool? value) {
ref
.read(workoutListProvider.notifier)
.toggleIsSuccess(e.name);
}))
.toList()),
);
}
}
Future 타입을 반환 하며 , 특정행동뒤에 재실행 기능이 없기때문에 유용하지 않다.
import 'package:flutter_riverpod/flutter_riverpod.dart';
final futureProvider = FutureProvider<List<int>>((ref) async {
await Future.delayed(const Duration(seconds: 5));
return [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
});
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_riverpod_playground/riverpod/future_provider.dart';
class FutureProviderScreen extends ConsumerWidget {
const FutureProviderScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final AsyncValue state = ref.watch(futureProvider);
return Scaffold(
appBar: AppBar(
title: Text('FutureProviderScreen'),
),
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// AsyncValue일 경우, 사용 가능
state.when(
data: (data) {
return Text(
data.toString(),
textAlign: TextAlign.center,
);
},
error: (err, stack) => Text(err.toString()),
loading: () => const Center(
child: CircularProgressIndicator(),
),
),
],
),
);
}
}
Stream을 반환을 하며 , socket 사용시 사용에 용의하다.
import 'package:flutter_riverpod/flutter_riverpod.dart';
final streamProvider = StreamProvider<List<int>>((ref) async* {
for (var i = 0; i < 10; i++) {
await Future.delayed(Duration(seconds: 1));
yield List.generate(3, (index) => index * i);
}
});
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_riverpod_playground/layout/default_layout.dart';
import 'package:flutter_riverpod_playground/riverpod/stream_provider.dart';
class StreamProviderScreen extends ConsumerWidget {
const StreamProviderScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final state = ref.watch(streamProvider);
return DefaultLayout(
title: 'StreamProviderScreen',
body: Center(
child: state.when(
data: (data) => Text(data.toString()),
error: (err, stack) => Text(err.toString()),
loading: () => const CircularProgressIndicator())));
}
}
이렇게 Provider들의 종류와 사용방법에 대해 알아보았다.
Riverpod의 Listener와 Selector는 상태 관리를 더 효과적으로 수행하고 앱의 성능을 최적화하기 위한 도구로 사용됩니다. 이들을 사용하면 불필요한 렌더링 , 리스닝을 최소화하여 상태 관리 코드를 더 구조화하고 유지 관리하기 쉽게 만들 수 있습니다. 코드와 함께 실행화면을 통해 정리하겠다.
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_riverpod_playground/model/workout_model.dart';
final workoutProvider = StateNotifierProvider<WorkoutNotifier, WorkoutModel>(
(ref) => WorkoutNotifier());
class WorkoutNotifier extends StateNotifier<WorkoutModel> {
WorkoutNotifier()
: super(
WorkoutModel(name: '벤치프레스', weight: 110, isSuccess: false),
);
upWeigth() {
state = state.copyWith(weight: state.weight + 5);
}
toggleIsSuccess() {
state = state.copyWith(isSuccess: !state.isSuccess);
}
}
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_riverpod_playground/model/workout_model.dart';
import 'package:flutter_riverpod_playground/riverpod/workout_provider.dart';
class WorkoutScreen extends ConsumerWidget {
const WorkoutScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final model = ref.watch(workoutProvider);
final state = ref.watch(workoutProvider.select((value) => value.isSuccess));
ref.listen(workoutProvider.select((value) => value.weight),
(previous, next) {
if (next == 140) {
showDialog(
context: context,
builder: (BuildContext context) {
return const AlertDialog(
title: Text('목표 성공'),
);
},
);
}
});
return Scaffold(
appBar: AppBar(
title: const Text('WorkOutScreen'),
),
body: SizedBox(
height: MediaQuery.of(context).size.height,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(model.name),
Text('${model.weight}'),
Text(state.toString()),
],
),
const SizedBox(
width: 16,
),
ElevatedButton(
onPressed: () {
ref.read(workoutProvider.notifier).upWeigth();
},
child: const Text('Up Weight')),
const SizedBox(
width: 16,
),
ElevatedButton(
onPressed: () {
ref.read(workoutProvider.notifier).toggleIsSuccess();
},
child: const Text('Success Toggle')),
],
),
),
);
}
}
여기 까지 Provider의 기본적인 종류와 selector,listener에 대해 알아보았다. 정리할 내용과 코드들이 조금 많아서 다음에 이어서 정리해보겠다.
참고
https://velog.io/@leeeeeoy/Flutter-Riverpod-%EC%82%AC%EC%9A%A9%ED%95%B4%EB%B3%B4%EA%B8%B0-1
https://www.inflearn.com/course/%ED%94%8C%EB%9F%AC%ED%84%B0-%EC%8B%A4%EC%A0%84/dashboard