Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Flutter的项目实践 #35

Open
FrizzleFur opened this issue Apr 13, 2019 · 21 comments
Open

Flutter的项目实践 #35

FrizzleFur opened this issue Apr 13, 2019 · 21 comments
Assignees

Comments

@FrizzleFur
Copy link
Owner

学习Flutter还是为了在项目中利用Flutter的优势,提升开发效率和性能。

先分flutter的介绍,语法重点,底层原理,和项目实战,和注意坑点。

@FrizzleFur FrizzleFur self-assigned this Apr 13, 2019
@FrizzleFur
Copy link
Owner Author

FrizzleFur commented Apr 13, 2019

什么是Flutter

Flutter 是一个跨平台(Android 和 iOS)的移动开发框架,使用的是 Dart 语言。

Flutter 的目标是用来创建高性能、高稳定性、高帧率、低延迟的 Android 和 iOS 应用。并且开发出来的应用在不同的平台用起来跟原生应用具有一样的体验。不同的平台的原生体验应该得到保留,让该应用看起来同整个系统更加协调。不同平台的滚动操作、字体、图标 等特殊的特性 应该和该平台上的其他应用保持一致,让用户感觉就像操作原生应用一样。

为何选择使用 Flutter?

  • 开发效率高,一套代码可以开发出 Android 、 iOS、Fuchsia平台应用
  • 同样的功能只需要很少的代码,如果你只开发一个平台的应用,使用 时髦的、更具有表达性的开发语言,也可以让你用更少的代码来实现同样的功能。
  • 开发原型和迭代更加方便
  • 在应用运行的时候就可以修改代码并重新加载修改后的功能(hot reload)
  • 直接修改崩溃的 bug,然后继续从崩溃的地方执行调试
  • 创建优雅的、可定制的用户界面
  • Flutter采用 Materal Design 设计语言(规范),该规范定义了用户界面上的元素的用途、外观、展现形式以及形态变化的规范
  • 提供了可定制的 UI 框架,不再受制于手机平台控件的支持。
  • 完全从头设计的系统,不会有为了兼容考虑的历史包袱——比如 Java 虚拟机的慢速,又比如 Android 较慢的渲染速度。因此也可以把体积做的很小,塞到存储容量很小的设备中。

Flutter&ReactNative

ReactNative –> Facebook,Flutter–> Google

  • ReactNative的控件都是在原生控件上的扩展,如ReactNative里的ListView在android平台下的实现就是继承于android里的ListView。
  • 在Flutter中,它的所有 UI 组件都是一帧一帧画出来的。这样也能够很准确,也很灵活的做出你想要的 UI 。

@FrizzleFur
Copy link
Owner Author

FrizzleFur commented May 2, 2019

Flutter 与React Native 对比

  • 性能上的差距,由于ReactNative里的动画都是由js驱动的所以在动画处理或者高帧率刷新的界面上可能会有一点卡顿或者延迟。
  • 热加载,虽然ReactNative二次加载上也很快但是跟Flutter的 hot reload比起来真的还是有一定差距的,毕竟flutter的 hot reload是支持数据缓存的。
  • 开发语言,ReactNative 是使用js和xml驱动的,Flutter使用的Dart开发。
  • Flutter采用不同的方法来避免由于使用编译的编程语言而导致的性能问题。Flutter使用AOT编译成多个平台的本地代码。这使得Flutter可以与平台进行通信,而无需通过执行上下文切换的JavaScript桥。编译为本机代码也可以提高应用程序的启动时间。

[关于性能]

跨平台开发第一个考虑的就是性能问题

  • RN的效率由于是将View编译成了原生View,所以效率上要比基于Cordova的HTML5高很多,但是它也有效率问题,RN的渲染机制是基于前端框架的考虑,复杂的UI渲染是需要依赖多个view叠加.比如我们渲染一个复杂的ListView,每一个小的控件,都是一个native的view,然后相互组合叠加.想想此时如果我们的list再需要滑动刷新,会有多少个对象需要渲染.所以也就有了前面所说的RN的列表方案不友好;
  • Flutter 吸收了前两者的教训之后,在渲染技术上,选择了自己实现(GDI),由于有更好的可控性,使用了新的语言Dart,避免了RN的那种通过桥接器与Javascript通讯导致效率低下的问题,所以在性能方面比RN更高一筹;有经验的开发者可以打开Android手机开发者选项里面的显示边界布局,发现Flutter的布局是一个整体.说明Flutter的渲染没用使用原生控件进行渲染
  • [关于开发体验]
  • Hot Reload(热重载),这个对于原生开发者是一个福音,特别是Android开发者,Android原生改一个东西运行需要好久时间;对于Cordova和RN,差别不大,两个基本都可以支持热重载
  • Widget(组件)
  • 这点和RN的component有些类似,但是比RN更彻底,Flutter自身提供了很多基于 Material Design and Cupertino (iOS-flavor) 风格Widget,这点也和Google对它的定位有关(Flutter is Google’s mobile UI framework),大家在开发的时候更简单,特别对于美工UI设计师薄弱中小公司更是福音;​ 其他
  • Dart Flutter使用了新的开发语言,这个语言相信大家都没听说过,这个google推出的新的编程语言,属于动态语言,”结构化的web编程”语言,语法与Javascript 的ES6标准有些类似,有Java和C#语言基础,理解起来不难.缺点是程序猿们又要学习一门新语言了;
  • 除了效率和Widget之外,最期待的还是Flutter的渲染方式,不说跨平台的开发语言,就单单Android原生开发,不同的手机对不同控件的渲染可能就有不同,希望Flutter自己的渲染平台能解决这个问题

Flutter 和 ReactNative 的区别

从实现原理上来讲 ReactNative 提供的组件都是继承自原生 Native 的 View 组件,比如ReactNative 中的 ListView 在 Android 中就是继承自 ListView ,还有 RecycleView。然而 Flutter 则不同,它的所有 UI 组件都是一帧一帧画出来的。这样也能够很准确,也很灵活的做出你想要的 UI 。其次它还非常人性化的贴近了平台的特性,比如 Android 的 Material Design 在 Flutter 就默认支持了进去。其实话说回来,在开发者角度来讲这两个跨平台都是一样的使用效果,毕竟都是通过一套语言来搭建可运行不同平台的应用。然而,Flutter 使用 Dart 语言开发而 ReactNative 则使用 JS 结合 XML 来开发的。这就有问题了。

React Native VS Flutter评测

React Native较小的安装包带来的优势不仅是降低了下载门槛、减少了磁盘占用, 而且对于混合开发的压力也更小. 试想一个原生App因为几个页面使用了Flutter, 一行代码没写安装包就凭空增加了30MB, 这样的结果不是开发人员希望看到的.
启动速度RN依然与原生不相上下, 并且要比Flutter好上一个等级. RN虽然使用js来构建应用程序, 但最终RN会将js转化成原生代码. 这就让RN的运行效率高出Flutter好几倍.
内存占用方面, RN显然没有原生做的好, 与Flutter的差距也不大, 而且经测试发现, RN的内存占用不是很稳定, 页面刚生成的时候内存占用会高一点, 之后缓慢回落.

流言终结者- Flutter和RN谁才是更好的跨端开发方案?

  1. Flutter在高低端机的CPU上的表现都优于RN,尤其在低端的小米2s上有着更优的表现
  2. Android端在原来FPS基础上增加了流畅度的指标,FPS和流畅度的表现Flutter优于RN(计算规则见附参考文章)
  3. Android端的内存也是值得关注的一点,在小米2s上起始内存Flutter明显比RN多40M,RN在测试过程中内存飞涨,Flutter相比之下会更稳定,内存上RN侧的代码是需要调优的,同一套代码Flutter在Android和iOS上并没有很大的差异,但是RN的却要在单端调优,Flutter在这项比拼上又更胜一筹。 比较奇怪的是三星S8上Flutter和RN的初始内存是一致的,猜测是RN也Android高端机型上也会预分配一些内存,具体细节还需要更进一步的研究。

高频数据交换下Flutter与ReactNative的对比

  • ReactNative在满帧的情况下实际是卡住的,不能切换路由,后端服务关闭后仍然会执行一段时间,证明有事件堆积。
  • 而Flutter不会影响路由切换,因为Flutter的路由切换在ui主线程上,而react-navigation跑在js线程上。Flutter虽不满帧但可以实时更新数据。
  • 总结:在某些高性能场景下仍需要跨平台,Flutter是你唯一的选择
  • Flutter的性能还可以进一步优化,例如采用udp发送行情数据、protobuf替换掉json也会有比较好的收益

总结Flutter与React Native

  • Flutter为布局提供了丰富的组件类,具体请查看API https://flutterchina.club/docs/。但是相对比较麻烦的,比如Image组件就分为new Image.network(加载网络图片)和 Image.asset(加载本地图片),在测试中我使用过这个组件,但是不知道什么原因,width属性设置了也没见效果,height和其他的属性倒是没什么问题。
  • React Native带来了组件化开发的思想,所谓组件,即封装起来的具有独立功能的UI部件。React Native 推荐以组件的方式去重新思考UI构成,将UI上每一个功能相对独立的模块定义成组件,然后将小的组件通过组合或者嵌套的方式构成大的组件,最终完成整体UI的构建(摘自网络)。
  • React Native使用两种数据来控制一个组件:props和state。props是在父组件中指定,而且一经指定,在被指定的组件的生命周期中则不再改变。 对于需要改变的数据,则需要使用state(摘自网络)。
  • 在性能方面Flutter还是比React Native快一点,开发难度方面React Native比Flutter更加简洁方便,架构思想上React Native也相对较好。

