Flutter ຈັດການຂໍ້ມູນໃນ App ດ້ວຍ Provider

Khatthaphone
GDG Vientiane
Published in
7 min readMar 9, 2020
Original Photo by fabio on Unsplash

State Management ຫຼື ການຈັດການຂໍ້ມູນໃນ Application ເປັນສິ່ງໜຶ່ງທີ່ຢູ່ຄຽງຄູ່ກັບ ການຂຽນ Mobile App ແລະ Web App ມາແຕ່ໃດໆ ນັກພັດທະນາຫຼາຍຄົນພາກັນນິຍົມໃຊ້ຫຼາຍໆວິທີ ຫຼາກຫຼາຍ Library ມາຊ່ວຍຈັດການ ຖ້າໃຜທີ່ໄປທາງເວັບ ເຊັ່ນ React ກໍ່ຈະຕ້ອງເຄີຍໃຊ້ Redux ຫຼື MobX ຖ້າເປັນ Vue ກໍ່ບໍ່ພົ້ນ Vuex ແລະ ອີກຫຼາຍໆຕົວເລືອກ ຂຶ້ນກັບຄວາມມັກ ແລະ ຄວາມຖະນັດຂອງນັກພັດທະນາວ່າໃຊ້ໂຕໃດໃຫ້ຕອບໂຈດກັບການຈັດການຂໍ້ມູນໃນ App ທີ່ສຸດ

ໃນ Flutter ເອງກໍ່ເຊັ່ນກັນ ມີຫຼາກຫຼາຍວິທີ ຕັ້ງແຕ່ລະດັບພື້ນໆລົງເລິກແນ່ເຊັ່ນ Inherited Widget ຫຼື Package ທີ່ມີໃຫ້ໃຊ້ເຊັ່ນ ScopedModel, BloC, Redux (ໂຕດຽວກັບທີ່ໃນ React ແຕ່ຂຽນດ້ວຍ Dart) ແລະ Provider ໃນບົດຄວາມນີ້ເຮົາຈະມາສຶກສາການນຳໃຊ້ Provider ໃນການຈັດການກັບ State ຫຼື ຂໍ້ມູນໃນ Flutter ກັນ ສຳລັບຜູ້ຂຽນແລ້ວ Provider ຖືວ່າຕອບໂຈດຫຼາຍໆຢ່າງ ແລະ ພຽງພໍກັບໂປຣເຈັກທັງນ້ອຍ ແລະ ໃຫຍ່ເລີຍ

ແຕ່ກ່ອນຈະໄປສຶກສາວິທີໃຊ້ ເຮົາກັບມາເບິ່ງກ່ອນວ່າແອັບເຮົາຈຳເປັນຕ້ອງໄດ້ໃຊ້ເຄື່ອງມືຊ່ວຍຈັດການ State ຫຼືບໍ່? ຖ້າ App ທີ່ຂຽນມີ Widget ຫຼື ໜ້າພຽງແຕ່ 2–3 Widget ການແບ່ງປັນຂໍ້ມູນລະຫວ່າງ Widget ທຳມະດາກໍ່ຖືວ່າພໍແລ້ວ

ຕົວຢ່າງ Code ທີ່ສົ່ງຕໍ່ຂໍ້ມູນ Widget ຫາ Widget:

class Home extends StatelessWidget {
final postData = [];

@override
Widget build(BuildContext context) {
return Posts(postData);
}
}

class Posts extends StatefulWidget {
final List<dynamic> postData;

Posts(this.postData);

@override
_PostsState createState() => _PostsState();
}

class _PostsState extends State<Posts> {
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: widget.postData.length,
itemBuilder: (context, index) => InkWell(
onTap: () => Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
PostDetail(widget.postData[index]['title']))),
child: ListTile(
title: widget.postData[index].title,
),
));
}
}

class PostDetail extends StatelessWidget {
final dynamic post;

PostDetail(this.post);

@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
children: <Widget>[
Text(post['title']),
Text(post['subtitle']),
Image.network(post['image'])
],
),
),
);
}
}

