Scoped Model 模式回顾:`AnimatedBuilder` 与 `Listenable` 的组合使用

Scoped Model 模式回顾:AnimatedBuilderListenable 的组合使用

大家好,今天我们来深入探讨Flutter中的Scoped Model模式,以及如何巧妙地利用AnimatedBuilderListenable来实现高效且可维护的状态管理。Scoped Model本身并非Flutter框架原生提供,而是一种设计模式,它旨在将应用的状态(Model)传递给组件树中的子组件,同时允许子组件监听状态的改变并进行相应的更新。

什么是 Scoped Model?

Scoped Model是一种状态管理模式,它允许你将数据模型“作用域化”到Widget树的某个部分。这意味着只有在该作用域内的Widget才能访问和修改模型中的数据。它的核心思想是将状态集中管理,并通过一个特殊的Widget(通常称为ScopedModel)将其提供给子树。子树中的Widget可以通过ScopedModelDescendant或类似机制来访问和监听状态的改变。

Scoped Model模式的优点包括:

  • 状态集中管理: 将应用状态集中在一个或多个模型中,易于维护和调试。
  • 减少样板代码: 避免了手动传递状态的繁琐过程。
  • 细粒度更新: 只有依赖状态的Widget才会更新,提高性能。
  • 易于测试: 模型可以独立于UI进行测试。

然而,传统的Scoped Model实现(例如使用StatefulWidgetsetState)可能会导致不必要的Widget重建,影响性能。这就是为什么我们今天会重点讨论如何使用AnimatedBuilderListenable来优化Scoped Model的实现。

Listenable 接口

Listenable 是一个简单的接口,它定义了如何添加和移除监听器。任何实现了Listenable接口的类都可以通知其监听器状态的改变。Flutter SDK 中已经提供了 ChangeNotifier 类,它实现了 Listenable 接口,并提供了一个 notifyListeners 方法,用于通知所有监听器。

import 'package:flutter/foundation.dart';

class MyModel extends ChangeNotifier {
  int _counter = 0;

  int get counter => _counter;

  void incrementCounter() {
    _counter++;
    notifyListeners();
  }
}

在这个例子中,MyModel 继承了 ChangeNotifier,这意味着它现在可以被监听。当 incrementCounter 方法被调用时,_counter 的值会增加,并且 notifyListeners 方法会被调用,通知所有注册的监听器。

AnimatedBuilder Widget

AnimatedBuilder 是一个 Widget,它根据 Listenable 的改变来重建其子 Widget。它接受一个 Listenable 对象和一个 builder 函数作为参数。当 Listenable 对象发出通知时,builder 函数会被调用,并返回一个新的 Widget 树。

AnimatedBuilder 的关键优势在于,它只重建 builder 函数返回的 Widget 树,而不会重建整个 Widget 树。这可以显著提高性能,尤其是在需要频繁更新UI的场景中。

import 'package:flutter/material.dart';

class CounterDisplay extends StatelessWidget {
  final MyModel model;

  CounterDisplay({required this.model});

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: model,
      builder: (context, child) {
        return Text('Counter: ${model.counter}');
      },
    );
  }
}

在这个例子中,CounterDisplay Widget 使用 AnimatedBuilder 来监听 MyModel 的改变。每当 MyModel 发出通知时,builder 函数会被调用,并返回一个新的 Text Widget,显示当前的计数器值。重要的是,只有 Text Widget 会被重建,而 CounterDisplay Widget 本身不会被重建。

组合 AnimatedBuilderListenable 实现 Scoped Model

现在,让我们将 AnimatedBuilderListenable 组合起来,实现一个简单的 Scoped Model。

首先,我们需要一个 ScopedModel Widget,它将我们的模型提供给子树。

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

class ScopedModel<T extends ChangeNotifier> extends StatefulWidget {
  final T model;
  final Widget child;

  ScopedModel({Key? key, required this.model, required this.child}) : super(key: key);

  @override
  _ScopedModelState<T> createState() => _ScopedModelState<T>();

  static T of<T extends ChangeNotifier>(BuildContext context) {
    final _ScopedModelInherited<T>? inherited =
        context.dependOnInheritedWidgetOfExactType<_ScopedModelInherited<T>>();
    if (inherited == null) {
      throw FlutterError(
        'No ScopedModel<$T> found in context.n'
        'Wrap your widget with a ScopedModel<$T>.',
      );
    }
    return inherited.model;
  }
}

class _ScopedModelState<T extends ChangeNotifier> extends State<ScopedModel<T>> {
  @override
  void initState() {
    super.initState();
    widget.model.addListener(_didChangeModel);
  }

