Flutter: การใช้ Listenables และ ValueNotifier สำหรับควบคุม State ของแอป

ตัว Flutter framework มีวิธีในการช่วยให้ผู้พัฒนาแอป สามารถควบคุม State ของแอปได้ ทั้งจากการสร้าง StatefulWidget โดยตรง การใช้ InheritedWidget แต่ในกรณีที่ผู้พัฒนาต้องการสร้าง class เพื่อเอาไว้จัดการข้อมูลที่จะใช้งานในแอป โดยไม่ต้องเอาไปใส่ใน widget tree โดยตรง สามารถใช้งาน Listenable class เพื่อเก็บค่า State ที่จะใช้ในแอป และแจ้งเตือนตัว widget ที่นำเอาข้อมูลไปใช้งานให้ปรับปรุง State เมื่อมีการเปลี่ยนแปลงได้ โดยตัว widget ที่จะเอาค่าของ Listenable ไปใช้งานจะต้องครอบด้วย ListenableBuilder

ในกรณีที่ข้อมูลที่จะใช้งานมีแค่เพียงค่าเดียว ไม่มีคำสั่งพิเศษใด ๆ แจ้งให้ปรับปรุง state เมื่อมีการเปลี่ยนแปลง value โดยตรง ก็สามารถใช้ ValueNotifier เพื่อใช้งานได้เหมือนกัน ผู้พัฒนาสามารถเลือกใช้งานได้ตามความเหมาะสม

ข้อมูลใน note ตัวนี้มาจาก Learn Flutter → State management → Using listenables

การสร้าง Listenables class

ให้นำข้อมูลที่จะใช้สำหรับ widgets ในแอปเอาไปไว้ใน class ที่ extends ChangeNotifier และทำการเรียกคำสั่ง notifyListeners() เพื่อแจ้งตัว widgets ที่นำเอาค่าไปใช้งานให้ปรับปรุง state

ตัวอย่าง สร้างตัวนับเลข เพิ่มขึ้นทีละ 1 ชื่อ CounterNotifier เมื่อต้องการเพิ่มจำนวน ให้เรียกคำสั่ง increment() ในตัวคำสั่งจะเรียก notifyListeners()

class CounterNotifier extends ChangeNotifier {
  int _count = 0;
  int get count => _count;

  void increment() {
    _count++;
    notifyListeners();
  }
}

การใช้ ListenableBuilder เพื่อรับการแจ้งเตือนจาก ChangeNotifier

เอา ListenableBuilder ไปครอบ widget ที่ต้องการให้สามารถรับการแจ้งเตือนจาก ChangeNotifier ได้ โดยมีรูปแบบดังนี้

const ListenableBuilder({
  Key? key,
  required Listenable listenable,
  required TransitionBuilder builder,
  Widget? child,
})

โดยตัว builder จะเป็น TransitionBuilder callback ที่ใช้สร้าง widget ที่ต้องการ

ตัวอย่าง เอา CounterNotifier มาใช้งาน โดยเมื่อปุ่ม +1 ถูกกด ก็จะไปเรียก increment()

import 'package:flutter/material.dart';

class CounterNotifier extends ChangeNotifier {
  int _count = 0;
  int get count => _count;

  void increment() {
    _count++;
    notifyListeners();
  }
}

void main() {
  var counterNotifier = CounterNotifier();

  var widget = Center(
    child: Column(
      mainAxisAlignment: MainAxisAlignment.spaceAround,
      children: [
        ListenableBuilder(
          listenable: counterNotifier,
          builder: (BuildContext context, Widget? child) => Text(
            'Counter: ${counterNotifier.count}',
            textScaler: TextScaler.linear(3),
          ),
        ),
        OutlinedButton(
            onPressed: () => counterNotifier.increment(),
            child: Icon(Icons.plus_one))
      ],
    ),
  );

  runApp(MaterialApp(
    home: Scaffold(
      body: widget,
    ),
  ));
}

เมื่อกดปุ่ม ตัวเลขจะเพิ่มขึ้น และปรับปรุงหน้าจอ

หากมี subtree ที่ไม่ต้อง update

ในกรณีที่ตัว widget ที่จะปรับปรุง state อาจอยู่บน subtree หรือมี widgets บางส่วนที่ไม่ได้มีการปรับปรุง หากต้องการเพิ่มประสิทธิภาพในการทำงานของแอป ลดการคำนวณสร้าง widget ที่ไม่จำเป็นทุกครั้ง สามารถกำหนดค่า child สำหรับ builder เพื่อจะได้ไม่ต้องสร้าง widget ใหม่ทุกครั้งได้

ListenableBuilder(
  listenable: counterNotifier,
  builder: (BuildContext context, Widget? child) {
	return Column(children: [
	  Text(
		'Counter: ${counterNotifier.count}',
		textScaler: TextScaler.linear(3),
	  ),
	  child! // pre build widget from [child:]
	]);
  },
  child: Text('This is static Text'), // add pre build widget here
),

การใช้ child เพื่อลดการสร้าง subtree ที่ไม่จำเป็น

การใช้ ValueNotifier

ในกรณีที่ข้อมูลที่ต้องการใช้งานเป็นข้อมูลแค่ชิ้นเดียว และต้องการแค่ปรับปรุง state เมื่อค่าเปลี่ยนแปลงโดยไม่มีคำสั่งพิเศษอื่น ๆ สามารถเลือกใช้ ValueNotifier หลักการทำงานจะเหมือน ChangeNotifier แต่เรียบง่ายกว่า

ตัวอย่าง เปลี่ยนจาก ChangeNotifier เป็น ValueNotifier เพื่อเก็บค่าตัวเลขที่ได้จากการกดปุ่ม โดยทุกครั้งที่กดปุ่มจะไปเพิ่ม .value ของตัว ValueNotifier โดยตรงแทน ส่วนตัวสร้าง widget จะใช้ ValueListenableBuilder แทน

import 'package:flutter/material.dart';

void main() {
  ValueNotifier<int> counterNotifier = ValueNotifier(0);

  var widget = Center(
    child: Column(
      mainAxisAlignment: MainAxisAlignment.spaceAround,
      children: [
        ValueListenableBuilder(
          valueListenable: counterNotifier,
          builder: (BuildContext context, int value, Widget? child) {
            return Column(children: [
              Text(
                'Counter: $value',
                textScaler: TextScaler.linear(3),
              ),
              child!
            ]);
          },
          child: Text('This is static Text'),
        ),
        OutlinedButton(
            onPressed: () => counterNotifier.value++,
            child: Icon(Icons.plus_one))
      ],
    ),
  );

  runApp(MaterialApp(
    home: Scaffold(
      body: widget,
    ),
  ));
}

ข้อดีข้อเสีย

ข้อดีของการใช้ Listenables และ ValueNotifier คือ มันไม่ต้องไปผูกกับ widget tree โดยตรง ทำให้สะดวกในการที่จะเอาไปใส่ตรงจุดที่ต้องการจะปรับปรุง state ได้โดยตรง

ส่วน ข้อเสีย เนื่องจากมันจะไปอยู่ตรงไหนของ widget tree และอยู่ได้ในทุกส่วนของแอป ทำให้ต้องวางแผนการใช้งานให้ดี ถ้าแอปใหญ่มากอาจมีปัญหาในการไล่จุดที่จะไปแก้ไขลำบาก อาจมีผลต่อการบำรุงรักษาแอปในภายหลัง