@FrizzleFur
Copy link
Owner Author

FrizzleFur commented May 2, 2019

Dart

Dart是Google于2011年发布的一门开源编程语言,旨在帮助开发者克服JavaScript的缺点,当然现在Dart已经是一门全平台的语音,不仅可以开发服务端,现在借助于flutter也可以开发原生Android和Is应用。

Dart语言特点

  • 全平台开发语音
  • 每个对象都是一个类的实例,null在Dart中也是一种类型
  • Dart支持顶层函数、高阶函数
  • 生产和检查模式支持
  • 原生支持异步操作
var name ="flyou";
String name1="flyou";

var age =25;
int age1 =25;

void main(){
print(name);
 	print(name1);
	print(age);
 	print(age1);
}
// console
/*
flyou
flyou
25
25
*/
  • 在Dart中可以使用关键字 var 声明一个变量,Dart编译器会根据变量的值自动推断出变量的类型和使用已知类型声明变量的是一样的。

  • main()方法是Dart的入口方法,和JAVA中的main方法很类似。

常量

使用finalconst关键字都可以声明一个常量,但是与final不同的是const是编译期常量,也就是说在编译器编译时使用const声明的常量必须是已经确定的值。使用final 和const修饰的变量不可以从新赋值.

内置类型

  • numbers
  • strings
  • booleans
  • lists (和数组是一个概念)
  • maps 字典
  • runes ( Unicode 字符串)
  • symbols 符号

方法(函数)

Dart是完全面向对象的语言,函数在Dart中也是一种对象,也就是说函数也可以像其他参数一样在函数间传递。

bool isEvenNumber(int num) {
  return num % 2 == 0;
}

void main() {
  print(isEvenNumber(11));
}

控制台输出:

false

上面是函数定义的最基本的方法,和其他语言定义函数的方法基本一致。

对于只包含一个表达式的函数可以简写为以下的方式

bool isEvenNumber(int num) => num % 2 == 0;

可以使用=>替换原来的 return和{} 类似 => expr{ return expr; }

可选参数

void printStr1([String name,int age]){
  print("Name is $name age is $age");
}

在调用这个函数的时候我们可以选择只输入name或者只输入age,当然上面的函数如果我们只输入一个函数值另一个函数的值就会被赋为null

printStr1("flyou");

输出:

Name is flyou age is null

如果我输入一个int类型的值,Dart会将它自动赋值给第一个符合该类型的入参上

printStr1(25);

输出:

Name is null age is 25

如果函数有多个相同类型的函数参数,这个时候只输入一个值,它默认会赋值给第一个参数如:

void printStr2([String name,String sex,String address]){
 print("Name is $name age is $sex address is $address");
}

printStr2("男");

输出:

Name is 男 sex is null address is null

当然这并不是我们想要的,所以就需要看下可选择名称的函数

可选择名称的函数(具名函数)

void printStr3({String name,int age}){
 print3("Name is $name age is $age");
}

使用{}包括函数参数就可以把原来的函数变成可选择名称函数,在调用时可以直接指定参数名传值。
当然,我们也可以选择只传入一个参数,另一个参数在使用的地方做判断或者使用函数的默认值(下面会说)

printStr4(name: "flyou");

输出:

Name is flyou age is null

同样如果有多和相同类型的入参,我们可以根据声明参数名调用的方式来调用函数如:

void printStr3({String name,String sex,String address}){
print("Name is $name sex is $sex address is $address");
}

printStr3(sex: "男");
输出:

Name is null sex is 男 address is null

当然,就算我们少输入了一个参数但是每次都给我输出个null也非常的不友好啊,当然在Dart中你可以使用参数默认值来解决这个问题。

函数参数的默认值

函数的默认值,顾名思义就是你就算不输入这个参数我也会默认给你一个值的,当然前提是这个参数你可以省略的前提下(上面两类函数啊)

可选参数

void printStr1([String name,int age=25]){
  print("Name is $name age is $age");
}

printStr2("男");

输出:

Name is 男 sex is null address is 25

具名函数

void printStr3({String name,int age=25}){
 print3("Name is $name age is $age");
}

printStr3("男");

```输出:

Name is 男 sex is null address is 25

### 对象传递

函数可以把自己作为一个对象传递给另外一个函数


```dart
void printStr4(String str) {
print(str);

var listStr=["hello ","world","haha","!"];
  listStr.forEach(printStr4);

输出:

hello 

world

haha

!

@FrizzleFur
Copy link
Owner Author

操作符

描述 操作者
一元后缀 expr++ expr– () [] . ?.
一元前缀 -expr !expr ~expr ++expr –expr
乘除 * / % ~/
加减 + -
位操作 << >>
按位与 &
按位XOR ^
按位或 |
关系和判断 >= > <= < as is is!
相等与不相等 == !=
逻辑与 &&
逻辑OR ||
如果为null ??
三目运算 expr1 ? expr2 : expr3
级联 ..

异常处理

使用 Throw可以向上层抛出一个异常

例如:

void throwException(){

throw new FormatException("异常了");
}
这样就可以将一个异常跑向上层调用处,如果上层不处理这个异常则会引发崩溃

void main() {

 throwException();
}

异常信息

Unhandled exception:
FormatException: 异常了
#0      throwException (file:///D:/Learn1/learn/Learn4.dart:15:3)
#1      main (file:///D:/Learn1/learn/Learn4.dart:8:3)
#2      _startIsolate.<anonymous closure> (dart:isolate-patch/dart:isolate/isolate_patch.dart:277)
#3      _RawReceivePortImpl._handleMessage (dart:isolate-patch/dart:isolate/isolate_patch.dart:163)
try catch

和其他语言类似Dart可以使用try catch未处理的异常

void main() {
  try {
    throwException();
  } catch (e) {
print(e);
  }
}

控制台输出:

FormatException: 异常了

无论是否异常,都会执行的语句块

@FrizzleFur
Copy link
Owner Author

FrizzleFur commented May 2, 2019

Widget

  • Widget是Flutter中所有View的基类,widget在不同的平台借助于C++编译成原生代码
  • statelessWdiget是无状态组件,statefulWdiget是有状态组件,InheritedWidget可以向子View树中传递状态和信息
  • 在Flutter中我们所使用到的View基本上都继承于statelessWdiget、statefulWdiget或者InheritedWidget
  • MaterialApp是Flutter中满足Google Material Design设计的View组件,通常作为app的入口View
  • Scaffold是帮助我们快速开发app的“脚手架”,借助于Scaffold我们可以快速构架我们应用的大体样子。

Text

在前面的例子中我们已经用了许多次Text,顾名思义Text就是用来展示文本的类似于Android上的TextView。

构造方法如下:

const Text(this.data//内容, {
Key key,
this.style//样式
this.textAlign//对齐方式,
this.textDirection//文本方向,
//是否换行显示,
this.overflow//超出文本的处理方式,
this.textScaleFactor//每个逻辑像素的字体像素数,控制字体大小,
this.maxLines//最大行数,
  })
  • textAlign文本的对齐方式,【left:左对齐】、【right:右对齐】、【center:居中对齐】、【justify:自适应】、【start:文本开头,和textDirection有关】、【end:文本结尾,,和textDirection有关】
  • textDirection文本方向,【rtl:right to left 从右向左】、【ltr:left to right 从左向右】
  • overflow超出屏幕,是否换行显示 bool,传入true则换行,传入false这不换行
  • textScaleFactor传入double值,值越大字体大小越大,默认为1.0
  • maxLines最大显示行数
  • fontWeight :FontWeight.bold参数使文字表粗
  • fontStyle :FontStyle.italic参数使文字倾斜
  • decoration: TextDecoration.underline参数给文字加上下划线
  • decorationColor: Colors.red参数使文字下划线的颜色变成红色
  • letterSpacing: 8.0 参数调整字与字之间的距离

Image

在Flutter中系统为我们提供了可以加载图片的控件Image,Image 控件提供了如下几种用于加载不同方式的图片。

  • new Image, 用于从ImageProvider获取图像。
  • new Image.asset, 用于从AssetBundle获取图像。
  • new Image.network, 用于从URL获取图像。
  • new Image.file, 用于从文件中获取图像。
  • new Image.memory, 用于从内存中获取图像
  • 在flutter中Image支持JPEG, PNG, GIF, Animated GIF, WebP, Animated WebP, BMP, 和 WBMP这几种图片格式。

从asset获取图片

Image.asset(String name, {
        Key key,
    AssetBundle bundle,
    double scale,
    this.width,
    this.height,
    this.color,
    this.colorBlendMode,
    this.fit,
    this.alignment: Alignment.center,
    this.repeat: ImageRepeat.noRepeat,
    this.centerSlice,
        this.matchTextDirection: false,
    this.gaplessPlayback: false,
        String package,
  })

从network获取图片

Image.network(String src, {
    Key key,
    double scale: 1.0,//缩小倍数
    this.width,//宽
    this.height,//高
    this.color,
    this.colorBlendMode,
    this.fit,
    this.alignment: Alignment.center,
    this.repeat: ImageRepeat.noRepeat,
    this.centerSlice,
    this.matchTextDirection: false,
    this.gaplessPlayback: false,
    Map<String, String> headers,
  })

Icon

const Icon(this.icon//IconDate, {
   Key key,
   this.size,//大小
   this.color,//颜色
   this.semanticLabel,//标志位
   this.textDirection,//绘制方向,一般使用不到
 }) 

IconData

 const IconData(
  this.codePoint,//必填参数,fonticon对应的16进制Unicode {
  this.fontFamily,//字体库系列
  this.fontPackage,//字体在那个包中,不填仅在自己程序包中查找
  this.matchTextDirection: false,图标是否按照图标绘制方向显示
});

