Flutter状态管理——ScopedModel

一、前言

Flutter的很多灵感来自于React,它的设计思想是数据与视图分离,由数据映射渲染视图。所以在Flutter中,它的Widget是immutable的,而它的动态部分全部放到了状态(State)中。

什么是Scoped_model

Scoped_model是一个dart第三方库,提供了让您能够轻松地将数据模型从父Widget传递到它的后代的功能。此外,它还会在模型更新时重新渲染使用该模型的所有子项。

它直接来自于Google正在开发的新系统Fuchsia核心Widgets 中对Model类的简单提取,作为独立使用的独立Flutter插件发布。

实现原理

Scoped model使用了观察者模式,将数据模型放在父代,后代通过找到父代的model进行数据渲染,最后数据改变时将数据传回,父代再通知所有用到了该model的子代去更新状态。

而我们则需要将它们放在顶层入口MaterialApp之上,这样就能进行全局的状态管理了。

这里page3,page4代表使用到该状态(model)的子页面。

二、引入ScopedModel第三方库

// pubspec.yaml
dependencies:
  flutter:
    sdk: flutter

  scoped_model: ^1.0.1

由于版本冲突添加失败请参考: 

Flutter | 如何优雅的解决依赖版本冲突 - 掘金Google推出flutter这样一个新的高性能跨平台(Android,ios)快速开发框架之后,被业界许多开发者所关注。我在接触了flutter之后发现这个确实是一个好东西,好东西当然要和大家分享,对吧。 今天要跟大家分享的是如何解决flutter中依赖版本冲突。 这篇文章最…https://juejin.cn/post/6844903667955400718

三、新增Model

// CountModel.dart
import 'package:scoped_model/scoped_model.dart';

class CountModel extends Model {
  int _count = 0;
  int get count => _count;

  void increment() {
    _count++;
    notifyListeners();
  }
}

四、局部刷新(单组件/单页面内部状态)

4.1 新增页面(ScopedModelPage)

Scoped_model提供了两种方式在子页面中获取model。

我们先来介绍第一种,使用ScopedModelDescendant获取model。

// ScopedModelPage.dart
import 'package:flutter/material.dart';
import 'package:scoped_model/scoped_model.dart';
import 'package:stateresearch/model/CountModel.dart';

class ScopedModelPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ScopedModel<CountModel>(
      model: CountModel(),
      child: Scaffold(
        appBar: AppBar(
          title: Text("ScopedModelPage"),
        ),
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              Text('You have pushed the button this many times:'),
              ScopedModelDescendant<CountModel>(
                builder: (context, child, model) => Text('${model.count}'),
              ),
            ],
          ),
        ),
        floatingActionButton: ScopedModelDescendant<CountModel>(
          builder: (context, child, model) {
            return FloatingActionButton(
              onPressed: model.increment,
              tooltip: 'Increment',
              child: Icon(Icons.add),
            );
          },
        ),
      ),
    );
  }
}

ScopedModelDescendant<T extends Model>是一个Stateless Widget,它接收三个参数。

  ScopedModelDescendant({
    required this.builder,
    this.child,
    this.rebuildOnChange = true,
  });

builder是一个ScopedModelDescendantBuilder,它接收三个参数。

typedef Widget ScopedModelDescendantBuilder<T extends Model>(
  BuildContext context,
  Widget? child,
  T model,
);

在builder中能够通过model来获取CountModel实例。 

rebuildOnChange属性能够控制当该状态发生变化时,是否rebuild,作用等同于setState。也就是说我们调用改变状态的一些方法时,不必再setState。

第二种获取model的方式——使用ScopedModel.of

final countModel = ScopedModel.of<CountModel>(context);
countModel.increment();

或者在Model中重写of方法

class CountModel extends Model{
  int _count = 0;
  get count => _count;

  void increment(){
    _count++;
    notifyListeners();
  }
//重写of方法
  CountModel of(context) =>
      ScopedModel.of<CountModel>(context);
}

