Flutter signal get_it 状态管理快速指南
视频
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://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 学习路径
- Flutter 优秀插件推荐 https://flutter.ducafecat.com
- Flutter 基础篇1 - Dart 语言学习 https://ducafecat.com/course/dart-learn
- Flutter 基础篇2 - 快速上手 https://ducafecat.com/course/flutter-quickstart-learn
- Flutter 实战1 - Getx Woo 电商APP https://ducafecat.com/course/flutter-woo
- Flutter 实战2 - 上架指南 Apple Store、Google Play https://ducafecat.com/course/flutter-upload-apple-google
- Flutter 基础篇3 - 仿微信朋友圈 https://ducafecat.com/course/flutter-wechat
- Flutter 实战3 - 腾讯 tim 即时通讯开发 https://ducafecat.com/course/flutter-tim
© 猫哥 ducafecat.com
end