学习如何利用 Flutter signal 状态管理插件实现懒加载、全平台支持以及小颗粒度局部刷新,配合 get_it 深入掌握移动应用开发技巧。

Flutter signal get_it 状态管理快速指南

视频

https://youtu.be/xFcsaim1aPA

https://www.bilibili.com/video/BV14j421R79L/

前言

原文 https://ducafecat.com/blog/flutter-signal-state-management-get-it-quickstart

学习如何利用 Flutter signal 状态管理插件实现懒加载、全平台支持以及小颗粒度局部刷新,配合 get_it 深入掌握移动应用开发技巧。

代码

https://github.com/ducafecat/flutter_develop_tips/tree/main/flutter_application_signals

参考

https://dartsignals.dev/

https://pub.dev/packages/signals

https://pub.dev/packages/get_it

例子: 计数器

安装 signals package

pubspec.yaml

dependencies:
  flutter:
    sdk: flutter

  ...

  dio: ^5.4.1
  get_it: ^7.6.7
  signals_flutter: ^4.5.1

编写计数器

lib/pages/counter.dart

定义响应对象

final counter = signal(0);

点击后更新数据

  Widget _buildView(context) {
    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          // 按钮
          ElevatedButton(
            onPressed: () {
              counter.value++;
            },
            child: const Text('加法计数'),
          ),

          // 显示
          Text(counter.watch(context).toString()),
        ],
      ),
    );
  }

通过 counter.value++; 访问修改响应变量

通过 counter.watch( 方式监听变化

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('计数器'),
      ),
      body: _buildView(context),
    );
  }

菜单页

lib/pages/index.dart

class IndexPage extends StatelessWidget {
  const IndexPage({super.key});

  Widget _buildView(context) {
    return Column(
      children: [
                // 计数器
        ListTile(
          title: const Text("计数器"),
          onTap: () {
            Navigator.push(context, MaterialPageRoute(builder: (context) {
              return const CounterPage();
            }));
          },
        ),

      ],
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Flutter signals 状态管理'),
      ),
      body: _buildView(context),
    );
  }
}

启动

lib/main.dart

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      ...
      home: const IndexPage(),
    );
  }
}

例子: 拉取数据列表

定义商品 entity

lib/entity/product.dart

// To parse this JSON data, do
//
//     final productModel = productModelFromJson(jsonString);

import 'dart:convert';

ProductEntrty productModelFromJson(String str) =>
    ProductEntrty.fromJson(json.decode(str));

String productModelToJson(ProductEntrty data) => json.encode(data.toJson());

class ProductEntrty {
  int? id;
  String? name;
  String? slug;
  String? permalink;
  DateTime? dateCreated;
  DateTime? dateCreatedGmt;
  DateTime? dateModified;
  DateTime? dateModifiedGmt;
  String? type;
  String? status;
  bool? featured;
  String? catalogVisibility;
  String? description;
  String? shortDescription;
  String? sku;
  String? price;
  String? regularPrice;
  String? salePrice;
  dynamic dateOnSaleFrom;
  dynamic dateOnSaleFromGmt;
  dynamic dateOnSaleTo;
  dynamic dateOnSaleToGmt;
  bool? onSale;
  bool? purchasable;
  int? totalSales;
  bool? virtual;
  bool? downloadable;
  List<dynamic>? downloads;
  int? downloadLimit;
  int? downloadExpiry;
  String? externalUrl;
  String? buttonText;
  String? taxStatus;
  String? taxClass;
  bool? manageStock;
  dynamic stockQuantity;
  String? backorders;
  bool? backordersAllowed;
  bool? backordered;
  dynamic lowStockAmount;
  bool? soldIndividually;
  String? weight;
  Dimensions? dimensions;
  bool? shippingRequired;
  bool? shippingTaxable;
  String? shippingClass;
  int? shippingClassId;
  bool? reviewsAllowed;
  String? averageRating;
  int? ratingCount;
  List<dynamic>? upsellIds;
  List<dynamic>? crossSellIds;
  int? parentId;
  String? purchaseNote;
  List<Category>? categories;
  List<Category>? tags;
  List<Image>? images;
  List<Attribute>? attributes;
  List<dynamic>? defaultAttributes;
  List<dynamic>? variations;
  List<dynamic>? groupedProducts;
  int? menuOrder;
  String? priceHtml;
  List<int>? relatedIds;
  List<MetaDatum>? metaData;
  String? stockStatus;
  bool? hasOptions;
  Links? links;

