
一般原则
Flutter是一个反应式框架,对于专门从事本机开发的开发人员而言,他的哲学可能并不寻常。 因此,我们从简短的回顾开始。
与大多数现代框架一样,Flutter上的用户界面由一棵组件(小组件)树组成。 组件更改时,将重新呈现此组件及其所有子组件(通过内部优化,如下所述)。 当显示全局更改(例如,旋转屏幕)时,将重绘整个窗口小部件树。
这种方法看似无效,但实际上它使程序员可以控制工作速度。 如果您在不需要的情况下以最高级别更新界面,那么一切都会缓慢进行,但是使用正确的窗口小部件布局,Flutter上的应用程序会非常快。
Flutter有两种类型的小部件-无状态和有状态。 前者(类似于React中的Pure Components)没有状态,并由其参数完整描述。 如果显示条件没有发生变化(例如,应在其中显示小部件的区域的大小)及其参数,则系统将重用先前创建的小部件的可视表示形式,因此使用无状态小部件会对性能产生良好的影响。 在这种情况下,无论如何,每次重新绘制窗口小部件时,都会正式创建一个新对象并启动构造函数。
有状态的小部件在渲染之间保持某种状态。 为此,用两个类来描述它们。 类的第一个是小部件本身,它描述在每次渲染期间创建的对象。 第二类描述小部件的状态,并将其对象转移到创建的小部件对象。 有状态状态窗口小部件是界面重绘的主要来源。 为此,您需要在对SetState方法的调用内更改其属性。 因此,与许多其他框架不同,Flutter不具有隐式状态跟踪-在SetState方法外部对小部件的属性进行任何更改都不会导致重新绘制接口。
现在,在描述了基础知识之后,您可以从一个使用无状态和有状态小部件的简单应用程序开始:
基础应用import 'dart:math'; import 'package:flutter/material.dart'; void main() => runApp(new MyApp()); class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return new MaterialApp( title: 'Flutter Demo', theme: new ThemeData( primarySwatch: Colors.blue, ), home: Scaffold( appBar: AppBar( title: Text('Sample app'), ), body: new MyHomePage(), ), ); } } class MyHomePage extends StatefulWidget { @override _MyHomePageState createState() => _MyHomePageState(); } class _MyHomePageState extends State<MyHomePage> { Random rand = Random(); @override Widget build(BuildContext context) { return new ListView.builder(itemBuilder: (BuildContext context, int index) { return Text('Random number ${rand.nextInt(100)}',); }); } }
完整的例子
如果您需要更顽强的条件
让我们继续前进。 小部件的状态保持在重绘界面之间,但是只要需要小部件,即 确实位于屏幕上。 让我们进行一个简单的实验-将列表放在标签上:
标签应用 class _MyHomePageState extends State<MyHomePage> with SingleTickerProviderStateMixin { Random rand = Random(); TabController _tabController; final List<Tab> myTabs = <Tab>[ new Tab(text: 'FIRST'), new Tab(text: 'SECOND'), ]; @override void initState() { super.initState(); _tabController = new TabController(vsync: this, length: myTabs.length); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Sample app'), ), body: new TabBarView( controller: _tabController, children: [ new ListView.builder(itemBuilder: (BuildContext context, int index) { return Text('Random number ${rand.nextInt(100)}',); }), Text('Second tab'), ],), bottomNavigationBar: new TabBar( controller: _tabController, tabs: myTabs, labelColor: Colors.blue, ), ); } }
完整的例子
在启动时,您可以看到在选项卡之间切换时,状态被删除(调用dispose()方法),返回状态时,将再次创建状态(initState()方法)。 这是合理的,因为存储不可显示的小部件的状态将消耗系统资源。 如果小部件的状态必须在其完全隐藏的状态下仍然存在,则可以采用以下几种方法:
首先,您可以使用单独的对象(ViewModel)来存储状态。 语言级别的Dart支持工厂构造函数,可用于创建存储必要数据的工厂和单调。
我更喜欢这种方法,因为 它使您能够将业务逻辑与用户界面隔离。 由于Flutter Release Preview 2增加了为iOS创建像素完美界面的功能,因此这一点尤其如此,但是您当然需要在相应的小部件上执行此操作。
其次,当数据存储在位于上游的组件中时,可以使用React程序员熟悉的状态提升方法。 由于Flutter仅在调用setState()方法时才重绘接口,因此可以更改和使用此数据而无需渲染。 这种方法稍微复杂一些,并增加了结构中小部件的连接性,但是允许您逐点指定数据存储级别。
最后,还有诸如flutter_redux之类的状态存储库。
为简单起见,我们使用第一种方法。 让我们创建一个单独的ListData类(单例),该类存储列表的值。 在显示时,我们将使用此类。
选项卡式数据恢复应用程序 class _MyHomePageState extends State<MyHomePage> with SingleTickerProviderStateMixin { TabController _tabController; final List<Tab> myTabs = <Tab>[ new Tab(text: 'FIRST'), new Tab(text: 'SECOND'), ]; @override void initState() { super.initState(); _tabController = new TabController(vsync: this, length: myTabs.length); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Sample app'), ), body: new TabBarView( controller: _tabController, children: [ new ListView.builder(itemBuilder: ListData().build), Text('Second tab'), ],), bottomNavigationBar: new TabBar( controller: _tabController, tabs: myTabs, labelColor: Colors.blue, ), ); } } class ListData { static ListData _instance = ListData._internal(); ListData._internal(); factory ListData() { return _instance; } Random _rand = Random(); Map<int, int> _values = new Map(); Widget build (BuildContext context, int index) { if (!_values.containsKey(index)) { _values[index] = _rand.nextInt(100); } return Text('Random number ${_values[index]}',); } }
完整的例子
保存滚动位置
如果从上一个示例中向下滚动列表,然后在各个选项卡之间切换,则很容易注意到滚动位置未保存。 这是合乎逻辑的,因为它没有存储在我们的ListData类中,并且小部件的自身状态无法在选项卡之间切换时幸免。 我们手动实现滚动状态存储,但为了娱乐起见,我们不会将其添加到单独的类和ListData中,而不会添加到更高级别的状态中,以显示如何使用它。
注意ScrollController和NotificationListener小部件(以及以前使用的DefaultTabController)。 没有自己显示的窗口小部件的概念应为使用React / Redux的开发人员所熟悉-容器组件已在此捆绑包中积极使用。 在Flutter中,非显示窗口小部件通常用于向子窗口小部件添加功能。 这使您可以使可视窗口小部件本身保持轻巧状态,而不用在不需要它们的地方处理系统事件。
该代码基于MarcinSzałek在Stakoverflow( https://stackoverflow.com/questions/45341721/flutter-listview-inside-on-a-tabbarview-loses-its-scroll-position )上提出的解决方案。 该计划如下:
- 将ScrollController添加到列表中以使用滚动位置。
- 将NotificationListener添加到列表以传递滚动状态。
- 我们将滚动位置保存在_MyHomePageState(位于选项卡上方的一级)中,并将其与列表滚动相关联。
具有保存滚动位置的应用 class _MyHomePageState extends State<MyHomePage> with SingleTickerProviderStateMixin { double listViewOffset=0.0; TabController _tabController; final List<Tab> myTabs = <Tab>[ new Tab(text: 'FIRST'), new Tab(text: 'SECOND'), ]; @override void initState() { super.initState(); _tabController = new TabController(vsync: this, length: myTabs.length); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Sample app'), ), body: new TabBarView( controller: _tabController, children: [new ListTab( getOffsetMethod: () => listViewOffset, setOffsetMethod: (offset) => this.listViewOffset = offset, ), Text('Second tab'), ],), bottomNavigationBar: new TabBar( controller: _tabController, tabs: myTabs, labelColor: Colors.blue, ), ); } } class ListTab extends StatefulWidget { ListTab({Key key, this.getOffsetMethod, this.setOffsetMethod}) : super(key: key); final GetOffsetMethod getOffsetMethod; final SetOffsetMethod setOffsetMethod; @override _ListTabState createState() => _ListTabState(); } class _ListTabState extends State<ListTab> { ScrollController scrollController; @override void initState() { super.initState(); //Init scrolling to preserve it scrollController = new ScrollController( initialScrollOffset: widget.getOffsetMethod() ); } @override Widget build(BuildContext context) { return NotificationListener( child: new ListView.builder( controller: scrollController, itemBuilder: ListData().build, ), onNotification: (notification) { if (notification is ScrollNotification) { widget.setOffsetMethod(notification.metrics.pixels); } }, ); } }
完整的例子
遇到应用程序关闭
在应用程序运行期间保存信息是件好事,但是通常您希望在会话之间保存信息,尤其是考虑到在内存不足时操作系统关闭后台应用程序的习惯。 Flutter中用于持久数据存储的主要选项是:
- 共享首选项( https://pub.dartlang.org/packages/shared_preferences )是NSUserDefaults(在iOS上)和SharedPreferences(在Android上)的包装,并允许您存储少量的键/值对。 非常适合存储设置。
- sqflite( https://pub.dartlang.org/packages/sqflite )是用于使用SQLite的插件(有一些限制)。 同时支持低级查询和助手。 此外,与Room相似,它允许您使用数据库架构版本并设置用于在更新应用程序时更新架构的代码。
- Cloud Firestore( https://pub.dartlang.org/packages/cloud_firestore )是官方FireBase插件家族的一部分。
为了演示,我们将滚动状态保存在“共享”首选项中。 为此,请在初始化_MyHomePageState状态时添加滚动位置的恢复,并在滚动时进行保存。
在这里,我们需要详细介绍异步Flutter / Dart模型,因为所有外部服务都可以异步调用。 该模型的操作原理与node.js相似-存在一个执行主线程(thread),该主线程被异步调用中断。 在随后的每个中断(UI使它们连续不断)时,将处理已完成的异步操作的结果,同时,可以在后台线程中运行大量计算(通过计算功能)。
因此,在SharedPreferences中写入和读取是异步完成的(尽管该库允许从缓存中进行同步读取)。 首先,我们将处理阅读。 异步数据检索的标准方法如下所示-启动异步过程,完成异步过程后,执行SetState,写入接收到的值。 结果,将使用接收到的数据更新用户界面。 但是,在这种情况下,我们不是在处理数据,而是在滚动位置。 我们不需要更新接口,我们只需要在ScrollController上调用jumpTo方法。 问题在于,处理异步请求的结果可以随时返回,并且不一定是滚动内容和滚动位置。 为了确保在完全初始化的接口上执行操作,我们需要...仍在setState中滚动。
我们得到如下代码:
状态设定 @override void initState() { super.initState(); //Init scrolling to preserve it scrollController = new ScrollController( initialScrollOffset: widget.getOffsetMethod() ); _restoreState().then((double value) => scrollController.jumpTo(value)); } Future<double> _restoreState() async { SharedPreferences prefs = await SharedPreferences.getInstance(); return prefs.getDouble('listViewOffset'); } void setScroll(double value) { setState(() { scrollController.jumpTo(value); }); }
有了记录,一切都会变得更加有趣。 事实是,在滚动过程中,有关此事件的事件不断发生。 每次更改值时开始异步记录可能会导致应用程序错误。 我们只需要处理链中的最后一个事件。 在反应式编程方面,这称为反跳,我们将使用它。 Dart支持通过数据流进行反应式编程的主要功能,因此我们需要从滚动位置更新创建一个流并进行订阅,然后使用Debounce对其进行转换。 要进行转换,我们需要stream_transform库 。 作为一种替代方法,您可以使用RxDart并根据ReactiveX进行工作。
原来的代码如下:
状态记录 StreamSubscription _stream; StreamController<double> _controller = new StreamController<double>.broadcast(); @override void initState() { super.initState(); _tabController = new TabController(vsync: this, length: myTabs.length); _stream = _controller.stream.transform(debounce(new Duration(milliseconds: 500))).listen(_saveState); } void _saveState(double value) async { SharedPreferences prefs = await SharedPreferences.getInstance(); await prefs.setDouble('listViewOffset', value); }
完整的例子