  @override
  void didUpdateWidget(covariant ScopedModel<T> oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (widget.model != oldWidget.model) {
      oldWidget.model.removeListener(_didChangeModel);
      widget.model.addListener(_didChangeModel);
    }
  }

  @override
  void dispose() {
    widget.model.removeListener(_didChangeModel);
    super.dispose();
  }

  void _didChangeModel() {
    setState(() {
      // This call to setState ensures that the inherited widget rebuilds,
      // causing any descendants using `ScopedModel.of` to update.
    });
  }

  @override
  Widget build(BuildContext context) {
    return _ScopedModelInherited<T>(
      model: widget.model,
      child: widget.child,
    );
  }
}

class _ScopedModelInherited<T extends ChangeNotifier> extends InheritedWidget {
  final T model;

  _ScopedModelInherited({
    Key? key,
    required this.model,
    required Widget child,
  }) : super(key: key, child: child);

  @override
  bool updateShouldNotify(_ScopedModelInherited<T> oldWidget) {
    return model != oldWidget.model;
  }
}

ScopedModel Widget 使用 InheritedWidget 来将模型传递给子树。它还使用 StatefulWidget 来监听模型的改变,并在模型改变时调用 setState,从而触发 InheritedWidget 的重建,通知子树中的 Widget。

接下来,我们需要一个 ScopedModelDescendant Widget,它允许子树中的 Widget 访问和监听模型。

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

class ScopedModelDescendant<T extends ChangeNotifier> extends StatelessWidget {
  final Widget Function(BuildContext context, T model) builder;

  ScopedModelDescendant({Key? key, required this.builder}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    final model = ScopedModel.of<T>(context);
    return AnimatedBuilder(
      animation: model,
      builder: (context, child) {
        return builder(context, model);
      },
    );
  }
}

ScopedModelDescendant Widget 使用 AnimatedBuilder 来监听模型的改变,并在模型改变时调用 builder 函数,从而重建其子 Widget。

现在,我们可以将这些 Widget 组合起来,创建一个简单的计数器应用。

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

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Scoped Model Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'Scoped Model Demo Home Page'),
    );
  }
}

class MyHomePage extends StatelessWidget {
  MyHomePage({Key? key, required this.title}) : super(key: key);

  final String title;
  final MyModel model = MyModel(); // Instantiate the model here

  @override
  Widget build(BuildContext context) {
    return ScopedModel<MyModel>(
      model: model,
      child: Scaffold(
        appBar: AppBar(
          title: Text(title),
        ),
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              Text(
                'You have pushed the button this many times:',
              ),
              ScopedModelDescendant<MyModel>(
                builder: (context, model) => Text(
                  '${model.counter}',
                  style: Theme.of(context).textTheme.headlineMedium,
                ),
              ),
            ],
          ),
        ),
        floatingActionButton: FloatingActionButton(
          onPressed: () => model.incrementCounter(), // Use the instantiated model
          tooltip: 'Increment',
          child: Icon(Icons.add),
        ),
      ),
    );
  }
}

在这个例子中,MyHomePage Widget 使用 ScopedModel 来将 MyModel 提供给子树。ScopedModelDescendant Widget 用于访问模型,并在模型改变时重建 Text Widget。

进一步优化:避免不必要的重建

虽然 AnimatedBuilder 已经可以显著提高性能,但我们仍然可以进一步优化,避免不必要的重建。例如,如果我们的 Widget 只依赖模型中的一部分数据,我们可以只监听这部分数据的改变。

class MyModel extends ChangeNotifier {
  int _counter = 0;
  String _message = 'Hello, world!';

  int get counter => _counter;
  String get message => _message;

  void incrementCounter() {
    _counter++;
    notifyListeners();
  }

  void updateMessage(String newMessage) {
    _message = newMessage;
    notifyListeners();
  }
}

class CounterDisplay extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final model = ScopedModel.of<MyModel>(context);
    return AnimatedBuilder(
      animation: model,
      builder: (context, child) {
        return Text('Counter: ${model.counter}');
      },
    );
  }
}

class MessageDisplay extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final model = ScopedModel.of<MyModel>(context);
    return AnimatedBuilder(
      animation: model,
      builder: (context, child) {
        return Text('Message: ${model.message}');
      },
    );
  }
}

在这个例子中,CounterDisplay Widget 只依赖 MyModelcounter 属性,而 MessageDisplay Widget 只依赖 MyModelmessage 属性。当 MyModelcounter 属性改变时,只有 CounterDisplay Widget 会被重建。当 MyModelmessage 属性改变时,只有 MessageDisplay Widget 会被重建。

