I. GIỚI THIỆU
  • Nếu đã quen với cách quản lý state trong Reactive App có thể bỏ qua phần này. Có một số khác biết, hãy xem tại đây list of different approaches.

s

  • Khi phát triển app, chắc chắn bạn sẽ gặp trường hợp cần phải chia sẻ trạng thái ưng dụng giữa các màn hình. Bài viết này hướng dẫn cơ bản xử lý State trong Flutter.
I. START THINK DECLARATIVELY
  • Nếu bạn mới chuyển qua code Flutter, trước đó bạn đa code app Android, iOS native, thì bạn cần thay đổi một chút hướng phát triển app, phát triển app từ flutter có một số điểm khác biệt.

  • Có rất nhiều tình huống mà bạn không thể áp dụng vào Flutter. Flutter sẽ rebuild lại từ đầu thay vì chỉnh sửa chúng. Flutter đủ nhanh để làm điều này.

  • Flutter là declartive. Nghĩa là nó sẽ build để phản chiếu trạng thái hiện tại của app.

s

  • Khi State app của bạn có những thay đổi, bạn thay đổi State và kích hoạt để vẽ lại UI
III. EPHEMERAL STATE VÀ APP STATE
  • State : là những thứ tồn tại trong memory khi app đang chạy. Bao gồm :

    • Các tài nguyên ứng dụng

    • Tất cả các biến dùng để duy trì thông tin về UI

    • Trạng thái của Animaion

    • Fonts…

  • Đầu tiên, có một thứ bạn không cần quản lý State như là liketure

  • State được chia làm 2 loại : Ephemeral State và AppState.

  • Ephemeral State :

    • Giống như biến local, State này chỉ dùng bên trong một SingleWidget

    • Ví dụ :

      • Một Page đang hiển thị nằm bên trong PageView

      • Trạng thái hiện tại của Animation

      • Vị trí Tab được chọn trong BottomNavigationBar

    • Đặc điểm :

      • Chỉ dùng nội bộ bên trong Widget, các Widget bên ngoài không cần nó.

      • Không cần dùng kĩ thuật quản lý State (VD : ScopeModel, Redux…). Tất cả thứ bạn cần là StatefulWidget

    • Demo :

      • filed private _index dùng để lưu vị trí tab được chọn của BottomNavigationBar

      • Bên ngoài widget không thể truy cập đến field này.

        class MyHomepage extends StatefulWidget {
          const MyHomepage({super.key});
              
          @override
          State<MyHomepage> createState() => _MyHomepageState();
        }
              
        class _MyHomepageState extends State<MyHomepage> {
          int _index = 0;
              
          @override
          Widget build(BuildContext context) {
            return BottomNavigationBar(
              currentIndex: _index,
              onTap: (newIndex) {
                setState(() {
                  _index = newIndex;
                });
              },
              // ... items ...
            );
          }
        }
        
  • App State :

    • State mà bạn muốn chia sẻ bất cứ đâu trong app (nó như field Static vậy)

    • Một số trường hợp :

      • Thông tin đăng nhập

      • Trạng thái đã đọc của một bài báo

      • Giỏ hàng trong app mua sắm

  • There Is No Clear-Cut Rule (Không Có Quy Tắc Rõ Ràng)

    • Bạn có thể dùng State và setState để quản lý tất cả trạng thái của app. Các sample team Flutter cũng đang làm như vậy

    • Đôi lúc bạn cần quyết định state đó nên là Ephemeral State hay là App State, ví dụ :

      • _index filed của BottomNavigationBar cần được truy cập từ bên ngoài, lúc này _index phải là AppState. Nó không bắt buộc _index phải luôn là Ephemeral State.
    • Tổng kết lại, có hai loại State trong bất kì app flutter nào.

      • Ephemeral State : Có thể dùng State và setState() và luôn nằm trong một SingleWidget

      • 2 loại state này bạn có thể đặt bất cứ đâu trong app, sự phân chia phụ thuộc vào độ phức tạp của app.

      • Theo dõi sơ đồ dưới đây :

        s

#####

III. MỘT APP ĐƠN GIẢI VỀ STATE MANAGEMENT

  • Mô tả App Demo :

    • App có 2 màn hình Catalog, Cart

    • Màn hình Catalog bao gồm MyAppBar và ScrollingView có rất nhiều MyListItem