然后直接通过CountModel获取model实例

final countModel2 = CountModel().of(context);

这种方式似乎让我们的代码有更好的可阅读性。

【注意:】

我们在使用第二种方式的时候,rebuildOnChange的值改为false,会导致无法刷新(同步)状态的情况发生,需要官方默认指定rebuildOnChange:true,平时开发无需手动指定rebuildOnChange的值。

4.2、修改main文件

// 改写 main.dart
import 'package:flutter/material.dart';
import 'package:stateresearch/pages/ScopedModelPage.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter状态管理',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: ScopedModelPage(),
    );
  }
}

五、全局刷新(页面/组件状态共享)

5.1、新增两个页面(ScopedModelPageTwo和ScopedModelPageThree)

// ScopedModelPageTwo.dart
import 'package:flutter/material.dart';
import 'package:scoped_model/scoped_model.dart';
import 'package:stateresearch/model/CountModel.dart';
import 'package:stateresearch/pages/ScopedModelPageThree.dart';

class ScopedModelPageTwo extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("ScopedModelPageTwo"),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text('You have pushed the button this many times:'),
            ScopedModelDescendant<CountModel>(
              builder: (context, child, model) => Text('${model.count}'),
            ),
          ],
        ),
      ),
      floatingActionButton: ScopedModelDescendant<CountModel>(
        builder: (context, child, model) {
          return FloatingActionButton(
            onPressed: () {
              model.increment();
              Future.delayed(Duration(seconds: 2), () {
                Navigator.of(context).push(MaterialPageRoute(builder: (BuildContext context) {
                  return ScopedModelPageThree();
                }));
              });
            },
            tooltip: 'Increment',
            child: Icon(Icons.add),
          );
        },
      ),
    );
  }
}
// ScopedModelPageThree.dart
import 'package:flutter/material.dart';
import 'package:scoped_model/scoped_model.dart';
import 'package:stateresearch/model/CountModel.dart';

class ScopedModelPageThree extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("ScopedModelPageThree"),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text('You have pushed the button this many times:'),
            ScopedModelDescendant<CountModel>(
              builder: (context, child, model) => Text('${model.count}'),
            ),
          ],
        ),
      ),
      floatingActionButton: ScopedModelDescendant<CountModel>(
        builder: (context, child, model) {
          return FloatingActionButton(
            onPressed: model.increment,
            tooltip: 'Increment',
            child: Icon(Icons.add),
          );
        },
      ),
    );
  }
}

5.2、修改main文件

import 'package:flutter/material.dart';
import 'package:scoped_model/scoped_model.dart';
import 'package:stateresearch/model/CountModel.dart';
import 'package:stateresearch/pages/ScopedModelPageTwo.dart';

void main() {
  runApp(
     // APP顶层进行全局监听
     // route 会进行向下传递该 Model
    // 因此其它页面无需 ScopedModel
    // 只需要通过 ScopedModelDescendant<T> 获取 Model 即可
    ScopedModel<CountModel>(
        model: CountModel(),
        child: MyApp(),
    ),
  );
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter状态管理',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: ScopedModelPageTwo(),
    );
  }
}

六、多Model全局共享

6.1、新增Model(ListModel)和 mixin的Model(GlobalScopedModel)

// ListModel.dart
import 'package:scoped_model/scoped_model.dart';

class ListModel extends Model {
  List<String> _list = [];
  List<String> get list => _list;

  void push(String value) {
    _list.add(value);
    notifyListeners();
  }
}
// GlobalScopedModel.dart
import 'package:scoped_model/scoped_model.dart';
import 'package:stateresearch/model/CountModel.dart';
import 'package:stateresearch/model/ListModel.dart';

class GlobalScopedModel extends Model with CountModel, ListModel {}

6.2、新增analysis配置