ແຕ່ຖ້າ App ຫາກມີຫຼາຍ Widget ມີຫຼາຍໜ້າ ແຕ່ລະ Widget ຕ້ອງການເຂົ້າຫາຂໍ້ມູນອັນດຽວກັນເຊັ່ນ token ສຳລັບ Authentication/Authorization ຕ່າງໆນາໆ ແລະ ຂໍ້ມູນເຫຼົ່ານັ້ນ ເມື່ອມີການປ່ຽນແປງ Widget ກໍ່ຕ້ອງ update ໜ້າ UI ໄປຕາມຂໍ້ມູນທັນທີ ຖ້າແມ່ນກໍ່ຕອບໄດ້ບໍ່ຍາກວ່າ Provider ຈະຊ່ວຍຊີວິດເຮົາໄດ້

ໃນຂະນະທີ່ຕົວອື່ນໆຄື Bloc ຫຼື Redux ກໍ່ເຮັດໄດ້ຄືກັນ ແລະ ບາງສະຖານະການກໍ່ດີກວ່າຊ້ຳ ແຕ່ Provider ເປັນໂຕທີ່ໃຊ້ງ່າຍທີ່ສຸດແລ້ວໃນບັນດາ Library ເຫຼົ່ານີ້

ທຳອິດແມ່ນຕ້ອງເພີ່ມ Package ໃສ່ໃນ pubspec.yml ກ່ອນ ນະຕອນນີ້ແມ່ນ version 4.0.4 ເຂົ້າໄປເບິ່ງ version ຫຼ້າສຸດໄດ້ທີ່ https://pub.dev/packages/provider ໃນແຖບ Installing

dependencies:
flutter:
sdk: flutter

# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^0.1.3
provider: ^4.0.2
intl: ^0.16.1

ໃຫ້ເພີ່ມ package intl ມາເພາະຈະໄດ້ໃຊ້ format number ນຳ

ຈາກນັ້ນໃຫ້ສ້າງ ​Class ທີ່ຈະໃຊ້ເກັບຂໍ້ມູນເຮົາ ໃນນີ້ຈະຈຳລອງການເກັບຂໍ້ມູນຜູ້ໃຊ້ເຊິ່ງຈະມີຂໍ້ມູນ name, dateOfBirth, job, money ໂດຍສ້າງໄຟລ໌ຊື່ວ່າ user.dart ເກັບໃນ folder ຊື່ວ່າ model

class User {
String name = '';
DateTime dateOfBirth;
String job = '';
double money = 0;
User(this._name, this._dateOfBirth, this._job, this._money);
}

ຕາມຫຼັການ OOP (Object Oriented Programming) ແລ້ວ ບັນດາ Properties ຄວນຈະເປັນ private ຄືບໍ່ສາມາດເຂົ້າເຖິງ ແລະ ປ່ຽນແປງຂໍ້ມູນໄດ້ໂດຍກົງ ແຕ່ໃຫ້ທຳການອ່ານ ແລະ ແກ້ໄຂຜ່ານ getter ແລະ setter ແທນ ໃນພາສາ Dart ການຈະເຮັດໃຫ້ຕົວປ່ຽນເປັນ private ແມ່ນພຽງແຕ່ເພີ່ມ _ ໃສ່ໜ້າຊື່ຕົວປ່ຽນ ສະນັ້ນເຮົາຈະເລີ່ມເຮັດໃຫ້ຕົວປ່ຽນທັງໝົດເປັນ private ພ້ອມກັບປະກາດ getter ໃຫ້ທຸກຕົວປ່ຽນ

class User {
String _name = '';
DateTime _dateOfBirth;
String _job = '';
double _money = 0;
User(this._name, this._dateOfBirth, this._job, this._money); String get name => _name;
DateTime get dateOfBirth => _dateOfBirth;
String get job => _job;
double get money => _money;
}

