จัดการ State อย่างมีประสิทธิภาพด้วย Provider ใน Flutter
ในบทความที่แล้ว เราได้เรียนรู้เกี่ยวกับการสร้าง Layout ที่ซับซ้อนด้วย Custom Layouts ใน Flutter ซึ่งช่วยให้เราควบคุมการจัดเรียง Widget ได้อย่างละเอียดมากยิ่งขึ้น แต่เมื่อแอปพลิเคชันของเรามีขนาดใหญ่ขึ้น การจัดการ State (สถานะของข้อมูล) ก็กลายเป็นเรื่องที่ท้าทายมากยิ่งขึ้นเช่นกัน ในบทความนี้ เราจะมาเรียนรู้วิธีการจัดการ State อย่างมีประสิทธิภาพด้วย Provider ซึ่งเป็นหนึ่งใน State Management Solutions ที่ได้รับความนิยมอย่างมากใน Flutter
Provider ช่วยให้เราสามารถเข้าถึง State ได้จากทุกส่วนของ Widget Tree โดยไม่ต้องส่งผ่านข้อมูล (Prop Drilling) ให้ยุ่งยาก และยังช่วยลดความซับซ้อนในการจัดการ State ในแอปพลิเคชันขนาดใหญ่ได้อีกด้วย
ทำไมต้อง State Management?
ลองนึกภาพแอปพลิเคชันที่มีข้อมูลที่ต้องแสดงผลในหลายๆ หน้าจอ และข้อมูลนั้นมีการเปลี่ยนแปลงอยู่ตลอดเวลา หากเราไม่ใช้ State Management Solution ที่ดี เราจะต้องส่งผ่านข้อมูลจาก Widget หนึ่งไปยังอีก Widget หนึ่ง ซึ่งอาจทำให้โค้ดของเราซับซ้อนและยากต่อการบำรุงรักษา
State Management Solution ช่วยให้เราสามารถจัดการ State ได้อย่างเป็นระบบ ทำให้โค้ดของเราอ่านง่าย เข้าใจง่าย และบำรุงรักษาง่ายขึ้น
Provider คืออะไร?
Provider คือ Package ที่ช่วยให้เราสามารถจัดการ State ใน Flutter ได้อย่างง่ายดาย โดย Provider จะทำหน้าที่เป็น "ผู้ให้บริการ" ที่คอยจัดหา State ให้กับ Widget ที่ต้องการใช้งาน
Provider มีหลายประเภท แต่ในบทความนี้ เราจะเน้นที่ 3 ประเภทหลักๆ คือ:
- ChangeNotifierProvider: สำหรับจัดการ State ที่มีการเปลี่ยนแปลงและแจ้งเตือน Widget ที่ใช้งานอยู่
- StreamProvider: สำหรับจัดการ State ที่เป็น Stream (ข้อมูลที่เปลี่ยนแปลงตลอดเวลา)
- MultiProvider: สำหรับรวม Provider หลายๆ ตัวเข้าด้วยกัน
ChangeNotifierProvider
ChangeNotifierProvider เป็น Provider ที่ใช้งานง่ายและเหมาะสำหรับจัดการ State ที่มีการเปลี่ยนแปลงอยู่ตลอดเวลา เช่น ข้อมูลผู้ใช้, จำนวนสินค้าในตะกร้า, หรือสถานะการโหลดข้อมูล
เพื่อให้เข้าใจการทำงานของ ChangeNotifierProvider มากขึ้น เราจะมาสร้างตัวอย่างง่ายๆ โดยสร้าง Counter App ที่มีปุ่มสำหรับเพิ่มและลดจำนวน
1. สร้าง Class ที่ extends ChangeNotifier:
import 'package:flutter/material.dart';
class Counter extends ChangeNotifier {
int _count = 0;
int get count => _count;
void increment() {
_count++;
notifyListeners(); // แจ้งเตือน Widget ที่ใช้งานว่า State มีการเปลี่ยนแปลง
}
void decrement() {
_count--;
notifyListeners(); // แจ้งเตือน Widget ที่ใช้งานว่า State มีการเปลี่ยนแปลง
}
}
ใน Class Counter เรามี:
_count: ตัวแปรสำหรับเก็บค่า Countercount: Getter method สำหรับเข้าถึงค่า_countincrement()และdecrement(): Method สำหรับเพิ่มและลดค่า_countและเรียกnotifyListeners()เพื่อแจ้งเตือน Widget ที่ใช้งานว่า State มีการเปลี่ยนแปลง
2. ห่อ Widget ด้วย ChangeNotifierProvider:
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
void main() {
runApp(
ChangeNotifierProvider(
create: (context) => Counter(),
child: MyApp(),
),
);
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Counter App',
home: MyHomePage(),
);
}
}
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Counter App'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
Consumer<Counter>( // ใช้ Consumer เพื่อเข้าถึง State
builder: (context, counter, child) {
return Text(
'${counter.count}',
style: Theme.of(context).textTheme.headline4,
);
},
),
],
),
),
floatingActionButton: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
FloatingActionButton(
onPressed: () {
Provider.of<Counter>(context, listen: false).decrement(); // ใช้ Provider.of เพื่อเรียก method decrement
},
tooltip: 'Decrement',
child: Icon(Icons.remove),
),
SizedBox(width: 10),
FloatingActionButton(
onPressed: () {
Provider.of<Counter>(context, listen: false).increment(); // ใช้ Provider.of เพื่อเรียก method increment
},
tooltip: 'Increment',
child: Icon(Icons.add),
),
],
),
);
}
}
ใน Code นี้ เราใช้:
ChangeNotifierProviderในmain()เพื่อสร้าง Instance ของCounterและทำให้ Instance นี้พร้อมใช้งานสำหรับ Widget ทั้งหมดภายใต้MyAppConsumer<Counter>ในMyHomePageเพื่อเข้าถึงค่าcountและแสดงผลบนหน้าจอ เมื่อค่าcountเปลี่ยนแปลงConsumerจะ Rebuild Widget ที่อยู่ภายในโดยอัตโนมัติProvider.of<Counter>(context, listen: false).increment()และProvider.of<Counter>(context, listen: false).decrement()ในFloatingActionButtonเพื่อเรียก Methodincrement()และdecrement()ของCounter(listen: falseหมายถึงเราไม่ต้องการให้ Widget นี้ Rebuild เมื่อ State เปลี่ยนแปลง)
StreamProvider
StreamProvider เหมาะสำหรับจัดการ State ที่เป็น Stream เช่น ข้อมูลจาก Firebase Realtime Database หรือข้อมูลจาก Sensor ต่างๆ
สมมติว่าเรามี Stream ที่ปล่อยตัวเลขสุ่มออกมา เราสามารถใช้ StreamProvider เพื่อแสดงผลตัวเลขสุ่มบนหน้าจอได้ดังนี้:
import 'dart:async';
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
Stream<int> randomNumberStream() {
return Stream.periodic(Duration(seconds: 1), (count) {
return Random().nextInt(100); // สร้างเลขสุ่มระหว่าง 0-99
});
}
void main() {
runApp(
StreamProvider<int>(
create: (context) => randomNumberStream(),
initialData: 0, // ค่าเริ่มต้นของ Stream
child: MyApp(),
),
);
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Random Number App',
home: Scaffold(
appBar: AppBar(
title: Text('Random Number App'),
),
body: Center(
child: Consumer<int>(
builder: (context, randomNumber, child) {
return Text(
'Random Number: $randomNumber',
style: TextStyle(fontSize: 24),
);
},
),
),
),
);
}
}
ใน Code นี้ เราใช้:
randomNumberStream(): Function ที่สร้าง Stream ที่ปล่อยตัวเลขสุ่มออกมาทุกๆ 1 วินาทีStreamProvider<int>ในmain()เพื่อสร้าง Provider ที่คอยรับข้อมูลจากrandomNumberStream()และทำให้ข้อมูลนี้พร้อมใช้งานสำหรับ Widget ทั้งหมดภายใต้MyAppConsumer<int>ในMyAppเพื่อเข้าถึงตัวเลขสุ่มล่าสุดและแสดงผลบนหน้าจอ
MultiProvider
MultiProvider ช่วยให้เราสามารถรวม Provider หลายๆ ตัวเข้าด้วยกันได้ ซึ่งมีประโยชน์อย่างมากเมื่อแอปพลิเคชันของเรามี State ที่ต้องจัดการหลายส่วน
สมมติว่าเรามี Provider สำหรับจัดการข้อมูลผู้ใช้ (UserProvider) และ Provider สำหรับจัดการ Theme ของแอปพลิเคชัน (ThemeProvider) เราสามารถใช้ MultiProvider เพื่อรวม Provider ทั้งสองเข้าด้วยกันได้ดังนี้:
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class User {
final String name;
final String email;
User({required this.name, required this.email});
}
class UserProvider extends ChangeNotifier {
User _user = User(name: 'Guest', email: 'guest@example.com');
User get user => _user;
void updateUser(String name, String email) {
_user = User(name: name, email: email);
notifyListeners();
}
}
class ThemeProvider extends ChangeNotifier {
ThemeMode _themeMode = ThemeMode.light;
ThemeMode get themeMode => _themeMode;
void toggleTheme() {
_themeMode = _themeMode == ThemeMode.light ? ThemeMode.dark : ThemeMode.light;
notifyListeners();
}
}
void main() {
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(create: (context) => UserProvider()),
ChangeNotifierProvider(create: (context) => ThemeProvider()),
],
child: MyApp(),
),
);
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
final themeProvider = Provider.of<ThemeProvider>(context);
return MaterialApp(
title: 'MultiProvider Example',
theme: ThemeData.light(),
darkTheme: ThemeData.dark(),
themeMode: themeProvider.themeMode,
home: MyHomePage(),
);
}
}
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final userProvider = Provider.of<UserProvider>(context);
final themeProvider = Provider.of<ThemeProvider>(context);
return Scaffold(
appBar: AppBar(
title: Text('MultiProvider Example'),
actions: [
IconButton(
icon: Icon(themeProvider.themeMode == ThemeMode.light ? Icons.dark_mode : Icons.light_mode),
onPressed: () {
themeProvider.toggleTheme();
},
),
],
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('User Name: ${userProvider.user.name}'),
Text('User Email: ${userProvider.user.email}'),
SizedBox(height: 20),
ElevatedButton(
onPressed: () {
userProvider.updateUser('John Doe', 'john.doe@example.com');
},
child: Text('Update User'),
),
],
),
),
);
}
}
ใน Code นี้ เราใช้:
UserProvider: Provider สำหรับจัดการข้อมูลผู้ใช้ThemeProvider: Provider สำหรับจัดการ Theme ของแอปพลิเคชันMultiProvider: เพื่อรวมUserProviderและThemeProviderเข้าด้วยกัน
สรุป
Provider เป็นเครื่องมือที่มีประสิทธิภาพในการจัดการ State ใน Flutter ช่วยให้เราสามารถเข้าถึง State ได้จากทุกส่วนของ Widget Tree และลดความซับซ้อนในการจัดการ State ในแอปพลิเคชันขนาดใหญ่ ในบทความนี้ เราได้เรียนรู้เกี่ยวกับ ChangeNotifierProvider, StreamProvider, และ MultiProvider ซึ่งเป็น Provider ประเภทหลักๆ ที่เราสามารถนำไปประยุกต์ใช้ในการพัฒนาแอปพลิเคชัน Flutter ได้
ในบทความถัดไป เราจะมาเรียนรู้เกี่ยวกับการใช้งาน FutureBuilder และ StreamBuilder อย่างละเอียด ซึ่งเป็น Widget ที่ช่วยให้เราจัดการกับ Asynchronous Operations ได้อย่างง่ายดาย