性能考量与最佳实践

在使用 AnimatedBuilderListenable 实现 Scoped Model 时,需要注意以下几点:

  • 避免在 builder 函数中执行昂贵的操作: builder 函数会被频繁调用,因此应避免在其中执行昂贵的操作,例如网络请求或复杂的计算。
  • 只监听需要的属性: 如果 Widget 只依赖模型中的一部分数据,应只监听这部分数据的改变,避免不必要的重建。
  • 使用 const 关键字: 对于静态的 Widget,应使用 const 关键字,避免重复创建。
  • 谨慎使用 setStateScopedModel_didChangeModel 方法中调用 setState 是必要的,但应尽量减少调用次数,避免不必要的重建。

替代方案

虽然 AnimatedBuilderListenable 的组合可以有效地实现 Scoped Model,但还有其他一些状态管理方案可供选择,例如:

  • Provider: Google 官方推荐的状态管理方案,基于 InheritedWidget,但提供了更简洁的API。
  • Riverpod: Provider 的升级版,解决了 Provider 的一些问题,例如依赖注入和测试。
  • Bloc/Cubit: 基于响应式编程的状态管理方案,适用于复杂的应用。
  • Redux: 基于单向数据流的状态管理方案,适用于大型应用。
特性/方案 AnimatedBuilder + Listenable Provider Riverpod Bloc/Cubit Redux
基础机制 Listenable, AnimatedBuilder InheritedWidget Provider, Scoped Streams, Sinks Reducer, Store
学习曲线 中等 简单 中等 中等 复杂
性能 良好 (细粒度更新) 良好 良好 良好 良好
易用性 中等 简单 简单 中等 中等
测试性 良好 良好 良好 良好 良好
适用场景 中小型应用, 特定场景优化 中小型应用 中小型应用 中大型应用 大型应用
状态可变性 可变 可变 可变 可变 不可变
样板代码 较少 较少 较少 较多 较多

选择哪种状态管理方案取决于应用的复杂度和团队的偏好。对于简单的应用,Provider 或 Riverpod 可能更合适。对于复杂的应用,Bloc/Cubit 或 Redux 可能更合适。而 AnimatedBuilderListenable 的组合则可以在特定场景下提供更细粒度的控制和优化。

实际案例分析

假设我们正在开发一个电商应用,需要显示购物车中的商品数量。我们可以使用 AnimatedBuilderListenable 实现一个 CartModel,并在 Widget 树中共享该模型。

class CartModel extends ChangeNotifier {
  List<String> _items = [];

  List<String> get items => _items;

  void addItem(String item) {
    _items.add(item);
    notifyListeners();
  }

  void removeItem(String item) {
    _items.remove(item);
    notifyListeners();
  }

  int get itemCount => _items.length;
}

class CartIcon extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final cart = ScopedModel.of<CartModel>(context);
    return Stack(
      children: [
        Icon(Icons.shopping_cart),
        Positioned(
          right: 0,
          child: AnimatedBuilder(
            animation: cart,
            builder: (context, child) {
              return Container(
                padding: EdgeInsets.all(2),
                decoration: BoxDecoration(
                  color: Colors.red,
                  borderRadius: BorderRadius.circular(10),
                ),
                constraints: BoxConstraints(
                  minWidth: 16,
                  minHeight: 16,
                ),
                child: Text(
                  '${cart.itemCount}',
                  style: TextStyle(
                    color: Colors.white,
                    fontSize: 12,
                  ),
                  textAlign: TextAlign.center,
                ),
              );
            },
          ),
        ),
      ],
    );
  }
}

在这个例子中,CartModel 维护了购物车中的商品列表,并提供了添加和移除商品的方法。CartIcon Widget 使用 AnimatedBuilder 来监听 CartModel 的改变,并在购物车中的商品数量改变时更新购物车图标上的数字。

总结

今天我们深入探讨了 Scoped Model 模式,以及如何使用 AnimatedBuilderListenable 来实现高效且可维护的状态管理。我们了解了 Listenable 接口和 AnimatedBuilder Widget 的作用,并学习了如何将它们组合起来,创建一个简单的 Scoped Model。此外,我们还讨论了性能考量和最佳实践,以及其他一些状态管理方案。希望今天的分享对大家有所帮助。

灵活运用组合模式

Scoped Model结合AnimatedBuilderListenable 提供了一种细粒度的状态管理方法。通过只重建必要的部分,可以优化性能。它也让代码更易于维护和测试。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注