티스토리 뷰
새 객체 인스턴스 노출하기
Providers는 단순히 값을 노출시켜줄 뿐만 아니라, 값의 생성(create), 수신(listen) 그리고 해제(dispose)를 할 수 있도록 합니다.
[O] Provider의 create 안에서 신규 객체를 생성하세요.
Provider(
create: (_) => MyModel(),
child: ...
)
[X] Provider.value에서 객체를 생성하지 마세요. 메모리 누수와 올바른 동작을 하지 않을 수 있습니다.
ChangeNotifierProvider.value(
value: MyModel(),
child: ...
)
[X] 시간에 따라 변경될 수 있는 변수로 객체를 만들지 마세요.
만약 그렇게 생성하였다면, 생성된 객체는 값이 변화해도 업데이트되지 않습니다.
int count;
Provider(
create: (_) => MyModel(count),
child: ...
)
[O] 만약 시간에 따라 변경될 수 있는 변수를 객체에 전달하려면 ProxyProvider 사용하세요.
int count;
ProxyProvider0(
update: (_, __) => MyModel(count),
child: ...
)
참고 : provider의 create/update 콜백을 사용할 때, 이 콜백이 기본적으로 Lazy 하게 호출된다는 점에 유의해야 합니다.즉, 해당 값을 한 번 이상 호출하기 전에는 create/update 콜백이 호출되지 않습니다. 이 동작은 lazy 파라미터를 사용해 일부 로직을 사전 연산(pre-compute)하고자 하는 경우 비활성화될 수 있습니다.
MyProvider(
create: (_) => Something(),
lazy: false,
)
기존 객체 인스턴스를 재사용하기
객체 인스턴스가 이미 생성되었고, 해당 객체를 노출시키길 원하는 경우 provider의 .value 생성자를 사용하는 것이 가장 좋습니다.
그렇지 않고 생성자를 사용하면 객체가 아직 사용되고 있는 도중에 dispose 메소드가 호출될 수 도 있습니다.
[O] 이미 존재하는 ChangeNotifier를 공급(provide)하기 위해서 ChangeNotifierProvider.value를 사용하세요.
MyChangeNotifier variable;
ChangeNotifierProvider.value(
value: variable,
child: ...
)
[X] 이미 존재하는 ChangeNotifier를 기본 생성자를 사용해서 재사용하지 마세요.
MyChangeNotifier variable;
ChangeNotifierProvider(
create: (_) => variable,
child: ...
)
이 방법은 ChangeNotifierProvider뿐만 아니라 Provider에도 동일하게 적용됩니다.
값 읽기
값을 읽는 가장 쉬운 방법은 BuildContext의 확장 메소드를 활용하는 것입니다.
- context.watch() : 위젯이 T의 변화를 감지할 수 있도록 합니다.
- context.read() : 변화 감지 없이 T를 return 합니다.
- context.select<T, R>(R cb(T value)) : T의 일부 작은 영역에 대해서만 위젯이 변화를 감지할 수 있도록 합니다.
또한 watch와 유사하게 동작하는 정적 메서드(static method)인 Provider.of(context)를 사용할 수도 있습니다. Provider.of(context, listen: false)처럼 listen 파라미터를 false로 하면 read와 유사하게 동작합니다.
context.read()는 값이 변경되었을 때 위젯을 재빌드하지 않음으로 StatelessWidget.build/State.build 안에서 호출될 수 없음을 유의하세요. 반면, 이러한 메서드들 밖에서는 자유롭게 호출될 수 있습니다.
이러한 메소드들은 전달된 BuildContext와 관련된 위젯에서 시작해 위젯 트리에서 발견되며, 발견된 가장 가까운 T 타입 변수를 반환합니다. (아무것도 찾을 수 없는 경우 예외가 발생합니다.)
이 작업은 O(1)입니다. 작업에 위젯 트리를 순차적으로 탐색하는 일이 포함되어있지 않습니다.
이 위젯은 첫 번째 예시인 값 노출하기에서 노출된 String을 읽고 "Hello World."를 렌더 합니다.
class Home extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Text(
// watch에 얻을 타입을 전달하는 것을 잊으면 안됩니다.
context.watch<String>(),
);
}
}
이러한 메서드들을 사용하는 대신에, 우리는 Consumer와 Selector를 사용할 수 있습니다.
이 기능은 성능을 최적화하거나, provider의 BuildContext 하위 항목에 접근하기 어려울 때 유용하게 활용할 수 있습니다.
provider의 선택적 의존
때때로 우리는 provider가 존재하지 않는 경우를 지원하고 싶을 수도 있습니다. 예를 들어 provider 외부 등 다양한 위치에서 사용될 수 있는 위젯의 경우가 있습니다.
그렇게 하기 위해서, context.watch/context.read을 호출할 때 generic 타입 대신 nullable 타입을 사용합니다. 예를 들어 아래와 같이 사용하는 경우 :
context.watch<Model>()
매칭되는 provider를 찾지 못한 경우 ProviderNotFoundException 예외가 발생합니다. 대신 아래와 같이 사용하면 :
context.watch<Model?>()
매칭되는 provider를 찾지 못하더라도 예외를 발생시키는 대신, null을 반환합니다.
MultiProvider
규모가 큰 애플리케이션에서 많은 값을 주입하면 Provider가 급격하게 중첩될 수 있습니다.
Provider<Something>(
create: (_) => Something(),
child: Provider<SomethingElse>(
create: (_) => SomethingElse(),
child: Provider<AnotherThing>(
create: (_) => AnotherThing(),
child: someWidget,
),
),
),
이를 아래와 같이 작성할 수 있습니다.
MultiProvider(
providers: [
Provider<Something>(create: (_) => Something()),
Provider<SomethingElse>(create: (_) => SomethingElse()),
Provider<AnotherThing>(create: (_) => AnotherThing()),
],
child: someWidget,
)
두 코드는 완전히 동일하게 동작합니다. MultiProvider는 오직 코드의 외관을 바꿔줄 뿐입니다.
ProxyProvider
3.0.0 버전부터 새로운 provider인 ProxyProvider가 추가되었습니다.
ProxyProvider는 다른 provider들의 여러 값을 하나의 객체로 묶어 Provider로 전달하는 provider입니다.
그러면 해당 신규 객체는 우리가 의존하는 provider 중 하나가 업데이트될 때마다 업데이트됩니다.
아래 예제에서는 다른 provider에서 온 counter를 기반으로 translations를 빌드하기 위해 ProxyProvider를 사용하고 있습니다.
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => Counter()),
ProxyProvider<Counter, Translations>(
update: (_, counter, __) => Translations(counter.value),
),
],
child: Foo(),
);
}
class Translations {
const Translations(this._value);
final int _value;
String get title => 'You clicked $_value times';
}
이것은 아래와 같은 다양한 변형이 있습니다 :
- ProxyProvider vs ProxyProvider2 vs ProxyProvider3, ...
- 클래스 이름 뒤의 숫자는 ProxyProvider가 의존하는 다른 공급자의 수입니다.
- ProxyProvider vs ChangeNotifierProxyProvider vs ListenableProxyProvider, ...
- 모두 비슷하게 동작하지만, ChangeNotifierProxyProvider는 값을 그 결과를 Provider를 보내는 대신, ChangeNotifierProvider로 보냅니다.
FAQ
# 내 객체들을 인스펙터에서 확인할 수 있나요?
Flutter는 지정된 시점에 위젯 트리가 어떤 것인지 보여주는 devtool이 함께 제공됩니다.
provider는 위젯이기 때문에 마찬가지로 devtool에서 볼 수 있습니다.
여기에서 한 provider를 클릭하면 해당 provider가 노출하고 있는 값을 볼 수 있습니다.
# devtool에 "Instance of MyClass" 밖에 안 보여요. 어떻게 해야 하나요?
기본적으로 devTool은 toString에 의존하기 때문에 기본 설정인 "Instance of MyClass"로 보여집니다.
보다 유용하게 사용하기 위해서, 다음과 같은 두 가지 솔루션이 있습니다.
* Flutter의 Diagnosticable API를 사용하세요.
대부분의 경우 개체에 DiagnosticableTreeMixin을 사용하고 debugFillProperties를 사용자 지정으로 구현합니다.
class MyClass with DiagnosticableTreeMixin {
MyClass({this.a, this.b});
final int a;
final String b;
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
// 여기에 클래스의 모든 속성을 나열합니다.
// 자세한 내용은 debugFillProperties 문서를 참조하세요.
properties.add(IntProperty('a', a));
properties.add(StringProperty('b', b));
}
}
* toString을 재정의(override) 하세요.
만약 DiagnosticableTreeMixin를 사용할 수 없다면 (Flutter를 사용하지 않는 패키지 등), toString를 재정의해서 사용할 수 있습니다.
이것은 DiagnosticableTreeMixin를 사용하는 것보다 쉽지만, 객체의 세부정보를 확장하거나 축소할 수 없기에 덜 강력합니다.
class MyClass with DiagnosticableTreeMixin {
MyClass({this.a, this.b});
final int a;
final String b;
@override
String toString() {
return '$runtimeType(a: $a, b: $b)';
}
}
# Provider를 initState안에 넣었을 때 예외가 발생합니다. 어떻게 해야 하나요?
이 예외는 다시 호출되지 않는 생명 주기(life-cycle)에서 provider를 감지하려고 하기 때문에 발생합니다. 때문에 build와 같은 다른 생명 주기에서 사용하거나, 업데이트에 관련 없음을 명시해주어야 합니다.
때문에 아래와 같이 작성하는 대신 :
initState() {
super.initState();
print(context.watch<Foo>().value);
}
이렇게 작성할 수 있습니다.
Value value;
Widget build(BuildContext context) {
final value = context.watch<Foo>().value;
if (value != this.value) {
this.value = value;
print(value);
}
}
이는 값이 변경될 때마다(그리고 변경될 때만) 'value가 출력됩니다.
또는 다음과 같이 작성할 수 있습니다.
initState() {
super.initState();
print(context.read<Foo>().value);
}
이는 value를 한번 출력하고 업데이트를 무시합니다.
객체들의 hot-reload를 어떻게 다룰 수 있나요?
제공된 객체에 ReassembleHandler를 구현할 수 있습니다.
class Example extends ChangeNotifier implements ReassembleHandler {
@override
void reassemble() {
print('Did hot-reload');
}
}
그 후 일반적으로 provider와 함께 사용합니다.
ChangeNotifierProvider(create: (_) => Example()),
ChangeNotifier를 사용하며 업데이트할 때 예외가 발생합니다. 무슨 일이 일어나고 있는 거죠?
위젯 트리가 빌드되는 동안 하위 항목 중 하나에서 ChangeNotifier를 수정하고 있기 때문에 문제가 발생할 수 있습니다.
일반적으로 이러한 상황은 notifier 안에 future가 저장된 상태에서 http 요청을 시작할 때 발생합니다.
initState() {
super.initState();
context.read<MyNotifier>().fetchSomething();
}
상태 업데이트가 동기화되어 있으므로 이 작업은 허용되지 않습니다.
이는 즉, 어떤 위젯은 변경이 일어나기 전에 빌드될 수 있으며(오래된 값을 받음), 또 어떤 위젯은 변경이 완료된 후에 빌드될 수 있습니다(새로운 값을 받음). 이로 인해 UI에 불일치가 발생할 수 있으므로, 이는 허용되지 않습니다.
대신 전체 트리에 동일하게 영향을 미치는 위치에서 변경을 수행해야 합니다.
* 모델의 provider 생성자 create인자 안에서 직접 수행하기 :
class MyNotifier with ChangeNotifier {
MyNotifier() {
_fetchSomething();
}
Future<void> _fetchSomething() async {}
}
외부 변수가 없는 경우 유용합니다.
* 프레임 끝에 비동기식으로 수행하기 :
initState() {
super.initState();
Future.microtask(() =>
context.read<MyNotifier>().fetchSomething(someValue);
);
}
약간 덜 이상적이지만, 변경 사항에 매개변수를 전달할 수 있습니다.
복잡한 상태의 경우 ChangeNotifier를 써야 하나요?
아닙니다. 모든 객체를 사용하여 상태를 나타낼 수 있습니다.
예를 들어 대체 구조로 Provider.value()와 StatefulWidget를 결합하여 사용할 수 있습니다.
아래 예시는 이러한 구조를 사용한 반증 사례입니다.
class Example extends StatefulWidget {
const Example({Key key, this.child}) : super(key: key);
final Widget child;
@override
ExampleState createState() => ExampleState();
}
class ExampleState extends State<Example> {
int _count;
void increment() {
setState(() {
_count++;
});
}
@override
Widget build(BuildContext context) {
return Provider.value(
value: _count,
child: Provider.value(
value: this,
child: widget.child,
),
);
}
}
다음 작업을 통해 상태를 읽을 수 있습니다.
return Text(context.watch<int>().toString());
또한 다음과 같이 상태를 수정할 수 있습니다.
return FloatingActionButton(
onPressed: () => context.read<ExampleState>().increment(),
child: Icon(Icons.plus_one),
);
또는 직접 provider를 만들 수도 있습니다.
나만의 provider를 만들 수 있나요?
네. provider는 provider를 완전하게 구성하는 작은 컴포넌트들을 모두 공개하고 있습니다.
이는 아래와 같은 것들을 포함합니다.
- 어떤 위젯이던지 MultiProvider와 함께 동작하도록 만들어주는 SingleChildStatelessWidget을 제공합니다. 이 인터페이스는 package:provider/single_child_widget의 일부로 노출됩니다.
- context.watch를 수행할 때 얻는 일반 상속 위젯인 상속된 제공자입니다.
다음은 ValueNotifier를 상태로 사용하는 사용자 지정 provider의 예입니다. https://gist.github.com/rrousselGit/4910f3125e41600df3c2577e26967c91
위젯이 너무 자주 재빌드됩니다. 어떻게 하나요?
context.watch를 사용하는 대신, 객체의 특정한 부분 만을 추적하는 context.select를 사용할 수 있습니다.
예를 들어 다음과 같이 작성한다면,
Widget build(BuildContext context) {
final person = context.watch<Person>();
return Text(person.name);
}
name이 아닌 다른 프로퍼티가 변경되어도 위젯이 재빌드될 것입니다.
대신에 context.select를 사용해 name 프로퍼티만 추적하게 하면,
Widget build(BuildContext context) {
final name = context.select((Person p) => p.name);
return Text(name);
}
이렇게 하면 name 프로퍼티가 아닌 변화가 발생하더라도 불필요하게 위젯을 재빌드하지 않습니다.
유사하게 Consumer/Selector를 사용할 수도 있습니다. 이들이 가지고 있는 child 매개변수는 위젯 트리의 특정 부분만 재빌드할 수 있도록 해줍니다.
Foo(
child: Consumer<A>(
builder: (_, a, child) {
return Bar(a: a, child: child);
},
child: Baz(),
),
)
위 예시에서 A가 업데이트되었을 때 오직 Bar만 재빌드됩니다. Foo는 불필요하게 재빌드되지 않습니다.
동일한 타입을 사용하는 다른 provider들을 함께 사용할 수 있나요?
없습니다. 여러 provider가 동일한 타입을 공유할 수 있지만, 위젯은 가장 가까운 상위 provider 하나 만을 가져올 수 있습니다.
대신 두 provider에게 명시적으로 다른 타입을 제공하면 좋습니다.
아래와 같이 작성하는 대신,
Provider<String>(
create: (_) => 'England',
child: Provider<String>(
create: (_) => 'London',
child: ...,
),
),
이렇게 작성하는 것이 좋습니다.
Provider<Country>(
create: (_) => Country('England'),
child: Provider<City>(
create: (_) => City('London'),
child: ...,
),
),
인터페이스를 사용하거나 구현을 제공할 수 있나요?
네, 생성 시 제공된 구현과 함께 인터페이스가 사용될 것임을 나타내는 형식 힌트를 컴파일러에 제공해야 합니다.
abstract class ProviderInterface with ChangeNotifier {
...
}
class ProviderImplementation with ChangeNotifier implements ProviderInterface {
...
}
class Foo extends StatelessWidget {
@override
build(context) {
final provider = Provider.of<ProviderInterface>(context);
return ...
}
}
ChangeNotifierProvider<ProviderInterface>(
create: (_) => ProviderImplementation(),
child: Foo(),
),
현재 제공되는 provider
provider는 다른 객체 타입에 대해 몇 가지 다른 "provider"를 제공합니다.
모든 객체의 리스트는 여기에서 확인할 수 있습니다.
- Provider - 가장 기본적인 형태의 Provider. 값이 무엇이든 간에 값을 취하고 값을 노출시킵니다.
- ListenableProvider - Listenable Object에 대한 특정 공급자입니다. ListenableProvider는 개체를 듣고 수신자가 호출될 때마다 개체에 의존하는 위젯을 재구성하도록 요청합니다.
- ChangeNotifierProvider - ChangeNotifier용 ListenableProvider의 사양입니다. 필요할 때 ChangeNotifier.dispose를 자동으로 호출합니다.
- ValueListenableProvider - ValueListenable을 듣고 ValueListenable.value만 노출합니다.
- StreamProvider - 스트림을 듣고 최신 값을 표시합니다.
- Future Provider - 미래를 선택하고 미래가 완료되면 종속변수를 업데이트합니다.
공급자가 너무 많아서 응용 프로그램에서 StackOverflowError가 발생합니다. 어떻게 해야 합니까?
공급자 수가 매우 많은 경우(150+), 한 번에 너무 많은 위젯을 구축하기 때문에 일부 장치에서 StackOverflowError가 발생할 수 있습니다.
이 경우 몇 가지 해결책이 있습니다:
* 응용프로그램에 스플래시 스크린이 있는 경우 한꺼번에 설치하는 대신 시간이 지남에 따라 공급업체를 설치해 보십시오.
MultiProvider(
providers: [
if (step1) ...[
<lots of providers>,
],
if (step2) ...[
<some more providers>
]
],
)
스플래시 스크린 애니메이션 중에는 다음과 같은 작업을 수행할 수 있습니다:
bool step1 = false;
bool step2 = false;
@override
initState() {
super.initState();
Future(() {
setState(() => step1 = true);
Future(() {
setState(() => step2 = true);
});
});
}
* MultiProvider를 사용하지 않는 것을 고려해 보겠습니다.
MultiProvider는 모든 제공자 사이에 위젯을 추가하여 작동합니다. MultiProvider를 사용하지 않으면 StackOverFlowError에 도달하기 전에 제한이 증가할 수 있습니다.
'Flutter > 상태 관리' 카테고리의 다른 글
플러터] Provider - Provider.of 정적 메서드 (0) | 2024.07.02 |
---|---|
플러터] 상태 관리 (State management) (0) | 2024.07.02 |
플러터] Provider - Selector<A, S> 클래스 (0) | 2024.07.02 |
플러터] Provider - Consumer 클래스 (0) | 2024.07.01 |
플러터] Provider - ChangeNotifierProxyProvider 클래스 (0) | 2024.07.01 |