ຈະເຫັນໄດ້ວ່າ Code ພາສາ Dart ຈະຂຽນໄດ້ສັ້ນ ຖ້າທຽບກັບການຂຽນ class ແບບດຽວກັນນີ້ໃນພາສາ Java ຈະຕ້ອງຂຽນປະມານນີ້

class User {
private String name = '';
private DateTime dateOfBirth;
private String job = '';
private double money = 0;
public User(String name, DateTime dateOfBirth, String job, double money) {
this.name = name;
this.dateOfBirth = dateOfBirth;
this.job = job;
this.money = money;
}
public String getName() { return name; }
public DateTime getDateOfBirth() { return dateOfBirth; }
public String getJob() { return job; }
public double getMoney() { return money; }
}

ນະຕອນນີ້ Class ຂອງເຮົາສາມາດເກັບຂໍ້ມູນ ແລະ ສະໜອງຂໍ້ມູນຜ່ານ getter ໄດ້ແລ້ວ ແຕ່ຖ້າຕ້ອງການ update ຂໍ້ມູນເດ? ແລະ ທຸກເທື່ອທີ່ update ເຮົາຈະໃຫ້ Widget ທີ່ດຶງຂໍ້ມູນຮູ້ໄດ້ແນວໃດວ່າຂໍ້ມູນມີການປ່ຽນແປງ ແລະ ຕ້ອງປ່ຽນ UI ໃໝ່ຕາມຂໍ້ມູນ ສິ່ງຕໍ່ໄປທີ່ຕ້ອງເຮັດຄືໃຫ້ class ຂອງເຮົາໃຊ້ mixin ChangeNotifier ດ້ວຍການເພີ່ມ with ChangeNotifier ຕໍ່ທ້າຍຊື່ class ຈາກນັ້ນເຮົາກໍ່ຈະທຳການສ້າງ setter ໃຫ້ແຕ່ລະຕົວປ່ຽນໄປນຳ

import 'package:flutter/material.dart';

class User with ChangeNotifier {
String _name = '';
DateTime _dateOfBirth;
String _job = '';
double _money = 0;

User(this._name, this._dateOfBirth, this._job, this._money);
String get name => _name;
DateTime get dateOfBirth => _dateOfBirth;
String get job => _job;
double get money => _money;

set name(String name) {
_name = name;
notifyListeners();
}
set dateOfBirth(DateTime dateOfBirth) {
_dateOfBirth = dateOfBirth;
notifyListeners();
}
set job(String job) {
_job = job;
notifyListeners();
}
set money(double money) {
_money = money;
notifyListeners();
}
}

ສັງເກດວ່າທຸກໆ setter ຈະເອີ້ນ function ທີ່ຊື່ notifyListeners(); ເຊິ່ງເຮັດໜ້າທີ່ແຈ້ງໃຫ້ກັບ Widget ທີ່ດຶງຂໍ້ມູນຈາກ class ນີ້ໃຫ້ຮູ້ເມື່ອມີການອັບເດດຂໍ້ມູນ

ຫຼັງຈາກໄດ້ class ສຳລັບ Provider ແລ້ວ ຕໍ່ໄປເຮົາກໍ່ຕ້ອງ attach ໂຕ class ເຂົ້າໄປໃນ Widget Tree ຂອງເຮົາ ບ່ອນທີ່ແນະນຳກໍ່ຄືຢູ່ບ່ອນທີ່ເຮັດວຽກຕົ້ນຕໍ່ທຳອິດທຳອໍຂອງ App ເລີຍຄື MaterialApp ທີ່ main.dart ໂດຍໃຫ້ຄອບດ້ວຍ Widget ທີ່ຊື່ວ່າ ChangeNotifierProvider

class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (context) => User('Tony Stark', DateTime.parse('1970-05-29'),
'Billionaire', double.infinity),
child: MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyWidget(),
),
);
}
}

