Flutter Provider之简单的应用程序状态管理

625 阅读7分钟

好久没更新了,这段一直忙于交接工作换工作,疏于更新。 虽然一直听到大家持有悲观的态度,对于现在的经济,对于现在的大环境,对于现在互联网没有之前那么火爆了,对于ChatGPT-4逐渐出现在大众视野,对于现在应届毕业生很难找到心仪的工作,所有的一切都让人心感焦虑。其实前段时间我也焦虑,因为也到30岁的年龄,开始担忧以后就业的机会越来越难,之前还有新闻说大厂把招聘年龄卡在30岁。

这段时间自己也想通了,我虽然没赶上互联网的浪潮,但是也赶了个晚潮;自己的技术呢,也处于中游水平,前前后后做过H5、JQuery、Android、iOS、嵌入式、Cocos、Flutter,前前后后也经历过培训、买过网课、也刷过算法、也看过源码,之前的种种努力并没有白费,总会在以后的的某个时间会有结果。与其每天焦虑,不如每天的持续精进,哪怕技术被淘汰了,我有持续学习的能力,换个方向换个职业也会做的不错。

下面是我比较喜欢的一张图片,希望能带来些心理安慰

image.png


废话不多说,进入正题

如果您是新手,我建议您可以尝试provider,因为这个包很容易理解,并且没有使用太多代码。 如果您在其他反应式框架的状态管理方面有很强的背景,您可以选择用其他方法,例如:Redux、Rx、hooks等等。

下面我们用一个例子来一步步带入讲解如何使用。

Example

例子里面有两个独立的屏幕:目录和购物车(分别由MyCatalogMyCart小部件表示。)它可以是一个购物应用程序,但您可以想象一个简单的社交网络应用程序中的相同结构。

model-shopper-screencast.gif

目录屏幕包括自定义用栏(MyAppBar)和许多列表图项的滚动视图(MyListItem

这是可视化为小部件树的应用程序。

image.png

所以我们至少有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小部件就会消失,并完全被新的小部件取代。

image.png

这就是我们所说的小部件不可变的意思。它们不会改变——它们会被替换。 现在我们知道在哪里放置购物车的状态,让我们看看如何访问它。

访问状态

当用户单击目录的其中一项时,他会添加到购物车中。但是由于购物车位于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,这些机制只是特殊类型的小部件——InheritedWidgetInheritedNotifierInheritedModel等等。我们不会在这里涵盖这些,因为它们对于我们正在尝试的事情来说有点偏底层。

相反,我们将使用一个与底层小部件一起工作但易于使用的包。它被称为提供者。

在使用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中的其他所有内容都是模型本身及其业务逻辑。

ChangeNotifierflutter: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,这意味着位于MyCartMyCatalog之上的某处。

您不想将ChangeNotifierProvider放置得比必要的更高(因为您不想污染范围)。但在我们的例子中,唯一同时位于MyCartMyCatalog之上的小部件是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.