지금까지는 위젯이 상태를 관리했는데 -> setState
위젯과 상태관리를 분리시키자.
플러터의 상태관리를 하는 여러 라이브러리가 있는데 그 중에서 Riverpod
을 이용해서 상태관리를 해보자
https://riverpod.dev/docs/getting_started 로 가서 의존성을 추가한다.
dependencies:
flutter:
sdk: flutter
flutter_riverpod: ^2.3.2 // 추가
Provider
패턴은 객체지향 디자인패턴의 하나로 상태를 캡슐화하고 상태 객체를 컨슈머에게 제공해서 재사용성과 유지보수성을 높이기 위한 패턴이다.
Provider
를 이용하면 여러 위치에서 해당 상태에 쉽게 액세스할 수 있다.
Provider
는 필요한 위젯만 렌더링 하므로 최적화에 좋다.
비용이 많이 드는 상태 계산을 캐싱하기에도 좋다.
일반적으로 아래와 같은 방법으로 생성한다.
final myProvider = Provider((ref) { // 반드시 fianl
return MyValue();
});
Provider
는 final
이므로 불변의 특징을 가진다.
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
// We create a "provider", which will store a value (here "Hello world").
// By using a provider, this allows us to mock/override the value exposed.
final helloWorldProvider = Provider((_) => 'Hello world');
void main() {
runApp(
// For widgets to be able to read providers, we need to wrap the entire
// application in a "ProviderScope" widget.
// This is where the state of our providers will be stored.
ProviderScope(
child: MyApp(),
),
);
}
// Extend ConsumerWidget instead of StatelessWidget, which is exposed by Riverpod
class MyApp extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final String value = ref.watch(helloWorldProvider);
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('Example')),
body: Center(
child: Text(value),
),
),
);
}
}
예제를 보면 먼저 Provider
를 생성한다.
final helloWorldProvider = Provider((_) => 'Hello world');
// 원래 코드에서 제네릭이 생략되어 있다.
final helloWorldProvider = Provider<String>((_) => 'Hello world');
// dart에서는 매개변수에 들어온 변수를 사용하지 않을경우 _를 넣을 수 있다.
Provider
가 가지고 있는 매개변수를 스토어라고 한다 -> ((_) => 'Hello world'
)
스토어에는 상태가 저장되고 이러한 스토어를 Provider
로 관리하는 패턴을 Provider패턴이라고 한다.
다음으로 Provider
를 제공할 범위를 지정한다.
ProviderScope(
child: MyApp(),
),
일반적으로 루트 위젯을 ProviderScope
로 감싸서 하위 위젯트리에서 Provider
객체에 접근할 수 있도록 한다.
이를 통해서 Provider
객체들의 상태를 전역적으로 관리하고 생성 또한 담당한다.
상태를 구독하기 위해 ConsumerWidget
을 상속한 클래스를 만든다.
class MyApp extends ConsumerWidget
ConsumerWidget
는 Provider
객체의 상태를 구독하고, 상태 변경이 있을 때마다 build
를 호출하여 화면을 다시 그린다.
Widget build(BuildContext context, WidgetRef ref)
WidgetRef
는 위젯 트리 내의 Provider 객체에 대한 참조를 제공하며, 이를 통해 하위 위젯에서 Provider 객체를 생성하거나 액세스 할 수 있다.
WidgetRef
가 Provider
객체에 액세스하여 상태변경을 감지하면 현재 위젯에 다시 빌드를 요청한다.
또한 WidgetRef
는 context
와 같은 역할을 해서 다른위젯에서도 상태를 공유하는 역할을 한다.
이를 통해 상태 관리를 보다 효율적으로 할 수 있다.
WidgetRef 의 메소드
// Provider를 초기에 한 번만 읽음 - 값이 변경되더라도 build를 요청하지 않음
final String value = ref.read(helloWorldProvider);
// Provider를 반복해서 관찰하고 상태가 변화면 build를 호출함
final String value = ref.watch(helloWorldProvider);
이를 통해 상태에 변화가 생기면 build
가 호출되어 화면의 데이터가 변하게 된다.
또한 Provider
는 다른 Provider
를 포함할 수 있다.
final byeWorldProvider = Provider<String>((ref) => 'bye');
final helloWorldProvider = Provider<String>((ref) {
final String value = ref.read(byeWorldProvider); // 상태 조합이 가능하다
return 'Hello world: '+value;
});
Provider의 종류
컨슈머가 데이터를 요청하면 공급자는 서버에 데이터를 요청하면서 I/O가 발생하는데 이와 동시에 공급자는 데이터를 가지고 있지 않지만 컨슈머에게 future
를 반환한다. ( Promise와 유사 )
공급자가 데이터를 얻는 즉시 데이터를 주입해준다.
미래에 데이터를 제공해주기 때문에 비동기 특성을 가진다.
이때 데이터를 모두 다운받게 되면 공급자는 컨슈머에게 콜백을 날리고 컨슈머는 read
를 하게 된다.
FutureProvider
가 아닌 Provider
를 이용하면 비동기 통신을 지원하지 않아 컨슈머는 null 데이터를 받게 된다.
간단하게 구현하는 경우에는 StateNotifierProvider
의 watch
를 이용하는데 자원을 철저하게 관리하는 경우에 FutureProvider는
를 사용한다.
StateNotifier
는 상태를 가지고 있으며 상태가 변경되면 리스너들에게 알린다.
class Counter extends StateNotifier<int>{ // int를 관리하는 스토어
Counter(int num) : super(num);
void increment(){
state++; // state의 타입은 StateNotifier의 제네릭 타입이다.
// super 의 파라미터 자리의 데이터를 바꿀 수 있다.
// 제네릭에 User 클래스 만들어 넣었다면 state.name 처럼 접근가능함
}
}
final counterProvider = StateNotifierProvider<Counter, int>((ref) {
return Counter(0);
});
void main() {
runApp(
ProviderScope(
child: MyApp(),
),
);
}
class MyApp extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final int value = ref.watch(counterProvider);
// ref.read를 하면 한번만 읽으므로 초기화면에서 바뀌지 않는다.
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('Example')),
body: Center(
child: Text(value.toString()),
),
floatingActionButton: FloatingActionButton(
onPressed: (){
ref.read(counterProvider.notifier).increment();
// notifier는 변경 불가능한 일반적인 Provider와 달리 상태를 변경하는 Provider를 생성한다.
},
child: Icon(Icons.add),
),
),
);
}
}
notifier
는 StateNotifier
를 구현하는 객체로 버튼을 클릭하면 생성된 StateNotifierProvider
의
버튼을 클릭하면 increment
가 호출되어 Provider
의 상태가 변경되고 구독하는 위젯의 화면이 갱신된다.
버튼을 누르면 숫자가 변한다.
마찬가지로 StateNotifier
을 상속한 클래스를 만들어서 상태를 저장한다.
class Counter extends StateNotifier<int> {
Counter(super.state);
void add() {
state++;
}
void down() {
state--;
}
}
final countProvider = StateNotifierProvider<Counter, int>((ref) {
return Counter(1);
});
변경하고 싶은 부분만 특정하고 싶을때 Consumer
를 사용한다.
class HeaderPage extends StatelessWidget {
HeaderPage({Key? key}) : super(key: key);
Widget build(BuildContext context) {
return Container(
color: Colors.red[300],
child: Align(
child: ProviderScope( // 스코프지정
child: Consumer( // 해당 위젯만 변경하고자 할때
builder: (context, ref, child) {
final int number = ref.watch(countProvider);
return Text(
"$number",
style: TextStyle(
color: Colors.white,
fontSize: 100,
fontWeight: FontWeight.bold),
);
},
),
)
),
);
}
}
다른 위젯의 버튼으로 상태를 변경해서 데이터를 바꾼다.
WidgetRef
을 이용한다.
build(BuildContext context, WidgetRef ref) {
return Container(
color: Colors.blue[300],
child: Align(
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green[200], minimumSize: Size(300, 200)),
onPressed: () {
text == "증가" ? (ref.read(countProvider.notifier).add())
: (ref.read(countProvider.notifier).down());
},
child: Text(
text,
style: TextStyle(fontSize: 100, fontWeight: FontWeight.bold),
),
),
),
);
}
Widget
A 위젯의 버튼을 눌러서 스토어의 상태를 변경하고 Provider를 구독중인 B의 위젯의 데이터가 변경된다.
상태가 변경될 때 UI를 업데이트해야하는 경우에 특히 유용합니다.
컨슈머가 데이터를 요청하면 공급자는 데이터를 서버에 요청해 I/O가 발생하는 동안 컨슈머에게 stream
을 반환한다.
서버에서 계속해서 데이터를 받기때문에 StreamProvider
는 비동기를 지원한다.
다음과 같이 순차적으로 스트림을 보낸다면
final streamProvider = StreamProvider<int>((ref) async* {
for (var i = 1; i <= 10; i++) {
await Future.delayed(Duration(seconds: 1));
yield i;
}
});
컨슈머는 watch
로 스트림을 읽을 수 있다.
또한 사용자가 원하는때에 Provider 구독을 취소할 수 있는 autoDispose
매개변수도 제공한다.
Consumer(
builder: (context, ref, child) {
final streamData = ref.watch(streamProvider);
return Text(
'${streamData.data}',
style: TextStyle(fontSize: 24),
);
},
),