ScopedModel已通过度到了Provider的模式了。不用深刻本文,就能够看到ScopedMode里的 VM这一层都是经过调用notifyListeners
方法来通知界面更新的,ScopedModel
和ScopedModelDescendant
也和Provider
模式下的Consumer
相差无几,底层也许有区别不过本质都是一个组件。并且也是用在须要更新的组件子树上一层来保证更新范围最小。在 VM的组织上基本也是同样,用VM层来调用各类服务。因此,若是你已经了解Provider模式,那么本片能够不用看。不了解Provider也能够直接跳过本文看Provider模式。
本文但愿在尽可能接近实战的条件下能清晰的讲解如何使用ScopedModel架构。视频教程在这里。git
我(做者)在帮一个客户使用Flutter重制一个App。设计差强人意,性能更是差的离谱。可是我(做者)接手这个项目的时候还只用了Flutter三个星期。调研了ScopedMode和Redux以后就准备用ScopedModel了,BLoC彻底不在考虑范围内。github
我发现ScopedModel很是容易使用,并且从我开发这个app里我也有不少的收获。数据库
ScopedModel不止有一种实现方式。根据功能组织Model,或者根据页面来组织Model。两种方法里model都须要和服务(service)交互,服务则处理全部的逻辑而且根据返回的数据处理状态(state)。咱们来快速的过一下这两种方式。后端
在这个状况下你有一个AppModel,它会从根组件(root widget)一直传递到须要的子组件上。AppModel能够经过mixin的方式来扩展它所支持的功能好比:网络
/// Feature model for authentication class AuthModel extends Model { // ... } /// App model class AppModel extends Model with AuthModel {}
若是你仍是不清楚是怎么回事的话,能够看这个例子。架构
这样一个ScopedModel就直接和一个页面或者组件关联了。可是也会产生不少的固定模式的代码,毕竟你要为每一个页面写一个Model。app
在(做者)的生产app上,使用了单一AppModel和多个功能mixin的方式。随着App规模的变大,常常会有一个model处理多个页面(组件)的状态的状况,这样就有点郁闷了。因而就迁移到了另一种作法上。每一个页面/组件一个Model,加上GetIt作为IoC容器,这样就简单了不少。本文的剩余部分也会继续讲述这个模式。less
若是要动手实践的话能够从这个repo里代代码开始。用你喜欢的IDE打开start目录。异步
这么作是为了更加容易开始,也容易找到切入点。每一个视图会有一个根Model继承自ScopedModel。ScopedModel对象将会从locator
里得到。叫作locator是由于它就是用来定位服务和Model的。每一个页面/组件的model都会代理专门的服务的方法,好比网络请求或者数据库操做等,并根据返回的结果更新组件的状态。async
首先,咱们来安装GetIt和ScopedModel。
在咱们的包清单pubspec里添加scoped_model
和get_it
依赖:
... dependencies: flutter: sdk: flutter # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^0.1.2 # scoped model scoped_model: ^1.0.1 # dependency injection get_it: ^1.0.3 ...
在lib目录下新建一个service_locator.dart文件。添加以下代码:
import 'package:get_it/get_it.dart'; GetIt locator = GetIt(); void setupLocator() { // Register services // Register models }
你会在这里注册你全部的Model和服务对象。以后在main.dart文件里添加setupLocator()
的调用, 以下:
... import 'service_locator.dart'; void main() { // setup locator setupLocator(); runApp(MyApp()); } ...
以上就配置完了app所须要的所有依赖了。
咱们来添加一个Home页面。如今是每一个页面都有一个scoped model,那么也新建一个相关的model,并经过locator
把他们两个关联起来。首先咱们准备好他们要存放的地方。在lib目录下新建一个ui目录,在里面再新建一个view目录用来存放拆分出来的视图。
在lib目录下新建scoped_model目录来存放model。
首先在view目录下新建一个home_view.dart的文件。
import 'package:flutter/material.dart'; import 'package:scoped_model/scoped_model.dart'; class HomeView extends StatelessWidget { @override Widget build(BuildContext context) { return ScopedModel<HomeModel>( child: Scaffold( )); } }
咱们须要一个HomeModel
来获取各类咱们须要的对应的信息。在lib/scoped_model目录先新建home_model.dart文件。
import 'package:scoped_model/scoped_model.dart'; class HomeModel extends Model { }
接下来咱们要把咱们的页面和scoped model关联到一块儿。这个时候就该以前提到的locator
上场了。可是,还要完成一些locator
的注册工做。要适用locator
就须要先注册。
import 'package:scoped_guide/scoped_models/home_model.dart'; ... void setupLocator() { // register services // register models locator.registerFactory<HomeModel>(() => HomeModel()); }
HomeModel
已经在locator
里完成了注册。咱们能够在任何地方经过locator
拿到它的实例了。
首先须要引入ScopedModel
,这里用到了泛型,因此它的类型参数就是咱们定义的HomeModel
。把它做为一个组件放进build
方法里。model
属性就用到了locator
。在用到HomeModel
实例的地方使用ScopedModelDescendant
。它也须要一个类型参数,这里一样是HomeModel
。
import 'package:flutter/material.dart'; import 'package:scoped_model/scoped_model.dart'; import 'package:scoped_guide/scoped_models/home_model.dart'; import 'package:scoped_guide/service_locator.dart'; class HomeView extends StatelessWidget { @override Widget build(BuildContext context) { return ScopedModel<HomeModel>( model: locator<HomeModel>(), child: ScopedModelDescendant<HomeModel>( builder: (context, child, model) => Scaffold( body: Center( child: Text(model.title), ), ))); } }
这里的model的title
属性能够设置为HomeModel。
新建一个lib/services目录。这里咱们会添加一个假的服务,它只会延时两秒执行,以后返回一个true。添加一个storage_service.dart文件。
class StorageService { Future<bool> saveData() async { await Future.delayed(Duration(seconds: 2)); return true; } }
在locator
里注册这个服务:
import 'package:scoped_guide/services/storage_service.dart'; ... void setupLocator() { // register services locator.registerLazySingleton<StorageService>(() => StorageService()); // register models locator.registerFactory<HomeModel>(() => HomeModel()); }
就如上文所述,咱们用service来完成须要的工做,并使用返回的数据来更新须要更新的组件。可是,这里还有一个model做为代理。因此咱们须要用locator
来把注册好的服务和model关联。
import 'package:scoped_guide/service_locator.dart'; import 'package:scoped_guide/services/storage_service.dart'; import 'package:scoped_model/scoped_model.dart'; class HomeModel extends Model { StorageService storageService = locator<StorageService>(); String title = "HomeModel"; Future saveData() async { setTitle("Saving Data"); await storageService.saveData(); setTitle("Data Saved"); } void setTitle(String value) { title = value; notifyListeners(); } }
HomeModel
里的saveData
方法才是组件须要调用到的。这个方法也就是服务的一个大力方法。具体的能够参考MVVM的模式,这里就不过多叙述。
在saveData
方法里,存数据完成以后调用了setTitle
方法。这个方法根据service返回的值设置了title
属性,并调用了notifyListeners
方法发出通知。通知须要更新的组件能够把数据显示上去了。
在HomeView
的Scaffold
里添加一个浮动按钮,并在里面调用HomeModel
的saveData
方法。那么,从接收用户的输入到“保存数据”,再到最后的更新界面一套流程在代码里就所有实现完成了。
咱们一块儿来回顾一下在实际开发中常常会用到的内容。
若是你的app要从网络或者本地数据库读取数据,那么就会有四个基本状态须要处理:idel(空闲),busy(获取数据中),retrieved(成功取得数据)和error。全部的视图的视图都会用到这四个状态,因此比较好的选择的是在一开始的时候就把他们写到model里。
新建lib/enum目录,在里面新建一个view_states.dart文件。
/// Represents a view's state from the ScopedModel enum ViewState { Idle, Busy, Retrieved, Error }
如今视图的model就能够引入ViewState
了。
import 'package:scoped_guide/service_locator.dart'; import 'package:scoped_guide/services/storage_service.dart'; import 'package:scoped_model/scoped_model.dart'; import 'package:scoped_guide/enums/view_state.dart'; class HomeModel extends Model { StorageService storageService = locator<StorageService>(); String title = "HomeModel"; ViewState _state; ViewState get state => _state; Future saveData() async { _setState(ViewState.Busy); title = "Saving Data"; await storageService.saveData(); title = "Data Saved"; _setState(ViewState.Retrieved); } void _setState(ViewState newState) { _state = newState; notifyListeners(); } }
ViewState
会经过一个getter
暴露出去。一样的,这些状态也都须要对应的视图能够捕捉到,并在发生变化的时候更新界面。因此,状态变化的时候也须要调用notifyListeners
来通知视图,或者说更新视图的状态。
你能够看到,状态变化的时候一个叫作_setState
的方法被调用了。这个方法专门去负责调用notifyListeners
来通知视图去作更新。
如今咱们调用了_setState
,ScopedModel
就会收到通知,而后UI里的某部分就回发生更改。咱们会显示一个旋转的菊花来代表服务正在请求数据,也许是经过网络获取后端数据也许是本地数据库的数据。如今来更新一下Scaffold
的代码:
... body: Center( child: Column( mainAxisSize: MainAxisSize.min, children: <Widget>[ _getBodyUi(model.state), Text(model.title), ] ) ) ... Widget _getBodyUi(ViewState state) { switch (state) { case ViewState.Busy: return CircularProgressIndicator(); case ViewState.Retrieved: default: return Text('Done'); } }
_getBodyUi
方法会更具ViewState
的值来显示不一样的界面。
一个数据的变化会影响到多个界面的状况是实际开发中常常发生的。在处理完单个界面更新的简单状况后咱们能够开始处理多个界面的问题了。
在前面的例子中你会看到不少的模板代码,好比:ScopedModel
,ScopedModelDescendant
以及从locator
里获取model、service之类的对象。这些都是模板代码,不是不少,可是咱们还可让它更少。
首先,咱们来新建一个BaseView
。
import 'package:flutter/material.dart'; import 'package:scoped_model/scoped_model.dart'; import 'package:scoped_guide/service_locator.dart'; class BaseView<T extends Model> extends StatelessWidget { final ScopedModelDescendantBuilder<T> _builder; BaseView({ScopedModelDescendantBuilder<T> builder}) : _builder = builder; @override Widget build(BuildContext context) { return ScopedModel<T>( model: locator<T>(), child: ScopedModelDescendant<T>( builder: _builder)); } }
在BaseView
里已经有了ScopedModel
和ScopedModelDescendant
的调用。那么就不不要在每一个界面里都放这些调用了。好比HomeView
就使用BaseView
并去掉这些无关的代码了。
... import 'base_view.dart'; @override Widget build(BuildContext context) { return BaseView<HomeModel> ( builder: (context, child, model) => Scaffold( ... )); }
这样咱们能够用更少的代码作更多的事了。你能够给IDE里注册一段代码段,这样几个字符输入了就能够有一段基本完整的功能的代码出现了。咱们在lib/ui/views目录新建一个模板文件template_view.dart。
import 'package:flutter/material.dart'; import 'package:scoped_guide/scoped_models/home_model.dart'; import 'base_view.dart'; class Template extends StatelessWidget { @override Widget build(BuildContext context) { return BaseView<HomeModel>( builder: (context, child, model) => Scaffold( body: Center(child: Text(this.runtimeType.toString()),), )); } }
咱们分发出去的状态也不是只是专属于一个界面的,而是能够多个界面共享的,因此咱们也新建一个BaseModel
来处理这个问题。
import 'package:scoped_guide/enums/view_state.dart'; import 'package:scoped_model/scoped_model.dart'; class BaseModel extends Model { ViewState _state; ViewState get state => _state; void setState(ViewState newState) { _state = newState; notifyListeners(); } }
修改HomeModel
的代码,让他从BaseModel
继承。
... class HomeModel extends BaseModel { ... Future saveData() async { setState(ViewState.Busy); title = "Saving Data"; await storageService.saveData(); title = "Data Saved"; setState(ViewState.Retrieved); } }
对于多个界面的支持的代码准备都完成了。咱们有BaseView
和BaseModel
能够分别服务于视图和model了。
接下来就是导航了。根据template_view.dart来新建两个视图error_view.dart和success_view.dart。记得在这些代码里面作适当的修改。
接下来新建两个model,一个是SuccessModel
一个是ErrorModel
。他们都继承自BaseModel
,而不是Model
。而后记得在locator
里面注册这些model。
基本的导航都很相似。咱们可使用导航器(Navigator)来初始导航栈上的视图。
如今对咱们的HomeModel#saveData
来作一些更改。
Future<bool> saveData() async { _setState(ViewState.Busy); title = "Saving Data"; await storageService.saveData(); title = "Data Saved"; _setState(ViewState.Retrieved); return true; }
在HomeView
里,咱们来更新浮动按钮的onPress
方法。让它成为一个异步方法,等待saveData
执行的结果,并根据结果导航到对应的界面。
floatingActionButton: FloatingActionButton( onPressed: () async { var whereToNavigate = await model.saveData(); if (whereToNavigate) { Navigator.push(context,MaterialPageRoute(builder: (context) => SuccessView())); } else { Navigator.push(context,MaterialPageRoute(builder: (context) => ErrorView())); } } )
在多个几面里都有获取数据的服务,那么他们也就都须要显示忙碌状态:一个旋转的菊花。那么,这个组件就是能够在不一样的界面之间共享的。
新建一个BusyOverlay
组件,把它放在lib/ui/views目录,命名为busy_overlay.dart。
import 'package:flutter/material.dart'; class BusyOverlay extends StatelessWidget { final Widget child; final String title; final bool show; const BusyOverlay({this.child, this.title = 'Please wait...', this.show = false}); @override Widget build(BuildContext context) { var screenSize = MediaQuery.of(context).size; return Material( child: Stack(children: <Widget>[ child, IgnorePointer( child: Opacity( opacity: show ? 1.0 : 0.0, child: Container( width: screenSize.width, height: screenSize.height, alignment: Alignment.center, color: Color.fromARGB(100, 0, 0, 0), child: Column( mainAxisSize: MainAxisSize.min, children: <Widget>[ CircularProgressIndicator(), Text(title, style: TextStyle( fontSize: 16.0, fontWeight: FontWeight.bold, color: Colors.white)), ], ), )), ), ])); } }
如今咱们能够界面里使用这个组件了。在HomeView
里,把Scaffold
放进BusyOverlay
里面:
@override Widget build(BuildContext context) { return BaseView<HomeModel>(builder: (context, child, model) => BusyOverlay( show: model.state == ViewState.Busy, child: Scaffold( ... ))); }
如今,当你点击浮动按钮的时候你会看到一个“请稍等”的提示。你也能够把BusyOverlay
组件的调用放进BaseView
里面。记住你的忙碌提示组要在builder里面,这样它才能更具model的返回值做出正确的反应。
咱们已经处理了根据不一样的model返回值来显示对应的界面。如今咱们要处理另一个常见的问题,那就是异步问题的处理。
当你有一个列表,点了某行要看到更多的详细信息的时候基本就会遇到一个异步场景。当进入详情页面的时候,咱们就会根据传过来的这个特定数据的ID等相关数据来请求后端得到更多的详细数据。
请求通常都是发生在StatefulWidget
的initState
方法内。本例不打算添加太多的界面,咱们只关注在架构上面。咱们会写死一个返回值,让这个值在“请求成功”的时候返回给界面。
首先,咱们来更新SuccessModel
。
import 'package:scoped_guide/scoped_models/base_model.dart'; class SuccessModel extends BaseModel { String title = "no text yet"; Future fetchDuplicatedText(String text) async { setState(ViewState.Busy); await Future.delayed(Duration(seconds: 2)); title = '$text $text'; setState(ViewState.Retrieved); } }
如今咱们能够在视图建立的时候调用model的方法了。不过这须要咱们把BaseView
换成StatefulWidget
。在BaseView
的initState
方法里调用model的异步方法。
import 'package:flutter/material.dart'; import 'package:scoped_model/scoped_model.dart'; import 'package:scoped_guide/service_locator.dart'; class BaseView<T extends Model> extends StatefulWidget { final ScopedModelDescendantBuilder<T> _builder; final Function(T) onModelReady; BaseView({ScopedModelDescendantBuilder<T> builder, this.onModelReady}) : _builder = builder; @override _BaseViewState<T> createState() => _BaseViewState<T>(); } class _BaseViewState<T extends Model> extends State<BaseView<T>> { T _model = locator<T>(); @override void initState() { if(widget.onModelReady != null) { widget.onModelReady(_model); } super.initState(); } @override Widget build(BuildContext context) { return ScopedModel<T>( model: _model, child: ScopedModelDescendant<T>( child: Container(color: Colors.red), builder: widget._builder)); } }
而后更新你的SuccessView
,在onMondelReady
属性里传入你要调用的方法。
class SuccessView extends StatelessWidget { final String title; SuccessView({this.title}); @override Widget build(BuildContext context) { return BaseView<SuccessModel>( onModelReady: (model) => model.fetchDuplicatedText(title), builder: (context, child, model) => BusyOverlay( show: model.state == ViewState.Busy, child: Scaffold( body: Center(child: Text(model.title)), ))); } }
最后在导航的时候传入参数。
Navigator.push(context, MaterialPageRoute(builder: (context) = > SuccessView(title: 'Pass in from home')));
这样就能够了。如今你能够在ScopedModel架构下跑起来你的app了。
本文基本覆盖了使用ScopedModel开发app所须要的所有内容。在这个时候你已经能够来实现你本身的服务了。一个很重要可是本文没有提到的问题是测试。
咱们也能够经过构造函数来实现依赖注入,好比经过构造函数的依赖注入来往model里注入service。这样咱们也能够注入一些假的service。我(做者)是没有对model层作测试的,由于他们都是彻底的依赖于服务层。而服务层我都作了充分的测试。