이전 블로그에서는 Stateless Widget 과 Stateful Widget 에 대해 알아보았다.
상태가 존재하는 Stateful Widget 에서 상태가 변경되면 rebuild 함으로써 UI 도 업데이트하게 된다.
그렇다면, 동일한 상태를 여러 위젯에서 사용하고자 할 때는 부모 위젯과 자식 위젯(공유 상위 위젯) 간에 상태를 공유하면서 rebuild 하는 과정이 불필요하게 많아지게 되고 낮은 성능과 유지보수가 많아지게 된다.
이 문제를 해결하기 위해 상태를 전역으로 관리하는 Provider 를 사용한다.
Provider 은 크게 생성 부분과 소비 부분으로 나눈다.
생성 부분 (공급자) 은 사용할 데이터 타입을 결정하고 해당 데이터에 대한 Provider 를 생성한다.
소비 부분 (소비자) 은 생성된 Provider 를 통해 데이터를 불러오거나 업데이트하는 등의 작업을 수행한다.
flutter pub add provier
명령어를 통해 Provider 패키지를 설치한다.
pubspec.yaml
파일에 추가된 패키지를 볼 수 있다.
dependencies:
flutter:
sdk: flutter
provider: ^4.3.2
import 'package:flutter/material.dart';
class AppState with ChangeNotifier {
String _username = '';
String get username => _username;
void setUserName(String newUsername) {
_username = newUsername;
notifyListeners();
}
}
상태 변수를 정의하고 상태를 변경하는 getter 를 이용한다.
ChangeNotifer 를 상속하고, notifyListeners() 를 호출하여 상태 변경을 알린다.
ChangeNotifierProvider 가 상태 변경을 감지하고 자동으로 위젯 트리(UI)를 업데이트되기 때문에 편리하게 상태를 관리할 수 있다.
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'app_state.dart'; // 생성한 모델 클래스의 파일
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (context) => AppState(),
child: MaterialApp(
title: 'Flutter Provider Example',
home: MyHomePage(),
),
);
}
}
먼저 main.dart
파일에서 ChangeNotifierProvider 를 사용하여 전역 상태를 제공한다.
Note! 여러 개의 ChangeNotifierProvider 를 사용하려면 MultiProvider 로 단일 위젯으로 감싸 이를 통해 각각의 프로바이더를 관리할 수 있게 해준다.
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'app_state1.dart'; // 첫 번째 모델 클래스의 파일
import 'app_state2.dart'; // 두 번째 모델 클래스의 파일
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
Widget build(BuildContext context) {
return
//두 개 이상의 ChangeNotifierProvider 일 경우
MultiProvider(
providers: [
ChangeNotifierProvider(create: (context) => AppState1()),
ChangeNotifierProvider(create: (context) => AppState2()),
],
child: MaterialApp(
title: 'Flutter MultiProvider Example',
home: MyHomePage(),
),
);
}
}
class MyHomePage extends StatelessWidget {
Widget build(BuildContext context) {
// Provider.of를 사용하여 상태에 접근
final appState = Provider.of<AppState>(context);
return Scaffold(
appBar: AppBar(
title: Text('Flutter Provider Example'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Username: ${appState.username}'),
TextField(
onChanged: (newUsername) {
// Provider.of를 사용하여 상태 업데이트
appState.setUserName(newUsername);
},
),
],
),
),
);
}
}
가장 기본적이고 간단한 형태의 상태 소비 방법이다.
Provider.of
를 사용하여 위젯 트리 상에서 가장 가까운 ChangeNotifierProvider 찾아
해당 모델 클래스에 접근할 수 있다. (Provider.of<AppState>(context);
)
위젯 트리의 어느곳에서든 사용 가능하지만, 주로 build 메서드 내에서 사용된다.
다만, 이 방식은 해당 위젯에서 상태에 접근하므로, 해당 위치에 위젯이 rebuild 될 때마다 새로운 인스턴스가 생성될 수 있다.
이 경우, rebuild 에 영향을 받지 않는 방법으로 Consumer 을 사용한다.
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'app_state.dart'; // 생성한 모델 클래스의 파일
class MyHomePage extends StatelessWidget {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Flutter Provider Example'),
),
body: Center(
child: Consumer<AppState>(
builder: (context, appState, child) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Username: ${appState.username}'),
TextField(
onChanged: (newUsername) {
appState.setUserName(newUsername);
},
),
],
);
},
),
),
);
}
}
Consumer 위젯은 AppState 의 인스턴스를 감지하고, builder 함수를 호출하여 해당 상태가 변경될 때마다 UI 를 업데이트 한다.
이때 appState 매개변수를 통해 state 인 username 을 가져와서 화면에 표시하고, TextField 의 onChanged 콜백에서 setUserName 메서드를 호출하여 상태를 업데이트할 수 있다.
Consumer 는 효율적으로 필요한 부분만 rebuild 되기 때문에 특정 부분만 업데이트 되어 성능을 더욱 향상시킬 수 있다.(특정 상태를 감시하고 해당 상태가 변경될 때만)
class MyHomePage extends StatelessWidget {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Flutter Provider Example'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// watch를 사용하여 상태 감시
Text('Username: ${context.watch<AppState>().username}'),
// read를 사용하여 상태 읽기
TextField(
onChanged: (newUsername) {
// read를 사용하여 상태 업데이트
context.read<AppState>().setUserName(newUsername);
},
),
],
),
),
);
}
}
read 는 간단한 읽기, watch 는 읽기와 동시에 해당 Provider 가 변경될 때 자동으로 rebuild 한다.
주로 함수형 위젯 내에 사용되며, read 는 어디서든 사용가능하나 watch 는 주로 build 메서드 내에서 사용되어 변경될 때만 rebuild 되어 성능 상 이점이 있다.
종합적으로,
- 간편한 사용과 특정 위치에서만 사용해야하는 경우에는
Provider.of
를 선택한다.- 리빌드 효율성이 중요하고, 특정 Provider 에 의존하는 경우
Consumer
를 사용한다.- 함수형 스타일이나 특정상황에서는
read
나watch
를 사용한다.
Provider 를 사용하고 정리하면서 소비하는 부분에 있어서 Provider.of 외에도 consumer과 read, watch 를 사용하는 방법에 대해서도 더 배웠다. 아무래도 최적화시키기에는 다양한 방법이 있기 마련이다. 가장 기본적인 Provider.of 를 사용했으니 더 성능상 이점이 있는 consumer 를 사용해 보고자하는 욕심도 생긴다.