여기에 3개의 UI가 있다고 하겠습니다. 이 UI들은 각각 동일한 데이터를 공유해야 된다고 하겠습니다. 그렇다면 우리는 해당 위젯을 아래처럼 아규먼트를 넘겨주면서 데이터를 전달할 수 있을 것입니다.
이렇게 되면 한가지 문제점이 나타날 수 있습니다. 만약 서로 공유하고 있는 아규먼트 데이터가 세번째 UI에서 변경되었을 때, 이를 공유하고 있는 UI는 리빌드 되야 해당 데이터의 변경을 적용할 수 있습니다. 우리는 이 문제를 어떻게 해결할 수 있을까요?
이를 해결하기위하여 상태관리라는 개념이 만들어졌으며, 맨 처음 Flutter로 상태관리를 입문하는 것이 Provider 입니다.
Provider는 GetX, BloC과 같은 상태관리 솔루션 라이브러리입니다. 현재 해당 라이브러리는 Riverpod를 만들게 된 계기가 되었지만 처음 나왔을때는 굉장히 획기적인 상태관리 솔루션이었습니다. 이제, Provider를 이용하여 상태관리를 해볼까요?
아래와 같은 예제앱이 있습니다.
import 'package:flutter/material.dart';
import 'package:flutter_provider/src/counter.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return const MaterialApp(
home: Counter(),
);
}
}
import 'package:flutter/material.dart';
import 'package:flutter_provider/src/counter_controll.dart';
class Counter extends StatelessWidget {
const Counter({super.key});
Widget build(BuildContext context) {
int count = 0;
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"$count",
style: const TextStyle(fontSize: 40),
),
ElevatedButton(
onPressed: () {
Navigator.of(context).push(MaterialPageRoute(
builder: (context) => CounterControll(count: count)));
},
child: const Text("카운터 상세"))
],
),
),
);
}
}
// ignore_for_file: must_be_immutable
import 'package:flutter/material.dart';
class CounterControll extends StatefulWidget {
int count;
CounterControll({super.key, required this.count});
State<CounterControll> createState() => _CounterControllState();
}
class _CounterControllState extends State<CounterControll> {
void increase() => setState(() {
widget.count++;
});
void decrease() => setState(() {
widget.count--;
});
Widget build(BuildContext context) {
return Scaffold(
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
"${widget.count}",
style: const TextStyle(fontSize: 40),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
ElevatedButton(onPressed: increase, child: const Icon(Icons.add)),
ElevatedButton(
onPressed: decrease, child: const Icon(Icons.remove)),
],
)
],
),
);
}
}
해당 예제는 초기 UI에서 나타난 카운터를 그 다음 페이지가 이어받아서 해당 카운터를 증가 감소 시킬 수 있는 앱입니다.
하지만 보다시피 해당 앱을 통해 두번째 UI에서만 숫자가 변할 뿐 이전 화면에서는 전혀 숫자가 변하지 않습니다. 게다가 대규모 앱에서는 동일한 데이터를 더 많은 화면에서 접근하려 하겠죠. 이를 해결하기 위해서 Provider를 이용할 수 있습니다.
Provider를 사용하기 위해서는 해당 라이브러리를 설치해야 합니다.
$ flutter pub add provider
이제 가장 중요한 CounterModel을 생성하겠습니다. 우리는 이 Model을 통해서 모든 화면에서 접근하는 counter에 대한 값을 처리할 것입니다.
import 'package:flutter/material.dart';
class CounterModel with ChangeNotifier {
int _count = 0;
int get count => _count;
void increase() {
_count++;
notifyListeners();
}
void decrease() {
_count--;
notifyListeners();
}
}
가장 중요한 것은 ChangeNotifier를 mixin 하는 것입니다. notifyListeners()를 통해서 해당 Provider를 참조하는 모든 위젯에 정보를 뿌려줄 수 있습니다. 이제 MaterialApp에 Provider를 적용해야합니다.
import 'package:flutter/material.dart';
import 'package:flutter_provider/src/counter.dart';
import 'package:flutter_provider/src/model/counter_model.dart';
import 'package:provider/provider.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => CounterModel(),
child: const MaterialApp(home: Counter()),
);
}
}
하나의 위젯이 아니라 앱 전체에서 사용하기 위해서는 ChangeNotifierProvider를 이용해서 MaterialApp을 감싸주어야 합니다. 그렇지 않은 경우에는 해당하는 child에만 Provider를 감싸줍니다.
import 'package:flutter/material.dart';
import 'package:flutter_provider/src/counter_controll.dart';
import 'package:flutter_provider/src/model/counter_model.dart';
import 'package:provider/provider.dart';
class Counter extends StatelessWidget {
const Counter({super.key});
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Consumer<CounterModel>(builder: (context, counter, child) {
return Text(
"${counter.count}",
style: const TextStyle(fontSize: 40),
);
}),
ElevatedButton(
onPressed: () {
Navigator.of(context).push(MaterialPageRoute(
builder: (context) => const CounterControll()));
},
child: const Text("카운터 상세"))
],
),
),
);
}
}
import 'package:flutter/material.dart';
import 'package:flutter_provider/src/model/counter_model.dart';
import 'package:provider/provider.dart';
class CounterControll extends StatelessWidget {
const CounterControll({
super.key,
});
Widget build(BuildContext context) {
return Scaffold(
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Consumer<CounterModel>(builder: (context, counter, child) {
return Text(
"${counter.count}",
style: const TextStyle(fontSize: 40),
);
}),
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
ElevatedButton(
onPressed: context.read<CounterModel>().increase,
child: const Icon(Icons.add)),
ElevatedButton(
onPressed: context.read<CounterModel>().decrease,
child: const Icon(Icons.remove)),
],
)
],
),
);
}
}
이제 Provider에 접근하는 방법에 대해서 알아보겠습니다. 위에처럼 Consumer를 통해서 접근할 수도 있으나, Provider.of(context)를 통해서 접근하는 것이 더 간단할 수도 있습니다. 이제 앱 전역에서 Provider로 선언한 Counter 변수에 접근할 수 있습니다.