ປົກກະຕິເຮົາຈະໃຫ້ Widget ໃດໜຶ່ງ listen ຂໍ້ມູນຈາກ Class ຕົວຢ່າງໃນນີ້ແມ່ນ Class ທີ່ເປັນ ChangeNotifier ເຮົາກໍ່ຈະໃຊ້ Widget ChangeNotifierProvider ຄອບ Widget ຫຼັກໄວ້ ແຕ່ຖ້າສົມມຸດວ່າເຮົາມີຫຼາຍ Provider class ທີ່ເອົາໄວ້ເກັບຂໍ້ມູນອື່ນຕື່ມ ເຮົາສາມາດຄອບ ChangeNotifierProvider ຊ້ອນອີກໄດ້ເລື້ອຍ ແຕ່ກໍ່ບໍ່ແນະນຳ ເຮົາຈະໃຊ້ Widget ໂຕໜຶ່ງແທນ ເຊິ່ງຈະຊ່ວຍໃຫ້ເຮົາປະກາດ Provider ຫຼາຍຕົວພ້ອມກັນໄດ້ດ້ວຍ MultiProvider

class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider(
create: (context) => User('Tony Stark',
DateTime.parse('1970-05-29'), 'Billionaire', double.infinity),
),
// ChangeNotifierProvider(...),
// StreamProvider(...),
],
child: MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyWidget(),
),
);
}
}

ກໍລະນີນີ້ ຖ້າຕ້ອງການເພີ່ມ Provider ອື່ນໆກໍ່ພຽງແຕ່ເພີ່ມເຂົ້າໄປໃນ parameter providers ຂອງ MultiProvider ທຸກໆ Widget ທີ່ຢູ່ໃນ child ກໍ່ຈະສາມາດເຂົ້າເຖິງຂໍ້ມູນໄດ້ ບໍ່ວ່າຈະຢູ່ເລິກຊ້ອນ Widget ກັນຫຼາຍເທົ່າໃດກໍ່ຕາມ ແລ້ວຈະເຂົ້າແນວໃດລະ? ເຮົາມາເບິ່ງນຳກັນເລີຍ

class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
final user = Provider.of<User>(context);

final inputBorder = OutlineInputBorder(
borderSide: BorderSide(color: Theme.of(context).primaryColor));
final f = NumberFormat('#,###', 'en_US');

final dobController = TextEditingController(
text: user.dateOfBirth.toIso8601String().substring(0, 10));

return Scaffold(
appBar: AppBar(
title: Text('Flutter Demo Home Page'),
),
body: Center(
child: Container(
padding: EdgeInsets.all(16),
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
TextFormField(
initialValue: user.name,
decoration: InputDecoration(
labelText: 'Name',
enabledBorder: inputBorder,
focusedBorder: inputBorder,
),
onChanged: (text) {
user.name = text;
},
),
SizedBox(height: 10),
TextFormField(
initialValue: user.job,
decoration: InputDecoration(
labelText: 'Job',
enabledBorder: inputBorder,
focusedBorder: inputBorder,
),
onChanged: (text) {
user.job = text;
},
),
SizedBox(height: 10),
TextFormField(
decoration: InputDecoration(
labelText: 'Date of Birth',
enabledBorder: inputBorder,
focusedBorder: inputBorder,
),
controller: dobController,
onTap: () async {
final date = await showDatePicker(
context: context,
initialDate: user.dateOfBirth,
firstDate: DateTime.parse('1970-01-01'),
lastDate: DateTime.now());
if (date != null) {
user.dateOfBirth = date;
dobController.text =
date.toIso8601String().substring(0, 10);
}
},
),
SizedBox(height: 10),
TextFormField(
initialValue: '${user.money}',
decoration: InputDecoration(
labelText: 'Money',
enabledBorder: inputBorder,
focusedBorder: inputBorder,
),
inputFormatters: [WhitelistingTextInputFormatter.digitsOnly],
onChanged: (text) {
if (text.length > 0) {
user.money = int.parse(text);
} else {
user.money = 0;
}
},
),
SizedBox(height: 50),
Card(
child: Padding(
padding: EdgeInsets.all(16),
child: Column(
children: <Widget>[
Text('Name: ${user.name}'),
Text(
'Name: ${user.dateOfBirth.toIso8601String().substring(0, 10)}'),
Text('Job: ${user.job}'),
Text('Money: \$${f.format(user.money)}'),
],
)),
elevation: 5,
)
],
),
),
),
);
}
}

