Flutter: Provider ตัวจัดหา จัดหาอะไรหรือ จัดหาหัวใจ?

Grassroot Engineer
4 min readFeb 28, 2021

--

Provider (n) = ผู้ให้บริการ หรือ ผู้จัดหา แต่ไม่ได้ให้บริการด้านความรักนะคับ จิงๆคือ เป็นตัวที่ให้บริการด้านการจัดการ state ในแอฟ Flutter

State management เป็นเรื่องที่ค่อนข้างซับซ้อนจึงต้อง practice เพื่อให้เข้าใจ จริงๆแล้วใน Flutter มีหลาย package ที่ให้เราใช้งานได้ไม่ว่าจะเป็น

https://flutter.dev/docs/development/data-and-backend/state-mgmt/options
https://pub.dev/packages/provider

ประโยชน์ที่เราจะต้องใช้ Provider คือ ปกติถ้าเราจะส่งข้อมูลข้าม page เรามักจะส่งผ่าน argument ใน navigator แต่ถ้าส่งข้อมูลผ่าน multi pages หรือ multi widgets ล่ะ ถ้าเรายังทำวิธีเดิมคือส่งผ่าน argument อาจจะไม่เหมาะสมนัก

วิธีที่ดีกว่าคือ การใช้ provider มาเก็บ

Let’s get started.

  1. สร้าง counter app จาก standard code in Flutter.
counter tree stateful
จะมีการ build ทั้งหน้า app ใหม่ทั้งหมด เมื่อยังไม่มีการใช้ state management.

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()
แยก logic ในการนับ ออกมาเป็น class ของ ChangeNotifier.

3. ให้กลับไปหน้า main.dart เลย เพื่อ add provider ไว้ที่ parent widget ของ project เลย

  • เราจะ wrap widget หน้าแรกที่เราใช้งานด้วย ChangeNotifierProvider<Counter>() โดยให้ type เป็น Counter
  • เพิ่ม create: (context)=> Counter()
เพิ่ม ChangeNotifierProvider() ระบุ type <Counter> ด้วย
Counter tree provider

ตอนนี้เราก้อจะได้ Provider เป็น parent ของ MyHomePage() แล้ว ตามรูปด้านบน

4. กลับมาที่หน้า MyHomePage() สร้างตัวแปร counter ขึ้นมาเพื่อรอรับค่าที่จะมาจาก notifyListeners() หรือคือ Provider ของ class Counter ที่เราได้สร้างไว้นั่นเอง และลบ code เดิมที่ไม่ใช้แล้วออก เพราะ Logic ตอนนี้เราได้แยกออกไปไว้ที่อื่นแล้วนั่นเอง (ใน counter.dart)

final counter = Provider.of<Counter>(context);

5. ฉะนั้นตอนนี้ใน homepage.dart เราก้อสามารถใช้เป็น Stateless แทน Stateful ได้เลย เพราะ State ถูกแยกออกไปข้างนอกแล้ว

Change from Stateful to Stateless.

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 เปลี่ยน เท่ากับว่าไม่ต่างจากของเดิมเลย

ทั้งหมดของ scaffold เลยที่เป็น listener ซึ่งไม่ต่างจากของเดิมและไม่ใช่วิธีที่ดี (all descendant are rebuilt).

ฉะนั้นเราจึงต้องเพิ่ม 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>แบบนี้

wrap ด้วย Consumer()

แต่จิงๆแล้ว เราต้องการใช้ค่า argument เฉพาะ value เท่านั้น ดังนั้นค่าที่ไม่ใช้ ไม่ว่าจะเป็น context หรือ child เราสามารถแทนที่ได้ด้วย underscore and dunder แบบนี้

Consumer<Counter>(
builder: (_, value, __) => Text(
'${counter.value}',
style: Theme.of(context).textTheme.headline4,
),
),
generic tree of provider.

Recap:

จากตอนแรกที่เป็น stateful of counter ซึ่งหลักจากวิเคราะห์แล้ว เป็น code ที่ไม่มีประสิทธิภาพเท่าที่ควรเพราะมีการ rebuilt widgets ทั้งหมดเลยเราจึง

  1. แยก State ออกมาโดยนำส่วนที่มีการเปลี่ยนแปลงของ State มาสร้างเป็น class ของ ChangeNotifier() และใส่ logic เข้าไปในคลาสด้วย เมื่อมีการเปลี่ยนแปลงก็ให้เรียก notifyListeners() เพื่อแจ้งให้ส่วนที่คอย listen ได้รับรู้เพื่อ update UI.
  2. ประกาศ 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:

main.dart
homepage.dart
counter.dart

เพิ่มเติมนะคับ จริงๆแล้ว 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.
counter tree provider (valueNotifier)

ซึ่งเมื่อเราใช้วิธีของ ValueNotifier จะต้องเปลี่ยน type ทั้งหมดให้เป็น ValueNotifier() ด้วยแบบนี้

แปลงจาก ChangeNotifier เป็น ValueNotifier.

เมื่อมีการเปลี่ยนแปลง ก็จะเปลี่ยนเฉพาะ Widget ที่มี value ไม่เหมือนเดิมเท่านั้น และ widget ไหนๆก็สามารถมารับค่าจาก State ตรงนี้ไปใช้ได้ด้วยการ register ให้เป็น listener.

When pressing count up and build widget work only 1 times.

PS ก่อนจบ

เมื่อเรามี provider มากกว่า 1 class เราสามารถประกาศในหน้า main ด้วย class MultiProvider() ได้แบบนี้

main() {
runApp(MultiProvider(
providers: [
ChangeNotifierProvider(create: (context)=>ApproveProvider()),
],

child: LoginPage()));
}

My repo

--

--

Grassroot Engineer
Grassroot Engineer

Written by Grassroot Engineer

ATM engineer who is interested in CODING and believe in EFFORT. — https://grassrootengineer.com

No responses yet