距离Flutter正式版出来已经有很长的时间了,目前大家对于Flutter的呼声也是很高,就算是平时不了解移动开发的朋友们也开始好奇Flutter究竟是个什么东西。就连我朋友的老板都开始问,公司产品能不能换成Flutter来开发?
那么Flutter究竟是什么呢?它是一个声明式
的移动UI框架
,附带了自己的渲染引擎,类似于React框架自带了浏览器渲染引擎的感觉。它可以使app的界面编写更加简单直接,且不必在UI设计上做妥协。自带渲染引擎听起来侵入性比较强,没有使用对应平台的渲染机制,不过从RN的现状看来,通过bridge
的形式依附于对应平台的渲染机制在性能上体验不佳,而Flutter会直接编译成native code
,从理论上来说,跟我们用Java开发app相比不会有性能上的损失,写界面也变得更简单了。
Flutter使用Dart这门语言进行开发,Flutter本质上也就是个Dart类库。所有的控件,所有的代码都是用Dart编写的。一开始我很拒绝Dart这门语言,一个默默无闻好多年的语言,跟着Flutter才为人所知晓,谷歌推了这么久的kotlin,用kotlin来开发多好啊,我们学习迁移的成本也能大大降低。在这一年多的Flutter学习过程中,我发现谷歌这么做也有自己的考虑。首先Dart是谷歌自己的语言,想想它跟Oracle的官司打了多少年。其次Dart同时支持AOT
跟JIT
编译,JIT
使得我们可以快速修改原型,我们做的修改一秒不到就可以更新到我们的设备上,而AOT
保证我们发布的时候app不会有不必要的性能损失。那为什么不用go呢?算了吧,go只适合系统编程?。
Flutter宣称Everything is Widget
,这句话对,也不全对,在Flutter里面,主题,边距,图片,动画等等都是通过widget
来实现的,任何UI效果都可以抽象成Widget
,其实在Flutter里面是有其它对象的,比如Element
,比如RenderBox
,(这些东西是什么我们下回再说)只不过对于我们开发者而言,我们日常开发中需要关心的就只是Widget
了。Flutter中的Widget
基本上可以分为两大类:StatefulWidget
和StatelessWidget
。这俩的区别可以直接从它们的名字上看出来,一个有状态,一个无状态。有状态的Widget
会自己维护一些逻辑数据,而无状态的widget
的内容由它的parent传递过来。简单来说,一个Widget
如果包含了业务逻辑数据,这些数据在它销毁重建的时候需要做一些处理,那它就是有状态的。比如我们的购物车,它需要记录里面的商品的数量,它就是有状态的,而一个提交订单按钮它就是无状态的,它只关心在被点击的时候执行一个回调。两者在生命周期回调上也有所不同。有的同学第一次接触Flutter的可能觉得很绕,没关系,等会儿我们来简单上手一个小例子感受一下就懂了。
StatelessWidget
会调用它的build
方法来描述它的view,而StatefulWidget
有一个与之配套的State
对象,它只会调用createState
方法去创建一个State
对象,在这个State
对象里也有一个build
方法来描述view。这些build
方法都必须返回另一个Widget
。
当我们新建一个Flutter项目的时候,默认给我们生成了一个计数器的demo。我们接下来就通过把玩这个项目来感受下flutter的魅力。Flutter提倡组合优于继承
,所有的复杂的widget
都是通过组合细小的Widget
而来,我们只需要在build
方法里面组合不同的widget
就能定义出自己想要的UI。而Flutter也给我们提供了丰富的控件,我们日常开发就是不断组合Widget
来构建我们的app,就连我们的app本身也是一个巨大的widget
,不会有什么特殊的类去组合Widget
来构建我们的app。
创建完项目,目录下的文件似乎有很大的一坨:
代码语言:javascript复制counter_app
|- android
|- ios
|- lib
|- main.dart
|- test
|- widget_test.dart
.gitignore
pubspec.yaml
pubspec.lock
README.md
我们主要的代码目录在lib目录下,android跟ios目录只有在特定平台相关的代码时才会用到。初始项目代码很少但是注释很多,官方给我们详细注释了使用到的Widget
的用途。
运行刚创建的项目:
初始界面
点击那个加号,屏幕中央的数字就会增加。
我们来看下我们的代码,在main.dart
这个文件里,跟我们的Java的规则一样,程序的入口是一个main函数:
void main() => runApp(MyApp());
我们实际项目中main函数不会这么简单,我们会做一些全局的配置,不过现在我们暂且不管,现在这么多就足够啦,这个MyApp就是对应到我们安卓里的Application了,我们来分析一下。
虽然我们的app代码很少,可以学到的东西可不少:
代码语言:javascript复制class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
我们可以看到,我们的app也是一个Widget
,而且还是一个StatelessWidget
,我们重写了build
方法,返回了另一个Widget
,它是一个MaterialApp
,然后这个Widget
还可以有自己的子Widget
。通过这种方式我们的app其实就形成了一个Widget
树,包含着其它包含子Widget
的Widget
。然后我们可以尝试修改它,比如把这个primarySwatch
的颜色换掉:Colors.orange
,然后只要我们按下Ctrl S,修改分分钟在我们的设备上生效,主题颜色立马改变了,这就是Flutter宣传时吹爆的热加载的能力,惊艳吧?
修改主题颜色
再来看看这个MyHomePage
,它是MaterialApp
的home属性的值,MaterialApp
是一个应用骨架,提供了很多MD风格的配置,这个配置会通过Widget
树往下传给所有子Widget
,它的home属性代表着我们的主界面,我们来看看:
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
@override
_MyHomePageState createState() => _MyHomePageState();
}
代码量很少,它是一个StatefulWidget
,只是重写了createState
方法。在这个_MyHomePageState
里,重写了build
方法实际描述了它的view。StatefulWidget
的生命周期比较有意思:
StatefulWidget 生命周期
创建后会立即调用createState
方法,在这里会调用State
的构造器,然后走initState
方法,然后调用build方法去描述它的view。而且Flutter是一个响应式的框架,我们通过setState
方法去更新一些状态,每当setState
方法被调用的时候,状态会被标记为dirty
,然后Flutter会重新绘制。也就是说,我们可以通过setState
方法去通知界面更新。
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter ;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.display1,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}
我们可以看到,我们想要UI成什么样子我们就声明出来就好了,想要居中就声明,想要竖直排列就声明,很直观,对于UI的描述都在State
的build
方法里了,在这儿一切都是Widget
。Center
是让子Widget
居中的Widget
,Colunm
是让其它Widget
纵向排列的Widget
。Text
是展示文字的Widget
。为什么这么做呢,怎么不在Widget
里面去实现build
方法呢?因为Widget
都是不可修改(immutable)的,StatelessWidget
能够实现build
方法是因为它所有的信息都来自于外界,它本身木有什么状态可修改,而StatefulWidget
则需要维护自己的状态,状态更新的时候还需要重新绘制,所以State
对象才存在并且还有一系列方法通知系统去重新绘制。
再来看看这个FAB,点击它会增加屏幕中间的数字。
代码语言:javascript复制floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
)
使用起来也很简单,我们知道Flutter是个声明式的UI框架,我们只需要声明它的子Widget
,在这儿就是个Icon
,以及被点击的回调就好了。在这里我们声明了点击调用_incrementCounter
这个方法,这个方法里会通过setState
去更新状态并通知系统重绘,那么所有依赖_counter
这个变量的view都会重绘。Flutter给我们内置了很多MD的图标,如果大家对MD的图标比较满意,那直接通过Icons
这个类就可以获取,省的UI再切图了。
瞧瞧,简单这些点代码,我们就实现了一个美观的计数器app。但是感觉功能有点单调,可能我们还有点手痒痒想动手试试,来,我们来自己增加些功能,实现增加,减少,重置的功能。
首先我们在FAB上方添加两个按钮来实现数字的加减,我们知道我们的UI整体在一个叫Column
的widget里面,我们的按钮横向排列,当然得放在一个Row
里面啦。
Row(
children: <Widget>[
RaisedButton(
child: Text('增加'),
onPressed: _incrementCounter,
),
RaisedButton(
child: Text('减少'),
onPressed: _decrementCounter,
),
],
)
这边的_decrementCounter
与_incrementCounter
代码类似,不过是减少数值。
增加按钮
按钮是加上来了,但是颜色有点单调,而且太靠左了,居中显示更加好看一点。这是因为Row
是一个flex Widget
,默认在横向上占有它能占有的所有空间,占据整个屏幕的宽度无所谓,我们得让这俩按钮居中,这里我们就用到Row
的mainAxisAlignment
属性了,用它来分配主轴上的空白空间。我们这里使用MainAxisAlignment.spaceAround
,它会把空白空间分成两边小中间大的样子,大家可以自由的尝试一下这个枚举类的不同的值,感受他们的效果。
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: <Widget>[
RaisedButton(
color: Colors.orange,
child: Text('增加'),
onPressed: _incrementCounter,
),
RaisedButton(
color: Colors.amber,
child: Text('减少'),
onPressed: _decrementCounter,
),
],
)
最后我们需要把FAB改成重置的效果,只需要写一个_resetCounter
方法把_counter
设为0就好啦。
void _resetCounter() {
setState(() => _counter = 0);
}
图标也要替换掉,好在Flutter给我们内置了好多MD图标,Icons
里面就有刷新图标:Icons.refresh
。再次Ctrl S:
最终效果
好了,我们的最终效果已经实现了,增加了加减,重置功能,顺便也体验了一把编写Flutter应用有多爽。Flutter代码的编写流程就是这么简单,就是组合!组合小的形成大的Widget
,组合已有的形成之前没有的Widget
。在初步了解了Flutter之后,有些同学可能好奇,Flutter不停地销毁Widget
再重建,它是怎么做到快速绘制如原生般流畅的?放心,下次我们就来了解一下Flutter的渲染流程,了解它为什么不停地创建销毁Widget
却仍然丝滑。