Flutter状态管理——ScopedModel
Posted 一叶飘舟
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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());
);
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的主要内容,如果未能解决你的问题,请参考以下文章
Flutter:我可以在 BLoC 架构中使用 setState() 吗?