  ProductEntrty({
    this.id,
    this.name,
    this.slug,
    this.permalink,
    this.dateCreated,
    this.dateCreatedGmt,
    this.dateModified,
    this.dateModifiedGmt,
    this.type,
    this.status,
    this.featured,
    this.catalogVisibility,
    this.description,
    this.shortDescription,
    this.sku,
    this.price,
    this.regularPrice,
    this.salePrice,
    this.dateOnSaleFrom,
    this.dateOnSaleFromGmt,
    this.dateOnSaleTo,
    this.dateOnSaleToGmt,
    this.onSale,
    this.purchasable,
    this.totalSales,
    this.virtual,
    this.downloadable,
    this.downloads,
    this.downloadLimit,
    this.downloadExpiry,
    this.externalUrl,
    this.buttonText,
    this.taxStatus,
    this.taxClass,
    this.manageStock,
    this.stockQuantity,
    this.backorders,
    this.backordersAllowed,
    this.backordered,
    this.lowStockAmount,
    this.soldIndividually,
    this.weight,
    this.dimensions,
    this.shippingRequired,
    this.shippingTaxable,
    this.shippingClass,
    this.shippingClassId,
    this.reviewsAllowed,
    this.averageRating,
    this.ratingCount,
    this.upsellIds,
    this.crossSellIds,
    this.parentId,
    this.purchaseNote,
    this.categories,
    this.tags,
    this.images,
    this.attributes,
    this.defaultAttributes,
    this.variations,
    this.groupedProducts,
    this.menuOrder,
    this.priceHtml,
    this.relatedIds,
    this.metaData,
    this.stockStatus,
    this.hasOptions,
    this.links,
  });

