Flutter : การใช้ Provider กับ State Management
สวัสดีผู้อ่านครับ บล็อกนี้ผมจะพามาลองเล่น Provider ซึ่งเป็นคลาสสำหรับช่วยจัดการเรื่อง State Management ที่มาก็มาจาก ในงาน Google I/O 2019 ที่ผ่านมา ใน session ของ Flutter ทีมงาน Flutter ได้อธิบายเกี่ยวเรื่อง State management ว่าทำอย่างไร โดยได้หยิบยกตัวอย่างการใช้ Provider ซึ่งจริงๆแล้วมันก็ใช้งานเหมือนคล้ายกับ
InheritedWidget + ScopeModel + BLoC รวมกัน ทำให้การใช้งานง่ายมากขึ้น
เริ่มต้น
เริ่มต้น ตัวอย่าง จากโปรเจคต้นแบบของ Flutter คือแอป counter สำหรับนับเลข
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.display1,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}
จากปัญหาของเรื่อง State ของแอปนี้ คือ เมื่อเรากดปุ่มบวก แอปจะเรียก setState แล้ว build Widget ใหม่ทั้งหมด ทำให้ประสิทธิภาพของแอปไม่ดีอย่างที่ควรจะเป็น แถมตัวโค้ดยังผูกกับ widget มากเกินไป
แนวทางคือการใช้ State management แบบต่างๆมาช่วย ซึ่งมีหลายตัว เช่น Redux , BLoC โดยสิ่งที่เพิ่มเข้ามาจะช่วยแยก State ให้จัดการง่ายขึ้น แยก business logic และช่วยเพิ่มประสิทธิภาพของแอป เราสามารถเขียนให้ build เฉพาะในสิ่งที่จำเป็น
เพิ่ม Dependencies
เพิ่ม dependencies ของ provider ใน pubspec.yaml
dependencies:
flutter:
sdk: flutter
...
provider: 2.0.1+1
Model Provider
สร้างคลาส provider ของเรา ซึ่งในที่นี้ คือ Counter โดยมันจะเก็บข้อมูลต่างๆ รวมทั้ง method การทำงานเอาไว้ และจะสามารถอัพเดท Widget ได้ ซึ่งความสามารถในการอัพเดท widget เมื่อข้อมูลเปลี่ยนแปลง จะใช้คลาสที่ชื่อว่า notifier โดยหลักๆมีอยู่ 2 แบบ
ChangeNotifier
แบบแรก คือให้คลาส provider ของเราสืบทอด ChangeNotifier จะ extends หรือ with ก็ได้ โดยข้อดีคือสามารถกำหนดตัวแปรกี่ตัวให้ provider ก็ได้ แต่จะต้องเรียก notifyListener() เมื่อต้องการอัพเดท UI
class CounterProvider with ChangeNotifier {
int counter;
CounterProvider({this.counter = 0});
increment() {
counter++;
notifyListeners();
}
}
ValueNotifier
อีกรูปแบบคือ ValueNotifier ข้อแตกแตกต่างจาก ChangeNotifier คือ มันจะมีข้อมูลในตัวเองได้ตัวเดียว โดยใช้ชื่อว่า value และเมื่อเราเปลี่ยนแปลงค่า value มันจะอัพเดท UI ให้อัตโนมัตินั่นเอง
class CounterProvider extends ValueNotifier<int> {
CounterProvider({int counter = 0}) : super(counter);
increment() {
value++;
}
get counter => value;
}
สรุป ChangeNotifier กับ ValueNotifier มีข้อแตกต่างเรื่องการใช้งานนิดหน่อย ต้องเลือกใช้กับ provider ของเราให้เหมาะสม
Provider
ลองใช้งาน provider ในตัว widget กันบ้าง ก่อนอื่น import provider เข้ามาก่อน
import 'package:provider/provider.dart';
ที่ root ของแอป เราจะต้องกำหนด provider ให้กับแอป โดยใช้คลาส Provider แล้วกำหนด counter provider ของเราลงไปใน builder ทำให้ตอนนี้เราสามารถเข้าถึง
counter provider ได้จากทุกที่แล้ว
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter App',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: Provider(
builder: (context) => CounterProvider(counter: 0),
child: MyHomePage(title: 'Flutter App')),
);
}
}
ChangeNotifierProvider + Consumer
จากนั้นเมื่อไหร่ที่เราต้องใช้ counter provider ก็เรียกด้วยคำสั่ง Provider.of<CounterProvider>(context);
ทีนี้จุดที่เราต้องการให้ build Widget เฉพาะกับข้อมูลเที่เราอัพเดท เราจะใช้ ChangeNotifierProvider แล้วใส่ provider เข้าไปให้มัน แล้วกำหนด child เป็น Consomer โดย Consumer ก็จะมีหน้าที่นำข้อมูลจาก provider มา build เป็น widget ส่วนที่ปุ่ม action ก็แค่เรียก counterProvider.increment(); เพื่ออัพเดทค่าใน provider แล้วมันจะจัดการ build Widget ใน ChangeNotifierProvider ใหม่ให้เอง
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
CounterProvider counterProvider = Provider.of<CounterProvider>(context);
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
ChangeNotifierProvider<CounterProvider>(
builder: (context) => counterProvider,
child: Consumer<CounterProvider>(
builder: (context, data, child) =>
buildText(counterProvider.counter)))
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
counterProvider.increment();
},
child: Icon(Icons.add),
),
);
}
buildText(int value) {
print("Text");
return Text(
'$value',
style: Theme.of(context).textTheme.display1,
);
}
}
ตอนนี้แอปของเราก็จะ build เฉพาะตรงจุดที่เรากำหนดแล้ว ตาม provider ของเรา
ซึ่งลองเข้ามาดู คลาส Consumer ว่าภายในมันทำงานอย่างไร ก็จะพบว่าจริงๆแล้วมันก็คือๆไปเรียก Provider.of เพื่อให้ได้ provider มาเท่านั้น ไม่มีอะไรเลยจริงๆ
class Consumer<T> extends StatelessWidget {
Consumer({
Key key,
@required this.builder,
this.child,
}) : assert(builder != null),
super(key: key);
final Widget child;
final Widget Function(BuildContext context, T value, Widget child) builder;
@override
Widget build(BuildContext context) {
return builder(
context,
Provider.of<T>(context),
child,
);
}
}
Multi-Provider
เมื่อแอปของเรามี provider มากขึ้น เราสามารถเขียน provider นึงเป็น child ของอีกตัวนึง แล้วซ้อนๆกันไปเรื่อยๆก็ได้อยู่ แต่มันไม่สวย จึงเป็นที่มาของ MultiProvider ที่เราสามารถกำหนด provider เป็น list ได้เลย จากนั้นมันก็เอาไปวนลูปมาสร้างเป็น tree ให้
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter App',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MultiProvider(providers: [
Provider(builder: (context) => CounterProvider(counter: 0)),
Provider(builder: (context) => UserProvider()),
Provider(builder: (context) => BookProvider()),
], child: MyHomePage(title: 'Flutter App')));
}
Multi-Consumer
ส่วนที่ Consumer หาก widget ที่เราต้องการ build ร่วมกับ provider หลายๆตัว Flutter ก็ได้เตรียม Consumer ที่รองรับ provider มากทึ่สุดถึง 6 ตัว เช่น Consumer3 จะสามารถใช้ provider ได้ 3 ตัว วิธีการใช้ก็เหมือนกับ Consumer ปกติ
Consumer3<CounterProvider, BookProvider, UserProvider>(
builder: (context, counter, book, user, child) =>
buildText(counter, book, user))
ลองดูการทำงานภายในของคลาส Consumer3 ไม่ได้ต่างกับ Consumer เลย แค่เพิ่ม provider.of มาสำหรับตัวที่ 2 และ 3 เท่านั้น ดังนั้นจะมีกี่ provider เราก็ทำได้
class Consumer3<A, B, C> extends StatelessWidget {
Consumer3({
Key key,
@required this.builder,
this.child,
}) : assert(builder != null),
super(key: key);
final Widget Function(
BuildContext context, A value, B value2, C value3, Widget child) builder;
@override
Widget build(BuildContext context) {
return builder(
context,
Provider.of<A>(context),
Provider.of<B>(context),
Provider.of<C>(context),
child,
);
}
}
สรุป
พอจะเห็นภาพแล้วใช่มัยครับ ว่า Provider ช่วยจัดการกับปัญหา State ได้อย่างไร ส่วนตัวผมคิดว่า Provider เหมือนเป็น BLoC แบบปรับปรุงให้ง่ายขึ้นนั่นเอง เพราะคอนเซ้ปการใช้มันคล้ายกันมาก แค่ไม่ต้องไปยุ่งกับ Stream เขียนแค่กับที่เราสนใจก็พอ
หากยังไม่ได้อ่านเรื่อง BLoC Pattern อ่านได้ที่ลิงค์ด้านล่าง
หากยังไม่ได้อ่านเรื่อง Redux Pattern อ่านได้ที่ลิงค์ด้านล่าง