ຕາມ code ຂ້າງເທິງເຮົາກໍ່ຈະໄດ້ໜ້າ App ງ່າຍໆອອກມາເປັນແແບນີ້:

ຈະເຫັນວ່າເຮົາດຶງຂໍ້ມູນຈາກ User ດ້ວຍຄຳສັ່ງ Provider.of<User>(context) ມາໃສ່ໃນ Initial Value ຂອງແຕ່ລະ TextField ເມື່ອເຮົາພິມ ໂຕ TextFormField ກໍ່ຈະເອີ້ນ onChanged ພ້ອມກັບໃຫ້ text ຕົວໃໝ່ມາເຮົາກໍ່ພຽງແຕ່ເອົາ text ໄປ set ໃຫ້ກັບຕົວປ່ຽນໃນ User ເທົ່ານັ້ນ ສິ່ງທີ່ຕ່າງຈາກໝູ່ແນ່ກໍ່ແມ່ນໂຕ money ທີ່ເກັບຄ່າເປັນ int ຈຶ່ງຕ້ອງກຳນົດຮັບຄ່າສະເພາະຕົວເລກດ້ວຍ inputFormatters: [WhitelistingTextInputFormatter.digitsOnly] ແລະ ປ່ຽນເປັນ text ເປັນ integer ດ້ວຍ function int.parse

ຈາກນັ້ນເຮົາກໍ່ນຳມາສະແດງຢູ່ໃນ Widget Card ທາງລຸ່ມ ເມືອເຮົາພິມຫຍັງລົງໄປໃນ TextFormField ຂໍ້ມູນໃນ Provider ກໍ່ຈະ update ແລະ ຂໍ້ຄວາມໃນ Text ກໍ່ຈະປ່ຽນໄປນຳທັນທີ

ໃນນີ້ຕົວໜຶ່ງທີ່ບໍ່ຄື TextFieldForm ອື່ນເລີຍກໍ່ແມ່ນຕົວ DateOfBirth ເພາະເຮົາຈະບໍ່ໄດ້ໃຫ້ຜູ້ໃຊ້ຂຽນວັນທີໃສ່ໂດຍກົງ ແຕ່ຈະໃຊ້ DatePicker ດ້ວຍ showDatePicker ໃຫ້ສາມາດເລືອກວັນທີໄດ້ແບບປະຕິທິນ ແລ້ວຈາກນັ້ນຈຶ່ງ update ຂໍ້ມູນໃນ User Provider ແລະ ຕ້ອງ update ຂໍ້ມູນກັບຄືນເຂົ້າໄປໃນ TextFormField ນຳອີກ ຈຶ່ງໃຊ້ຕົວ TextEdittingController ເຂົ້າມາຊ່ວຍໃຫ້ແກ້ຂໍ້ມູນໃນ TextFormField ໄດ້ ເຊິ່ງ Field ນີ້ຈະບໍ່ມີ initialValue ແຕ່ຈະປະກາດໃນຕອນສ້າງ TextEdittingController(text: “…”) ແທນ

ຖືວ່າແອັບເຮົາສາມາດແບ່ງປັນຂໍ້ມູນ ຫຼື State ຮ່ວມກັນໄດ້ແລ້ວເນາະ ແຕ່ວ່າຕອນນີ້ເຮົາມີພຽງແຕ່ 2–3 Widget ເທົ່ານັ້ນ ຖ້າຈະໃຫ້ເຫັນພາບວ່າເຮົາສາມາດ Provider ໄດ້ຈາກ Widget ໃດກໍ່ໄດ້ໃນ Widget Tree ຕ້ອງເພີ່ມໃຫ້ມັນຫຼາຍກວ່ານີ້ອີກ ເຮົາຈະສ້າງ Widget ທີ່ຊ້ອນກັນຫຼາຍອັນ ແລ້ວດຶງເອົາຂໍ້ມູນຈາກ User ດ້ວຍຄຳສັ່ງ Provider.of<User>(context) ຄືກັນກັບຂ້າງເທິງ