  factory ProductEntrty.fromJson(Map<String, dynamic> json) => ProductEntrty(
        id: json["id"],
        name: json["name"],
        slug: json["slug"],
        permalink: json["permalink"],
        dateCreated: json["date_created"] == null
            ? null
            : DateTime.parse(json["date_created"]),
        dateCreatedGmt: json["date_created_gmt"] == null
            ? null
            : DateTime.parse(json["date_created_gmt"]),
        dateModified: json["date_modified"] == null
            ? null
            : DateTime.parse(json["date_modified"]),
        dateModifiedGmt: json["date_modified_gmt"] == null
            ? null
            : DateTime.parse(json["date_modified_gmt"]),
        type: json["type"],
        status: json["status"],
        featured: json["featured"],
        catalogVisibility: json["catalog_visibility"],
        description: json["description"],
        shortDescription: json["short_description"],
        sku: json["sku"],
        price: json["price"],
        regularPrice: json["regular_price"],
        salePrice: json["sale_price"],
        dateOnSaleFrom: json["date_on_sale_from"],
        dateOnSaleFromGmt: json["date_on_sale_from_gmt"],
        dateOnSaleTo: json["date_on_sale_to"],
        dateOnSaleToGmt: json["date_on_sale_to_gmt"],
        onSale: json["on_sale"],
        purchasable: json["purchasable"],
        totalSales: json["total_sales"],
        virtual: json["virtual"],
        downloadable: json["downloadable"],
        downloads: json["downloads"] == null
            ? []
            : List<dynamic>.from(json["downloads"]!.map((x) => x)),
        downloadLimit: json["download_limit"],
        downloadExpiry: json["download_expiry"],
        externalUrl: json["external_url"],
        buttonText: json["button_text"],
        taxStatus: json["tax_status"],
        taxClass: json["tax_class"],
        manageStock: json["manage_stock"],
        stockQuantity: json["stock_quantity"],
        backorders: json["backorders"],
        backordersAllowed: json["backorders_allowed"],
        backordered: json["backordered"],
        lowStockAmount: json["low_stock_amount"],
        soldIndividually: json["sold_individually"],
        weight: json["weight"],
        dimensions: json["dimensions"] == null
            ? null
            : Dimensions.fromJson(json["dimensions"]),
        shippingRequired: json["shipping_required"],
        shippingTaxable: json["shipping_taxable"],
        shippingClass: json["shipping_class"],
        shippingClassId: json["shipping_class_id"],
        reviewsAllowed: json["reviews_allowed"],
        averageRating: json["average_rating"],
        ratingCount: json["rating_count"],
        upsellIds: json["upsell_ids"] == null
            ? []
            : List<dynamic>.from(json["upsell_ids"]!.map((x) => x)),
        crossSellIds: json["cross_sell_ids"] == null
            ? []
            : List<dynamic>.from(json["cross_sell_ids"]!.map((x) => x)),
        parentId: json["parent_id"],
        purchaseNote: json["purchase_note"],
        categories: json["categories"] == null
            ? []
            : List<Category>.from(
                json["categories"]!.map((x) => Category.fromJson(x))),
        tags: json["tags"] == null
            ? []
            : List<Category>.from(
                json["tags"]!.map((x) => Category.fromJson(x))),
        images: json["images"] == null
            ? []
            : List<Image>.from(json["images"]!.map((x) => Image.fromJson(x))),
        attributes: json["attributes"] == null
            ? []
            : List<Attribute>.from(
                json["attributes"]!.map((x) => Attribute.fromJson(x))),
        defaultAttributes: json["default_attributes"] == null
            ? []
            : List<dynamic>.from(json["default_attributes"]!.map((x) => x)),
        variations: json["variations"] == null
            ? []
            : List<dynamic>.from(json["variations"]!.map((x) => x)),
        groupedProducts: json["grouped_products"] == null
            ? []
            : List<dynamic>.from(json["grouped_products"]!.map((x) => x)),
        menuOrder: json["menu_order"],
        priceHtml: json["price_html"],
        relatedIds: json["related_ids"] == null
            ? []
            : List<int>.from(json["related_ids"]!.map((x) => x)),
        metaData: json["meta_data"] == null
            ? []
            : List<MetaDatum>.from(
                json["meta_data"]!.map((x) => MetaDatum.fromJson(x))),
        stockStatus: json["stock_status"],
        hasOptions: json["has_options"],
        links: json["_links"] == null ? null : Links.fromJson(json["_links"]),
      );