Container

Container({
   	Key key,
   	this.alignment,//内部widget对齐方式
    this.padding,//内边距
   	Color color,//背景颜色,与decoration只能存在一个
    Decoration decoration,//背景装饰,与decoration只能存在一个
    this.foregroundDecoration//前景装饰,
    double width,//容器的宽
   	double height,//容器的高
   	BoxConstraints constraints//,
   	this.margin,
   	this.transform,
   	this.child,
})
  • alignment: 内部Widget对齐方式,左上对齐topLeft、垂直顶部对齐,水平居中对齐topCenter、右上topRight、垂直居中水平左对齐centerLeft、居中对齐center、垂直居中水平又对齐centerRight、底部左对齐bottomLeft、底部居中对齐bottomCenter、底部右对齐bottomRight
  • padding: 内间距,子Widget距Container的距离。
  • color: 背景颜色* decoration: 背景装饰
  • foregroundDecoration: 前景装饰
  • width:容器的宽
  • height:容器的高* constraints:容器宽高的约束,容器最终的宽高最终都要受到约束中定义的宽高影响
  • margin:容器外部的间隔* transform: Matrix4变换
  • child:内部子Widget

Container构成以及绘制过程

Container作为Flutter中的用来布局的Widget,可以对子widget进行 绘制(painting)、定位(positioning)、调整大小(sizing)操作。

绘制过程(painting)

  • transform
  • Matrix4 transform
  • decoration
  • Decoration
  • paints the child
  • paints the foregroundDecoration

调整大小(sizing)

看下Container 构造方法,下面的大小约束会更好理解。

Container({
    Key key,
    this.alignment,
    this.padding,
    Color color,
    Decoration decoration,
    this.foregroundDecoration,
    double width,
    double height,
    BoxConstraints constraints,
    this.margin,
    this.transform,
    this.child,
  })
  • 没有子节点的Container试图尽可能大,除非传入的约束(constraints)是无限制(unbounded)的,在这种情况下,它们尽可能地小。
  • 拥有子节点的Container,大小会按照子节点的大小进行适应。如果Container设置了大小参数(例如:width, height, constraints),则按照Container的大小参数来。

布局策略顺序

Container tries, in order: to honor alignment, to size itself to the child, to honor the width, height, and constraints, to expand to fit the parent, to be as small as possible.

也就是说,Container会按照下面的这个顺序去进行布局操作:

  • 对齐(alignment)
  • 调节自身大小去适应子节点
  • 优先使用width 、 height、constraints
  • 放大自己去填充父节点
  • 尽可能的变小

总结

  • Container是Flutter中的容器,类似于Css中的盒子模型

  • margin为容器的外边距,padding为容器的内间距

  • alignment属性么可以控制容器内部Widget的对齐方式

  • decoration属性可以控制容器的背景

  • Flutter Container - 简书

@FrizzleFur
Copy link
Owner Author

Layout

在前面,学习了很多Flutter中最基本的Widget,可能会发现,这些Widget可以传入一个child对象有的不能传入child对象,但是都不能传入多个child,这样一来可就大大限制了我们的想象力。所以,今天我们就来讲讲可以传入多个child(children)的Widget,Row和Column。

  • Row和Column都是Flutter中的Layout,可以传入多个child

  • Row是行布局,children都显示在同一行上

  • Column是列布局,children都显示在同一列上

  • 使用Expanded可以让Row和Column按照比例布局并自动换行。

  • 借助于Expanded Widget可以实现Text自动换行,并且可以实现子Widget宽(高)按比例布局。

  • 在Expanded的构造方法中只有一个必填的child参数和一选填flex参数。刚才说到使用Expanded可以实现内部Widget的自动布局,那么我们按照上面的例子来试一下吧。

