今年I/O大会关于Flutter部分最大的动作之一就是Google Flutter团队宣布Flutter开始支持全平台,包括了移动设备(Android & IOS)、Web、PC(Windows、Mac和Linux)以及嵌入式设备 https://developers.googleblog.com/2019/05/Flutter-io19.html
足以证明Google对Flutter,甚至可预见是对Fuchsia寄予的厚望。
其实久仰Flutter的大名了,但是由于实际项目中没有用到,一直处于观望和碎片时间阅读一下文档和demo的状态。顺便说下,个人感觉对于技术,特别是这种在国内来说算是比较新的技术,一定要读原汁原味的英文原版文档,跟别人嚼过的还是不太一样的,而且还把英文练了。我就是在地铁上通勤时间读了dart 和 flutter的官方文档,阅读英文文档其实没有想象那么难。 dart-language-tour、flutter docs 。今年的I/O大会详细关注了和flutter、dart相关的部分。最近小组有一个项目恰好不用集成到系统大包中,仅供给测试人员使用,所以就用了flutter来开发,所以有很多感触,今天整理一下。
闲言碎语不多讲了,先说下初体验吧:
首先是爽,dart的现代化语法、flutter返璞归真的思想(一切皆widget)都让人感觉爽,当然,还有官方最为津津乐道的Hot Reload。
再说说问题和疑惑吧:在上文说到的那个项目中,我兴致勃勃写完之后,发现很完美,所有的UI和交互都完成了,并且刻意用了一些Cupertino的Widget,发现确实比用原生的组件要漂亮。但是总感觉哪里不对。原来所有的代码都在一个文件中,UI、数据、交互都在一个叫做MyHomePage的StatefulWidget中完成了,刷新UI的时候简单的setState就搞定了。难道真是这么玩吗?这不是在开历史倒车吗?Android实践积累出来的各种架构思想,MVC、MVP、MVVM、Clean等等,不就是为了将视图、数据、usecase解耦吗?难道这些最佳实践都和flutter无关?另外每次都刷新当前页面的每一个Widget岂不是很不环保?
带着上述疑问,看了一些flutter的开源项目,还有一些文章,感觉之前的想法就像最开始把所有代码都堆到Activity时的想法是类似的。对了,I/0大会有一场是专门讲这个的,感觉很精彩:Pragmatic State Management in Flutter (Google I/O’19),
还有这篇文章也很有帮助:Flutter app architecture 101: Vanilla, Scoped Model, BLoC
本文部分内容直接引述或翻译上述视频以及文字材料
先奉上一段感觉很有道理的话:
真的算是计算机哲学了😄
事实上,并没有一种万能的架构模式能够应对所有的产品形态和需求,但是形形色色的需求都最终不离其宗,一个典型的手机互联网产品做的事情无非是如下几种:
- 从网络上传/下载数据
- 遍历,转换,准备数据,然后展示给用户
- 向数据库保存数据/从数据库获取数据
本文将通过一个简单的从网络拉取数据并展示的例子,介绍以下几种flutter现阶段广泛使用的架构:
- SetState
- Scoped Model
- BLoC
- Provider
实现效果如下:
网络请求和数据封装
封装了一个Repository类,提供一个返回值为Future
|
|
SetState
在Flutter app architecture 101: Vanilla, Scoped Model, BLoC文章中把它叫做”Vanilla”,它也是用AS等IDE新建一个Flutter项目时,默认生成的演示代码中在使用的一种方式,简单来说,就是当数据来时,刷新用户可见的那个root widget,它会遍历地刷新每一个children。
|
|
当SetStatePage这个widget的State被create时,会调用_SetStatePageState的build函数,从这里去创建UI。在这个过程中,即通过初始状态对绘制哪一个Widget做了决策:
|
|
当用户做出点击动作时为了回馈,展示给用户一个Loading的UI状态,调用了State类的setState函数;在从网络拿到数据之后,把数据展现给用户时,又调了一次setState:
|
|
调用 setState函数会通知framework,当前类对象的内部状态已经发生了变化,会对当前Widget以及Widget树的UI造成影响,所以framework就执行了当前State类对象(本例中是_SetStatePageState)的build方法,从而导致_SetStatePageState内所有的Widget树都会刷新。
使用setState的优点是,简单直观,也不用引入第三方库。
缺点是显而易见的:
- 首先,整个widget数都会刷新,不管你预期要更新的是那部分。比如上述demo中的AppBar是不需要刷新的,造成性能浪费
- Widget不但承担了UI的展示,而且还处理业务逻辑、处理数据、管理状态。这显然违反了单一职责原则。
- 这样的代码可扩展性和维护性极差,像极了所有的东西堆在一个Activity中的方式
Scoped Model
Scoped Model不是flutter 框架中自带的,是一个第三方的库,地址在:https://pub.dev/packages/scoped_model。要使用Scoped Model,需要在pubspec.yaml的dependencies 下导入,可以看到最新的版本是1.0.1:
|
|
Scoped Model库的官方描述如下:
A set of utilities that allow you to easily pass a data Model from a parent Widget down to it’s descendants. In addition, it also rebuilds all of the children that use the model when the model is updated. This library was originally extracted from the Fuchsia codebase.
大概意思是Scoped Model提供了这样的一组工具包,你可以用他轻松地把数据实体从父Widget传递给所有需要的子孙Widget。此外它还可以在对应的数据更新的时候,重新构建(刷新)视图。
我们还是用和上面同样功能的一个例子来演示一下:
|
|
与SetState不同,使用Scoped Model最大的特点就是不用通过Widget的setState()函数来更新视图了,这样就可以做到最经济环保——哪个Widget需要关注什么数据的变化,就监听这个数据的Model,按需刷新。下文要讲的BLoC、Provider也都是这样。
示例中的ScopedModelDescendant
CityModel的代码如下,核心在于notifyListeners():
|
|
我们还可以看到,使用Scoped Model之后另一个好处就是,视图层ScopedModelPage不用承担数据和状态的管理和业务逻辑的职责了,只负责绘制和更新视图。
总结一波:
使用Scoped Model的好处:
- 把业务逻辑、数据、状态管理与UI解耦,较SetState更符合单一职责原则
- 部分刷新UI,减少不必要的开销
- 易学易用
缺点:
- 需要集成第三方库
- 需要在每次状态和数据刷新的时候都要调用notifyListeners(),如果数据和业务逻辑越来越复杂,这部分也不太好维护
BLoC
BLoC的全称是Business Logic Components,是Google开发和使用的一套设计模式,BLoC的设计思想大概是这样的:
- 通过Stream来管理数据,就像一个管道。其中StreamSink可以理解为数据流的“入口”,Steam可以理解为“出口”
- StreamController用来管理Stream
- 在页面上使用StreamBuilder这个Widget来管理数据流,省去了手动维护数据流的订阅以及Widget的重绘
|
|
来看下CityBloc这个类:
|
|
优点:
- Google官方推荐,使用范围较广
- 分离了展示逻辑和业务逻辑
- 不需要使用第三方库
- 响应式编程,天然也支持异步。也不需要像Scoped Model那样在数据变化之后notifyListeners()
- 对于Android开发者来说很熟悉,因为很像ViewModel、LiveData
缺点:
- 对于数据流的操作sink、stream,都是异步的
- 可能对Stream、RXdart有一定的了解和经验
Provider
在I/O大会上展示时Pragmatic State Management in Flutter (Google I/O’19) Matt “vindicate”他自己的方式就是使用了Provider
据称Provider是被设计为ScopedModel的替代品,是google最近才推出的一个工具库。看官方介绍:
A dependency injection system built with widgets for widgets. provider is mostly syntax sugar for InheritedWidget, to make common use-cases straightforward.
是一种用widget为widget提供依赖注入的框架(怎么突然想到了请君入瓮这个词儿),提供了一种InheritedWidget 的语法糖(其实ScopedModel也是借助InheritedWidget来实现的),让使用者觉得更加好用。
目前provider还是需要导入的方式进行依赖:
|
|
和Scoped_model一样,Provide也是借助了InheritWidget,将共享状态放到顶层MaterialApp之上。底层部件通过Provier获取该状态,并通过混合ChangeNotifier通知依赖于该状态的组件刷新。
|
|
CitySchedule:
|
|
优点:
- 使用方式接近ScopedModel,简单好用,学习成本低
- 还可以很方便的支持多状态的动态更新,MultiProvider,比如说很方便地刷新ListView的item data
- 官方推荐,应该会越来越强大
缺点:
- 目前还是需要作为第三方库引入
- 据说Provide.stream目前还有一些问题:https://juejin.im/post/5c6d4b52f265da2dc675b407#heading-11,不知道现在有没有修复
另外,根据provider官方的解释和I/O大会上演示的场景,似乎更适合一个widget影响另一个widget的状态时使用,比如I/O大会上用户操作Slider时对PieChart的影响,也就是说一个widget不应该知道和它无干系的另一个widget的存在(这就是为什么flutter没有findViewById类似的东西)。但是不知道provider适不适合网络数据驱动UI改变这种场景?
示例源码
https://github.com/guoxiaojiang/flutter_architecture
参考
Flutter app architecture 101: Vanilla, Scoped Model, BLoC