  Map<String, dynamic> toJson() => {
        "id": id,
        "name": name,
        "slug": slug,
        "permalink": permalink,
        "date_created": dateCreated?.toIso8601String(),
        "date_created_gmt": dateCreatedGmt?.toIso8601String(),
        "date_modified": dateModified?.toIso8601String(),
        "date_modified_gmt": dateModifiedGmt?.toIso8601String(),
        "type": type,
        "status": status,
        "featured": featured,
        "catalog_visibility": catalogVisibility,
        "description": description,
        "short_description": shortDescription,
        "sku": sku,
        "price": price,
        "regular_price": regularPrice,
        "sale_price": salePrice,
        "date_on_sale_from": dateOnSaleFrom,
        "date_on_sale_from_gmt": dateOnSaleFromGmt,
        "date_on_sale_to": dateOnSaleTo,
        "date_on_sale_to_gmt": dateOnSaleToGmt,
        "on_sale": onSale,
        "purchasable": purchasable,
        "total_sales": totalSales,
        "virtual": virtual,
        "downloadable": downloadable,
        "downloads": downloads == null
            ? []
            : List<dynamic>.from(downloads!.map((x) => x)),
        "download_limit": downloadLimit,
        "download_expiry": downloadExpiry,
        "external_url": externalUrl,
        "button_text": buttonText,
        "tax_status": taxStatus,
        "tax_class": taxClass,
        "manage_stock": manageStock,
        "stock_quantity": stockQuantity,
        "backorders": backorders,
        "backorders_allowed": backordersAllowed,
        "backordered": backordered,
        "low_stock_amount": lowStockAmount,
        "sold_individually": soldIndividually,
        "weight": weight,
        "dimensions": dimensions?.toJson(),
        "shipping_required": shippingRequired,
        "shipping_taxable": shippingTaxable,
        "shipping_class": shippingClass,
        "shipping_class_id": shippingClassId,
        "reviews_allowed": reviewsAllowed,
        "average_rating": averageRating,
        "rating_count": ratingCount,
        "upsell_ids": upsellIds == null
            ? []
            : List<dynamic>.from(upsellIds!.map((x) => x)),
        "cross_sell_ids": crossSellIds == null
            ? []
            : List<dynamic>.from(crossSellIds!.map((x) => x)),
        "parent_id": parentId,
        "purchase_note": purchaseNote,
        "categories": categories == null
            ? []
            : List<dynamic>.from(categories!.map((x) => x.toJson())),
        "tags": tags == null
            ? []
            : List<dynamic>.from(tags!.map((x) => x.toJson())),
        "images": images == null
            ? []
            : List<dynamic>.from(images!.map((x) => x.toJson())),
        "attributes": attributes == null
            ? []
            : List<dynamic>.from(attributes!.map((x) => x.toJson())),
        "default_attributes": defaultAttributes == null
            ? []
            : List<dynamic>.from(defaultAttributes!.map((x) => x)),
        "variations": variations == null
            ? []
            : List<dynamic>.from(variations!.map((x) => x)),
        "grouped_products": groupedProducts == null
            ? []
            : List<dynamic>.from(groupedProducts!.map((x) => x)),
        "menu_order": menuOrder,
        "price_html": priceHtml,
        "related_ids": relatedIds == null
            ? []
            : List<dynamic>.from(relatedIds!.map((x) => x)),
        "meta_data": metaData == null
            ? []
            : List<dynamic>.from(metaData!.map((x) => x.toJson())),
        "stock_status": stockStatus,
        "has_options": hasOptions,
        "_links": links?.toJson(),
      };
}

class Attribute {
  int? id;
  String? name;
  int? position;
  bool? visible;
  bool? variation;
  List<String>? options;

  Attribute({
    this.id,
    this.name,
    this.position,
    this.visible,
    this.variation,
    this.options,
  });

  factory Attribute.fromJson(Map<String, dynamic> json) => Attribute(
        id: json["id"],
        name: json["name"],
        position: json["position"],
        visible: json["visible"],
        variation: json["variation"],
        options: json["options"] == null
            ? []
            : List<String>.from(json["options"]!.map((x) => x)),
      );

  Map<String, dynamic> toJson() => {
        "id": id,
        "name": name,
        "position": position,
        "visible": visible,
        "variation": variation,
        "options":
            options == null ? [] : List<dynamic>.from(options!.map((x) => x)),
      };
}

class Category {
  int? id;
  String? name;
  String? slug;

  Category({
    this.id,
    this.name,
    this.slug,
  });

  factory Category.fromJson(Map<String, dynamic> json) => Category(
        id: json["id"],
        name: json["name"],
        slug: json["slug"],
      );

  Map<String, dynamic> toJson() => {
        "id": id,
        "name": name,
        "slug": slug,
      };
}

class Dimensions {
  String? length;
  String? width;
  String? height;

  Dimensions({
    this.length,
    this.width,
    this.height,
  });

  factory Dimensions.fromJson(Map<String, dynamic> json) => Dimensions(
        length: json["length"],
        width: json["width"],
        height: json["height"],
      );

  Map<String, dynamic> toJson() => {
        "length": length,
        "width": width,
        "height": height,
      };
}

class Image {
  int? id;
  DateTime? dateCreated;
  DateTime? dateCreatedGmt;
  DateTime? dateModified;
  DateTime? dateModifiedGmt;
  String? src;
  String? name;
  String? alt;

  Image({
    this.id,
    this.dateCreated,
    this.dateCreatedGmt,
    this.dateModified,
    this.dateModifiedGmt,
    this.src,
    this.name,
    this.alt,
  });

