[TOC]
flutter介绍
flutter是什么
Flutter 是Google开源的UI工具包,帮助开发者通过一套代码库高效构建多平台精美应用,支持移动、Web、桌面和嵌入式平台。Flutter 开源、免费,拥有宽松的开源协议,适合商业项目。
对于应用层开发人员来说,flutter就是一个用于绘制UI界面的SDK,一套代码,多端运行。
跨平台方案对比
自移动开发兴起以来,跨平台技术就不断涌现,从webview到RN,再到现在的flutter,可以说是一个逐步完善的过程,下面是跨平台技术的对比:
Web/Hybrid:
也被成为Hybrid技术,它是基于web h5界面来实现跨平台的,市面上有些应用为了减少开发成本,APP就是个壳子,所有界面都在h5中,系统服务则通过一个中间层桥接到JaveScript中去,APP只需要一个webview即可。
hybrid技术本身是没什么问题的,拉新、活动等等都可以用到H5页面。但是APP完全是个壳,所有界面都在h5中,把APP当浏览器用,那么用户体验必定是极差的,原生性能也得不到发挥。所以必须明确一点:移动应用不是浏览器。
JSCore(ReacNative, Weex):
记得RN兴起的时候好多大厂也在用,毕竟是脸书Facebook开发维护的,国内开发人员也争相学习。
这种方式比起web h5来说,不仅将系统服务桥接到JavaScript中,并且将原生UI也桥接过去了。也就是说,一套JS代码通过DOM树可以映射为原生不同平台的UI,确实比上面那种好多了。
但是缺点也很明显:
- 在渲染UI前先要执行映射操作,在滑动时想保证60FPS很难,用户可以感觉到很明显的掉帧。
- 就算映射为原生控件了,那么不同平台上展现出的UI还是有平台差异,没有真正达到跨平台的目的。
原生
苹果2008年发布iOS,Google 2009年发布Android,它们的SDK是基于两种不同的编程语言Objective-C 和 Jave.现在又有了Swift和Kotlin。
上面是原生App的一个简单架构,开发人员直接调用平台SDK进行UI开发。由于语言及SDK的不同,所以开发人员必须为两个平台分别开发App。
Flutter
-
本地服务:Flutter使用Dart语言开发,Dart可以被编译(AOT)成不同平台的本地(native)代码,让Flutter可以直接和平台通讯而不需要一个中间的桥接过程,从而提高了性能。
-
UI渲染:
可以看到,在绘制方面,android原生使用jni(java native interface)调用C的Skia引擎,而flutter是RenderObject调用skia引擎进行绘制,所以在底层绘制方面,flutter和原生其实没什么区别。
widget tree和element tree是一对一的关系,但是由于缓存原因,最后真正渲染的render object会减少。
flutter优缺点
优点
- UI性能:在保证原生级性能的前提下实现了UI完全跨平台。
- 开发方面:
- 热重载,ctrl + s,随时更新UI。
- 上手非常简单,和微信小程序类似,写UI就像在写配置一样。而且dart在一定程度上兼容了java的语法,几乎没有太大差别,android开发非常容易上手。
- 能有效降低人力成本
- 路由设计和动画设计优秀,可以通过await关键字直接获取出栈页面的返回值,android需要startActivityForResult。
缺点
- 依赖原生:系统服务依赖原生,如语音、摄像头等硬件方面依赖原生,期待后面第三方库。
- 生产包较大:由于引入了flutterlib.so库以及自带的material/cupertino asset资源,比原生大8M左右。
- UI编译阶段不可视化:可读性较差,不像android有xml可以直接看到视图。这样widget多的话找一个widget可能得找半天。
- widget API提供较少,特别是ListView滑动组件。开源库较少,不比原生丰富。
dart特点
异步非常简单
Dart 是单线程模型,因为它天然不存在资源竞争和状态同步的问题。这就意味着,一旦某个函数开始执行,就将执行到这个函数结束,而不会被其他 Dart 代码打断。所以 Dart 中并没有线程,只有 Isolate(隔离区)。Isolate之间不会共享内存,就像几个运行在不同进程中的 worker,通过事件循环(Event Looper)在事件队列(Event Queue)上传递消息通信
void main() {
printName();
}
Future<void> printName() async {
String name = await getNameFromNet();
print(name);
}
Future<String> getNameFromNet() async {
await Future.delayed(Duration(milliseconds: 1000));
return "name";
}
这里模拟联网请求操作,在一秒后返回name,打印操作同步等待联网请求完毕才执行。
getNameFromNet返回了一个Future对象,表示返回值现在不能给到,在未来某个时间点给到再return。
可选参数
const Text(
this.data,
{
Key key,
this.style,
this.strutStyle,
this.textAlign,
this.textDirection,
this.locale,
this.softWrap,
this.overflow,
this.textScaleFactor,
this.maxLines,
this.semanticsLabel,
this.textWidthBasis,
this.textHeightBehavior,
}
) : assert(
这是flutter Text widget的构造方法,可以看到,只有text的data是必传参数,其他参数都是可选的用{}括起来。这也是为什么flutter就像在写配置的原因,主要是依赖dart特性,方法可选参数。
兼容java语法且比java简单
// 定义类
class Person {
// 基本数据类型
String name;
int age;
double height;
// 构造方法
Person(this.name, {this.age, this.height});
}
void main() {
// 创建对象,new可以省略
Person person = Person("lzj", age: 23, height: 175.0);
// 方法调用
introduce(person);
}
void introduce(Person person) {
print("My name is ${person.name}, Im ${person.age} years old and ${person.height} cm");
}
My name is lzj, Im 23 years old and 175.0 cm.
flutter for web
配置好flutter开发环境后,在项目目录下运行指令下载web开发支持:
flutter packages pub global activate webdev
运行web服务指令:
flutter packages pub global run webdev serve
flutter开发
hot reload热重载
只需要ctrl s保存即可热重载,部分服务端下发数据无法热重载的话ctrl shift \,瞬间重启程序也可以热重载。对于开发来说是非常方便的。
声明式+指令式
相对于原生的指令式更新UI,flutter提供了声明式+指令式模式,比如:
在android原生中,我们在xml中声明UI,在java中指令式修改UI,比如给一个TextView赋值:
textView.setText("...");
setText就好比一个指令,告诉CPU要去重新渲染TextView的值了,这个是需要开发人员手动调用的。
而声明式则不需要手动调用来改变UI状态,所有widget都是一开始声明好的,如果widget状态发生改变才去build渲染,而widget状态又是由数据驱动的,所以:数据 -> widget状态 -> 渲染。
Text(data);
Text声明好之后就就不用管了,如果要改变Text值,只需要改变data即可,而不直接对Widget进行操作。
但flutter也是支持指令式的,比如:
widget.addWidget();
三种运行模式
- debug:支持热重载、支持性能检测,开发调试用此模式。
- profile:不支持热重载,支持性能检测,一般性能检测时用此方式运行,既不会因为支持热重载降低性能,同时也可以检测程序性能。
- release:不支持热重载和性能检测,用于生产,性能最高。
项目结构
在dart中,一切都是object,不同于java,一个int也是对象,默认初始值是null,而java的int是基本数据类型,不是对象,初始值是0。
在flutter中,一切都是widget,一个文本是widget,一个输入框是widget,一张图片也是由widget负责加载(网络、本地),所以一切UI都是widget。他们的父类要么是StatelessWidget,要么是StatefullWidget。
widget分为有状态和无状态,无状态的widget不需要通过状态来改变自己,而有状态的widget需要通过状态改变自己,势力代码如下:
class StateWidgetPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("widgets' state"),
),
body: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
TextWithoutState(),
SizedBox(
height: 50,
),
TextWithState(
interval: 2,
),
],
),
);
}
}
class TextWithoutState extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Text("我是一个无状态的文本widget");
}
}
/// 一个无状态的text widget
class TextWithState extends StatefulWidget {
final int interval;
const TextWithState({Key key, this.interval}) : super(key: key);
@override
_TextWithStateState createState() => _TextWithStateState();
}
/// 一个有状态的text widget
class _TextWithStateState extends State<TextWithState> {
String _text = "";
Timer _timer;
@override
void initState() {
_timer = Timer.periodic(Duration(seconds: widget.interval), (timer) {
setState(() {
_text = "我是一个有状态的widget,每${widget.interval}秒更新自己的状态:${_getCurrentTime()}";
});
});
super.initState();
}
@override
void dispose() {
_timer.cancel();
_timer = null;
super.dispose();
}
@override
Widget build(BuildContext context) {
return Text(_text);
}
String _getCurrentTime() {
return DateTime.fromMillisecondsSinceEpoch(DateTime.now().millisecondsSinceEpoch).toString().substring(11, 19);
}
}
使用Scaffold快速定制页面
Scaffold翻译为中文是脚手架的意思,我们可以利用Scaffold快速实现一个页面,头部标题栏,中间body页面内容,底部(导航栏、悬浮按钮、底部工具栏等等),都在Scaffold中,我们只需要对其进行配置即可:
class ScaffoldPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("头部标题栏"),
actions: <Widget>[
Icon(Icons.delete),
SizedBox(width: 20),
Icon(Icons.cancel),
SizedBox(width: 20),
Icon(Icons.more),
SizedBox(width: 20),
],
),
body: Center(
child: Text("页面部分"),
),
bottomSheet: Container(
height: 50,
width: double.infinity,
color: Colors.red,
child: Center(child: Text("底部工具栏")),
),
floatingActionButton: Container(
margin: EdgeInsets.only(bottom: 50),
child: FloatingActionButton(
child: Icon(Icons.add),
onPressed: (){
print('点击悬浮按钮');
},
),
),
);
}
}
CustomListView强大的列表支持
原生需要多个ViewHolder支持,ViewHolder过多会增加文件数量,flutter只需要一个CustomListView即可往里面放不同类型的widget。实例代码:
class CustomListViewPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: _getListWidget(),
);
}
Widget _getListWidget() {
return CustomScrollView(
slivers: <Widget>[
//AppBar,包含一个导航栏
SliverAppBar(
pinned: true,
expandedHeight: 250.0,
flexibleSpace: FlexibleSpaceBar(
title: const Text('Custom List View'),
background: Image.asset(
"assets/images/cover_img.jpg",
fit: BoxFit.cover,
),
),
),
SliverPadding(
padding: const EdgeInsets.all(8.0),
sliver: new SliverGrid(
//Grid
gridDelegate: new SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2, //Grid按两列显示
mainAxisSpacing: 10.0,
crossAxisSpacing: 10.0,
childAspectRatio: 4.0,
),
delegate: new SliverChildBuilderDelegate(
(BuildContext context, int index) {
//创建子widget
return new Container(
alignment: Alignment.center,
color: Colors.cyan[100 * (index % 9)],
child: new Text('grid item $index'),
);
},
childCount: 20,
),
),
),
//List
new SliverFixedExtentList(
itemExtent: 50.0,
delegate: new SliverChildBuilderDelegate((BuildContext context, int index) {
//创建列表项
return new Container(
alignment: Alignment.center,
color: Colors.lightBlue[100 * (index % 9)],
child: new Text('list item $index'),
);
}, childCount: 50 //50个列表项
),
),
],
);
}
}
flutterVS原生android
文档:flutterVSnative.md
链接:http://note.youdao.com/noteshare?id=7122d6e0e841545dbbb3d54212b68572&sub=46E7B0FBC27549319467136DEDFC7A67
与原生交互
android
class NativeChannelTestPage extends StatefulWidget {
@override
_NativeChannelTestPageState createState() => _NativeChannelTestPageState();
}
class _NativeChannelTestPageState extends State<NativeChannelTestPage> {
MethodChannel _methodChannel;
@override
void initState() {
_methodChannel = const MethodChannel("top.tupobi.demo/channel_connection");
_methodChannel.setMethodCallHandler((handler) {
switch (handler.method) {
case "flutterFunction":
print("native invoke dart function message = ${handler.arguments["message"]}");
break;
}
return null;
});
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Native Channel"),
),
body: Column(
children: <Widget>[
FlatButton(
child: Text("调用native吐司"),
onPressed: () {
_methodChannel.invokeMethod("nativeToast", {"message": "flutter"});
},
)
],
),
);
}
}
class NativePluginForFlutter : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware {
private lateinit var mRootActivity: Activity
private lateinit var mMethodChannel: MethodChannel
override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
val methodChannelName = "top.tupobi.demo/channel_connection"
mMethodChannel = MethodChannel(binding.binaryMessenger, methodChannelName)
mMethodChannel.setMethodCallHandler(this)
println("native 初始化mMethodChannel")
mMethodChannel.invokeMethod("flutterToast", mapOf("message" to "messageFromNative"))
}
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
}
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
when (call.method) {
"nativeToast" -> {
val message = call.argument<String>("message")
Toast.makeText(mRootActivity, "toast from native, message = $message", Toast.LENGTH_LONG).show()
mMethodChannel.invokeMethod("flutterFunction", mapOf("message" to "messageFromNative"))
}
}
}
override fun onDetachedFromActivity() {
}
override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
}
override fun onAttachedToActivity(binding: ActivityPluginBinding) {
this.mRootActivity = binding.activity
}
override fun onDetachedFromActivityForConfigChanges() {
}
}
状态管理
简单的项目可以直接setState()进行状态管理即可,简单的状态管理如下图:
但是当项目体积庞大起来的时候,随着业务逻辑复杂度增加,再使用setState()进行状态管理就会出现下面这种情况,widget之间的状态异常复杂,且业务逻辑与UI耦合在一起,这样非常不利于代码的维护和新功能的扩展。
使用provider
provider的原理是使用InheritedWidget实现共享数据,父类定义的数据可以被所有子widget共享,这样数据一旦发生变化,只需要发送一个通知,所有监听该数据变化的widget感知到后就可以改变自身的状态。
共享数据:
class SharedData with ChangeNotifier {
UserModel _userModel;
GoodModel _goodModel;
void updateUserModel() {
UserModel userModel = UserModel();
userModel.name = Random().nextInt(1000).toString();
userModel.age = Random().nextInt(100);
_userModel = userModel;
notifyListeners();
}
void updateGoodModel() {
GoodModel goodModel = GoodModel();
goodModel.goodName = Random().nextInt(1000).toString();
goodModel.goodPrice = Random().nextInt(100);
_goodModel = goodModel;
notifyListeners();
}
get userModel => _userModel;
get goodModel => _goodModel;
@override
String toString() {
return 'SharedData{userModel: $userModel, goodModel: $goodModel}';
}
}
class GoodModel {
String goodName;
int goodPrice;
@override
String toString() {
return 'GoodModel{goodName: $goodName, goodPrice: $goodPrice}';
}
}
监听者(widget):
class TextWidget1 extends StatelessWidget {
@override
Widget build(BuildContext context) {
print("TextWidget1 build");
// 这种方式context相关全部重建
// final userModel = Provider.of<SharedData>(context).userModel;
// 这种方式与consumer一致
// final userModel = context.watch<SharedData>().userModel;
// 这种方式修改SharedData中其他属性不会重建,与selector一致
final userModel = context.select((SharedData sharedData) => sharedData.userModel);
return Container(
width: 80.w,
child: Text(
userModel == null ? "null" : userModel.toString(),
),
);
}
}
数据变化事件:
class ProviderTestPage2 extends StatelessWidget {
final SharedData _sharedData = SharedData();
@override
Widget build(BuildContext context) {
print("ProviderTestPage2 build");
return Scaffold(
appBar: AppBar(
title: Text("provider test2"),
),
body: MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => _sharedData),
],
child: Container(
child: Column(
children: <Widget>[
FlatButton(
onPressed: () {
_sharedData.updateUserModel();
},
child: Text("change user model"),
),
FlatButton(
onPressed: () {
_sharedData.updateGoodModel();
},
child: Text("change good model"),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: <Widget>[
TextWidget1()
],
)
],
),
),
),
);
}
}
可以看到,观察者对要观察的数据的粒度是可以把控的:
- 最粗粒度:Provider.of<SharedData>这种是最粗的粒度,一旦ShareData包括其中内容发生改变就会通知所有context相关的widget进行build,这种方式是不可取的。
- 中等粒度:context.watch<SharedData>,这种对context进行了筛选,只有当ShareData及其中内容发生改变时,只会将事件通知给监听者,没有监听的widget是不会build的。
- 细粒度:context.select((SharedData sharedData) => sharedData.userModel),通过高阶函数筛选ShareData中的成员变量进行监听,如果没被监听的成员变量改变是不会通知该widget的。
其他状态管理方案
还有其他状态管理方案,比如bloc还有咸鱼推出的fishbloc等,感兴趣可以自行了解下。
flutter未来可期
未来flutter在谷歌的计划中可能不仅仅是一种跨平台技术,谷歌开发的新一代操作系统Fuchsia也是使用flutter进行开发。假如华为鸿蒙系统能与现如今的安卓操作系统平分秋色,那未来肯定还会和Fuchsia一战。