重庆分公司,新征程启航
为企业提供网站建设、域名注册、服务器等服务
最近公司做技术分享写的文章的demo
成都创新互联网站建设由有经验的网站设计师、开发人员和项目经理组成的专业建站团队,负责网站视觉设计、用户体验优化、交互设计和前端开发等方面的工作,以确保网站外观精美、成都做网站、网站建设易于使用并且具有良好的响应性。
Flutter中的InheritedWidget状态管理
1.InheritedWidget是什么?
InheritedWidget是Flutter中非常重要的一个功能型组件,它提供了一种数据在widget树中从上到下传递、共享的方式,比如我们在应用的根widget中通过InheritedWidget共享了一个数据,那么我们便可以在任意子widget中来获取该共享的数据!这个特性在一些需要在widget树中共享数据的场景中非常方便!如Flutter SDK中正是通过InheritedWidget来共享应用主题(Theme)和Locale (当前语言环境)信息的。
InheritedWidget和React中的context功能类似,和逐级传递数据相比,它们能实现组件跨级传递数据。InheritedWidget的在widget树中数据传递方向是从上到下的,这和通知Notification的传递方向正好相反。
2.源码分析
InheritedWidget
先来看下InheritedWidget的源码:
abstract class InheritedWidget extends ProxyWidget { const InheritedWidget({ Key key, Widget child }): super(key: key, child: child); @override InheritedElement createElement() =InheritedElement(this); @protected bool updateShouldNotify(covariant InheritedWidget oldWidget);}
它继承自ProxyWidget:
abstract class ProxyWidget extends Widget { const ProxyWidget({ Key key, @required this.child }) : super(key: key); final Widget child;}
可以看出Widget内除了实现了createElement方法外没有其他操作了,它的实现关键一定就是InheritedElement了。
InheritedElement 来看下InheritedElement源码
class InheritedElement extends ProxyElement { InheritedElement(InheritedWidget widget) : super(widget); @override InheritedWidget get widget = super.widget; // 这个Set记录了所有依赖的Elementfinal MapElement, Object _dependents = HashMapElement, Object();
//该方法会在Element mount和activate方法中调用,_inheritedWidgets为基类Element中的成员,用于提高Widget查找父节点中的InheritedWidget的效率,它使用HashMap缓存了该节点的父节点中所有相关的InheritedElement,因此查找的时间复杂度为o(1) @override void _updateInheritance() {final MapType, InheritedElement incomingWidgets = _parent?._inheritedWidgets;if (incomingWidgets != null) _inheritedWidgets = HashMapType, InheritedElement.from(incomingWidgets); else _inheritedWidgets = HashMapType, InheritedElement(); _inheritedWidgets[widget.runtimeType] = this; }
//该方法在父类ProxyElement中调用,看名字就知道是通知依赖方该进行更新了,这里首先会调用重写的updateShouldNotify方法是否需要进行更新,然后遍历_dependents列表并调用didChangeDependencies方法,该方法内会调用mardNeedsBuild,于是在下一帧绘制流程中,对应的Widget就会进行rebuild,界面也就进行了更新 @override void notifyClients(InheritedWidget oldWidget) { assert(_debugCheckOwnerBuildTargetExists('notifyClients'));for (Element dependent in _dependents.keys) { notifyDependent(oldWidget, dependent); } }
其中_updateInheritance方法在基类Element中的实现如下:
void _updateInheritance() {
_inheritedWidgets = _parent?._inheritedWidgets;
}
总结来说就是Element在mount的过程中,如果不是InheritedElement,就简单的将缓存指向父节点的缓存,如果是InheritedElement,就创建一个缓存的副本,然后将自身添加到该副本中,这样做会有两个值得注意的点:
InheritedElement的父节点们是无法查找到自己的,即InheritedWidget的数据只能由父节点向子节点传递,反之不能。
如果某节点的父节点有不止一个同一类型的InheritedWidget,调用inheritFromWidgetOfExactType获取到的是离自身最近的该类型的InheritedWidget。
看到这里似乎还有一个问题没有解决,依赖它的Widget是在何时被添加到_dependents这个列表中的呢?
回忆一下从InheritedWidget中取数据的过程,对于InheritedWidget有一个通用的约定就是添加static的of方法,该方法中通过inheritFromWidgetOfExactType找到parent中对应类型的的InheritedWidget的实例并返回,与此同时,也将自己注册到了依赖列表中,该方法的实现位于Element类,实现如下:
@overrideT dependOnInheritedWidgetOfExactType
// 这里通过上述mount过程中建立的HashMap缓存找到对应类型的InheritedElement final InheritedElement ancestor = _inheritedWidgets == null ? null : _inheritedWidgets[T];if (ancestor != null) { assert(ancestor is InheritedElement);return dependOnInheritedElement(ancestor, aspect: aspect); } _hadUnsatisfiedDependencies = true; return null;}
@overrideInheritedWidget dependOnInheritedElement(InheritedElement ancestor, { Object aspect }) { assert(ancestor != null);
// 这个列表记录了当前Element依赖的所有InheritedElement,用于在当前Element deactivate时,将自己从InheritedElement的_dependents列表中移除,避免不必要的更新操作 _dependencies ??= HashSetInheritedElement(); _dependencies.add(ancestor); ancestor.updateDependencies(this, aspect);return ancestor.widget;}
3.如何使用InheritedWidget
1)、创建一个类继承自Inheritedwidget
class InheritedContext extends InheritedWidget{ final InheritedTestModel inheritedTestModel; InheritedContext({ Key key, @required this.inheritedTestModel, @required Widget child}): super(key: key, child: child);static InheritedContext of (BuildContext context) { return context.dependOnInheritedWidgetOfExactTypeInheritedContext(); } @override bool updateShouldNotify(InheritedContext oldWidget) { return inheritedTestModel != oldWidget.inheritedTestModel; }}
2)、InheritedTestModel类为数据容器(这里定义了一个Listint数据源)
class InheritedTestModel{ final List _list; InheritedTestModel(this._list); List getList(){ return _list; }}
class ArrayListData{ static List _list ;static List getListData (){ _list = new List(); _list .add(1); _list .add(2); _list .add(3); _list .add(4);return _list ; }}
3)、定义一个Widget 使用 InheritedContext类的数据 InheritedTestModel
class ListDemo extends StatefulWidget{ @override State createState() { return new ListDemoState(); }}class ListDemoState extends StateListDemo{List _list; InheritedTestModel _inheritedTestModel; Timer _timer; Duration oneSec = const Duration(seconds: 1); @override void initState() { _list = ArrayListData. getListData (); _inheritedTestModel = new InheritedTestModel(_list); _timer = Timer.periodic(oneSec, (timer) { _doTimer(); }); } void _doTimer() { for(int i = 0; i _list.length; i++){ _list[i] = _list[i]+ 1; } setState(() { _inheritedTestModel = new InheritedTestModel(_list); }); }Widget _buildBody() { return Container(child: ListDemo2(), ); } @override Widget build(BuildContext context) { return InheritedContext(inheritedTestModel: _inheritedTestModel, child: Scaffold(appBar: AppBar(title: Text("ListDemo"), actions: Widget[ IconButton(icon: Icon(Icons. add ), ) ],), body: _buildBody(), ), ); } @override void dispose() { super.dispose();if (_timer != null) { _timer.cancel(); } }}
4)、在ListDemo中通过Timer更新InheritedTestModel 中的数据,然后再下一个Widget中获取更新的数据作为展示
class ListDemo2 extends StatefulWidget{ @override State createState() { return new ListDemoState2(); }}class ListDemoState2 extends StateListDemo2{InheritedTestModel _inheritedTestModel; Widget _buildListItem(BuildContext context,int index) { return Container(height: 50, width: 100, alignment: Alignment. center , child: Text(_inheritedTestModel.getList()[index].toString()), ); }Widget _buildBody() { _inheritedTestModel = InheritedContext. of (context).inheritedTestModel;return Container(child: ListView.builder(itemBuilder:(context, index)=_buildListItem(context,index),itemCount: _inheritedTestModel.getList().length,), ); } @override Widget build(BuildContext context) { return _buildBody(); }}
这样就可以在父widget中更新数据,子View不需任何操作直接从数据容器InheritedTestModel 中获取到更新后的新数据
这是一个数据共享的简单的例子,基本的流程,大致就是A去更新B的数据,A和B有一个共同的父类,实现数据的共享
4.上面说了原理和基本的使用,但是在实际项目当中,我当然不建议这样来使用,Google 已经为我们封装好了功能更加强大的插件Provider,其内部原理就是基于InheritedWidget来实现的,我们理解了基本原理,可以更好的在项目中运用Provider
在Tree中从上往下高效传递数据的基类widget , 定义为:abstract class InheritedWidget extends ProxyWidget
Flutter的响应式开发与React类似,数据都是自顶向下的。
假设有祖先组点A,中间经过结点B, C,然后到结点D,D需要从A中获取数据f,那按照自顶向下数据流转,f需要依次传递给B及C,最后才到C。这样开发极为不灵活,成本也比较高。所有Flutter需要有跨结点(只能是祖先后代节点,不能跨兄弟节点)高效传递数据的方案。
大体意思如下:
InheritedWidget 是在树中高效向下传递信息的基类部件;
调用[BuildContext.inheritFromWidgetOfExactType]方法可以从 BuildContext 中获取到最近的 InheritedWidget 类型的实例;
在 InheritedWidget 类型的控件被引用,也就是调用过 inheritFromWidgetOfExactType 方法后,当 InheritedWidget 自身状态改变时,会导致引用了 InheritedWidget 类型的子控件重构(rebuild)。
这里随便定义一个人 Person 类。
创建一个类继承 InheritedWidget,并实现 updateShouldNotify 方法。
之前说到调用[BuildContext.inheritFromWidgetOfExactType]方法可以从 BuildContext 中获取到最近的 InheritedWidget 类型的实例,所以此处定义一个静态的 of 方法,通过传入的 context 获取到最近的 InheriedDataWidget 实例。
1.定义数据模型
这里随便定义一个 Person 类。
2.自定义 InheritedWidget 控件类
创建一个类继承 InheritedWidget,并实现 updateShouldNotify 方法。
之前说到调用[BuildContext.inheritFromWidgetOfExactType]方法可以从 BuildContext 中获取到最近的 InheritedWidget 类型的实例,所以此处定义一个静态的 of 方法,通过传入的 context 获取到最近的 InheriedDataWidget 实例。
3.InheriedDataWidget 的使用
InheriedDataWidget 使用起来也很简单,它本身也是一个控件,只要在任意一个页面的子控件调用其构造方法就行,这里我们定义一个形如的 Widget 树。
WidgetA 是一个 StatefulWidget 类型的控件,可以调用 setState 刷新,如果是继承人 Stateless 类型的控件,那我们也可以通过 Stream 或者其他方式刷新数据,感兴趣的请看[什么是 Stream? Dart
WidgetA1_1 类
WidgetA1_2 类
WidgetA1_3 类
当我们点击 floatingActionButton 的时候,WidgetA1, WidgetA1_1, WidgetA1_2 的控件都会更新 Person 的信息,而且每点 floatingActionButton 一次, 当我们点击 floatingActionButton 的时候,WidgetA1, WidgetA1_1, WidgetA1_2 的控件都会更新 Person 的信息,而且每点 floatingActionButton 一次,都会输出:
如果我们试图在和 WidgetA 的同一层级的兄弟节点去访问 InheriedDataWidget 的 Person 数据,是不行的,因为父节点中并没有插入 InheriedDataWidget。
把 WidgetB 和 WidgetA 保持同一节点
这也体现了 Inheried(遗传) 这一单词的特性,遗传只存在于父子。兄弟不存在遗传的关系。
这种数据共享的方式在某些场景还是很有用的,就比如说全局主题,字体大小,字体颜色的变更,只要在 App 根层级共享出这些配置数据,然后在触发数据改变之后,所有引用到这些共享数据的地方都会刷新,这换主题,字体是不是就很轻松,事实上 Theme.of(context).primaryColor 之流就是这么干的。
以上就是有关InheritedWidget的使用。
自己也是从事Android开发5年有余了;整理了一些Android开发技术核心笔记和面经题纲,有关更多Android开发进阶技术资料、面经题纲、核心技术笔记; 想要进阶自己、拿高薪的同学请私信我回复“核心笔记”或“面试”领取!
hello world例子
在终端打印字符串‘Hello World!’
计算斐波那契数列
一个简单的类
计算两点距离
异步并发示例
使用了Isolate
1.面向对象
对于面向对象这个概念,相信了解Java的同学对这个概念一定不会陌生。
例如:我们有个Person Object他有很多特征和行为。
这些都是这个Person Object的属性。
也就是因为有了这些特征,行为等等才决定了这个人是谁。
那么回到Dart当中,所有的都是对象,那么在就可以可以跟进对象的属性的特征,方法等等来进行编程。
之所以我们在这里又特别强调了一下面向对象的概念,是因为这个概念在Dart语言当中,至关重要!
2.最重要的几个概念
3.Dart的部分特性
4.Dart的内置库
包名
描述
dart:asynv
异步编程,提供Future,Stream类
dart:collection
集合
dart:convert
不同类型的字符编码解码
dart:core
Dart语言核心功能,内置类型
dart:html
网页开发用到的库
dart:io
文件读写,IO相关
dart:math
数字常量以及函数,随机算法等
dart:svg
事件和动画矢量图支持
综上述所说要想学Flutter,先学Dart!关于Flutter下篇我会讲到。为什么离不开dart!
有关更多面经、核心技术笔记;自己也是从事Android开发5年有余了;整理了一些Android开发技术核心笔记和面经题纲, 如有需要的同学请私信我回复“核心笔记”或“面试”领取!
本文面向 Flutter 初学者,旨在用易懂的方式带大家入门。除了 Flutter 代码,还会介绍到语法、原理、特性等基础知识。相信本文能帮助你学习和理解 Flutter。
我们先看一下目前的一些跨平台方案,从前端渲染的角度来分类的话,大致可以分为以下几种方案。
WebView 渲染
这种方案就很好理解,现在很多项目都会嵌入 H5 的页面。就是用 JavaScript 等前端技术进行开发,在客户端上用 WebView 来进行渲染。微信小程序目前使用的就是这种方案。
它的优点很明显,使用成熟的前端技术进行开发,学习成本低,开发效率高,并且支持动态发布代码。
但缺点也很明显,在性能体验上,和原生还是存在较大差距的。
原生控件渲染
既然 WebView 的性能不够好,于是就有了使用原生控件进行渲染的方案。这种方案,同样也是使用 JavaScript 开发,区别是它最终是调用原生控件进行渲染的。这种方案的代表是 Facebook 的 React Native。
由于使用原生控件进行渲染,性能体验也会更接近原生。但也只是更接近,和原生还是有差距的,因为它需要频繁的进行 JavaScript 和原生之间的通信,这个通信效率是比较低的。
另外,由于需要适配各个平台的控件,那就有可能出现,系统控件更新了,而框架本身还没有更新,由此产生了一些问题。换句话说,这种方案是受到原生控件限制的。
绘图引擎渲染
接下来就是主角了。
在前端,如果完全不使用原生控件,我们可以通过系统的绘图 API 绘制出一个用户界面。从这个角度出发,可以在各个平台使用一个统一接口的绘图引擎来进行界面绘制,这个引擎最终调用的是系统的 API 绘制的。这样的话,它的性能可以做到接近原生,并且又不受原生控件的限制,在不同平台上能够做到 UI 统一。
Flutter 就是这样的一个开发框架。
一个跨平台 UI 解决方案
Flutter 是由 Google 开发的,一个跨平台 UI 解决方案。换句话说,它原则上只管 UI 的问题,如果涉及到平台本身的一些功能,比如调用蓝牙、摄像头,一般还是需要原生代码去操作。但现在也会有一些第三方库帮我们解决这些问题。
绘图引擎 Skia
Flutter 使用 Skia 作为它的绘图引擎。Skia 已经被 Google 收购,目前很多 Google 旗下的产品都是用 Skia 绘制的,包括 Android。
Android 内置了 Skia,但 iOS 没有,所以在打 iOS 安装包的时候,会把 Skia 一起打进去。这就导致了,用同一份 Flutter 代码打包之后,iOS 的包要比 Android 的包大一些。
开发语言 Dart
Flutter 使用的开发语言,叫 Dart。Dart 也是 Google 自家的,它是一门面向对象的语言,从它身上会看到一些其他开发语言的影子。学习起来难度不大的。
前面讲跨平台方案的时候,可以发现别的方案基本都是用 JavaScript 作为开发语言的,但为什么 Flutter 不用?就因为 Dart 是谷歌自家的吗?这个问题先留着,我们后面会提到。
这里部分就简单点带过了,具体的搭建流程可以在官网查看:
主要的搭建步骤如下:
下载 Flutter SDK
官网下载地址:
由于在国内访问可能受限,官方为中国开发者搭建了镜像:
更新环境变量
解压后,将 flutter\bin 的全路径添加到环境变量 PATH 中。
安装开发工具
理论上,任何文本编辑器都可以用来开发 Flutter 应用,但推荐的开发工具是 Android Studio、IntelliJ 以及 VS Code。因为在这些开发工具上,可以安装官方的 Flutter 和 Dart 插件,得到更好的开发体验。文章里使用 Android Studio 来演示。
如果你打算开发 iOS 应用,则还需要安装 Xcode。
安装插件
在开发工具的插件设置中,安装上面说到的 Flutter 和 Dart 插件。Flutter 插件用于支持 Flutter 的运行、调试、热重载等功能,而 Dart 插件则提供了代码的输入校验、代码补全等功能。
万物始于 Hello World,我们先来创建一个显示 Hello World 的 Flutter 项目。
在 Android Studio 的欢迎页面选择 Start a new Flutter project ,或者通过菜单栏的 File New New Flutter Project ,创建一个新的 Flutter 项目。
创建好的项目里面包含了 android 和 ios 两个文件夹,它们是标准的 Android 和 iOS 项目。我们的 Flutter 代码,存放在 lib 文件夹里。项目创建好后,会默认带一个计数器的示例,我们不管它,把 main.dart 的代码改成 Hello World:
启动一个模拟器,或者连上真机,点击 Run 运行一下,就能看这样一个界面了:
具体代码先混个眼熟就好,具体的后面会再讲到。
在写 Flutter 之前,还要先跟大家简单介绍一下 Dart 的语法。如果你有 Java 或 JavaScript 的开发经验,以及面向对象的编程思想,学起来是很快的。
我们可以在 test 文件夹下新建一个 dart 文件,用来写测试代码。
指定类型
var
但和 JavaScript 不同的是,以下代码在 JavaScript 是不会报错的,但在 Dart 里会报错:
Object
如果非要上面这样写,那也可以。把 var 换成 Object 就不报错了:
和 Java 类似,Object 是所有对象的根基类。但是这样的话,如果想打印一下 num 的字符串长度,是会报错的:
因为 length 是属于 String 的,但系统只知道 num 是一个对象,并不知道它是一个 String。
dynamic
如果还是非要这样写,那也可以。Dart 有一个特有的关键字 dynamic,把 Object 改成 dynamic 就不报错了:
我们运行一下这个文件,可以在控制台看到正确打印出了字符串长度。
函数
dynamic
在 Dart 里,函数也是可以不写返回类型的,不写的话会被当做 dynamic 来处理。这样的话,函数的类型就是 return 的类型,如果没有 return 则是 void 类型。比如可以这样:
运行之后是能正确打印出字符串长度的。
用于传参
Dart 里的函数也是一个对象,所以可以把函数作为参数来传递,比如:
可选参数
在 Dart 的函数传参里,有一个叫可选参数的概念,我们以文字控件 Text 为例,在源码里可以看到 Text 的构造函数是这样的:
首先,在参数里有一个 data,它是要显示的文字内容,是一个必填项。而 data 后面的一堆参数,是用一个大括号括起来的,这些参数就叫做可选参数,意思是这些参数可传可不传。
假如我们要显示一个比较长的文字,又想限制它最多显示两行,就可以这样来创建一个 Text:
可选参数,在 Flutter 里面用的非常多。
异步
Future
在 Dart 里使用 Future 来处理异步任务,比如我们现在延时一秒打印 666,代码如下:
Future 的语法和 Promise 非常像。任务执行成功会调用 then,执行失败会调用 catchError,而无论成功还是失败,都会调用 whenComplete。
async/await
如果你不喜欢上面那种写法,或者是想把异步转成同步,就可以用 async 和 await 这两个关键字来转换。
我们把上面的代码转换一下,写一个 getString 方法,返回的类型是 Future,它会延时返回一个字符串。在 main 函数后面加上 async 关键字,在 getString() 前面加上 await,代码如下:
运行之后可以看到,能正常延时一秒后,把字符串打印出来。这里 getString() 返回的类型是 Future,而 await getString() 则是返回了延时之后返回的字符串。await 要在 async 的函数里面才能使用。
async 和 await 其实是一个语法糖,它最终也是转换成 Future 调用链的形式执行的。
接下来回到 Flutter,Flutter 里最重要的一个概念是 Widget(下面翻译作控件)。
在原生开发里面,我们可能会在界面上区分,这是一个 View,这是一个 Layout,这是一个 View Controller。但在 Flutter 里面,它们全都属于一个统一的模型 Widget。可以说,在 Flutter 界面里,所有东西都是 Widget。
以前学面向对象的时候,我们都听过一句话,叫万物皆对象。我这里套用一下,在 Flutter 里, 万物皆控件 。
具体有哪些控件,我做了一下简单的分类。
根控件
所有的控件都属于 StatefulWidget 或 StatelessWidget 。它们的区别是,StatefulWidget 拥有状态 State ,而 StatelessWidget 没有。
StatefulWidget
当一个控件是可变的时候,就要使用 StatefulWidget 来构建。StatefulWidget 本身不可变,但它持有的状态 State 是可变的。
StatelessWidget
当一个控件状态是固定不可变的时候,就可以使用 StatelessWidget。前面我们写的 Hello World 就是使用 StatelessWidget。
容器控件
容器类控件一般是将某些属性或配置,作用在它的子控件上,比如控件所在的宽高、背景、位置等。
常用的容器控件有 Container、Center、Padding 等。
布局控件
布局控件可以类比作原生开发中的 Layout,通常它会拥有一个 children 的属性,用于接收一个控件数组,对这些控件进行特定的排版。
常用的布局控件有 Row、Column、Stack、Flex 等。
基础控件
基础控件就是常用的文字、按钮、图片等控件。
常用的基础控件有 Text、TextField、Button、Image 等。
功能控件
在 Flutter 里还有一类控件,它们不影响 UI 布局,但带有一些特定的功能,比如页面跳转、事件监听、定义主题等。我们把这一类控件称作功能控件。
常用的功能控件有 Navigator、NotificationListener、Theme 等。
开始写 Flutter 代码了。还记不记得,在 Flutter 项目创建之后,是自带一个计数器 demo 的,现在我们用自己的代码实现一遍。代码修改成如下:
运行之后,就可以看到这样的界面了:
按钮每点击一次,数字就会加一。下面我们来分析一下这段代码,看下里面用到的一些 Widget。
StatefulWidget
由于页面中的数字是跟随状态变化的,所以该页面改用 StatefulWidget。StatefulWidget 并不会直接返回一个 Widget,而是返回状态 State,在 State 里再返回 Widget。
Scaffold
Scaffold 是一个标准的 Material Design 页面,它包含了标题栏、浮动按钮、侧滑菜单、底部导航栏等配置。我们这里用到了标题栏 appBar、页面内容 body、浮动按钮 floatingActionButton。
AppBar
AppBar 就是标题栏,通过查看控件的构造方法,我们可以知道它可配置的属性。
AppBar 的可选参数除了标题 title,还可以配置标题前的内容 leading,右侧的操作按钮 anctions,控件垂直高度 elevation 等。我们只传了 title,其他属性都用默认值。
Center
Center 是一个容器类控件,它的作用就是让它的子控件居中显示。
FloatingActionButton
熟悉安卓开发的应该对这个控件比较熟悉,它就是页面右下角一个特定样式的 Button,参数里面的 onPressed 是一个必填项,要传一个点击之后的回调函数。
根据这个例子,下面给大家介绍一下 Flutter 两个比较重要的特性。
点击 Button 之后,我们把 num 变量加一,并使用 setState 通知状态发生了改变,Flutter 会根据新的状态更新 UI。如果有接触过小程序开发,setState 就和小程序的 setData 类似。
在 Flutter 里面我们不需要用 set 方法来更新 UI,可变控件是和状态绑定的,这就是 Flutter 的响应式 UI 编程。
在 Android Q 和 iOS 13 里都加入了暗黑模式,我们也换一个暗黑主题来玩一下。MaterialApp 里有一个 theme 的属性,我们把它配置一下:
这次改完之后不点 Run 了,我们点一下闪电图标 Flutter Hot Reload ,就能看到界面发生了变化:
这就是 Flutter 的 热重载 ,在修改完代码之后,通过热重载就能马上在设备上看到修改结果,可以很大程度上增加开发效率。
下面再给大家介绍几个 Flutter 里的常见操作。
在 Flutter 里,使用 Navigator 来管理页面跳转,比如要跳转到一个 NewPage 可以这样写:
进栈使用 push,出栈则是 pop。
使用 MaterialPageRoute 会模拟出 Android 上页面跳转的过场效果。
我们来看看怎么显示一张本地图片。
先在根目录新建一个存放图片的文件夹,比如叫 images,把图片 picture.png 放进去。
找到根目录下的 pubspec.yaml 文件,这个便是 Flutter 依赖配置文件,我们需要在这里配置一下刚才的图片:
这样,我们就能使用 Image 控件把这张图片显示出来了:
和 node 的 npm 以及 Android 的 jcenter 类似,Flutter 也拥有一个公共仓库 pub.dev。pub.dev 是 Google 官方的 Dart 仓库,在上面可以找到我们需要的包和插件。
Flutter 本身没有 Toast,我们来接入一个。在 pub.dev 上搜索后,我决定使用 fluttertoast:
按照说明,在 pubspec.yaml 文件里的 dependencies 下配置:
点一下 Android Studio 右上角的 Packages get 同步之后就可以使用了:
我们上面使用的都是 Material Design 的控件,它们都是在 flutter/material.dart 包里面的。如果要使用 iOS 风格的控件,则要用到 flutter/cupertino.dart 包:
iOS 风格的控件,基本都以 Cupertino 开头。我们把计时器页面里的控件替换一下:
效果如下:
代码的部分就到这里了,接下来跟大家聊一下编译方式,编程语言的编译方式有两种。
关于它们孰优孰劣,就要看从哪个角度去对比了。JIT 的话,它的一大特点就是支持动态发布代码,也就是支持热更新。但要是从性能的角度考虑,AOT 会更好,因为在运行的时候不用再进行编译的操作的,运行的效率会更高一些。
回到我们一开始的时候留下的问题,为什么别的跨平台方案都是用 JavaScript,而 Flutter 要用 Dart 来开发。JavaScript 的编译方式是 JIT 的,它不支持 AOT。而 Dart 同时支持 JIT 和 AOT。
Flutter 在开发阶段使用 JIT,让我们用上了热重载,增加了开发效率。在打包时改用 AOT,保证了正式版应用的性能。
最后讲一下大家比较关心的一个东西,Flutter 是否支持热更新?前面说到 Dart 支持 JIT,所以从技术层面它是支持的。但是目前是不支持的,在官方的计划文档中,可以看到:
至于原因,官方在这里进行了说明。总的来说,是由于政策的限制,以及出于对性能和安全性的考虑,暂时不支持了。
到这就结束啦。由于想把 Flutter 基础在一篇内讲完,没有涉及太多细节,如果要写 Flutter 代码还需要深入学习。但相信理解之后再学,会轻松很多。
Flutter支持稳定的桌面设备开发已经一段时间了,不得不说,Flutter多平台支持的特性真的很香。我本人并没有任何桌面开发的经验,但仍然使用Flutter开发出了一个桌面版小程序,功能很简单,就是对输入的json做格式化处理和转模型。
话不多说,先来看看实际效果。 项目源码地址
开发环境如下:
Flutter : 2.8.1
Dart : 2.15.1
IDE : VSCode
JSON作为我们日常开发工作中经常要打交道的一种数据格式,它共有6种数据类型: null , num , string , object , array , bool 。我们势必对它又爱又恨。爱他因为他作为数据处理的一种格式确实非常方便简洁。但是在我们做Flutter开发中,又需要接触到json解析时,就会感觉非常棘手,因为flutter没有反射,导致json转模型这块需要手写那繁杂的映射关系。就像下面这样子。
数据量少还能接受,一旦量大,那么光手写这个解析方法都能让你怀疑人生。更何况手写还有出错的可能。好在官方有个工具**json_serializable**可以自动生成这块转换代码,也解决了flutter界json转模型的空缺。当然,业界也有专门解析json的网站,可以自动生成dart代码,使用者在生成后复制进项目中即可,也是非常方便的。
本项目以json解析为切入点,和大家一起来看下flutter是如何开发桌面应用的。
要让我们的flutter项目支持桌面设备。我们首先需要修改下flutter的设置。如下,让我们的项目支持 windows 和 macos 系统。
接下来使用 flutter create 命令创建我们的模版工程。
创建完项目后,我们就可以 run 起来了。
先来看下整体界面,界面四块,分别为功能模块、文件选择模块、输入模块、输出模块。
我们在新建一个桌面应用时,默认的模版又一个Appbar,此时应用可以用鼠标拖拽移动,放大缩小,还可以缩到很小。但是,我们一旦去掉这个导航栏,那么窗口就不能用鼠标拖动了,并且我们往往不希望用户将我们的窗口缩放的很小,这会导致页面异常,一些重要信息都展示不全。因此这里需要借助第三方组件 bitsdojo_window 。通过 bitsdojo_window ,我们可以实现窗口的定制化,拖动,最小尺寸,最大尺寸,窗口边框,窗口顶部放大、缩小、关闭的按钮等。
通过 InkWell 组件,可以捕捉到手势、鼠标、触控笔的移动和停留位置
这个功能是鼠标移动后的UI交互界面。要在窗口上显示一个提示框,可以使用 Overlay 。需要注意的是,由于在 Overlay 上的 text 的根结点不是 Material 风格的组件,因此会出现黄色的下划线。因此一定要用 Material 包一下 text 。并且你必须给创建的 OverlayEntry 一个位置,否则它将全屏显示。
读取说表拖拽的文件一开始想尝试使用 InkWell 组件,但是这个组件无法识别拖拽中的鼠标,并且也无法从中拿到文件信息。因此放弃。后来从文章《Flutter-2天写个桌面端APP》中发现一个可读取拖拽文件的组件 desktop_drop ,能满足要求。
使用开源组件 file_picker ,选完图片后的操作和拖拽选择图片后的操作一致。
Textfield 如果要显示富文本,那么需要自定义 TextEditingController 。并重写 buildTextSpan 方法。
在做导出功能时遇到下列报错,保存提示为没有权限访问对应目录下的文件。
通过Apple的开发文档找到有关权限问题的说明。其中有个授权私钥的key为 com.apple.security.files.downloads.read-write ,表示 对用户的下载文件夹的读/写访问权限 。那么,使用Xcode打开Flutter项目中的mac应用,修改工程目录下的 DebugProfile.entitlements 文件,向 entitlements 文件中添加 com.apple.security.files.downloads.read-write ,并将值设置为YES,保存后重启Flutter项目。发现已经可以向下载目录中读写文件了。
当然,这是正常操作。还有个骚操作就是关闭系统的沙盒机制。将 entitlements 文件的 App Sandbox 设置为NO。这样我们就可以访问任意路径了。当然关闭应用的沙盒也就相当于关闭了应用的防护机制,因此这个选项慎用。
原文地址:
Flutter:支持多平台的轻量级UI框架
详见 Fuchsia 于2021年8月8日发布的文章: 为什么说 Flutter 充满机会:细数 Flutter 的优异特性和革命性创新之处