body: new Row(
        children: <Widget>[
          new Expanded(child:  new Text("Flutter is Google’s mobile UI framework ||"),),
          new Expanded(child:  new Text("Hello flyou||"),),
          new Expanded(child:  new Text("Hello Flutter"),),

        ],

在前面我们说过,可以实现child按照比例分配所占空间,那么是怎么实现的呢?对的,就是flex这个参数,在构造方法中这个参数的默认值是“1”,也就是这个Expanded内部child所占row(Column、Flex)的比例是1/N,这个N是布局中每个孩子flex属性的总和。

Table

在上面介绍了类似表格布局的组件,那么今天我们就来正式介绍下Flutter中的表格布局Table

Table({
    Key key,
    this.children: const <TableRow>[],
    this.columnWidths,//列宽
    this.defaultColumnWidth: const FlexColumnWidth(1.0),
    this.textDirection,//文字方向
    this.border,//边框
    this.defaultVerticalAlignment: TableCellVerticalAlignment.top,//对齐方式
    this.textBaseline//基线
  })

可以看到和Row、Column一样同样需要传入一个children对象,只不过Table传入的children要求是多个TableRow对象而不是Widget对象。

TableRow({ this.key, this.decoration, this.children })

Stack

按照翻译来讲的话是栈布局的意思,其实也很贴切,最先放入的必然在最下面显示。

Stack({
    Key key,
    this.alignment: AlignmentDirectional.topStart,//对齐方式
    this.textDirection,
    this.fit: StackFit.loose,//是否按照父类宽高处理自己大小
    this.overflow: Overflow.clip,//溢出处理方式
    List<Widget> children: const <Widget>[],
  })

IndexedStack

在上面我们看了下Stack,基本上知道了Stack的用法,其实IndexedStack的用法和Stack一样,只不过IndexedStack只显示指定位置的Widget,其他的位置的Widget不会显示。

其实这个IndexedStack还是很有用的,比如说你的应用现实的额多个状态(加载中、加载完成、网络错误)可以使用这个组件来很好的完成控制。

Scaffold

const Scaffold({
    Key key,
    this.appBar,//标题栏
    this.body,//内容
    this.floatingActionButton,//悬浮按钮
    this.persistentFooterButtons,底部持久化现实按钮
    this.drawer,//侧滑菜单左
    this.endDrawer,//侧滑菜单右
    this.bottomNavigationBar,//底部导航
    this.backgroundColor,//背景颜色
    this.resizeToAvoidBottomPadding: true,//自动适应底部padding
    this.primary: true,试用使用primary主色
  })
  • 从构造方法我们可以看到,Scaffold可以帮助我们事先类似于Android中toolbar、悬浮按钮、汉堡菜单、底部导航效果

Drawer

const Drawer({
    Key key,
    this.elevation: 16.0,
    this.child,
  })

好吧,构造方法足够的简单霸气,elevation设置阴影宽度,child就是菜单区域

BottomNavigationBar

接下来我们来看下 bottomNavigationBar底部导航按钮,相比大家都用过有类似功能的app这个底部导航按钮在Ios和Android中都非常的常见,所以今天也要给大家好好介绍下。

BottomNavigationBar({
    Key key,
    @required this.items,//List<BottomNavigationBarItem>
    this.onTap,//tap事件
    this.currentIndex: 0,//当前位置
    BottomNavigationBarType type,//底部item类型,fixed自适应,shifting选择放大
        this.fixedColor,选中颜色
    this.iconSize: 24.0,//图标大小
  })

BottomNavigationBarItem

const BottomNavigationBarItem({
    @required this.icon,
    @required this.title,
    this.backgroundColor,
  })

@FrizzleFur
Copy link
Owner Author

StatefulWidget

StatefulWidget又被称为有状态组件,开发者可以根据用户的操作来选择性的更新界面上的组件。

  • StateFulWidget是有状态组件,负责处理有状态的界面
  • 使用setState回调可以很轻松的更新界面上UI的显示效果
  • 有状态组件变更归根到底是根据数据变更来做的操作
  • VoidCallback是一个没有参数没有返回值的方法

@FrizzleFur
Copy link
Owner Author

Flutter中手势事件处理

在Flutter中的手势事件分为两层,

  • 第一层有原始指针事件,它描述了屏幕上指针(例如,触摸,鼠标和触控笔)的位置和移动。
    • PointerDownEvent 指针已经联系了特定位置的屏幕。
    • PointerMoveEvent 指针已经从屏幕上的一个位置移动到另一个位置。
    • PointerUpEvent 指针停止接触屏幕。
    • PointerCancelEvent 来自此指针的输入不再针对此应用。
  • 第二层有手势,描述由一个或多个指针移动组成的语义动作。
    • onTapDown 可能导致水龙头的指针已经在特定位置与屏幕接触。

    • onTapUp 将触发水龙头的指针已停止在特定位置与屏幕接触。

    • onTap 出现了水龙头。

    • onTapCancel先前触发的指针onTapDown不会导致敲击。

    • 双击

    • onDoubleTap 用户快速连续两次在同一位置轻敲屏幕。

    • 长按

    • onLongPress 指针在相同位置长时间保持与屏幕接触。

    • 垂直拖动

    • onVerticalDragStart 指针已经与屏幕联系并可能开始垂直移动。

    • onVerticalDragUpdate 与屏幕接触并垂直移动的指针已沿垂直方向移动。

    • onVerticalDragEnd 先前与屏幕接触并垂直移动的指针不再与屏幕接触,并且在停止接触屏幕时以特定速度移动。

    • 水平拖拽

    • onHorizontalDragStart 指针已经与屏幕联系并可能开始水平移动。

    • onHorizontalDragUpdate 与屏幕接触并水平移动的指针已沿水平方向移动。

    • onHorizontalDragEnd 先前与屏幕接触并水平移动的指针不再与屏幕接触,并且在停止接触屏幕时以特定速度移动。

指针Pointers

在指针向下时,框架对您的应用程序执行命中测试,以确定指针与屏幕相接的位置存在哪些小部件。指针向下事件(以及该指针的后续事件)然后被分派到由命中测试发现的最内部的小部件。

从那里开始,这些事件会冒泡到树中(事件分发),并被分派到从最内部的小部件到树根的路径上的所有小部件。没有任何机制可以取消或停止进一步调度指针事件。

由于指针不能直接用于控件的监听,所以我们在开发中一般使用Gesture来监听用户的各种事件。

手势Gestures

手势表示可以从多个单独的指针事件识别的语义动作(例如,轻敲,拖动和缩放),甚至可能是多个单独的指针。手势可以分派多个事件,对应于手势的生命周期(例如,拖动开始,拖动更新和拖动结束):

GestureDetector

要让Gesture可以监听Widget的各种事件需要使用GestureDetector来处理这些事件。

GestureDetector({
    Key key,
    this.child,
    this.onTapDown,
    this.onTapUp,
    this.onTap,
    this.onTapCancel,
    this.onDoubleTap,
    this.onLongPress,
    this.onVerticalDragDown,
    this.onVerticalDragStart,
    this.onVerticalDragUpdate,
    this.onVerticalDragEnd,
    this.onVerticalDragCancel,
    this.onHorizontalDragDown,
    this.onHorizontalDragStart,
    this.onHorizontalDragUpdate,
    this.onHorizontalDragEnd,
    this.onHorizontalDragCancel,
    this.onPanDown,
    this.onPanStart,
    this.onPanUpdate,
    this.onPanEnd,
    this.onPanCancel,
    this.onScaleStart,
    this.onScaleUpdate,
    this.onScaleEnd,
    this.behavior,
    this.excludeFromSemantics: false
  })

事件冲突处理

正常情况下,我们仅仅会在一个方向上操作界面,但是是也不排除用户在横向和竖向操作界面(或者是多个手势),这样一来屏幕就会给底层发来多个手势识别器,这样一样就会存在手势的冲突。

  • 如果两个手势都在操作,最后剩下的识别器会享有事件的处理权
  • 如果横竖两个识别器同时存在,最先划出屏幕的识别器享有事件处理权

@FrizzleFur
Copy link
Owner Author

Flutter中的操作提示

在原生客户端有着几种常用的用户提醒方式,如Dialog、Snackbar、BottomSheet等,今天我们就来介绍下Flutter中几种常用的提醒方式。

SnackBar

SnackBar是用户操作后,显示提示信息的一个控件,类似Toast,会自动隐藏。它还可以添加操作按钮,等等。SnackBar是通过Scaffold的showSnackBar方法来显示的。所以要显示一个SnackBar,要先拿到Scaffold。

const SnackBar({
    Key key,
    @required this.content,//内容
    this.backgroundColor,//背景
    this.action,//其他操作
    this.duration: _kSnackBarDisplayDuration,//显示时长
    this.animation,//进出动画
      })

Dialog

对话框在Ios和Android客户端中都很常见,在Flutter中常用的AlertDialog、SimpleDialog和AboutDialog。

今天我们就来介绍下这几种Dialog的用法 。
在Flutter中你可以使用ShowDialog方法来显示这些Dialog。
showDialog方法需要传入一个上下文对象和一个Widget对象

SimpleDialog

就是最简单的对话框,当然也是最灵活的对话框,对话框显示区域完全由你自己自定义,你可以根据自己的需要绘制自己想要的界面。

const SimpleDialog({
    Key key,
    this.title,//标题
    this.titlePadding,标题padding
    this.children,//内容
    this.contentPadding,内容padding
  })

AlertDialog

AlertDialog其实就是simpleDialog的封装,更加方便开发者使用,只不过在simpeDialog的基础上新增了action操作而已

AboutDialog

AboutDialog也是在SimpleDialog基础上的封装,可以很方便的显示关于应用的Dialog。由于跟上面的用法类似,这里就不在介绍它的够造方法了。

BottomSheet

也被称为底部菜单,通常情况下分享操作界面使用的比较多。

如果要显示BottomSheet我们可以调用,showBottomSheet()或者showModalBottomSheet()方法。这两种方法都可以显示BottomSheet,只不过第一个是从新打开了一个界面来显示,第二个方法是直接在当前界面的下面来显示。

小结

  • SnackBar可以快捷的在底部显示提示Tips
  • 使用showAlert方法可以显示SimpleDialog、AlertDialog和AboutDialog
  • 使用BottomSheet可以实现底部抽屉的效果

@FrizzleFur FrizzleFur pinned this issue May 2, 2019
@FrizzleFur
Copy link
Owner Author

ListView&GirdView

ListView

ListView就是我们常见的列表组件,在平时的应用开发中十分的常见,无论你做的是什么类型的应用都会多多少少会用到ListView

ListView({
    Key key,
    Axis scrollDirection: Axis.vertical,//滚动方向
    bool reverse: false,//十分反向显示数据
    ScrollController controller,
    bool primary,
    ScrollPhysics physics,//物理滚动
    bool shrinkWrap: false,
    EdgeInsetsGeometry padding,
    this.itemExtent,//item有效范围
    bool addAutomaticKeepAlives: true,//自动保存视图缓存
    bool addRepaintBoundaries: true,//添加重绘边界
    List<Widget> children: const <Widget>[],
  })

那么,我们可以尝试下ListView.builder()和ListView.custom()。

ListView.builder()和ListView.custom()的用法基本相同,只不过custom可以根据自己的需要控制Item显示方式,如Item显示大小。

我们今天来看下ListView.builder()

ListView.builder({
    Key key,
    Axis scrollDirection: Axis.vertical,
        bool reverse: false,
    ScrollController controller,
    bool primary,
    ScrollPhysics physics,
    bool shrinkWrap: false,
    EdgeInsetsGeometry padding,
    this.itemExtent,
    @required IndexedWidgetBuilder itemBuilder,//item构建者
        int itemCount,//item数量
    bool addAutomaticKeepAlives: true,
    bool addRepaintBoundaries: true,
  })

GridView

GirView的用法和ListView类似,只不过由于GridView可以在一列或者一行显示多个Item,所以在构造方法中就多了个参数来配置一行(列)有几个Item和Item的间隔

GridView({
    Key key,
    Axis scrollDirection: Axis.vertical,
    bool reverse: false,
    ScrollController controller,
    bool primary,
    ScrollPhysics physics,
    bool shrinkWrap: false,
    EdgeInsetsGeometry padding,
    @required this.gridDelegate,//没错,它就是控制GridView的
    bool addAutomaticKeepAlives: true,
    bool addRepaintBoundaries: true,
    List<Widget> children: const <Widget>[],
  })

看下gridDelegate参数

可以传入SliverGridDelegateWithFixedCrossAxisCount对象和SliverGridDelegateWithMaxCrossAxisExtent对象。

其中SliverGridDelegateWithFixedCrossAxisCount可以直接指定每行(列)显示多少个Item,SliverGridDelegateWithMaxCrossAxisExtent会根据GridView的宽度和你设置的每个的宽度来自动计算没行显示多少个Item

国际惯例,我们还是只介绍一个,那就SliverGridDelegateWithFixedCrossAxisCount吧。

看下代码,怎么用

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text("GridView"),
      ),
      body: new GridView(
        children: <Widget>[
          new Container(
            child: new Icon(
              Icons.cake,
              size: 50.0,
            ),
                        color: Colors.blue,
          ),
          new Container(

            child: new Icon(
              Icons.cake,
              size: 50.0,
            ),
            color: Colors.blue,
          ),
        ],
                reverse: false,
        gridDelegate: new SliverGridDelegateWithFixedCrossAxisCount(
            crossAxisCount: 2,
            mainAxisSpacing: 10.0,
            crossAxisSpacing: 10.0,
            childAspectRatio: 1.0),
      ),
    );
  }
}

当然,GridView你也可以使用builder()和custom()的方式实现

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text("ListView"),
      ),
      body: new GridView.builder(
        gridDelegate: new SliverGridDelegateWithFixedCrossAxisCount(
            crossAxisCount: 2,
            mainAxisSpacing: 10.0,
            crossAxisSpacing: 10.0,
                        childAspectRatio: 1.0),
        itemBuilder: (BuildContext context, int index) {
          return new ListItemWidget(listData[index]);
        },
        itemCount: listData.length,
      ),
    );
  }
}

class ListItem {
  final String title;
  final IconData iconData;

  ListItem(this.title, this.iconData);
}

class ListItemWidget extends StatelessWidget {
  final ListItem listItem;

  ListItemWidget(this.listItem);

  @override
  Widget build(BuildContext context) {
    return new GestureDetector(
      child: new Container(
        color: Colors.blue,
        child: new Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            new Icon(listItem.iconData, size: 50.0),
            new Text(listItem.title)
          ],
        ),
      ),
      onTap: () {
        Scaffold.of(context).showSnackBar(new SnackBar(
              content: new Text(listItem.title),
            ));
      },
    );
  }
}

小结

  • ListView就是我们常用的列表视图
  • GridView就是我们常用的宫格视图
  • ListView和GridView都可以使用new 或者builder()和custom()方法来创建对象

@FrizzleFur
Copy link
Owner Author

Flutter中的路由跳转

在Flutter中有着两种路由跳转的方式,

  • 一种是静态路由,在创建时就已经明确知道了要跳转的页面和值。
  • 另一种是动态路由,跳转传入的目标地址和要传入的值都可以是动态的。

静态路由