// analysis_options.yaml
// 该配置告诉Dart Analyzer放开minx的限制
// 默认with的类强制是继承于Object类
analyzer:
  errors:
    mixin_inherits_from_not_object: ignore

6.3、修改两个页面(ScopedModelPageTwo和ScopedModelPageThree)

// ScopedModelPageTwo.dart
import 'dart:math';

import 'package:flutter/material.dart';
import 'package:scoped_model/scoped_model.dart';
import 'package:stateresearch/model/GlobalScopedModel.dart';
import 'package:stateresearch/pages/ScopedModelPageThree.dart';

class ScopedModelPageTwo extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("ScopedModelPageTwo"),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text('You have pushed the button this many times:'),
            ScopedModelDescendant<GlobalScopedModel>(
              builder: (context, child, model) => Text('${model.count}'),
            ),
          ],
        ),
      ),
      floatingActionButton: ScopedModelDescendant<GlobalScopedModel>(
        builder: (context, child, model) {
          return FloatingActionButton(
            onPressed: () {
              model.increment();
              model.push("chris-${Random().nextInt(10)}");
              Future.delayed(Duration(seconds: 2), () {
                Navigator.of(context).push(MaterialPageRoute(builder: (BuildContext context) {
                  return ScopedModelPageThree();
                }));
              });
            },
            tooltip: 'Increment',
            child: Icon(Icons.add),
          );
        },
      ),
    );
  }
}
// ScopedModelPageThree.dart
import 'package:flutter/material.dart';
import 'package:scoped_model/scoped_model.dart';
import 'package:stateresearch/model/GlobalScopedModel.dart';

class ScopedModelPageThree extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("ScopedModelPageThree"),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text('You have pushed the button this many times:'),
            ScopedModelDescendant<GlobalScopedModel>(
              builder: (context, child, model) => Text('${model.count}'),
            ),
            ScopedModelDescendant<GlobalScopedModel>(
              builder: (context, child, model) => Text('${model.list}'),
            ),
          ],
        ),
      ),
      floatingActionButton: ScopedModelDescendant<GlobalScopedModel>(
        builder: (context, child, model) {
          return FloatingActionButton(
            onPressed: model.increment,
            tooltip: 'Increment',
            child: Icon(Icons.add),
          );
        },
      ),
    );
  }
}

6.4、修改main文件

// main.dart
import 'package:flutter/material.dart';
import 'package:scoped_model/scoped_model.dart';
import 'package:stateresearch/model/GlobalScopedModel.dart';
import 'package:stateresearch/pages/ScopedModelPageTwo.dart';

void main() {
  runApp(
    ScopedModel<GlobalScopedModel>(
      model: GlobalScopedModel(),
      child: MyApp(),
    ),
  );
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter状态管理',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: ScopedModelPageTwo(),
    );
  }
}

七、总结

ScopedModel可以全局+局部使用(即使用了全局ScopedModel,也不影响某个Widget使用自己的ScopedModel)

使用ScopedModel,其优点:

  • 显示逻辑与业务逻辑分离;

缺点:

  • 模型复杂时,notifyListeners的时机选择很重要,否则会频繁刷新;
  • Model的API内部是异步(Microtask),但其API名看不出来是异步;

延伸

源码之Model

abstract class Model extends Listenable {
  final Set<VoidCallback> _listeners = Set<VoidCallback>();
  int _version = 0;
  int _microtaskVersion = 0;

  /// [listener] will be invoked when the model changes.
  @override
  void addListener(VoidCallback listener) {
    _listeners.add(listener);
  }

  /// [listener] will no longer be invoked when the model changes.
  @override
  void removeListener(VoidCallback listener) {
    _listeners.remove(listener);
  }

  /// Returns the number of listeners listening to this model.
  int get listenerCount => _listeners.length;

