section overview
necessity of provider
- Widget A 에서 counter 라는 데이터와 increment 라는 함수가 필요한 상황이고, Widget B 에서는 counter 라는 데이터만 필요한 상황.
- Widget A, Widget B 모두에서 동일한 데이터인 counter 가 필요하므로 공통된 부모 Widget C 에서 정의.
- counter 와 increment 의 경우 Widget C 에서 선언하지만 정작 제어는 Widget A 에서 하므로 Inversion of control 발생.
- Widget B 에 counter 를 넘겨주기 위해서 Widget C 와 Widget B 사이의 Widget 이 필요하지도 않은 counter 라는 데이터를 갖게 됨.
managing state without provider
- Counter B 만 봐서는 어디서 rebuilding 이 발생하는지 추적하기가 쉽지 않다.
- 공통 상단 부모 Widget 에서 setState() 호출되므로 쓸데없이 더 많은 Widget 이 rebuild 대상이 되어 퍼포먼스가 떨어질 수 있다.
결국 StateManagement 는 아래 두 가지 행위가 핵심이다.
- Dependency Injection (Object를 Widget Tree 상에서 쉽게 접근할 수 있도록 한다.)
- Synchronizing data and UI
- Provider 는 Widget 에 Widget 이 아닌 데이터와 method 에 쉽게 접근할 수 있는 방법을 제공한다.
- 데이터가 변경 되었을 때 데이터가 변경되었다는 사실을 그 데이터가 필요한 Widget 에 제공해서 필요시 rebuild 될 수 있도록 한다.
- 결국 비즈니스 로직과 화면이 분리 되는 것이다.
dependency injection using provider
주목해야할 포인트는 아래 두 가지이다.
- Widget Tree 상에서 class Dog 로의 접근이 얼마나 쉽게 가능한지
- 이 방식이 생성자로 데이터를 넘겨주는 것보다 얼마나 더 간편한지
- Provider 역시 Widget 이다.
- create 프로퍼티에서 Widget 이 필요로 하는 dog 인스턴스를 만든다.
- create 에 assign 되는 함수가 return 하는 object 에 Provider 하위 Widget 들의 접근이 가능하다.
- Provider 의 static 함수인 of 를 이용하면 Widget Tree 를 위로 traverse 하면서 원하는 Type 의 인스턴스를 찾을 수 있다.
Provider.of<Dog>(context)
와 같은 식인데, 여기서 context 를 주는 이유는 context 를 이용해서 Widget Tree 를 위로 탐색하기 때문이다.
<T>
가 같은 두 개 이상의 인스턴스가 있는 경우에는 가장 가까운 인스턴스를 가져온다.
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Provider<Dog>(
create: (context) => Dog(
name: 'Sun',
breed: 'Bulldog',
age: 3,
),
child: MaterialApp(
title: 'Provider 02',
debugShowCheckedModeBanner: false,
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(),
),
);
}
}
class MyHomePage extends StatelessWidget {
const MyHomePage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Provider 02'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
Text(
'- name: ${Provider.of<Dog>(context).name}',
style: TextStyle(fontSize: 20.0),
),
SizedBox(height: 10.0),
BreedAndAge(),
],
),
),
);
}
}
ChangeNotifier & addListener
- addListener 는 자동으로 dispose 되지 않기 때문에 수동으로 직접 dispose 시켜줘야한다.
- 코드 내 Provider 제거 후 ChangeNotifier 와 addListener 를 같이 쓰면서 Listener 가 작동하는 것을 보고, 불편한 점을 살펴보았다.(Provider 삭제 후 생성자로 데이터를 넘겨주었다.)
ChangeNotifierProvider
Provider.of<T>(context)
로 인스턴스를 탐색 & 접근시 해당 데이터의 변경을 listen 해야할지의 필요성(데이터 변경에 따른 UI rebuild)이 있을때와 없을때 각각 옵션값이 다름을 인지한다.Provider.of<Dog>(context).age
Provider.of<Dog>(context, listen: false).age
결론적으로 아래와 같다. 결국 아래 두 가지가 State Management 이다.
- ChangeNotifierProvider 는 데이터를 필요로 하는 Widget 이 dependency injection 을 받음으로써 데이터(인스턴스)에 쉽게 접근할 수 있게 해준다.
- 데이터의 변경이 발생했을 때 이 데이터의 변화에 맞춰서 선택적으로 Widget 을 rebuild 할 수 있게 해준다.
import 'package:flutter/foundation.dart';
class Dog with ChangeNotifier {
final String name;
final String breed;
int age;
Dog({
required this.name,
required this.breed,
this.age = 1,
});
void grow() {
age++;
notifyListeners();
}
}
ChangeNotifier 의 notifyListeners() 는 이를 구독하고 있는 모든 listener 들에게 변경 사실을 알리고 rebuild 하도록 만든다. 공식문서를 참고하자.
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'models/dog.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider<Dog>(
create: (context) => Dog(name: 'dog04', breed: 'breed04'),
child: MaterialApp(
title: 'Provider 04',
debugShowCheckedModeBanner: false,
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(),
),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key}) : super(key: key);
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Provider 04'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
Text(
'- name: ${Provider.of<Dog>(context).name}',
style: TextStyle(fontSize: 20.0),
),
SizedBox(height: 10.0),
BreedAndAge(),
],
),
),
);
}
}
class BreedAndAge extends StatelessWidget {
const BreedAndAge({
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Column(
children: [
Text(
'- breed: ${Provider.of<Dog>(context).breed}',
style: TextStyle(fontSize: 20.0),
),
SizedBox(height: 10.0),
Age(),
],
);
}
}
class Age extends StatelessWidget {
const Age({
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Column(
children: [
Text(
'- age: ${Provider.of<Dog>(context).age}',
style: TextStyle(fontSize: 20.0),
),
SizedBox(height: 20.0),
ElevatedButton(
onPressed: () => Provider.of<Dog>(context, listen: false).grow(),
child: Text(
'Grow',
style: TextStyle(fontSize: 20.0),
),
),
],
);
}
}
read, watch, select extension methods
- 4.1 부터 Provider 를 더 간편하게 쓸 수 있게 해주는 extension 이 도입됨.
- 일종의 shortCut 이라고 보면 되고, 그냥 쓰지 말고 원형들이 뭔지 이해하고 있는게 중요하다.
context.select
는 다수의 property 를 가지고 있는 object 의 특정 property 의 변화만 listen 하고 싶을 때 사용한다.context.watch
는 특정 property 하나만 바뀌어도 rebuild 를 하는 것에 반해context.select
는 listen 하고 싶은 것만 선별적으로 listen 이 가능하다.(퍼포먼스 고려 측면에서 사용할 수 있다.)
context.watch vs context.select
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'models/dog.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider<Dog>(
create: (context) => Dog(name: 'dog05', breed: 'breed05', age: 3),
child: MaterialApp(
title: 'Provider 05',
debugShowCheckedModeBanner: false,
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(),
),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key}) : super(key: key);
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Provider 05'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
Text(
'- name: ${context.watch<Dog>().name}',
style: TextStyle(fontSize: 20.0),
),
SizedBox(height: 10.0),
BreedAndAge(),
],
),
),
);
}
}
class BreedAndAge extends StatelessWidget {
const BreedAndAge({
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Column(
children: [
Text(
'- breed: ${context.select<Dog, String>((Dog dog) => dog.breed)}',
style: TextStyle(fontSize: 20.0),
),
SizedBox(height: 10.0),
Age(),
],
);
}
}
class Age extends StatelessWidget {
const Age({
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Column(
children: [
Text(
'- age: ${context.select<Dog, int>((Dog dog) => dog.age)}',
style: TextStyle(fontSize: 20.0),
),
SizedBox(height: 20.0),
ElevatedButton(
onPressed: () => context.read<Dog>().grow(),
child: Text(
'Grow',
style: TextStyle(fontSize: 20.0),
),
),
],
);
}
}
Multiple Provider
- 하나의 Provider 를 사용하더라도 MultiProvider 를 사용해놓으면 확장성이 있다.
Future Provider
- 강사님은 개인적으로 쓸 일이 거의 없었다고 함.
- 만약 쓸 일이 있으면 FutureBuilder 를 쓸 것 같다고 함.
- Widget Tree 에는 빌드가 되었는데 사용하고자 하는 값이 아직 준비가 되지 않았을때 사용한다.
- Future 가 resolve 되지 않았을때, initialData 로 build 하고 Future 가 resolve 되면 rebuild 된다. 총 2번 build 가 된다는 것. (만약 여러번 build 를 원한다면 StreamProvider 사용한다.)
class Babies {
final int age;
Babies({
required this.age,
});
Future<int> getBabies() async {
await Future.delayed(Duration(seconds: 3));
if (age > 1 && age < 5) {
return 4;
} else if (age <= 1) {
return 0;
} else {
return 2;
}
}
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider<Dog>(
create: (context) => Dog(name: 'dog06', breed: 'breed06', age: 3),
),
FutureProvider<int>(
initialData: 0,
create: (context) {
final int dogAge = context.read<Dog>().age;
final babies = Babies(age: dogAge);
return babies.getBabies();
},
),
],
child: MaterialApp(
title: 'Provider 06',
debugShowCheckedModeBanner: false,
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(),
),
);
}
}
- 유심히 봐둬야할 부분은 FutureProvider 의 대상은 Future 이기만 하면 되는 것이라는 것이다. 즉, 인스턴스 내에 정의된 method 를 대상으로 하고 싶다면 인스턴스 전체를 return 할 필요가 없고 특정 메소드만 제공하는 형태로 사용해야 한다.
- FutureProvider 내에서 context.read 사용이 가능하다. 이유는 위에서 ChangeNotifierProvider 가 FutureProvider 보다 상위 Widget 으로 이미 사용되었기 때문이다.
StreamProvider
- 강사님 개인적으로는 FutureProvider 보다 StreamProvider 를 사용할 일이 더 많았다고 함.
- 연속되는 Future value 를 타겟으로 하는 Provider
StreamProvider<String>(
initialData: 'Bark 0 times',
create: (context) {
final int dogAge = context.read<Dog>().age;
final babies = Babies(age: dogAge * 2);
return babies.bark();
},
),
- create 는 한 번만 called 되기 때문에 watch 를 사용하면 에러가 발생한다. 논리적으로도 read 가 맞다.
class Babies {
final int age;
Babies({
required this.age,
});
Future<int> getBabies() async {
await Future.delayed(Duration(seconds: 3));
if (age > 1 && age < 5) {
return 4;
} else if (age <= 1) {
return 0;
} else {
return 2;
}
}
Stream<String> bark() async* {
for (int i = 1; i < age; i++) {
await Future.delayed(Duration(seconds: 2));
yield 'Bark $i times';
}
}
}
- async 와 async* 의 차이는 여기를 참고한다. 간략히 설명하면 return 타입이
Stream<T>
를 생산하면(+ method 를 떠나지 않으면서) async* 를 사용한다. - 이런 경우 return 을 사용하지 않는다. method 를 떠나지 않기 때문에 종결처리 하면 안된다. yield 를 사용하며, yield 의 사전적 의미는 ‘생산하다’ 라는 뜻이다.
Consumer
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Provider 08'),
),
body: Consumer<Dog>(
builder: (BuildContext context, Dog dog, Widget? child) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
child!,
SizedBox(height: 10.0),
Text(
'- name: ${dog.name}',
style: TextStyle(fontSize: 20.0),
),
SizedBox(height: 10.0),
BreedAndAge(),
],
),
);
},
child: Text(
'I like dogs very much',
style: TextStyle(fontSize: 20.0),
),
),
);
}
}
- Consumer 의 파라미터 (BuildContext context, Dog dog, Widget? child) 중 child 는 builder 내에서 rebuild 될 필요가 없는 Widget 이 있는 경우를 대비해서 위와 같이 사용한다. 위 예제에서는 어떤 경우든 rebuild 될 필요가 없는 Text Widget 을 child 로 빼주었다.
- 이해가 안가면 유투브에 남아있는 강의 에서 이 부분을 다시 보고 오자.
Consumer, builder, ProviderNotFoundException
- Consumer 를 쓰든 builder 를 쓰든 둘 중 편한 방법을 사용하자.
Selector
- Consumer 와 유사한데 Consumer 보다 더 세세한 컨트롤을 가능하게 해준다.
- 앞에서 학습한
context.select<T, R>((R selector(T value))) => R
과 유사한 개념이다. - 지금 학습한 Selector 라는 Widget 과
context.select<T, R>((R selector(T value))) => R
는 공통적으로 특정 property 의 변경에 대해서 react 할 수 있도록 하는 것.
ProviderNotFoundException 더 알아보기, Builder Widget
- extension 의 context 는 build method 의 buildContext 임을 명심. 그래서 그걸 그대로 사용하면 ProviderNotFoundException 가 발생할 수 밖에 없다.
- 이를 해결하려면 ‘Consumer, builder, ProviderNotFoundException’ 에서 학습한 builder 프로퍼티를 사용하거나 child 로 Widget 을 따로 빼서 별도의 Widget 으로 사용한다.
- Builder Widget 의 context 는 조상 Widget 의 BuildContext 가 아니고 Builder Widget 자체의 BuildContext 이기 때문에 이를 사용해서 Widget Tree 를 위로 탐색하면 원하는 T(type)에 대한 Provider 를 찾을 수 있다.
- builder 프로퍼티도 이미 학습한 바와 같이 syntax sugar 로 Builder Widget 이 사용된 것과 동일하다.
Provider Access - Anonymous route access
Consumer, builder, ProviderNotFoundException
에서 Error Message 2 의 경우를 학습함.Consumer, builder, ProviderNotFoundException
에서 Error Message 3 의 경우 parent-child 관계에서 child 로서 Widget Tree 를 위로 탐색하면서 Provider 를 찾아하는데 탐색시 사용하는 context 가 해당 child 의 context 가 아니라, Provider 위치와 같거나 혹은 그 이상의 level 의 context 일 경우 발생한 에러.- Error Message 2 도 parent-child 관계에서 child 로서 위로 탐색이 발생되어야 하는데, 다른 route 에서 탐색했으므로(여기서 예제는 sibling 관계) 못찾는 문제가 발생.
- 이 때의 해결 방법은 value constructor 이다. 새로운 sub-tree 에 이미 존재하는 class 에 대한 access 를 제공할 때 사용. value constructor 는 class 를 자동으로 close 하지 않는다.
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (_) {
return ChangeNotifierProvider.value(
value: context.read<Counter>(),
child: ShowMeCounter(),
);
}),
);
},
- context.read 시 사용하는 context 는 Navigator.push 의 context 를 사용해야 한다. MaterialPageRoute 의 context 가 아님을 명심. 위로 탐색하여 Provider 를 찾아야 하는데, MaterialPageRoute 의 경우 sibling 이기 때문에 MaterialPageRoute 의 context 를 사용할 경우 Provider 를 여전히 못찾는다.
아래는 코드 전체.
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'counter.dart';
import 'show_me_counter.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Anonymous Route',
debugShowCheckedModeBanner: false,
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: ChangeNotifierProvider<Counter>(
create: (context) => Counter(),
child: const MyHomePage(),
),
);
}
}
class MyHomePage extends StatelessWidget {
const MyHomePage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
child: Text(
'Show Me Counter',
style: TextStyle(fontSize: 20.0),
),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (_) {
return ChangeNotifierProvider.value(
value: context.read<Counter>(),
child: ShowMeCounter(),
);
}),
);
},
),
SizedBox(height: 20.0),
ElevatedButton(
child: Text(
'Increment Counter',
style: TextStyle(fontSize: 20.0),
),
onPressed: () {
context.read<Counter>().increment();
},
)
],
),
),
);
}
}
Provider Access - Named route access
계속해서 Provider Access 에 대해서 학습한다. 바로 위에서는 Anonymous route access 를 살펴보았다. 왜 위의 케이스가 Anonymous route 냐면 Navigator.push 를 사용했기 때문이다.
이번에는 Named route 에서의 Provider 에 대한 access 를 학습한다.
복습 차원에서 짚고 넘어가자면, Anonymous route 가 Anonymous 인 이유는 말 그대로 이름이 없어서다. 반대로 이름이 있다는 말은 MaterialApp 에서 routes property 내에 route 주소와 Screen 으로 사용할 Widget 이 등록되어 있다는 것을 의미한다. 바로 위 Anonymous route 는 Navigator.push 로 route 설정을 따로 해주지 않고 화면 Stack 을 바로 쌓아 올린 것이기 때문에 Anonymous 였다.
routes: {
'/': (context) => ChangeNotifierProvider.value(
value: _counter,
child: MyHomePage(),
),
'/counter': (context) => ChangeNotifierProvider.value(
value: _counter,
child: ShowMeCounter(),
),
},
위와 같이 routes 설정 부분에서 ChangeNotifierProvider.value 로 감싸주면 되는데, 주의할 점이 있다. create 를 사용해서 사용할 인스턴스를 생성할 경우 자동으로 dispose 를 해주는데 지금 위의 경우 create 로 인스턴스를 생성하지 않았고, 아래 전체 코드에 나와있다시피 state 에서 인스턴스를 직접 생성해줬다.
따라서 dispose 역시 아래와 같이 직접 해줘야한다.
@override
void dispose() {
_counter.dispose();
super.dispose();
}
아래는 전체 코드이다.
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'counter.dart';
import 'show_me_counter.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatefulWidget {
MyApp({Key? key}) : super(key: key);
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
final Counter _counter = Counter();
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Anonymous Route',
debugShowCheckedModeBanner: false,
theme: ThemeData(
primarySwatch: Colors.blue,
),
routes: {
'/': (context) => ChangeNotifierProvider.value(
value: _counter,
child: MyHomePage(),
),
'/counter': (context) => ChangeNotifierProvider.value(
value: _counter,
child: ShowMeCounter(),
),
},
);
}
@override
void dispose() {
_counter.dispose();
super.dispose();
}
}
class MyHomePage extends StatelessWidget {
const MyHomePage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
child: Text(
'Show Me Counter',
style: TextStyle(fontSize: 20.0),
),
onPressed: () {
Navigator.pushNamed(context, '/counter');
},
),
SizedBox(height: 20.0),
ElevatedButton(
child: Text(
'Increment Counter',
style: TextStyle(fontSize: 20.0),
),
onPressed: () {
context.read<Counter>().increment();
},
)
],
),
),
);
}
}
Provider Access - Generated route access, Global access
학습한 Provider Access 에 대해서 무엇을 했고, 무엇이 남았는지 다시 짚어본다.
- Provider Access
-
Anonymous route access
Navigator.push 내에서 사용할 screen 을 value constructor 로 감싸주되, context 를 Navigator.push 의 것을 사용한다. -
Named route access
routes 설정 부분에서 value constructor 로 감싸주되, 사용할 인스턴스가 create 로 만들어진게 아니라 자동으로 dispose 되지 않으므로 직접 해당 인스턴스를 dispose 처리 해준다. -
Generated route access
Named route access 과 거의 동일하다. 아래 코드를 보자.
-
class _MyAppState extends State<MyApp> {
final Counter _counter = Counter();
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Anonymous Route',
debugShowCheckedModeBanner: false,
theme: ThemeData(
primarySwatch: Colors.blue,
),
onGenerateRoute: (RouteSettings settings) {
switch (settings.name) {
case '/':
return MaterialPageRoute(
builder: (context) => ChangeNotifierProvider.value(
value: _counter,
child: MyHomePage(),
),
);
case '/counter':
return MaterialPageRoute(
builder: (context) => ChangeNotifierProvider.value(
value: _counter,
child: ShowMeCounter(),
),
);
default:
return null;
}
},
);
}
@override
void dispose() {
_counter.dispose();
super.dispose();
}
}
Global access 의 경우 아래와 같이 최 상단에 Provider 로 감싸두면 접근이 가능하다.
return Provider<T>(
create: (_) => T(),
child: MaterialApp(
...
),
);
하지만 분업 환경 및 유지보수 등을 고려할 때 적절지 못한 방식이다. 실무에서 이렇게 사용할 인스턴스가 있을까 싶다. 전에 로딩스피너를 이렇게 global access 가능하게 처리해서 사용했던 것 같기도 하고.. 오픈소스 프로젝트들을 탐구할 때 확인해보도록 하자.
ProxyProvider - 개요 1
- Provider 에서 다른 Provider 의 값이 필요한 경우에 ProxyProvider 를 사용한다.
- 다른 Provider 에 기반하여 value 를 만드는 Provider (A provider that builds a value based on other provider)
- 다른 Provider 의 값에 의존해서 value 를 만드는 Provider 인 것이다.
- 꼭 다른 Provider 에 의존하지 않더라도, 어떤 변하는 값에 의존해야 한다면 ProxyProvider 를 사용한다.
- ProxyProvider 도 종류가 많다.
- create 는 optional 이고 update 가 required 이다.
- ProxyProvider 가 자체적으로 만들고 관리해야할 value 가 있다면 create 가 필요하지만, 그런 경우가 아니라면 만들 필요가 없기 때문이다.
- 그래서 create 는 한 번만 called, 되고 update 는 여러번 called 된다.
- ProxyProvider 는 다른 Provider 가 제공하는 값에 의존하는데, 다른 Provider 가 제공하는 value 가 바뀌면 이를 바라보고 있는(의존하고 있는) ProxyProvider 가 제공하는 값이 바뀌어야 하므로 update 가 여러번 called 되는 것은 매우 당연한 일이다.
update 가 호출되는 경우들은 아래와 같다.
- ProxyProvider 가 가장 처음으로 의존하고 있는 다른 Provider 의 값을 얻은 경우
- ProxyProvider 가 의존하고 있는 다른 Provider 가 제공하는 값이 변경된 경우
- ProxyProvider 가 rebuild 되는 경우
ProxyProvider - 개요 2
- The instance of MyChangeNotifier is updated whenever myModel changes.
- The same instance of MyChangeNotifier is used again and again, not created again. (새로 만들어지지 않고 한번 만들어진 MyChangeNotifier 가 계속 사용된다.)
ProxyProvider - 실습 & 예제
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class Translations {
const Translations(this._value);
final int _value;
String get title => 'You clicked $_value times';
}
class WhyProxyProv extends StatefulWidget {
const WhyProxyProv({Key? key}) : super(key: key);
@override
_WhyProxyProvState createState() => _WhyProxyProvState();
}
class _WhyProxyProvState extends State<WhyProxyProv> {
int counter = 0;
void increment() {
setState(() {
counter++;
print('counter: $counter');
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Why ProxyProvider'),
),
body: Center(
child: Provider<Translations>(
create: (_) => Translations(counter),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ShowTranslations(),
SizedBox(height: 20.0),
IncreaseButton(increment: increment),
],
),
),
),
);
}
}
class ShowTranslations extends StatelessWidget {
const ShowTranslations({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final title = Provider.of<Translations>(context).title;
return Text(
title,
style: TextStyle(fontSize: 28.0),
);
}
}
class IncreaseButton extends StatelessWidget {
final VoidCallback increment;
const IncreaseButton({
Key? key,
required this.increment,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: increment,
child: Text(
'INCREASE',
style: TextStyle(fontSize: 20.0),
),
);
}
}
- create 는 한 번만 실행되므로 increment 에 의해서 counter 값이 증가해도 결국
create: (_) => Translations(counter)
에서 딱 한번 만들어진 인스턴스는 만들어지던 당시의 counter 로 만들어졌기 때문에final title = Provider.of<Translations>(context).title;
의 값은 create 때 만들어진 인스턴스 그대로다.
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class Translations {
const Translations(this._value);
final int _value;
String get title => 'You clicked $_value times';
}
class ProxyProvUpdate extends StatefulWidget {
const ProxyProvUpdate({Key? key}) : super(key: key);
@override
_ProxyProvUpdateState createState() => _ProxyProvUpdateState();
}
class _ProxyProvUpdateState extends State<ProxyProvUpdate> {
int counter = 0;
void increment() {
setState(() {
counter++;
print('counter: $counter');
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('ProxyProvider update'),
),
body: Center(
child: ProxyProvider0<Translations>(
update: (_, __) => Translations(counter),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ShowTranslations(),
SizedBox(height: 20.0),
IncreaseButton(increment: increment),
],
),
),
),
);
}
}
class ShowTranslations extends StatelessWidget {
const ShowTranslations({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final title = Provider.of<Translations>(context).title;
return Text(
title,
style: TextStyle(fontSize: 28.0),
);
}
}
class IncreaseButton extends StatelessWidget {
final VoidCallback increment;
const IncreaseButton({
Key? key,
required this.increment,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: increment,
child: Text(
'INCREASE',
style: TextStyle(fontSize: 20.0),
),
);
}
}
- 제공해야할 값이 가변적인 값인 counter 에 의존하는 인스턴스이므로 ProxyProvider 를 사용해준 코드이다.
- 이미 counter 값이 0 으로 초기화 되어 있어서, create 가 필요가 없다. 그리고 제공해야할 가변 인스턴스가 하나다. 그래서 ProxyProvider0 를 사용했다.(공식문서 참고)
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class Translations {
late int _value;
void update(int newValue) {
_value = newValue;
}
String get title => 'You clicked $_value times';
}
class ProxyProvCreateUpdate extends StatefulWidget {
const ProxyProvCreateUpdate({Key? key}) : super(key: key);
@override
_ProxyProvCreateUpdateState createState() => _ProxyProvCreateUpdateState();
}
class _ProxyProvCreateUpdateState extends State<ProxyProvCreateUpdate> {
int counter = 0;
void increment() {
setState(() {
counter++;
print('counter: $counter');
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('ProxyProvider create/update'),
),
body: Center(
child: ProxyProvider0<Translations>(
create: (_) => Translations(),
update: (_, Translations? translations) {
translations!.update(counter);
return translations;
},
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ShowTranslations(),
SizedBox(height: 20.0),
IncreaseButton(increment: increment),
],
),
),
),
);
}
}
class ShowTranslations extends StatelessWidget {
const ShowTranslations({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final title = context.watch<Translations>().title;
return Text(
title,
style: TextStyle(fontSize: 28.0),
);
}
}
class IncreaseButton extends StatelessWidget {
final VoidCallback increment;
const IncreaseButton({
Key? key,
required this.increment,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: increment,
child: Text(
'INCREASE',
style: TextStyle(fontSize: 20.0),
),
);
}
}
- create 에서 생성을 먼저 한 케이스다.
- update 에서 create 에서 생성된 객체를 받아 .update() 를 call 하고 return 한다.
- 여기서 또 update 발생하면 처음 create 발생시 만들어진 인스턴스(= 처음 update 때 사용된)를 계속 참조하여 재활용한다. (update 한다고 새로 만들어서 return 하는 것이 아니라는 것을 명심)
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class Translations {
const Translations(this._value);
final int _value;
String get title => 'You clicked $_value times';
}
class ProxyProvProxyProv extends StatefulWidget {
const ProxyProvProxyProv({Key? key}) : super(key: key);
@override
_ProxyProvProxyProvState createState() => _ProxyProvProxyProvState();
}
class _ProxyProvProxyProvState extends State<ProxyProvProxyProv> {
int counter = 0;
void increment() {
setState(() {
counter++;
print('counter: $counter');
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('ProxyProvider ProxyProvider'),
),
body: Center(
child: MultiProvider(
providers: [
ProxyProvider0<int>(
update: (_, __) => counter,
),
ProxyProvider<int, Translations>(
update: (_, value, __) => Translations(value),
),
],
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ShowTranslations(),
SizedBox(height: 20.0),
IncreaseButton(increment: increment),
],
),
),
),
);
}
}
class ShowTranslations extends StatelessWidget {
const ShowTranslations({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final title = context.watch<Translations>().title;
return Text(
title,
style: TextStyle(fontSize: 28.0),
);
}
}
class IncreaseButton extends StatelessWidget {
final VoidCallback increment;
const IncreaseButton({
Key? key,
required this.increment,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: increment,
child: Text(
'INCREASE',
style: TextStyle(fontSize: 20.0),
),
);
}
}
- 두 개의 ProxyProvider 를 사용하는 케이스.
- ProxyProvider0 는 가변적인 int value 를 return
- ProxyProvider 는 ProxyProvider0 가 제공하는 가변적인 int 를 받아서 새로운 인스턴스를 return
- 이 경우에 ProxyProvider 가 항상 새로운 인스턴스를 만들어서 return 하고 있다는 것을 명심. 매번 새로운 Translations 를 만들어서 return 하고 있다.
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class Counter with ChangeNotifier {
int counter = 0;
void increment() {
counter++;
notifyListeners();
}
}
class Translations with ChangeNotifier {
late int _value;
void update(Counter counter) {
_value = counter.counter;
notifyListeners();
}
String get title => 'You clicked $_value times';
}
class ChgNotiProvChgNotiProxyProv extends StatefulWidget {
const ChgNotiProvChgNotiProxyProv({Key? key}) : super(key: key);
@override
_ChgNotiProvChgNotiProxyProvState createState() =>
_ChgNotiProvChgNotiProxyProvState();
}
class _ChgNotiProvChgNotiProxyProvState
extends State<ChgNotiProvChgNotiProxyProv> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('ChangeNotifierProvider ChagneNotifierProxyProvider'),
),
body: Center(
child: MultiProvider(
providers: [
ChangeNotifierProvider<Counter>(
create: (_) => Counter(),
),
ChangeNotifierProxyProvider<Counter, Translations>(
create: (_) => Translations(),
update: (
BuildContext _,
Counter counter,
Translations? translations,
) {
translations!..update(counter);
return translations;
},
),
],
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ShowTranslations(),
SizedBox(height: 20.0),
IncreaseButton(),
],
),
),
),
);
}
}
class ShowTranslations extends StatelessWidget {
const ShowTranslations({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final title = context.watch<Translations>().title;
return Text(
title,
style: TextStyle(fontSize: 28.0),
);
}
}
class IncreaseButton extends StatelessWidget {
const IncreaseButton({
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: () => context.read<Counter>().increment(),
child: Text(
'INCREASE',
style: TextStyle(fontSize: 20.0),
),
);
}
}
- 정말 여러가지 방식으로 동일한 결과를 구현할 수 있다.
- Counter 가 Value Object 로 사용 되었다.
- IncreaseButton 에서는 단지 Counter 에 access 만 했다. increment() 를 실행하기만 할 목적이기 때문.
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class Counter with ChangeNotifier {
int counter = 0;
void increment() {
counter++;
notifyListeners();
}
}
class Translations {
const Translations(this._value);
final int _value;
String get title => 'You clicked $_value times';
}
class ChgNotiProvProxyProv extends StatefulWidget {
const ChgNotiProvProxyProv({Key? key}) : super(key: key);
@override
_ChgNotiProvProxyProvState createState() => _ChgNotiProvProxyProvState();
}
class _ChgNotiProvProxyProvState extends State<ChgNotiProvProxyProv> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('ChangeNotifierProvider ProxyProvider'),
),
body: Center(
child: MultiProvider(
providers: [
ChangeNotifierProvider<Counter>(
create: (_) => Counter(),
),
ProxyProvider<Counter, Translations>(
update: (_, counter, __) => Translations(counter.counter),
),
],
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ShowTranslations(),
SizedBox(height: 20.0),
IncreaseButton(),
],
),
),
),
);
}
}
class ShowTranslations extends StatelessWidget {
const ShowTranslations({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final title = context.watch<Translations>().title;
return Text(
title,
style: TextStyle(fontSize: 28.0),
);
}
}
class IncreaseButton extends StatelessWidget {
const IncreaseButton({
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: () => context.read<Counter>().increment(),
child: Text(
'INCREASE',
style: TextStyle(fontSize: 20.0),
),
);
}
}
- 직전에 살펴본 ChangeNotifierProvider, ChangeNotifierProxyProvider 조합은 기존의 Translations 를 mutation 시켜서 재활용 하는 반면 이 방식은 Counter 가 변할 때마다 매번 새로운 Translations 인스턴스를 return 한다.
- 강사님 이야기로는 단순히 computed state 를 만들어 내는 경우 ChangeNotifierProvider 를 쓰지 않고 ProxyProvider 를 사용하는 것이 매뉴얼에도 나와있는 preferred way 라고 한다.
단순히 computed state 를 만들어 내는 경우 ChangeNotifierProvider 를 쓰지 않고 ProxyProvider 를 사용하는 것이 매뉴얼에도 나와있는 preferred way 라고 한다.
Errors & addPostFrameCallback - 1
Errors & addPostFrameCallback - 2
- 위의 에러는 아래와 같은 코드에서 발생되었다.
- flutter 가 Widget 들을 그리고 있는 중에 다시 build 요청을 할 수 없다는 것.
class Counter with ChangeNotifier {
int counter = 0;
void increment() {
counter++;
notifyListeners();
}
}
@override
void initState(){
super.initState();
context.read<Counter>().increament();
myCounter = context.read<Counter>().counter + 10;
}
WidgetsBinding.instance!.addPostFrameCallback((_) {
context.read<Counter>().increment();
myCounter = context.read<Counter>().counter + 10;
});
- addPostFrameCallback 은 현재의 Frame 이 완성된 후 등록된 callback 을 실행시키도록 한다.
- add(추가) + PostFrameCallback(Frame 다 그려진 후 불려질 Callback)
- UI에 영향을 미치는 action 의 실행을 현재의 Frame 이후로 지연시킬 수 있다. ‘현재의 Frame 이후’ 라는 뜻은 결국 현재 싸이클의 build 가 끝난 후를 의미한다.
- 다시 말하면 해당 Widget 의 build 가 끝난 후 실행될 action 을 예약할 수 있는 것이다.
addPostFrameCallback 외에 아래와 같은 처리도 동일한 원리이다.
Future.delayed(Duration(seconds: 0), () {
context.read<Counter>().increment();
myCounter = context.read<Counter>().counter + 10;
});
Future.microtask(() {
context.read<Counter>().increment();
myCounter = context.read<Counter>().counter + 10;
});
- 위 error 는
context.read
에서 read 를 watch 로 바꾸면 볼 수 있는 에러이다. - Widget Tree 밖에서 listen 을 하려 했다는 것.
- 시간차를 두고 현재 Frame 끝나고 이걸 실행해달라는 예약 행위에서 listen 을 한다는 것이 논리적으로 맞지 않다. 이렇게 이해하자.
Errors & addPostFrameCallback - 3
Errors & addPostFrameCallback - 3
에서는 특정 화면에 진입했을때 혹은 가변적인 값이 특정 조건에 걸렸을 때 Dialog 를 띄우는 경우에 있어서 처리 방식 들을 살펴본다.
- Dialog 는 해당 Screen 이 다 그려지고 나서 그 위에 그려지는 overlay Widget.
- 따라서 initState 에서 그냥 호출시 위와 같은 에러가 발생하는 것이 당연하다.
- 이 경우도 아래와 같이 해당 Frame 이 다 그려지고 나서 그 이후에 그려지도록 처리해야 한다.
@override
void initState() {
super.initState();
WidgetsBinding.instance!.addPostFrameCallback((_) {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
content: Text('Be careful!'),
);
},
);
});
}
- 같은 맥락에서 가변적인 값을 바라보고 특정 조건에서 Dialog 를 띄우고 싶을때도 아래와 같이 addPostFrameCallback 로 처리해줘야한다.
- 해당 Frame 이 다 그려지고 나서 이후에 조건 검사를 한 후 overlay 하는 것이기 때문이다.
- 조건식을 포함한 Dialog call 자체를 모두 addPostFrameCallback 로 예약할 순 있으나 조건에 부합하지도 않는 경우에도 불필요하게 action 이 register 되므로 좋지 않은 코드가 된다.
@override
Widget build(BuildContext context) {
if (context.read<Counter>().counter == 3) {
WidgetsBinding.instance!.addPostFrameCallback((_) {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
content: Text('Count is 3'),
);
},
);
});
}
return Scaffold(
appBar: AppBar(
title: Text('Handle Dialog'),
),
body: Center(
child: Text(
'${context.watch<Counter>().counter}',
style: TextStyle(fontSize: 40.0),
),
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),
onPressed: () {
context.read<Counter>().increment();
},
),
);
}
Errors & addPostFrameCallback - 4
Errors & addPostFrameCallback - 4
에서는 State 가 변할 때 Navigate 하는 방법에 대해 살펴본다.
- Navigate 하는 것도 결국 Stack 위에 올리는 Overlay 행위이기 때문에 아래와 같이 예약을 걸어 주어야 한다.
- 만약 addPostFrameCallback 를 사용하지 않으면 build 하는 와중에 Navigator.push 가 처리 되는 것이고 이는 본 화면이 다 그려지기도 전에(정확히는 그려지는 중간에) Overlay 요청을 한 것과 같다. 그래서 위와 같은 에러를 만나게 된다.
if (context.read<Counter>().counter == 3) {
WidgetsBinding.instance!.addPostFrameCallback((_) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => OtherPage(),
),
);
});
}
- Dialog, Navigate, BottomSheet 모두 Overlay Widget 이므로 해당 Widget 을 사용할 경우 이번에 학습한 내용들을 잘 활용하도록 한다.
ChangeNotifier 의 addListener 를 이용한 action 처리
- 강사님은 두번째 혹은 세번째 방식을 추천.
- 두번째 방식만 아래에 코드로 정리.
Future<void> getResult(String searchTerm) async {
_state = AppState.loading;
notifyListeners();
await Future.delayed(Duration(seconds: 1));
try {
if (searchTerm == 'fail') {
throw 'Something went wrong';
}
_state = AppState.success;
notifyListeners();
} catch (e) {
_state = AppState.error;
notifyListeners();
rethrow;
}
}
void submit() async {
setState(() {
autovalidateMode = AutovalidateMode.always;
});
final form = formKey.currentState;
if (form == null || !form.validate()) return;
form.save();
try {
await context.read<AppProvider>().getResult(searchTerm!);
Navigator.push(context, MaterialPageRoute(
builder: (context) {
return SuccessPage();
},
));
} catch (e){
showDialog(
context: context,
builder: (context) {
return AlertDialog(
content: Text('Something went wrong'),
);
},
);
}
}