在runApp方法中需要传入一个MaterialApp的Widget,但是我们基本用到的都是home属性,但是其实MaterialApp方法里面有着很多的参数,其中routes参数就是定义路由的参数。

routes: const {}

routes需要传入类型的Map,第一个参数是目标路由的名称,第二个参数就是你要跳转的页面。

嗯,还是来个例子看看怎么用

第一个页面:

import 'package:flutter/material.dart';
import 'package:test1/route/Page2.dart';

void main() {
  runApp(new MaterialApp(
    home: new MyApp(),
    routes: <String, WidgetBuilder>{
      '/page2': (BuildContext context) => new Page2(),
    },
  ));
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text("Page1"),
      ),
      body: new Center(
        child: new RaisedButton(
          onPressed: () {
            Navigator.of(context).pushNamed("/page2");
                      },
          child: new Text("点我跳转"),
          color: Colors.blue,
          highlightColor: Colors.lightBlue,
        ),
      ),
    );
  }
}

在第一个页面在Main方法中我们定义了我们需要跳转的页面名称为“Page2”,要跳转的页面是Page2,每当我们点击屏幕正中央的button都会触发调用

Navigator.of(context).pushNamed(“/page2”);

Navigator就是在在Flutter中负责页面导航的,相信了解Android的童鞋都知道这个玩意。
使用pushNamed方法传入一个在routes定义的名字即可。

import 'package:flutter/material.dart';
class Page2 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text("Page2"),
      ),
      body: new Center(
        child: new Text(
          "Page2",
          style: new TextStyle(fontSize: 25.0),
        ),
      ),
    );
  }
}

好吧,那么尝试下往下个页面传递数据,其实也很简单,我们给第二个页面加一个构造函数,并且把从第一个页面传递过来的值赋给Text

import 'package:flutter/material.dart';

class Page2 extends StatelessWidget {
  final title;

  Page2(this.title);

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
          appBar: new AppBar(
        title: new Text("Page2"),
      ),
      body: new Center(
        child: new Text(
          title,
          style: new TextStyle(fontSize: 25.0),
        ),
      ),
    );
  }
}

在第一个页面定义路由时就需要把你需要传递的参数定义出来。

routes: <String, WidgetBuilder>{
      '/page2': (BuildContext context) => new Page2("I am from Page1"),
    },

这种定义路由并使用的方式非常的简单,但是大家肯定会发现一个问题,就是如果我需要传递给第二个页面的数据不是已知的话我就无法使用这种方式,因为我们无法动态改变上面定义的值。

所以,我们就需要了解下Flutter中的动态路由了。

动态路由

在Navigator中还有一个方法是push()方法,需要传入一个Route对象,在Flutter中我们可以使用PageRouteBuilder来构建这个Route对象。

Navigator.of(context).push(new PageRouteBuilder(
                        pageBuilder: (BuildContext context,
                            Animation<double> animation,
                            Animation<double> secondaryAnimation) {
                          return new Page2("some attrs you like ");

                        }))

这样的话,我们就可以把用户操作与交互的数据传递给下个页面。

还是结合前面的讲过的来举个例子。

在前面的文章中,我们使用TextField举过一个例子,对用户输入的用户名密码进行判断,当用户名是“flyou”,密码是“admin”时提示登录成功,否则提示登录失败。

今天我们稍微改动下以前这个例子,当用户名与密码相同时提示正确,否则就提示用户名密码有误。输入正确则直接跳转到第二个页面,并把登录成功的用户名给传递过去。

下面代码提到的DynamicPage就是我们第二个页面。

import 'package:flutter/material.dart';
import 'package:test1/route/DynamicPage.dart';


void main() {
  runApp(new MaterialApp(
    home: new MyApp(),
  ));
}

class MyApp extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    return new MyAppState();
  }
}

class MyAppState extends State<MyApp> {
  TextEditingController _userNameController = new TextEditingController();
  TextEditingController _userPasswordController = new TextEditingController();

  void onTextClear() {
    setState(() {
      _userNameController.text = "";
      _userPasswordController.text = "";
    });
  }

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
        appBar: new AppBar(
          title: new Text("TextField"),
        ),
        body: new Column(
                  children: <Widget>[
            new TextField(
              controller: _userNameController,
              decoration: new InputDecoration(
                  contentPadding: const EdgeInsets.only(top: 10.0),
                  icon: new Icon(Icons.perm_identity),
                  labelText: "请输入用户名",
                  helperText: "注册时填写的名字"),
            ),
            new TextField(
              controller: _userPasswordController,
              decoration: new InputDecoration(
                  contentPadding: const EdgeInsets.only(top: 10.0),
                  icon: new Icon(Icons.lock),
                  labelText: "请输入密码",
                  helperText: "登录密码"),
              obscureText: true,
            ),
            new Builder(builder: (BuildContext context) {
              return new RaisedButton(
                  onPressed: () {
                 var userName=   _userNameController.text.toString();
                 var passWord=   _userPasswordController.text.toString();
                    if (userName == passWord ) {
                      Navigator.of(context).push(new PageRouteBuilder(
                          pageBuilder: (BuildContext context,
                              Animation<double> animation,
                              Animation<double> secondaryAnimation) {
                                                          return new DynamicPage(userName);

                          }));
                    } else {
                      Scaffold.of(context).showSnackBar(
                          new SnackBar(content: new Text("登录失败,用户名密码有误")));
                    }
                    onTextClear();
                  },
                  color: Colors.blue,
                  highlightColor: Colors.lightBlueAccent,
                  disabledColor: Colors.lightBlueAccent,
                  child: new Text(
                    "登录",
                    style: new TextStyle(color: Colors.white),
                  ));
            })
          ],
                  ));
  }
}

每当我们点击登录按钮时都会判断用户名密码是否相等,如果相等则把用户输入的用户名传递给二个页面并显示出来。

第二个页面的代码非常的简单

import 'package:flutter/material.dart';

class DynamicPage extends StatelessWidget {
final userName;

DynamicPage(this.userName);

@override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text("DynamicPage"),
      ),
      body: new Center(
        child: new Text("登录成功,欢迎新用户:$userName")),
    );
  }
}

页面出栈

在Flutter中我们可以使用Navigator.of(context).pop()进行出栈操作,但是值得注意的时如果页面上有Dialog、BottomSheet、popMenu类似的Widget使用pop()方法会优先进行这些Widget的关闭操作。

小结

  • 使用Navigator.of(context).pushName(“/“)可以进行静态路由的跳转
  • 使用push(Route)可以进行态路由的跳转
  • 动态路由可以传入未知数据
  • 使用pop()可以进行路由的出栈并且可以传递参数
  • 可以使用Future接收上个页面返回的值。

@FrizzleFur
Copy link
Owner Author

Dart中的异步操作

在前面的文章中我们很多次提到了Future这个东西,这个单词翻译过来的意思是‘未来’的意思。在flutter中它表示一个未来某些时候返回数据的一个对象。

借助Future我们可以在Flutter实现异步操作,今天我们就来正式了解下Future。

为什么要用异步

首先我们知道Dart这门语言是单线程的。同步代码的执行会让我们的程序处于过长时间的等待状态终止ANR。

对于耗时的操作(I/O、网络操作等)我们必须要使用异步来处理它们,只有这样,才不会因为这些耗时的操作来影响程序的正常运行。

什么是Future

Future是在未来某个时间获得想要对象的一种手段。

当调用Future后系统会将使用Future的操作放入一个队列中去排队执行,并返回一个未完成的Future对象,当事件完成后或者有一个可用的值时Future就会调用自己的then回调让调用者去处理这个对象

在Flutter中我们可以使用如下两种方式来获取Future。

  • 使用Future APi
  • 使用async和await (Dart1.9后支持)

试一下 async

TODO: async未介绍

void main() {

print(enterRestaurant());
Future<String>  waitDinnerFuture=  waitForDinner();
waitDinnerFuture.then((str){
  print(str);
});
print(  startChat());
print(  playPhone());
}

Future的其他用法

考虑三个功能expensiveA(),expensiveB()以及expensiveC()都返回Future对象

使用then()链接函数调用

当Future-returning函数需要按顺序运行时,请使用链式then() 调用:

expensiveA()
    .then((aValue) => expensiveB())
    .then((bValue) => expensiveC())
    .then((cValue) => doSomethingWith(cValue));

Future.wait()

等待多个Future以完成使用Future.wait()

如果功能的执行顺序不重要,可以使用 Future.wait()。

这些功能快速连续触发; 当他们都完成一个价值,Future.wait()返回一个新的未来。这个未来完成了包含每个函数产生的值的列表。

Future
    .wait([expensiveA(), expensiveB(), expensiveC()])
    .then((List responses) => chooseBestResponse(responses))
    .catchError((e) => handleError(e));

小结

  • Dart是单线程的变成语言
  • 使用Future可以是同步操作异步化
  • Future可以使用async和await来回去
  • Future可以处理链式调用和多个Future同时返回结果

@FrizzleFur
Copy link
Owner Author

Flutter中的本地存储

Flutter本地存储
和Android、Ios类似,Flutter也支持Preferences(Shared Preferences and NSUserDefaults) 、文件、和Sqlite3。

Preferences存储
Flutter中本身并不支持Preferences存储,需要借助于第三发的组件来实现。

打开 https://github.com/flutter/plugins

或者 https://pub.dartlang.org/flutter

小结

可以在https://pub.dartlang.org/flutter获取第三发插件
在pubspec.yaml引入或者更新第三发插件
SharedPreferences、文件、数据库操作都是命耗时操作,需要异步执行

@FrizzleFur
Copy link
Owner Author

网络操作