  factory Image.fromJson(Map<String, dynamic> json) => Image(
        id: json["id"],
        dateCreated: json["date_created"] == null
            ? null
            : DateTime.parse(json["date_created"]),
        dateCreatedGmt: json["date_created_gmt"] == null
            ? null
            : DateTime.parse(json["date_created_gmt"]),
        dateModified: json["date_modified"] == null
            ? null
            : DateTime.parse(json["date_modified"]),
        dateModifiedGmt: json["date_modified_gmt"] == null
            ? null
            : DateTime.parse(json["date_modified_gmt"]),
        src: json["src"],
        name: json["name"],
        alt: json["alt"],
      );

  Map<String, dynamic> toJson() => {
        "id": id,
        "date_created": dateCreated?.toIso8601String(),
        "date_created_gmt": dateCreatedGmt?.toIso8601String(),
        "date_modified": dateModified?.toIso8601String(),
        "date_modified_gmt": dateModifiedGmt?.toIso8601String(),
        "src": src,
        "name": name,
        "alt": alt,
      };
}

class Links {
  List<Collection>? self;
  List<Collection>? collection;

  Links({
    this.self,
    this.collection,
  });

  factory Links.fromJson(Map<String, dynamic> json) => Links(
        self: json["self"] == null
            ? []
            : List<Collection>.from(
                json["self"]!.map((x) => Collection.fromJson(x))),
        collection: json["collection"] == null
            ? []
            : List<Collection>.from(
                json["collection"]!.map((x) => Collection.fromJson(x))),
      );

  Map<String, dynamic> toJson() => {
        "self": self == null
            ? []
            : List<dynamic>.from(self!.map((x) => x.toJson())),
        "collection": collection == null
            ? []
            : List<dynamic>.from(collection!.map((x) => x.toJson())),
      };
}

class Collection {
  String? href;

  Collection({
    this.href,
  });

  factory Collection.fromJson(Map<String, dynamic> json) => Collection(
        href: json["href"],
      );

  Map<String, dynamic> toJson() => {
        "href": href,
      };
}

class MetaDatum {
  int? id;
  String? key;
  String? value;

  MetaDatum({
    this.id,
    this.key,
    this.value,
  });

  factory MetaDatum.fromJson(Map<String, dynamic> json) => MetaDatum(
        id: json["id"],
        key: json["key"],
        value: json["value"],
      );

  Map<String, dynamic> toJson() => {
        "id": id,
        "key": key,
        "value": value,
      };
}

商品列表页

lib/pages/products.dart

商品列表

class _ProductsPageState extends State<ProductsPage> {
  // 商品列表
  final items = listSignal<ProductEntrty>([]);

拉取数据

  // 拉取数据
  Future fetchData() async {
    String url = "https://wpapi.ducafecat.tech/products";
    Response response = await Dio().get(url);
    List<ProductEntrty> list = [];
    for (var item in response.data) {
      list.add(ProductEntrty.fromJson(item));
    }
    items.value = list;
  }

初始

  @override
  void initState() {
    super.initState();
    fetchData();
  }

列表

  Widget _buildView() {
    return ListView.builder(
      itemCount: items.value.length,
      itemBuilder: (context, index) {
        return ListTile(
          title: Text(items.value[index].name ?? ""),
          subtitle: Text(items.value[index].description ?? ""),
        );
      },
    );
  }
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('拉取数据'),
      ),
      body: Watch.builder(
        builder: (context) => _buildView(),
      ),
    );
  }

首页入口

