강의를 듣다보니 지난번 정리를 해두었던 내용은 riverpod v1이었다. 현재는 riverpod v2를 사용하여 code generation 기능이 추가되어 보다 간편하게 riverpod를 사용할수 있다. code generation을 사용하지 않는다면 v1때 사용했던 코드를 사용해도 에러가 발생하지 않고 사용이 가능하다. 다만 v2를 사용하느것이 훨씬 간편해 보인다. 강제적인것이 아니기 때문에 상황에 맞게 사용하는것이 좋을것같다.
dependencies:
flutter_riverpod:
riverpod_annotation: ^2.1.6
build_runner: ^2.4.6
riverpod_generator: ^2.3.3
우선 riverpod에서 CodeGeneration을 하기 위해서는 기존 riverpod 버전을 2.x.x 버전을 사용을 해야하고, riverpd_annotation,build_runner,riverpod_generator를 추가를 해주어야 한다.
그럼 v2에서 개선된 점이 무엇인지 코드,결과를 보고 자세하게 알아보겠다.
기존 riverpod에서는 Provider를 생성할때는 Provider를 사용할지,FutureProvider를 사용할지,StreamProivder를 사용할지 고민할필요없이 반환형만 맞춰주면 코드제네레이션을 하게되면 자동으로 정해지고 기존방식과는 다르게 함수형태로 작성하기 때문에 훨씬 직관적이다. 코드를 작성해 보자.
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'code_generation_provider.g.dart';
// 기존 방식
final originalStateProvider = Provider<String>((ref) => 'riverpod V1');
final originalFutureProvider = FutureProvider<String>((ref) async {
await Future.delayed(const Duration(seconds: 3));
return 'Future v1';
});
// NotifierProvider
final originalStateNotifierProvider =
StateNotifierProvider<OriginalStateNotifier, int>(
(ref) => OriginalStateNotifier());
class OriginalStateNotifier extends StateNotifier<int> {
OriginalStateNotifier() : super(0);
increment() {
state++;
}
decrement() {
state--;
}
}
// CodeGeneration 방식
@riverpod
String newState(NewStateRef ref) {
return 'riverpod V2';
}
@riverpod
Future<String> newFuture(NewFutureRef ref) async {
await Future.delayed(Duration(seconds: 3));
return 'Future v2';
}
// NotifierProvider
@riverpod
class NewStateNotifier extends _$NewStateNotifier {
@override
int build() {
return 0;
}
increment() {
state++;
}
decrement() {
state--;
}
}
/// See also [newState].
@ProviderFor(newState)
final newStateProvider = AutoDisposeProvider<String>.internal(
newState,
name: r'newStateProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product') ? null : _$newStateHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef NewStateRef = AutoDisposeProviderRef<String>;
String _$newFutureHash() => r'f4d6a5f6c2ec5fa72b6a0566591e24eec50a5192';
/// See also [newFuture].
@ProviderFor(newFuture)
final newFutureProvider = AutoDisposeFutureProvider<String>.internal(
newFuture,
name: r'newFutureProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product') ? null : _$newFutureHash,
dependencies: null,
allTransitiveDependencies: null,
);
String _$newStateNotifierHash() => r'cc67d399ebd83fa20c9a33631c8112b8b8c4486c';
/// See also [NewStateNotifier].
@ProviderFor(NewStateNotifier)
final newStateNotifierProvider =
AutoDisposeNotifierProvider<NewStateNotifier, int>.internal(
NewStateNotifier.new,
name: r'newStateNotifierProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$newStateNotifierHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$NewStateNotifier = AutoDisposeNotifier<int>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
iimport '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/code_generation_provider.dart';
class CodeGeneration2Screen extends ConsumerWidget {
const CodeGeneration2Screen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final state1 = ref.watch(originalStateProvider);
final state2 = ref.watch(newStateProvider);
final future1 = ref.watch(originalFutureProvider);
final future2 = ref.watch(newFutureProvider);
final notifier1 = ref.watch(originalStateNotifierProvider);
final notifier2 = ref.watch(newStateNotifierProvider);
return Scaffold(
appBar: AppBar(
title: Text('CodeGeneration2Screen'),
),
body: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
state1,
textAlign: TextAlign.center,
),
Text(
state2,
textAlign: TextAlign.center,
),
future1.when(
data: (data) {
return Text(
'state2 : $data',
textAlign: TextAlign.center,
);
},
error: (err, stack) => Text(err.toString()),
loading: () => const Center(
child: CircularProgressIndicator(),
),
),
future2.when(
data: (data) {
return Text(
'state2 : $data',
textAlign: TextAlign.center,
);
},
error: (err, stack) => Text(err.toString()),
loading: () => const Center(
child: CircularProgressIndicator(),
),
),
Text(
'$notifier1',
textAlign: TextAlign.center,
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: () {
ref
.read(originalStateNotifierProvider.notifier)
.decrement();
},
child: Text('Decreament')),
ElevatedButton(
onPressed: () {
ref
.read(originalStateNotifierProvider.notifier)
.increment();
},
child: Text('Increament')),
],
),
Text(
'$notifier2',
textAlign: TextAlign.center,
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: () {
ref.read(newStateNotifierProvider.notifier).decrement();
},
child: Text('Decreament')),
ElevatedButton(
onPressed: () {
ref.read(newStateNotifierProvider.notifier).increment();
},
child: Text('Increament')),
],
),
],
),
);
}
}
기존 방식과 CodeGeneration 방식의 코드이다. 위의 방법처럼 작성하고 flutter pub run build_runner build를 해주면 기존에 사용하던 방식처럼 사용할수 있다. 아무래도 함수형태이다 보니 훨씬 직관적인것같다. 그리고 code_generation_provider.g.dart 파일을 보자. 전에 정리했던 내용을 생각해보면 autoDisposeModifier기능이 있었는데 CodeGeneration을 사용하여 Provider를 생성하면 AutoDisposeProvider로 생성이되어 위젯이 화면에서 사라지게되면 자동으로 dispose를 해준다. 만약 AutoDispose를 하고 싶지 않다면 @riverpod 대신 @Riverpod(keepAlive: true)를 사용해서 CodeGeneration을 하면된다.
기존의 Riverpod Provider는 파라미터를 받기 어려웠다. Family Modifier을 사용하여 파라미터를 넘길경우 최대 한개만 넘길수 있었다. 따라서 하나이상을 넘기려면 class를 만들어서 넘겨야 했다. 하지만 CodeGeneratin을 이용하면 파라미터 전달이 수월해진다. 코드로 작성해보자.
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'code_generation_provider.g.dart';
// 기존 Provider로 하나 이상의 파라미터를 넘기는 방법
class Parameter {
final int number1;
final int number2;
Parameter({
required this.number1,
required this.number2,
});
}
final originalFamilyProdiver = Provider.family<int, Parameter>(
(ref, parameter) => parameter.number1 + parameter.number2);
// CodeGeneration을 이용해 하나 이상의 파라미터를 넘기는 방법
@riverpod
int newFamily(NewFamilyRef ref, {required int number1, required int number2}) {
return number1 + number2;
}
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_riverpod_playground/riverpod/code_generation_provider.dart';
class CodeGeneration3Screen extends ConsumerWidget {
const CodeGeneration3Screen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final state1 =
ref.watch(originalFamilyProdiver(Parameter(number1: 10, number2: 20)));
final state2 = ref.watch(newFamilyProvider(number1: 100, number2: 200));
return Scaffold(
appBar: AppBar(
title: Text('CodeGeneration3Screen'),
),
body: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
'$state1',
textAlign: TextAlign.center,
),
Text(
'$state2',
textAlign: TextAlign.center,
)
],
),
);
}
}
확실히 간결해진 코드를 볼수 있다. 지금은 숫자두개를 더하는 간단한 기능이기 때문에 파라미터가 2개뿐이라 class를 넘기는 방법이 많이 길어보이지는 않는데 파라미터가 많아져 복잡해진다면 CodeGeneration을 하는 방법이 훨씬 좋다고 생각한다.
'유효하지 않다라'는 뜻을 가진 함수이다. invalidate() 함수를 발생시키면 provider의 state를 유효하지 않게 하여 초기의 상태로 돌리는 기능을 한다.
ElevatedButton(
onPressed: () {
ref.invalidate(newStateNotifierProvider);
},
child: Text('Invalidate'),
),
이렇게 ref로 invalidate()의 함수에 초기로 돌리고 싶은 상태의 Proivder를 넗어주면 된다.
Consumer 위젯은 Provider로부터 데이터를 읽고, 해당 데이터를 사용하여 위젯을 빌드하는 데 사용된다. 프로바이더에서 제공하는 데이터가 변경될 때만 해당 Consumer로 감싸진 위젯만 부분적으로 렌더링이되어 렌더링 효율이 좋아진다. 그리고 만약 Consumer위젯안에서 렌더링이 필요하지 않는 부분이 있다면 builder 함수안에 있는 child 파라미터를 사용하면 된다. 그맇게 되면 builder함수가 재실행이되어도 child 파라미터 부분은 렌더링이 되지않는다.
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_riverpod_playground/riverpod/code_generation_provider.dart';
class CodeGeneration3Screen extends ConsumerWidget {
const CodeGeneration3Screen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
print('build');
final state = ref.watch(originalStateProvider);
return Scaffold(
appBar: AppBar(
title: const Text(
'CodeGeneration3Screen',
),
),
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
state,
textAlign: TextAlign.center,
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const SizedBox(width: 16.0),
Consumer(
builder: (BuildContext context, WidgetRef ref, Widget? child) {
print('consumer build');
final consumerState = ref.watch(newStateNotifierProvider);
return Row(
children: [
Text(
'$consumerState',
textAlign: TextAlign.center,
),
const SizedBox(
width: 16,
),
if (child != null) child,
],
);
},
child: const Text('Consumer Child'),
),
const SizedBox(width: 16.0),
],
),
ElevatedButton(
onPressed: () {
ref.read(newStateNotifierProvider.notifier).increment();
},
child: const Text('Increment')),
ElevatedButton(
onPressed: () {
ref.read(newStateNotifierProvider.notifier).decrement();
},
child: const Text('Decrement'))
],
),
);
}
}
참고
https://www.inflearn.com/course/%ED%94%8C%EB%9F%AC%ED%84%B0-%EC%8B%A4%EC%A0%84/dashboard