在前面的文章中我们在Flutter中的本地存储,我们可以将用户的数据存储在移动设备上,但是当用户清空设备或者更换设置这些用户存储的信息就会面临丢失的问题。那么,我们就不得不考虑将用户的信息存储一个第三方的地方,没错就是服务器。

那么,今天我们就来看下Flutter中的网络操作。

import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;

void main() {
  runApp(new MaterialApp(home: new MyApp(),));
}

class MyApp extends StatelessWidget {
  void httpGet() {
    print("请求开始");
    http.get("https://api.github.com/users/flyou").then((response) {
      print("请求内容:"+response.body);
    }).catchError((error) {
      print("请求出错:" + error.toString());
    }).whenComplete(() {
      print("请求完成");
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("网络操作"),
      ),
      body: Center(
        child: RaisedButton(
          onPressed: () {httpGet();},
          child: Text("获取用户信息"),
        ),
      ),
    );
  }
}

上面的代码很简单,页面上只有一个很简单的RaisedButton,当用户点击时便会触发上,上面的httpGet方法。

httpGet方法里面会调用http的get请求,请求github api,使用then来接收正常的返回信息,使用catchError来接受异常的信息,当请求完成时会触发whenComplete

返回数据处理

现在我们使用的接口后台返回的一半都是Json的形式,所以我们也仅仅对json数据的处理做下介绍。

在Flutter中默认已经为我们提供了convert库来处理json字符串的转换

我们可以使用json.encode()或者json.decode()方法来序列化或者反序列化json字符。

好吧,还是来举个例子,还是跟上面的一样请求github api获取用户信息,但是这次我们根据用户输入的用户名来获取用户信息,并且把返回的用户信息合理的显示在屏幕上。

界面很简单,最上面一个image,下面是几个ListTitle,在下面就是一个TextField,最下面就是就是一个RaisedButton了。

当我们点击RaisedButton时就会获取TextField输入的内容并且去请求服务器并返回。

import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:test1/http/User.dart';

void main() {
  runApp(new MaterialApp(
    home: MyApp(),
  ));
}

class MyApp extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    return MyAppState();
  }
}

class MyAppState extends State<MyApp> {
  User user;

  void getUserInfo(String userName) {
    http.get("https://api.github.com/users/$userName").then((respsone) {
      setState(() {
        final responseJson = json.decode(respsone.body);
        user = new User.fromJson(responseJson);
        print(user.avatar_url);
      });
    }).catchError((error) {
      print(error.toString());
    }).whenComplete(() {
      print("请求完成");
    });
  }

  @override
  Widget build(BuildContext context) {
      TextEditingController controller = new TextEditingController();
    return Scaffold(
      appBar: AppBar(
        title: Text("网络操作"),
      ),
      body: new SingleChildScrollView(child: Column(
        children: <Widget>[
          new Container(
            child: Image.network(user != null
                            ? user.avatar_url
                : "https://avatars1.githubusercontent.com/u/6630762?v=4"),width: 50.0,height: 50.0,
          ),
          new ListTile(
            leading: Icon(Icons.person),
            title: Text("name:" + (user != null &&user.name!=null ? user.name : "暂无")),
          ),
          new ListTile(
            leading: Icon(Icons.location_city),
            title: Text("location:" + (user != null &&user.location!=null? user.location : "暂无")),
          ),
          new ListTile(
                        leading: Icon(Icons.web),
              title: Text("blog:" + (user != null &&user.blog!=null? user.blog : "暂无"))),
          new ListTile(
            leading: Icon(Icons.http),
            title: Text("html_url:" + (user != null&&user.html_url!=null ? user.html_url : "暂无")),
          ),
          new TextField(
            decoration: InputDecoration(labelText: "请输入你的github用户名"),
            controller: controller,
          ),
          RaisedButton(
            onPressed: () {
                          getUserInfo(controller.text.toString());
            },
            child: Text("Get请求"),
          ),
        ],
      ),),
    );
  }
}

// User.dart

class User {
  final String name;
  final String location;
  final String blog;
  final String avatar_url;
  final String html_url;

  User({this.name,this.location, this.blog, this.avatar_url, this.html_url});

  factory User.fromJson(Map<String, dynamic> json) {
    return new User(
      name: json['name'],
      location: json['location'],
      blog: json['blog'],
      avatar_url: json['avatar_url'],
      html_url: json['html_url'],
    );
  }
}

小结

http请求也是耗时操作,需要使用Future
使用 http库可以很方便的实现手机端的网络请求
使用json可以很方便的序列化或者反序列化json

@FrizzleFur
Copy link
Owner Author

Flutter调用平台代码

在前面的文章中我们讲了许多Flutter中的组件和Flutter中的特定操作,但是单单使用Flutter里的组件和方法是不够的。

就像以前我们讲到文件存储、数据库操作单单靠使用Flutter我们是不能完成的,因为这些数据最终需要存储在特定的终端平台上,我们需要通过特点的代码来实现与特点的平台交互,所以我们引入了第三方库来完成这些操作。

当然,这些第三方库帮我们实现了与不同平台交互的代码,所以我们不需要自己再去自己去编写这些与特定平台交互的代码。

当时我们你不可能一直使用人家的第三方库啊,一些特定的功能是没人能帮你的,所以我们还是很有必要来学习下如何跟特定的平台交互的

平台通道

flutter使用了一个灵活的系统,允许您调用特定平台的API,无论在Android上的Java或Kotlin代码中,还是iOS上的ObjectiveC或Swift代码中均可用。

Flutter平台特定的API支持不依赖于代码生成,而是依赖于灵活的消息传递的方式:

应用的Flutter部分通过平台通道(platform channel)将消息发送到其应用程序的所在的宿主(iOS或Android)。

宿主监听的平台通道,并接收该消息。然后它会调用特定于该平台的API(使用原生编程语言) - 并将响应发送回客户端,即应用程序的Flutter部分。

用平台通道在客户端(Flutter UI)和宿主(平台)之间传递消息

在客户端,MethodChannel 可以发送与方法调用相对应的消息。 在宿主平台上,MethodChannel 在Android((API) 和 FlutterMethodChannel iOS (API) 可以接收方法调用并返回结果。这些类允许您用很少的“脚手架”代码开发平台插件。

从平台获取数据

和上面的类似,我们可以调用系统的方法,我们同样刻印调用我们自己写的方法并且返回调用方法的值,那么我们还是举个例子看下吧。

我们通过flutter调用Android平台的方法获取当前格式化好的时间。

同样的我们还是用用和刚才一样的通道,只不过这一次我们需要更改我们调用的方法即可。

import 'dart:async';

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

void main() {
  runApp(MaterialApp(
    home: MyApp(),
  ));
}

class MyApp extends StatelessWidget {
  static const platform = const MethodChannel("com.flyou.test/android");

  Future<String> getAndroidTime() async {
    var str;
    try {
      str = await platform.invokeMethod("getAndroidTime");
    } on PlatformException catch (e) {
      print(e.toString());
    }
    return str;
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("platformChannels"),
      ),
      body: new Center(
        child: new Builder(builder: (BuildContext context) {
          return new RaisedButton(
            onPressed: () {
              getAndroidTime().then((str){
                Scaffold.of(context).showSnackBar(new SnackBar(content:Text(str!=null?str:"获取失败")));
              });
            },
            child: Text("点我获取Android平台数据"),
          );
        }),
      ),
    );
  }
}

@FrizzleFur
Copy link
Owner Author

FrizzleFur commented May 2, 2019

ListView下拉刷新与加载更多

下拉刷新

在Flutter中系统已经为我们提供了google material design的刷新效果,我们可以使用RefreshIndicator组件来实现Flutter中的下拉刷新,下面们还是先来看下如何使用吧

const RefreshIndicator({
   Key key,
   @required this.child,
   this.displacement: 40.0,下拉展示距离
   @required this.onRefresh,刷新回调
   this.color,//刷新进度颜色
   this.backgroundColor,//背景颜色
   this.notificationPredicate: defaultScrollNotificationPredicate,
 })
import 'dart:async';

import 'package:flutter/material.dart';

void main() {
  runApp(new MaterialApp(
    home: new MyApp(),
  ));
}

class MyApp extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => MyAppState();
}

class MyAppState extends State<MyApp> {
  List<int> items = List.generate(16, (i) => i);

  Future<Null> _handleRefresh() async {
    await Future.delayed(Duration(seconds: 5), () {
      print('refresh');
      setState(() {
        items.clear();
        items = List.generate(40, (i) => i);
        return null;
      });
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Refresh"),
      ),
      body: new RefreshIndicator(
        child: ListView.builder(
          itemCount: items.length,
          itemBuilder: (context, index) {
            return ListTile(
              title: Text("Index$index"),
            );
          },
        ),
        onRefresh: _handleRefresh,
      ),
    );
  }
}

上拉加载更多

对于加载更多的组件在Flutter中是没有提供的,所以在这里我们就需要考虑如何实现的。

在ListView中有一个ScrollController属性,它就是专门来控制ListView滑动事件,在这里我们可以根据ListView的位置来判断是否滑动到了底部来做加载更多的处理。

在这里我们可以使用如下代码来判断ListView 是否滑到了底部

_scrollController.addListener(() {
    if (_scrollController.position.pixels ==
        _scrollController.position.maxScrollExtent) {
      print("loadMore");
     
    }
  });

_scrollController是我们初始化的ScrollController对象,通过监听我们可以判断现在的位置是否是最大的下滑位置来判断是否下滑到了底部。

import 'dart:async';

import 'package:flutter/material.dart';

