好久没更新了,这段一直忙于交接工作换工作,疏于更新。 虽然一直听到大家持有悲观的态度,对于现在的经济,对于现在的大环境,对于现在互联网没有之前那么火爆了,对于ChatGPT-4逐渐出现在大众视野,对于现在应届毕业生很难找到心仪的工作,所有的一切都让人心感焦虑。其实前段时间我也焦虑,因为也到30岁的年龄,开始担忧以后就业的机会越来越难,之前还有新闻说大厂把招聘年龄卡在30岁。
这段时间自己也想通了,我虽然没赶上互联网的浪潮,但是也赶了个晚潮;自己的技术呢,也处于中游水平,前前后后做过H5、JQuery、Android、iOS、嵌入式、Cocos、Flutter,前前后后也经历过培训、买过网课、也刷过算法、也看过源码,之前的种种努力并没有白费,总会在以后的的某个时间会有结果。与其每天焦虑,不如每天的持续精进,哪怕技术被淘汰了,我有持续学习的能力,换个方向换个职业也会做的不错。
下面是我比较喜欢的一张图片,希望能带来些心理安慰
废话不多说,进入正题
如果您是新手,我建议您可以尝试provider
,因为这个包很容易理解,并且没有使用太多代码。
如果您在其他反应式框架的状态管理方面有很强的背景,您可以选择用其他方法,例如:Redux、Rx、hooks等等。
下面我们用一个例子来一步步带入讲解如何使用。
Example
例子里面有两个独立的屏幕:目录和购物车(分别由MyCatalog
和MyCart
小部件表示。)它可以是一个购物应用程序,但您可以想象一个简单的社交网络应用程序中的相同结构。
目录屏幕包括自定义用栏(MyAppBar
)和许多列表图项的滚动视图(MyListItem
)
这是可视化为小部件树的应用程序。
所以我们至少有5个Widget的子类。他们中的许多人需要访问“属于”别处的状态。例如,每个MyListItem
所需要能将自己添加到购物车。它还可能想查看当前显示的商品是否已在购物车中。
这就引出了我们的第一个问题:我们应该把购物车的当前状态放在哪里?
提升状态
在Flutter中,将状态保持在使用它的小部件之上似乎有意义的。
为什么?就像Flutter这样的声明式框架中,如果要更改UI,则必须重新构建它。没有简单的方法来拥有MyCart.updateWith(somethingNew)
.换句话说,很难通过调用方法从外部强制更改小部件。即使你能做到这一点,你也会与框架作斗争,而不是让它帮助你。
// BAD: DO NOT DO THIS
void myTapHandler() {
var cartWidget = somehowGetMyCartWidget()l
cartWidget.updateWith(item);
}
即使你让上面的代码工作,你也必须在MyCart
小部件中处理以下内容:
// BAD: DO NOT DO THIS
Widget build(BuildContext context) {
return SomeWidget(
// The initial state of the cart
);
}
void updateWith(Item item) {
// Somehow you need to change the UI from here.
}
你需要考虑UI的当前状态并键数据应用于它。这样很难避免错误。
在Flutter中,每当内容发生变化时,您都会构建一个新的小部件。您使用MyCart(contents)
(构造函数)代替MyCart.updateWith(somethingNew)
(方法调用)。因为只能在其parents的build
方法中构造新的widgets,所以如果要改变内容,需要住在MyCart
的parent或上面。
//GOOD
void myTapHandler(BuildContext context) {
var cartModel = somehowGetMyCartModel(context);
cartModel.add(item);
}
现在MyCart
只有一个代码路径可用于构建任何版本的UI。
// GOOD
Widget build(BuildContext context) {
val cartModel = somehoowGeetMyCartModel(context);
return SomeWidget(
// Just construct the UI once, using the current state of the cart.
);
}
在我们的示例中,内容需要存在于MyApp
中。每当它发生变化时,它都会从上面重建MyCart
(稍后会详细介绍)。正因为如此,MyCart
不需要担心声明周期——它只是声明要为任何给定的内容显示什么。当这种情况发生变化时,旧的MyCart
小部件就会消失,并完全被新的小部件取代。
这就是我们所说的小部件不可变的意思。它们不会改变——它们会被替换。 现在我们知道在哪里放置购物车的状态,让我们看看如何访问它。
访问状态
当用户单击目录的其中一项时,他会添加到购物车中。但是由于购物车位于MyListItem
之上,我们该怎么做呢?
一个简单的选项是提供MyListItem
在单击时可以调用的回调。Dart的函数是一流的对象,所以你可以随心所欲地传递它们。因此,在MyCatalog
中,您可以定义以下内容:
@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');
}
这工作正常,但对于需要从许多不同位置修改的应用程序状态,您必须传递大量回调——这很快就会变旧。
幸运的是,Flutter有机制让小部件为气候代提供数据和服务(换句话说,不仅仅是试他们的孩子,而是他们下面的任何小部件)。正如您对Flutter的期望Everything is a Widget
,这些机制只是特殊类型的小部件——InheritedWidget
、InheritedNotifier
、InheritedModel
等等。我们不会在这里涵盖这些,因为它们对于我们正在尝试的事情来说有点偏底层。
相反,我们将使用一个与底层小部件一起工作但易于使用的包。它被称为提供者。
在使用provider之前,不要忘记将对它的依赖添加到您的pubspec.yaml
中。
name: my_name
decription: Blah blah blah.
# ...
dependencies:
flutter:
sdk: flutter
provider: ^6.0.5
# ...
现在您可以import 'package:provider/provider.dart';
开始构建了
使用provider
,您无需担心回调或InheritedWidgets
。但是您确实需要了解3个概念:
- ChangeNotifier
- ChangeNotifierProvider
- Consumer
ChangeNotifier
ChangeNotifier
是Flutter SDK中包含的一个简单类,它像其监听器提供更改通知。换句话说,如果某物是ChangeNotifier
,您可以订阅它的变化。(对于熟悉该术语的人来说,这是Observable
的一种形式。)
在提供程序中,ChangeNotifiier
是封装应用程序状态的一种方式。对于非常简单的应用程序,您只需使用一个ChangeNotifier
。在复杂的模型中,您将有多个模型,因此会有多个ChangeNotifiers
。(您根本不需要将ChangeeeNotifier
与provider一起使用,但它是一个易于使用的类。)
在我们的购物应用实例中,我们希望在ChangeNotifier
中管理购物车的状态。我们创建一个扩展它的新类,如下所示:
class CartMadel extends ChangeNotifier {
/// Internal, private state of the cart
fianl List<Item> _items = [];
/// An unmodifiable view of the items in the cart.
UnmodifiableListView<Item> get items => UnmodifiableListView(_items);
/// The current total price of all times (assuming all items cost $42)
int get totalPrice => _items.length * 42;
/// Adds [item] to cast. This add [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 listeening to this model to rebuild
notifyListeners();
}
}
唯一特定于ChangeNotifier
的代码是对notifyListeners()
的调用。每当模型以可能改变应用程序UI的方式发生变化时调用此方法。CartModel
中的其他所有内容都是模型本身及其业务逻辑。
ChangeNotifier
是flutter:foundation
的一部分,不依赖于Flutter中的任何更高级别的类。它很容易测试(甚至你不需要为它使用Widget测试)。例如,这是CartModel
的一个简单单元测试:
test('addingg item increases total cost', () {
final cart = CartModel();
final startingPrice = cart.totalPrice;
var i = 0;
cart.addListener(() {
expect(cart.totalPrice, greaterThan(startingPrice));
i++;
});
cart.add(Item('Dash'));
expect(i, 1);
});
ChangeNotifierProvider
ChangeNotifierProdier
是向其后台提供ChangeNotifier
实例的小部件。它来自provider
程序包。
我们已经知道将ChangeNotifierProvider:
放在需要访问它的小部件上方的位置。对于CartModel
,这意味着位于MyCart
和MyCatalog
之上的某处。
您不想将ChangeNotifierProvider
放置得比必要的更高(因为您不想污染范围)。但在我们的例子中,唯一同时位于MyCart
和MyCatalog
之上的小部件是MyApp
.
void main() {
runApp(
ChangeNotifierProvider(
create: (context) => CartModel(),
child: const MyApp(),
),
);
}
请注意,我们正在定义一个创建CartModel
新实例的构建器。ChangeNotifierProvider
足够聪明,除非绝对必要,否则不会重建CartModel
。当不再需要该实例时,它还会自动自动调用CartModel
上的dispose()
。
如果要提供多个类,可以使用MultiProvider
:
void main() {
runApp(
MultiProvider(
provider: [
ChangeNotifierProvider(create: (context) => CartModel()),
Provider(create: (context) => SomeOtherClass()),
],
child: const MyApp(),
),
);
}
Consumer
现在CartModel
已通过顶部的ChangeNotifierProvider
声明提供给我们应用程序中的小部件,我们可以开始使用它了。
这是通过Consumer
小部件完成的。
return Consumer<CartModel>(
builder: (context, cart, child) {
return Text('Total price: ${cart.totalPrice}');
},
);
我们必须指定我们想要访问的模型的类型。在这种情况下,我们需要CartModel
,因此我们编写Consumer<CartModel>
。如果您不指定泛型(<CartModel>
),provider
包将无法帮助您。提供这是基于类型的,没有类型,它不知道你想要什么。
Consumer
小部件唯一需要的参数是构建器。Builder
是一个函数,只要ChangeNotifier
发生变化就会调用它。(换句话说,当您在模型中调用notifyListeners()
时,将调用是所有相应的Consumer
小部件的所有构建器的方法。)
使用三个参数调用构建器。第一个是context
,您也可以在每个构建器方法中获得它。
builder
函数的第二个参数是ChangeNotifier
的实例。这是我们一开始就要求的。您可以使用模型中的数据来定义UI在任何给定点的外观。
第三个参数是child
,这是为了优化。如果你的Consumer
下有一个很大的widget子树,当模型改变时他不会改变,您可以构造一次并通过构建器获取它。
return Consumer<CartModel>(
builder: (context, cart, child) => Stack(
children: [
// Use SomeExpensiveWidget here, without rebuilding every time.
if (child != null) child,
Text('Total price: ${cart.totalPrice}'_,
],
),
// Build the expensive widget here.
child: const SomeExpecsiveWidget(),
);
最好将Consumer
小部件尽可能深地放在树中。您不想仅仅因为某个地方的某些细节发生了变化而重建大部分UI。
// DON'T DO THIS
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
有时,您并不是真正需要模型中的数据来更改UI,但你仍然徐亚奥访问它。例如,ClearCart
按钮希望允许用户从购物车中删除所有内容。它不需要显示购物车的内容,只需要调用clear()
方法即可。
我们可以为此使用Consumer<CartModel>
,但那会很浪费。我们会要求框架重建一个不需要重建的小部件。
对于这个用例,我们可以使用Provider.of
,并将listen
参数设置为false。
Provider.of<CartModel>(context, listen: false).removeAll();
在调用notifyListeners
时,在构建方法中使用以上行不会导致小部件重建。
简单的示例就已经完成了,您想要进一步的使用provider,请查看 使用provider
.