web analytics

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 อ่านได้ที่ลิงค์ด้านล่าง