void main() {
  runApp(new MaterialApp(
    home: new MyApp(),
  ));
}

class MyApp extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => MyAppState();
}

class MyAppState extends State<MyApp> {
  bool isLoading = false;
  List<int> items = List.generate(16, (i) => i);
  ScrollController _scrollController = new ScrollController();

  @override
    void initState() {
        super.initState();
    _scrollController.addListener(() {
      if (_scrollController.position.pixels ==
          _scrollController.position.maxScrollExtent) {
        print("loadMore");
        _getMoreData();
      }
    });
  }
  Future _getMoreData() async {
    if (!isLoading) {
      setState(() => isLoading = true);
      List<int> newEntries = await mokeHttpRequest(items.length, items.length + 10);
      setState(() {
        items.addAll(newEntries);
        isLoading = false;
      });
    }
  }
  Future<List<int>> mokeHttpRequest(int from, int to) async {
    return Future.delayed(Duration(seconds: 2), () {
      return List.generate(to - from, (i) => i + from);
    });
  }


  @override
  Widget build(BuildContext context) {

    return Scaffold(
      appBar: AppBar(
        title: Text("LoadMore"),
      ),
      body:  ListView.builder(
          itemCount: items.length,
          itemBuilder: (context, index) {
              return ListTile(
                  title: Text("Index$index"));
          },
          controller: _scrollController,
        )
    );
  }
  @override
  void dispose() {
    super.dispose();
    _scrollController.dispose();
  }
}

加载更多提示

首先我们创建加载更多时显示的Vidget

Widget _buildLoadText() {
  return Container(child:  Padding(
    padding: const EdgeInsets.all(18.0),
    child: Center(
      child: Text("加载中……"),
    ),
  ),color: Colors.white70,);
}

然后修改ListView,使得itemCount数目加1,当是最后一条时显示加载中的View,不是最后一条显示正常的Widget

body: ListView.builder(
         itemCount: items.length + 1,
         itemBuilder: (context, index) {
           if (index == items.length) {
             return _buildLoadText();
           } else {
             return ListTile(title: Text("Index$index"));
           }
         },
         controller: _scrollController,
       )

小结

  • RefreshIndicator可以显示下拉刷新

  • 使用ScrollController可以监听滑动事件,判断当前view所处的位置

  • 可以根据item所处的位置来处理加载更多显示效果

@FrizzleFur
Copy link
Owner Author

Flutter动画【1】

补间动画的基本支持类

在Flutter中Animation对象是Flutter动画库中的一个核心类,它生成指导动画的值,没错它仅仅用来生成动画的值,这些值并不会直接没改变界面的展示效果。

Animation对象知道动画的当前状态(例如,它是开始、停止还是向前或向后移动),但它不知道屏幕上显示的内容。

在Flutter中我们使用AnimationController来管理动画,控制动画的开始、结束与快慢。

CurvedAnimation 可以说是动画的插值器,负责控制动画的行为,如是先快再慢还是先快再慢等。

入门补间动画

Animation在Flutter中是一个抽象类,我们并不能直接来是使用它,但是我们可以使用Tween这个子类来使用它。

我们可以使用addListener回调来监听动画值的改变,可以使用addStatusListener回调来监听动画状态的变更

刚刚我们说过,使用Animation并不能直接改变Widget,它只能生成一系列的值,那么到底是不是这样呢?我们还是看个例子

每次我们点击floatingActionButton都会触发动画开始的操作,然后通过监听把当前动画的值打印到控制台上。

import 'package:flutter/material.dart';

void main() {
  runApp(new MaterialApp(home: new MyApp(),));
}

class MyApp extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => MyAppState();
}

class MyAppState extends State<MyApp> with SingleTickerProviderStateMixin {
  Animation<double> doubleAnimation;
  AnimationController animationController;

  @override
  Widget build(BuildContext context) {
    return Scaffold(appBar: AppBar(title: Text("AnimAtion"),),
      floatingActionButton: FloatingActionButton(
                child: Icon(Icons.add), onPressed:onAnimationStart),);
  }

  @override
  void initState() {
    super.initState();
    animationController = new AnimationController(
        vsync: this, duration: const Duration(milliseconds: 2000));
    doubleAnimation =
        new Tween(begin: 0.0, end: 100.0).animate(animationController)..addListener((){
          print(doubleAnimation.value);
        });

  }

  onAnimationStart() {
    animationController.forward(from: 0.0);
  }

  @override
  void dispose() {
    super.dispose();
    animationController.dispose();
  }
}

AnimatedWidget

在上面的例子中我们必须需要通过addListener来获得对动画值变化的监听,但是通过AnimatedWidget我们可以直接获得动画的值并赋值给相应的Widget

import 'package:flutter/material.dart';

void main() {
  runApp(new MaterialApp(
    home: MyApp(),
  ));
}

class MyApp extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => MyAppState();
  }

class MyAppState extends State<MyApp> with SingleTickerProviderStateMixin {
  Animation<double> numberAnimation;
  AnimationController controller;

  @override
  void initState() {
    super.initState();
    controller = new AnimationController(
        duration: const Duration(milliseconds: 2000), vsync: this);
    numberAnimation = new Tween(begin: 0.0, end: 100.0).animate(controller);
  }


  onAnimationStart() {
    controller.forward(from: 0.0);
  }

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: AppBar(
              title: Text("Animation"),
      ),
      floatingActionButton: new FloatingActionButton(
        onPressed: onAnimationStart,
        child: AnimationText(animation: numberAnimation,),
      ),
    );
  }

  dispose() {
    controller.dispose();
    super.dispose();
  }
}

class AnimationText extends AnimatedWidget {
  AnimationText({Key key, Animation<double> animation})
        : super(key: key, listenable: animation);

  @override
    Widget build(BuildContext context) {
    final Animation<double> animation = listenable;
    return Text(animation.value.toInt().toString());
  }

其实使用起来也非常的简单,只不过我们自定义了一个AnimationText继承于AnimatedWidget来获得对动画的监听并给Text赋值,当然程序的运行效果跟上面的例子是一样的。

@FrizzleFur
Copy link
Owner Author

FrizzleFur commented May 2, 2019

SliverAppBar

SliverAppBar({
    Key key,
    this.leading,//前导标题
    this.automaticallyImplyLeading: true,
    this.title,//标题
    this.actions,//菜单
    this.flexibleSpace,//可以展开区域,通常是一个FlexibleSpaceBar
    this.bottom,//底部内容区域
    this.elevation,//阴影
    this.forceElevated: false,
    this.backgroundColor,背景颜色
    this.brightness,//主题明亮
    this.iconTheme,图标主题
    this.textTheme,//文字主题
    this.primary: true,//是否预留高度
    this.centerTitle,标题是否居中
    this.titleSpacing: NavigationToolbar.kMiddleSpacing,
    this.expandedHeight,//展开高度
    this.floating: false,//是否随着滑动隐藏标题
    this.pinned: false,//是否固定在顶部
    this.snap: false,//与floating结合使用
     })

![](https://pic-mike.oss-cn-hongkong.aliyuncs.com/ Blog/20190502203019.png)

构造方法也是非常的简单,但是我们却不能直接使用它,由官方文档可以看到我们通常结合ScrollView来使用它。

我们结合CustomScrollView来看下例子吧。

import 'package:flutter/material.dart';

void main() {
  runApp(new MaterialApp(
    home: new MyApp(),
  ));
}

class MyApp extends StatelessWidget {
  final List<ListItem> listData = [];

  @override
  Widget build(BuildContext context) {
    for (int i = 0; i < 20; i++) {
      listData.add(new ListItem("我是测试标题$i", Icons.cake));
          }
    return Scaffold(
      body: NestedScrollView(
        headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
          return <Widget>[
            SliverAppBar(
              expandedHeight: 200.0,
              floating: false,
              pinned: true,
              flexibleSpace: FlexibleSpaceBar(
                  centerTitle: true,
                  title: Text("我是一个帅气的标题",
                      style: TextStyle(
                        color: Colors.white,
                        fontSize: 16.0,
                      )),
                  background: Image.network(
                    "https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1531798262708&di=53d278a8427f482c5b836fa0e057f4ea&imgtype=0&src=http%3A%2F%2Fh.hiphotos.baidu.com%2Fimage%2Fpic%2Fitem%2F342ac65c103853434cc02dda9f13b07eca80883a.jpg",
                    fit: BoxFit.fill,
                  )),
            ),
          ];
        },
        body: Center(
          child: new ListView.builder(
            itemBuilder: (BuildContext context, int index) {
              return new ListItemWidget(listData[index]);
            },
            itemCount: listData.length,
          ),
        ),
      ),
    );
  }
}

class ListItem {
  final String title;
  final IconData iconData;

  ListItem(this.title, this.iconData);
}

class ListItemWidget extends StatelessWidget {
  final ListItem listItem;

  ListItemWidget(this.listItem);

  @override
  Widget build(BuildContext context) {
    return new InkWell(
      child: new ListTile(
        leading: new Icon(listItem.iconData),
        title: new Text(listItem.title),
      ),
      onTap: () {},
    );
  }
}

首先我们使用了NestedScrollView中的headerSliverBuilder属性添加了SliverAppBar

然后我们设置展开的高度为200,不让标题栏随着滑动滚动出可视区域

我们使用flexibleSpace来构建了一个可以滚动的区域

最后我们给NestedScrollView的body加了一个ListView

本期小结