class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
final user = Provider.of<User>(context);

final inputBorder = OutlineInputBorder(
borderSide: BorderSide(color: Theme
.of(context)
.primaryColor));

final dobController = TextEditingController(
text: user.dateOfBirth.toIso8601String().substring(0, 10));

return Scaffold(
appBar: AppBar(
title: Text('Flutter Demo Home Page'),
),
body: Center(
child: Container(
padding: EdgeInsets.all(16),
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
TextFormField(
initialValue: user.name,
decoration: InputDecoration(
labelText: 'Name',
enabledBorder: inputBorder,
focusedBorder: inputBorder,
),
onChanged: (text) {
user.name = text;
},
),
SizedBox(height: 10),
TextFormField(
initialValue: user.job,
decoration: InputDecoration(
labelText: 'Job',
enabledBorder: inputBorder,
focusedBorder: inputBorder,
),
onChanged: (text) {
user.job = text;
},
),
SizedBox(height: 10),
TextFormField(
decoration: InputDecoration(
labelText: 'Date of Birth',
enabledBorder: inputBorder,
focusedBorder: inputBorder,
),
controller: dobController,
onTap: () async {
final date = await showDatePicker(
context: context,
initialDate: user.dateOfBirth,
firstDate: DateTime.parse('1970-01-01'),
lastDate: DateTime.now());
if (date != null) {
user.dateOfBirth = date;
dobController.text =
date.toIso8601String().substring(0, 10);
}
},
),
SizedBox(height: 10),
TextFormField(
initialValue: '${user.money}',
decoration: InputDecoration(
labelText: 'Money',
enabledBorder: inputBorder,
focusedBorder: inputBorder,
),
inputFormatters: [WhitelistingTextInputFormatter.digitsOnly],
onChanged: (text) {
if (text.length > 0) {
user.money = int.parse(text);
} else {
user.money = 0;
}
},
),
SizedBox(height: 50),
Preview()
],
),
),
),
);
}
}

class Preview extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Card(color: Colors.red, elevation: 10,
child: Padding(padding: EdgeInsets.all(16), child: PreviewChild1()));
}
}

class PreviewChild1 extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Card(color: Colors.blue, elevation: 10,
child: Padding(padding: EdgeInsets.all(16), child: PreviewChild2()));
}
}

class PreviewChild2 extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Card(color: Colors.green, elevation: 10,
child: Padding(padding: EdgeInsets.all(16), child: PreviewChild3()));
}
}

class PreviewChild3 extends StatelessWidget {
@override
Widget build(BuildContext context) {
final user = Provider.of<User>(context);
final f = NumberFormat('#,###', 'en_US');

return Card(
elevation: 5,
child: Padding(
padding: EdgeInsets.all(16),
child: Column(
children: <Widget>[
Text('Name: ${user.name}'),
Text(
'Name: ${user.dateOfBirth.toIso8601String().substring(
0, 10)}'),
Text('Job: ${user.job}'),
Text('Money: \$${f.format(user.money)}'),
],
)),
);
}
}

ແລະ ເຮົາກໍ່ຍັງສາມາດເຂົ້າເຖິງຂໍ້ມູນ User ໄດ້ບໍ່ວ່າຈະຫ່າງຈາກ Widget ຫຼັກທີ່ເກັບ TextFormField ເທົ່າໃດກໍ່ຕາມ

ສາມາດເບິ່ງ code ທັງໝົດໄດ້ທີ່ gist.github.com

--

--