  /// Should be called only by [Model] when the model has changed.
  @protected
  void notifyListeners() {
    // We schedule a microtask to debounce multiple changes that can occur
    // all at once.
    if (_microtaskVersion == _version) {
      _microtaskVersion++;
      scheduleMicrotask(() {
        _version++;
        _microtaskVersion = _version;

        // Convert the Set to a List before executing each listener. This
        // prevents errors that can arise if a listener removes itself during
        // invocation!
        _listeners.toList().forEach((VoidCallback listener) => listener());
      });
    }
  }
}
  • ScopedModel

注意深追 AnimatedBuilder --> AnimatedWidget
_InheritedModel --> InheritedWidget

class ScopedModel<T extends Model> extends StatelessWidget {
  /// The [Model] to provide to [child] and its descendants.
  final T model;

  /// The [Widget] the [model] will be available to.
  final Widget child;

  ScopedModel({@required this.model, @required this.child})
      : assert(model != null),
        assert(child != null);

  @override
  Widget build(BuildContext context) {
    // 注意深追AnimatedBuilder,--> AnimatedWidget  
    return AnimatedBuilder(
      animation: model,
      builder: (context, _) => _InheritedModel<T>(model: model, child: child),
    );
  }
}
  • AnimatedWidget的state中

这里的listenable就是前面的model initState中去addListener,参数方法内是setState

abstract class AnimatedWidget extends StatefulWidget {
  const AnimatedWidget({
    Key key,
    @required this.listenable,
  }) : assert(listenable != null),
       super(key: key);

  final Listenable listenable;

  @override
  _AnimatedState createState() => _AnimatedState();

}

class _AnimatedState extends State<AnimatedWidget> {
  @override
  void initState() {
    super.initState();
    widget.listenable.addListener(_handleChange);
  }
  void _handleChange() {
    setState(() {
      // The listenable's state is our build state, and it changed already.
    });
  }

}

  • _InheritedModel

这是一个InheritedWidget,前面的model和通过model获取数据的Widget最终都是传递到这里来

class _InheritedModel<T extends Model> extends InheritedWidget {
  final T model;
  final int version;

  _InheritedModel({Key key, Widget child, T model})
      : this.model = model,
        this.version = model._version,
        super(key: key, child: child);

  @override
  bool updateShouldNotify(_InheritedModel<T> oldWidget) =>
      (oldWidget.version != version);
}

ScopedModelDescendant 是为了获取共享的model而做的一层封装,通过ScopedModel.of(context, rebuildOnChange: rebuildOnChange)获取到model,在通过builder传递出去。build的类型是ScopedModelDescendantBuilder

class ScopedModelDescendant<T extends Model> extends StatelessWidget {
 
  @override
  Widget build(BuildContext context) {
    return builder(
      context,
      child,
      ScopedModel.of<T>(context, rebuildOnChange: rebuildOnChange),
    );
  }
}

typedef Widget ScopedModelDescendantBuilder<T extends Model>(
  BuildContext context,
  Widget child,
  T model,
);

Scoped是如何做到同步不同页面中的状态的

从上面的源码可以看到,Model实现了Listenable接口,并重写了void addListener(VoidCallback listener),removeListener(VoidCallback listener)方法,实现了观察者模式。 每当我们调用notifyListeners()方法时,将会通知观察者更新状态。

Scoped如何做到数据能够互相共享的

在不同页面间的数据传递使用了InheritedWidget。

class _InheritedModel<T extends Model> extends InheritedWidget {
  final T model;
  final int version;

  _InheritedModel({Key? key, required Widget child, required T model})
      : this.model = model,
        this.version = model._version,
        super(key: key, child: child);

  @override
  bool updateShouldNotify(_InheritedModel<T> oldWidget) =>
      (oldWidget.version != version);
}

侵入性

由于Model必须继承至Model类,所以它就具有了侵入性。以后假如不用scoped进行状态管理那么必然会带来需要更改多处代码的情况。这并不是我们希望看到的结果。

参考文章:

Flutter状态管理:ScopedModel - 简书

猜你喜欢

转载自blog.csdn.net/jdsjlzx/article/details/123263160