  • 熟悉SliverAppBar的用法
  • 了解CustomScrollView的用法
  • 了解SliverPersistentHeader的用法

flutter SliverAppBar - 卓原的进化之旅 - CSDN博客
lizhuoyuan/flutter_oschina: 我也实现一个os China开源中国客户端当做对flutter的熟悉吧

@FrizzleFur
Copy link
Owner Author

FrizzleFur commented May 2, 2019

流式布局Wrap

流式布局是什么
其实理解起来很简单,无论在终端还是Web端每个屏幕都有自己的尺寸,当然每个存在于屏幕上的组件也会有自己的尺寸。

当然在我们知道每个组件尺寸或者屏幕尺寸固定的情况下我们可以很好地来布局我们的组件来达到最合适的显示效果。

但是如果我们的组件的尺寸是不固定的呢?在这种情况下我们的组件就很有可能会超出屏幕的范围,或者达不到我们预期的效果。

所以,我们就需要借助于流式布局来解决这个问题。流式布局会根据当前屏幕的尺寸和当前组件尺寸来看是否进行换行显示。

如:

当我们想要在一行显示多个Button时就会出现如图的问题,超出屏幕的Widget并不会显示。

Wrap({
   Key key,
   this.direction = Axis.horizontal,//方向
   this.alignment = WrapAlignment.start,//内容排序方式
   this.spacing = 0.0,//两个widget之间横向的间隔
   this.runAlignment = WrapAlignment.start,
      this.runSpacing = 0.0,两个widget之间纵向的间隔,当为纵向时则为横向间隔
   this.crossAxisAlignment = WrapCrossAlignment.start,
   this.textDirection,//文字排序方向
   this.verticalDirection = VerticalDirection.down,//direction为Vertical时排序顺序
   List<Widget> children = const <Widget>[],//widgets
 })

小结

  • 使用Wrap可以很轻松的实现流式布局效果
  • Wrap支持设置流式布局是纵向显示或者是横向显示
  • 可以使用alignment属性来控制widgets的布局方式

@FrizzleFur
Copy link
Owner Author

FrizzleFur commented May 2, 2019

Flutter中的自定义View

简单的步骤

  • 新建类继承于CustomPainter实现paint()和shouldRepaint()方法
  • 在paint方法中绘制你想要的内容
  • 借助于 CustomPaint Widget来构建自己的Widget

当然,上面仅仅是自定义的流程,具体的实现还是有很多细节需要处理的。

与绘制相关的知识
学过前端或者终端开发的童鞋,应该对绘制都比较熟悉,绘制主要还是靠画布canvas和画笔Paint和完成的,画布就是你绘制图形的地方,画笔就是你用来作画的笔。

画布canvas
画布是一个矩形区域,我们可以控制其每一像素来绘制我们想要的内容

canvas 拥有多种绘制点、线、路径、矩形、圆形、以及添加图像的方法,结合这些方法我们可以绘制出千变万化的画面。

虽然,画布可以画这些东西,但是决定这些图形颜色、粗细表现的还是画笔。

@FrizzleFur
Copy link
Owner Author

FrizzleFur commented May 2, 2019

Flutter 布局控件完结篇

1.2 本质
我数了一下我文章总结过的布局控件,总共有29种。乍看会觉得真鸡毛的多,不乍看,也会觉得鸡毛的真多。为什么其他的移动平台没有这么多布局控件呢?其实不然,其他平台没有这么细分。

以Android平台为例,最基础的几种布局例如LinearLayout、RelativeLayout、ConstraintLayout等等。很多Flutter的控件,对于Android来说,仅仅是一个属性的设置问题。

再往上看,iOS、Android、Web这些平台的布局,其实最基本就那几种,线性布局、绝对布局、相对布局等等。Flutter也逃不出这些,那为什么Flutter现在有这么多布局控件呢?

第一点,之前文章介绍过的,Flutter的理念是万物皆为widget。这和Flutter的实现机制有关,而不是因为它在布局上有什么特殊性,这也是最主要的一点。

第二点,我觉得是因为这是Flutter的初期,如果有经历过一个技术的完整发展周期,就会明白,前期只是提供各种零件,只有商业支撑或者人员支撑足够的时候,才会去优化零件。而现在就是这么一种资源不足的状态。各种组件可以合并的有很多,底层的实现机制不会变,只是再加一层即可,这也是可以造轮子的地方,例如封装一套适用于Android、iOS或者Web人员的控件库等。

第三点,跟初期相关,一套新的技术,各种东西不可能一下子全想明白,路总是走着走着才发现走歪了,就像一些控件,可能一些地方合适,但是一些新的地方又不太合适,所以就再造一个,所以有些控件看起来功能十分相似。

说了这么多,我其实就想说明一点,Flutter现在还只是处在社会发展的初级阶段,还处在温饱问题都解决不了的状态,想达到小康还需要很长的一段路要走。


上面是这18种控件的父节点层面的继承关系,唯一不同的一个控件就是Container。所以按照是否继承自SingleChildRenderObjectWidget的分类如下:

2.1.2 按照功能是否单一划分
分类如下:

功能不单一的控件,Container、Transform、FittedBox、SizedOverflowBox。
功能单一的控件,有Padding、Align、Center、AspectRatio、ConstrainedBox、Baseline、FractionallySizedBox、IntrinsicHeight、IntrinsicWidth、LimitedBox、Offstage、OverflowBox、SizedBox、CustomSingleChildLayout。
先在此处小结一下,可以看出Container的特殊之处了吧,为什么Container这么特殊了。这个特殊要从两个层面去看。

对于Flutter而言,Container是特殊的,因为它不是功能单一的控件,是一个组合的控件,所以它相对于Flutter是特殊的。
对于移动端开发者而言,它不是特殊的,因为很多UI都是一些基础功能组合的,这样能让开发者更方便的使用。
那能得出什么结论呢?我个人觉得,Container这种组合的控件会越来越多,也会有个人开发者去开发这种通用型的组合控件,这是一个大趋势,是Flutter走向易用的一小步。

2.1.3 按照功能划分
在此处我按照定位、尺寸、绘制三部分来尝试着去做功能的划分,当然这个划分并不绝对,仁者见仁吧。

定位控件:Container、Align、Center、FittedBox、Baseline、Transform。
尺寸控件:Container、FittedBox、AspectRatio、ConstrainedBox、FractionallySizedBox、IntrinsicHeight、IntrinsicWidth、LimitedBox、SizedBox、SizedOverflowBox。
绘制控件:Container、Padding、Offstage、OverflowBox、SizedOverflowBox、Transform。

2.2 使用
单节点控件虽然这么多,但是大部分不会挨个去尝试。对于大部分人而言,都是佛系的用法,一个控件能够使用,就一直用到死。

在布局上,大方向还是不停的拆,把一张设计图,拆成一棵树,每个节点根据需要,选择合适的控件,然后从根部开始不停嵌套,布局就完成了。

2.3 控件的选择
控件种类繁多,真正使用的时候该如何去选择呢?有万金油的做法,不管啥都用Container,这也是很多初接触的人经常干的方式。这么做的确可以按照设计图把布局给实现了,但是会涉及到一些性能上的问题。

控件的选择,按照控件最小功能的标准去选择。例如需要将子节点居中,可以使用Container设置alignment的方式,也可以使用Center。但是从功能上,Center是最小级别的,因此选择它的话,额外的开销会最小。

将UI实现了,这只是最基本的,当达到这一步了,应该更多的去思考,如何更好的布局,使得性能更高。

3. 多节点控件

多节点控件的种类就少了一些,虽然也有11种,但是功能和场景多了,所以选择上反而会简单一些。

3.1 分类
多节点控件内部实现比单节点控件复杂的多,会从继承以及功能两个方向去做分类。

从上图可以看出,多节点布局控件基本上可以分为三条线

  • 继承自BoxScrollView的控件,有GridView以及ListView;
  • 继承自MultiChildRenderObjectWidget的控件,有Row、Column、Flow、Wrap、Stack、IndexedStack、ListBody、CustomMultiChildLayout八种;
  • 继承自RenderObjectWidget的控件,有Table一种。

4. 性能优化

性能优化这块儿,可能仁者见仁,并没有一个统一的说法,毕竟现在Flutter各方面都还不完善。但是,大方向还是有的,尽量使用功能集更小的控件,这个对于渲染效率上还是有所帮助的。

4.1 优化
在这里我试着去列举一些,并不一定都正确。

  • 对于单节点控件,如果一个布局多个控件都可以完成,则使用功能最小的,可以参照上面控件分类中的功能划分来做取舍;
  • 对于多节点控件,如果单节点控件满足需求的话,则去使用单节点控件进行布局;
  • 对于ListView,标准构造函数适用于条目比较少的情况,如果条目较多的话,尽量使用ListView.builder;
  • 对于GridView,如果需要展示大量的数据的话,尽量使用GridView.builder;
  • Flow、Wrap、Row、Column四个控件,单纯论效率的话,Flow是最高效的,但是使用起来是最复杂的;
  • 如果是单行或者单列的话,Row、Column比Table更高效;
  • Stack和CustomMultiChildLayout如果同时满足需求的话,CustomMultiChildLayout在某些时候效率会更高一些,但是取决于Delegate的实现,且使用起来更加的复杂;

上面所列的比较杂,但是归纳起来,无非这几点:

  • 功能越少的控件,效率越高;
  • ListView以及GridView的builder构造函数效率更高;
  • 实现起来比较复杂的控件,效率一般会更高。

4.2 选择
控件的选择,个人觉得把握大方向就够了。如果时间紧急,以实现效率最优先,如果时间充裕的话,可以按照一些优化细则,去做一些选择。单纯控件层面,带来性能上的改进毕竟十分有限。

参考

@FrizzleFur FrizzleFur unpinned this issue Aug 7, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant