Flutter: Provider ตัวจัดหา จัดหาอะไรหรือ จัดหาหัวใจ?
Provider (n) = ผู้ให้บริการ หรือ ผู้จัดหา แต่ไม่ได้ให้บริการด้านความรักนะคับ จิงๆคือ เป็นตัวที่ให้บริการด้านการจัดการ state ในแอฟ Flutter
State management เป็นเรื่องที่ค่อนข้างซับซ้อนจึงต้อง practice เพื่อให้เข้าใจ จริงๆแล้วใน Flutter มีหลาย package ที่ให้เราใช้งานได้ไม่ว่าจะเป็น
ประโยชน์ที่เราจะต้องใช้ Provider คือ ปกติถ้าเราจะส่งข้อมูลข้าม page เรามักจะส่งผ่าน argument ใน navigator แต่ถ้าส่งข้อมูลผ่าน multi pages หรือ multi widgets ล่ะ ถ้าเรายังทำวิธีเดิมคือส่งผ่าน argument อาจจะไม่เหมาะสมนัก
วิธีที่ดีกว่าคือ การใช้ provider มาเก็บ
Let’s get started.
- สร้าง counter app จาก standard code in Flutter.
2. Insert provider เข้าไปใน widget tree และสร้าง
- Install provider in pubspec.yaml
- สร้าง counter class โดยนำ logic ที่ใช้งานแยกออกมาเก็บไว้ในที่นี่ แน่นอนว่า class นี้เมื่อมีการเปลียนแปลงจะต้อง notify Listener ด้วย จึงต้องเพิ่ม class ChangeNotifier เข้ามาด้วย (mixin other classes) โดย import foundation.dart
- สุดท้ายเมื่อตัวแปรมีการเปลี่ยนค่าแล้ว ให้ call method notifyListeners() เพื่อให้แจ้งเตื่อนกับ widget ที่คอยฟังอยู่ หรือ Consumer()
3. ให้กลับไปหน้า main.dart เลย เพื่อ add provider ไว้ที่ parent widget ของ project เลย
- เราจะ wrap widget หน้าแรกที่เราใช้งานด้วย ChangeNotifierProvider<Counter>() โดยให้ type เป็น Counter
- เพิ่ม create: (context)=> Counter()
ตอนนี้เราก้อจะได้ Provider เป็น parent ของ MyHomePage() แล้ว ตามรูปด้านบน
4. กลับมาที่หน้า MyHomePage() สร้างตัวแปร counter ขึ้นมาเพื่อรอรับค่าที่จะมาจาก notifyListeners() หรือคือ Provider ของ class Counter ที่เราได้สร้างไว้นั่นเอง และลบ code เดิมที่ไม่ใช้แล้วออก เพราะ Logic ตอนนี้เราได้แยกออกไปไว้ที่อื่นแล้วนั่นเอง (ใน counter.dart)
5. ฉะนั้นตอนนี้ใน homepage.dart เราก้อสามารถใช้เป็น Stateless แทน Stateful ได้เลย เพราะ State ถูกแยกออกไปข้างนอกแล้ว
6. ณ ตอนนี้ ด้วยคำสั่งนี้เวลาที่เราเรียกคำสั่ง Provider.of<Counter>(context);
final counter = Provider.of<Counter>(context);
# register widget as a listener
มีค่าเท่ากับว่าเราได้ registered widget ให้เป็น listener แล้วซึ่งเราจะได้รับ value/objects ที่เราขอซึ่งมันจะ rebuild ตัวของ widget ทั้งหมดเลยนั่นคือ widget build() ในทุกๆครั้งที่ counter value เปลี่ยน เท่ากับว่าไม่ต่างจากของเดิมเลย
ฉะนั้นเราจึงต้องเพิ่ม listen: false แบบนี้
final counter = Provider.of<Counter>(context,listen: false);
เมื่อเราทำแบบนี้ เรายังได้รับค่า counter value กลับมาอยู่นะ แต่เราไม่ได้ register widget as a listener สำหรับ counter ดังนั้นเมื่อกด button ก็จะไม่มีอะไรเปลี่ยนแปลง แม้ว่าจะได้รับค่า new counter กลับมาก็ตาม ( เพราะ build method ไม่ได้ถูกเรียก)
ฉะนั้นเราจึงต้องใช้ new widget Consumer() มาจัดการโดย Consumer()คือ widget ภายในของ provider package ซึ่งใช้สำหรับ register ตัวมันเองให้เป็น listener นั่นเอง
วิธีการใช้งานให้ wrap widget ที่เป็น dynamic ด้วย Consumer() โดยระบุ type ให้เป็น <Counter>แบบนี้
แต่จิงๆแล้ว เราต้องการใช้ค่า argument เฉพาะ value เท่านั้น ดังนั้นค่าที่ไม่ใช้ ไม่ว่าจะเป็น context หรือ child เราสามารถแทนที่ได้ด้วย underscore and dunder แบบนี้
Consumer<Counter>(
builder: (_, value, __) => Text(
'${counter.value}',
style: Theme.of(context).textTheme.headline4,
),
),
Recap:
จากตอนแรกที่เป็น stateful of counter ซึ่งหลักจากวิเคราะห์แล้ว เป็น code ที่ไม่มีประสิทธิภาพเท่าที่ควรเพราะมีการ rebuilt widgets ทั้งหมดเลยเราจึง
- แยก State ออกมาโดยนำส่วนที่มีการเปลี่ยนแปลงของ State มาสร้างเป็น class ของ ChangeNotifier() และใส่ logic เข้าไปในคลาสด้วย เมื่อมีการเปลี่ยนแปลงก็ให้เรียก notifyListeners() เพื่อแจ้งให้ส่วนที่คอย listen ได้รับรู้เพื่อ update UI.
- ประกาศ Provider ในหน้า main.dart โดยการ wrap parent widget ด้วย ChangeNotifierProvider()
home: ChangeNotifierProvider<Counter>(
create: (context) => Counter(),
child: MyHomePage(title: 'Flutter Demo Home Page'),
),
3. สร้าง instance ของ Provider ขึ้นมา จริงๆขั้นตอนนี้คือการ register widget ให้เป็น listener แต่มันจะ rebuilt ทั้งหมด ฉะนั้นเราจึงป้องกันด้วยกันเพื่อ argument listen: false และ ไปสร้าง Consumer ให้กับตัวที่ต้องการ register เท่านั้น
final counter = Provider.of<Counter>(context, listen: false);
4. Wrap widget ที่ต้องการ register ให้เป็น listener ด้วย Consumer() ซึ่งหลักกการคือ มันจะมีการ rebuilt ที่ widget นี้เท่านั้นเมื่อค่าหรือ state มีการเปลี่ยนแปลง
5. Clean code ได้เลย ไม่ว่าจะเป็น
- Redundant code ที่เราได้แยกออกไปสร้างเป็น class แล้ว ส่วนที่เหลือก็ไม่ได้ใช้
- เมื่อไม่ต้องเก็บ State แล้วก็สามารถทำให้เป็น Stateless ได้เลย
- Argument บางตัวที่ไม่ได้ใช้ สามารถแทนที่ได้ด้วย _ หรือ __
My code:
เพิ่มเติมนะคับ จริงๆแล้ว class ที่ชือว่า notifier จะมีอยู่ 2 แบบคือ
1. ChangeNotifier เป็นวิธีที่เราได้ใช้ไป โดยเป็นการสืบทอดมาจาก ChangeNotifier จะใช้ extends หรือ with ก็ได้ ข้อดีคือ สามารถกำหนดตัวแปรและ method ได้ตามที่เราต้องการเลย และเรียกใช้ notifyListeners() เมื่อต้องการ update ui
2. ValueNotifier จริงๆแล้วมันก้อ extends มาจาก ChangeNotifier เหมือนกันแต่ใช้จัดการกับอะไรง่ายๆ พวก single value และเมื่อเปลี่ยนแปลงค่า value ก็จะ update UI ให้อัตโนมัติโดยไม่ต้อง custom class ขึ้นมาใหม่เหมือนวิธีที่ 1 (วิธีนี้แค่ช่วยให้เราเขียน code น้อยลง)when [value] is replaces with something that is not equal to the old value as evaluated by the equality operator ==, this class notifies its listeners.
ซึ่งเมื่อเราใช้วิธีของ ValueNotifier จะต้องเปลี่ยน type ทั้งหมดให้เป็น ValueNotifier() ด้วยแบบนี้
เมื่อมีการเปลี่ยนแปลง ก็จะเปลี่ยนเฉพาะ Widget ที่มี value ไม่เหมือนเดิมเท่านั้น และ widget ไหนๆก็สามารถมารับค่าจาก State ตรงนี้ไปใช้ได้ด้วยการ register ให้เป็น listener.
PS ก่อนจบ
เมื่อเรามี provider มากกว่า 1 class เราสามารถประกาศในหน้า main ด้วย class MultiProvider() ได้แบบนี้
main() {
runApp(MultiProvider(
providers: [
ChangeNotifierProvider(create: (context)=>ApproveProvider()),
],
child: LoginPage()));
}