qư

 ư

  • Nhìn vào sơ đồ, chúng ta có 5 Widget (MyApp, MyCatalog, MyCart, MyAppBar, MyListItem ). Rất nhiều widget cần truy cập State ở bất cứ đâu

    • Mỗi item trong MyListItem có thể được thêm vào Cart.

    • Tại item đã được đêm cần hiện trạng thái nó đã được thêm vào Cart

  • Lifting state up

    • Trong Flutter, bạn nên giữ state ở trên các widget sử dụng nó.

      class MyWidget extends StatefulWidget {
          
        //Field checked này dùng để lưu trạng thái được chọn của CheckBox.
        bool checked = false;
          
        MyWidget({Key? key, required this.checked}) : super(key: key);
          
        @override
        State<MyWidget> createState() => _MyWidgetState();
      }
          
      class _MyWidgetState extends State<MyWidget> {
        @override
        Widget build(BuildContext context) {
          return Checkbox(value: widget.checked, onChanged: (value) {});
        }
      }
      
    • Minh hoạ một xử lý không tốt

      void myTapHandler() {
        var cartWidget = somehowGetMyCartWidget();
        //UI chỉ thật sự updte khi bên trong updateWith kích hoạt lại UI
        cartWidget.updateWith(item);
      }
      
      Widget build(BuildContext context) {
        return SomeWidget(
          // The initial state of the cart.
        );
      }
          
      void updateWith(Item item) {
        // Tại đây bạn sẽ làm gì đo để kích hoạt UI vẽ lại
        // ví dụ gọi hàm `SetState` trong StateFullWidget
        // Sau đó widget cũng cũng gọi hàm Build() để vẽ lại.
        // Thay vào đó, ta sẽ truyền thẳng content vào Contructors để nó vẽ lại
      }
      
    • Nên thay thế bởi xử lý này :

      // GOOD
      void myTapHandler(BuildContext context) {
        var cartModel = somehowGetMyCartModel(context);
        cartModel.add(item);
      }
      
      // GOOD
      Widget build(BuildContext context) {
        var cartModel = somehowGetMyCartModel(context);
        return SomeWidget(
          // Just construct the UI once, using the current state of the cart.
          // ···
        );
      }
      
    • Trong ví dụ trên, contents cần tồn tại trong MyApp. Bất cứ khi nào nó thay đổi, nó sẽ rebuild MyCart từ bên trên. MyCart nó không quan tâm về lifecycle chỉ đơn giản là nó sẽ build lại dựa trên data của contents, lúc này MyCart cũ sẽ được thay thế bằng MyCart mới.

      s

  • Truy Cập State

    • Khi user click vào một item trong màn hình Catalog, item sẽ được thêm vào Cart. Nhưng MyListItem lại nằm phía trên (không thể cập nhật UI Cart từ MyListItem).

    • Bạn có thể dùng một callback, để detech khi user click vào item.

      @override
      Widget build(BuildContext context) {
        return SomeWidget(
          // Construct the widget, passing it a reference to the method above.
          MyListItem(myTapCallback),
        );
      }
          
      void myTapCallback(Item item) {
        print('user tapped on $item');
      }
      
    • Cách này work ok, nhưng app state cái mà bạn cần chỉnh sửa rất nhiều nơi. Thật may, flutter có cung cấp một số package hỗ trợ cung cấp data cho bất cứ widget nào không cần là chỉ là widget con. Nó giống như chức năng của các widget : InheritedWidgetInheritedNotifierInheritedModel. Trong bài viết này sẽ không hướng dẫn sử dụng các widget trên vì nó quá thấp, để thực hiện cần qua nhiều bước. Thay vào đó chúng ta sử dụng package Provider.

    • Để thêm package, sử dụng lệnh

      flutter pub add provider
      
    • Với Provider bạn không cần quan tâm callback hoặc InheritedWidgets nhưng có 3 khái niệm cần biết :

      • ChangeNotifier

      • ChangeNotifierProvider

      • Consumer

  • ChangeNotifier

    • Hỗ trợ thông báo khi data có sự thay đổi, bằng cách kế thừa ChangeNotifier cho một model bất kì.

    • Để thông báo sử dụng hàm notifyListeners()

      class CartModel extends ChangeNotifier {
        /// Internal, private state of the cart.
        final List<Item> _items = [];
          
        /// An unmodifiable view of the items in the cart.
        UnmodifiableListView<Item> get items => UnmodifiableListView(_items);
          
        /// The current total price of all items (assuming all items cost $42).
        int get totalPrice => _items.length * 42;
          
        /// Adds [item] to cart. This and [removeAll] are the only ways to modify the
        /// cart from the outside.
        void add(Item item) {
          _items.add(item);
          // This call tells the widgets that are listening to this model to rebuild.
          notifyListeners();
        }
          
        /// Removes all items from the cart.
        void removeAll() {
          _items.clear();
          // This call tells the widgets that are listening to this model to rebuild.
          notifyListeners();
        }
      }
      
  • ChangeNotifierProvider

    • Widget này cung cấp các instance của ChangeNotifer

    • Cần đặt widget này ở cấp cao hơn các widget khác, các wdiget cần data của ChangeNotfier

      void main() {
        runApp(
          ChangeNotifierProvider(
            create: (context) => CartModel(),
            child: const MyApp(),
          ),
        );
      }
      
    • Trường hợp bạn muốn cung cấp nhiều model hơn

      void main() {
        runApp(
          MultiProvider(
            providers: [
              ChangeNotifierProvider(create: (context) => CartModel()),
              Provider(create: (context) => SomeOtherClass()),
            ],
            child: const MyApp(),
          ),
        );
      }
      
  • Consumer : với T đã một trong các model bạn đã khai báo ở ChangeNotifierProvider

    • Là widget hỗ trợ lắng nghe khi ChangeNotifer gửi thông báo đi

    • Nó có một builder giúp khởi tạo Widget để hiển thị data mới nhất.

      // Không nên làm như vậy
      // HumongousWidget và AnotherMonstrousWidget không cần data CartModel
      // Text mới thức sự cần.
      // Làm như vậy HumongousWidget, AnotherMonstrousWidget sẽ bị build lại
      return Consumer<CartModel>(
        builder: (context, cart, child) {
          return HumongousWidget(
            // ...
            child: AnotherMonstrousWidget(
              // ...
              child: Text('Total price: ${cart.totalPrice}'),
            ),
          );
        },
      );
      
      // DO THIS
      return HumongousWidget(
        // ...
        child: AnotherMonstrousWidget(
          // ...
          child: Consumer<CartModel>(
            builder: (context, cart, child) {
              return Text('Total price: ${cart.totalPrice}');
            },
          ),
        ),
      );
      
  • Provider.Of

    • Trường hợp bạn không cần cập nhật UI, chỉ muốn lấy data của model để xử lý (ví dụ thêm, xoá, sữa) trong list chẳng hạn, Provider.Of sẽ giúp bạn.

      Provider.of<CartModel>(context, listen: false).removeAll();
      
III. List Of State Management Approaches