lib/pages/index.dart

  Widget _buildView(context) {
    return Column(
      children: [
        
        ...
                
        ListTile(
          title: const Text("拉取数据"),
          onTap: () {
            Navigator.push(context, MaterialPageRoute(builder: (context) {
              return const ProductsPage();
            }));
          },
        ),

例子: 用户面板

用户 entity

lib/entity/user.dart

class UserEntrty {
  final String? id;
  final String? name;
  final String? email;

  UserEntrty({this.id, this.name, this.email});

  factory UserEntrty.fromJson(Map<String, dynamic> json) {
    return UserEntrty(
      id: json['id'],
      name: json['name'],
      email: json['email'],
    );
  }

  Map<String, dynamic> toJson() {
    return {
      'id': id,
      'name': name,
      'email': email,
    };
  }
}

用户数据模型

lib/model/user.dart

class UserModel {
  final _user = signal<UserEntrty>(UserEntrty());

  // 当前用户名
  String get userName => _user.value.name ?? "";

  // 当前用户Id
  String get userId => _user.value.id ?? "";

  // 登录 1
  Future login() async {
    // await Future.delayed(const Duration(seconds: 2));
    _user.value = UserEntrty(
      id: "1000001",
      name: "张三",
      email: "zhangshan@gmail.com",
    );
  }

  // 登录 2
  Future login2() async {
    // await Future.delayed(const Duration(seconds: 2));
    _user.value = UserEntrty(
      id: "1000002",
      name: "张三2",
      email: "zhangshan2@gmail.com",
    );
  }
}

Get_it 加载

lib/pages/dashboard.dart

class _DashboardPageState extends State<DashboardPage> {
  @override
  void initState() {
    super.initState();

    // 注册单例 - 当前用户
    GetIt.I.registerSingleton<UserModel>(
      UserModel(),
    );
  }
  @override
  void dispose() {
    // 释放
    GetIt.I.unregister<UserModel>();
    super.dispose();
  }

导航组件

lib/widget/nav.dart

import 'package:flutter/material.dart';
import 'package:get_it/get_it.dart';

import '../model/user.dart';

class NavWidget extends StatelessWidget {
  const NavWidget({super.key});

  Widget _buildView(context) {
    var g = GetIt.I;
    return Container(
      color: Colors.amber,
      child: Row(
        mainAxisAlignment: MainAxisAlignment.end,
        children: <Widget>[
          // 按钮
          ElevatedButton(
            onPressed: () {
              // 执行登录
              g.get<UserModel>().login();
            },
            child: const Text('登录'),
          ),
          // 按钮2
          ElevatedButton(
            onPressed: () {
              // 执行登录
              g.get<UserModel>().login2();
            },
            child: const Text('登录2'),
          ),
        ],
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return _buildView(context);
  }
}

用户组件

lib/widget/profile.dart

class ProfileWidget extends StatelessWidget {
  const ProfileWidget({super.key});

  Widget _buildView(context) {
    var g = GetIt.I;

    // 监听
    effect(() {
      print(g.get<UserModel>().userId);
      print(g.get<UserModel>().userName);
    });

    return Watch(
      (context) => Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text('用户ID:${g.get<UserModel>().userId}'),
            Text('用户称呼:${g.get<UserModel>().userName}'),
          ],
        ),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return _buildView(context);
  }
}

面板界面

lib/pages/dashboard.dart

  Widget _buildView(context) {
    return const Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: <Widget>[
        // 顶栏
        NavWidget(),

        // 用户信息
        Expanded(child: ProfileWidget()),
      ],
    );
  }
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('用户登录界面'),
      ),
      body: _buildView(context),
    );
  }

首页入口

lib/pages/index.dart

  Widget _buildView(context) {
    return Column(
      children: [
        
        ...
        
        ListTile(
          title: const Text("面板"),
          onTap: () {
            Navigator.push(context, MaterialPageRoute(builder: (context) {
              return const DashboardPage();
            }));
          },
        ),

代码

https://github.com/ducafecat/flutter_develop_tips/tree/main/flutter_application_signals

小结

signal + get_it 套件方式我总结 2 点:

  • 比 Provider 少了嵌套的繁琐
  • 虽然和 getx 一样轻巧,但是 getx 自带了 状态+依赖+路由

在本文中,我们深入探讨了 Flutter signal 状态管理插件的快速上手指南。通过使用 signal 插件,我们可以实现轻量级的状态管理,支持懒加载、全平台应用以及精细颗粒度的局部刷新,极大地提升了移动应用开发的效率和灵活性。结合 get_it 这一强大的依赖注入框架,我们能够应对复杂的移动应用场景,提升开发效率和代码质量。希望本文能够帮助读者更好地掌握 Flutter signal 状态管理插件,为日后的移动应用开发提供有力支持。

感谢阅读本文

如果有什么建议,请在评论中让我知道。我很乐意改进。


flutter 学习路径


© 猫哥 ducafecat.com

end