官方文档
- Flutter官方开发文档地址: https://flutter.dev/docs
- Flutter中文开发文档地址: https://flutterchina.club/docs/
预研内容主要分为几个部分:
- Flutter环境搭建(技术框架, 安装,编辑器)
- Flutter的UI及交互(布局,交互,手势,动画,路由导航)
- Flutter资源文件管理(图片资源,文件资源,文件的读写)
- Flutter数据存储
- Flutter的网络请求(网络HTTP,JSON序列化)
- Flutter与原生平台(平台特定代码交互)
- Flutter开发语言(Dart语言)
- Flutter其他
Flutter环境搭建
- 技术框架
在Flutter中用Widget来描述界面,Widget只是View的“配置信息”,编写的时候利用Dart语言一些声明式特性来得到类似结构化标记语言的可读性。Widget根据布局形成一个层次结构。每个widget嵌入其中,并继承其父项的属性。没有单独的“应用程序”对象,相反,根widget扮演着这个角色。在Flutter中,一切皆为Widget,甚至包括css样式。
- Flutter环境安装
mac下终端操作:
vim ~/.bash_profile 添加路径(没有.bash_profile的时候,需要通过vim命令创建)
export PUB_HOSTED_URL=https://pub.flutter-io.cn //国内用户需要设置
export FLUTTER_STORAGE_BASE_URL=https://storage.flutter-io.cn //国内用户需要设置
export PATH=PATH_TO_FLUTTER_GIT_DIRECTORY/flutter/bin:$PATH
修改 ~/.zshrc ,在其中添加:source ~/.bash_profile (没有.zshrc的时候,需要通过vim命令创建)
- 编辑器
Android Studio 安装
安装Flutter和Dart插件
在Flutter插件中,可使用以下模板:
前缀stless: 创建一个StatelessWidget的子类.
前缀stful: 创建一个StatefulWidget子类并且关联到一个State子类.
前缀stanim: 创建一个StatefulWidget子类, 并且它关联的State子类包括一个 AnimationController
Flutter的UI及交互
- 布局
参考:https://flutterchina.club/tutorials/layout/
Text:该 widget 可让创建一个带格式的文本。
Row、 Column: 这些具有弹性空间的布局类Widget可让您在水平(Row)和垂直(Column)方向上创建灵活的布局
Stack: 取代线性布局 (译者语:和Android中的LinearLayout相似),Stack允许子 widget 堆叠
Container: Container 可让您创建矩形视觉元素。container 可以装饰为一个BoxDecoration
为了继承主题数据,widget需要位于MaterialApp内才能正常显示, 因此我们使用MaterialApp来运行该应用。 //Scaffold是Material中主要的布局组件.
GestureDetector widget并不具有显示效果,而是检测由用户做出的手势。 当用户点击Container时, GestureDetector会调用它的onTap回调, 在回调中,将消息打印到控制台。您可以使用GestureDetector来检测各种输入手势,包括点击、拖动和缩放。
StatefulWidgets是特殊的widget,它知道如何生成State对象,然后用它来保持状态
在StatefulWidget调用createState之后,框架将新的状态对象插入树中,然后调用状态对象的initState。 子类化State可以重写initState,以完成仅需要执行一次的工作。 例如,您可以重写initState以配置动画或订阅platform services。initState的实现中需要调用super.initState当一个状态对象不再需要时,框架调用状态对象的dispose。 您可以覆盖该dispose方法来执行清理工作。例如,您可以覆盖dispose取消定时器或取消订阅platform services。 dispose典型的实现是直接调用super.dispose。
stateless widget 没有内部状态. Icon、 IconButton, 和Text 都是无状态widget, 他们都是 StatelessWidget的子类。stateful widget 是动态的. 用户可以和其交互 (例如输入一个表单、 或者移动一个slider滑块),或者可以随时间改变 (也许是数据改变导致的UI更新)
可以使用key来控制框架将在widget重建时与哪些其他widget匹配。默认情况下,框架根据它们的runtimeType和它们的显示顺序来匹配。 使用key时,框架要求两个widget具有相同的key和runtimeType。
各种widgets的目录索引,有UI不熟悉的,可以在这里找到说明:https://flutterchina.club/widgets/
各类Widgets的地址: https://flutterchina.club/widgets/basics/
各种UI布局需要用到的控件介绍: https://flutterchina.club/widgets/material/
iOS 风格的控件集合 介绍: https://flutterchina.club/widgets/cupertino/
- UI的一些注意事项
类MyAppBar和MyScaffold中使用了Container、Row、Column、Text、IconButton、Icon、BoxDecoration、Center、Expanded等常用Widget
Theme.of(context)将查找Widget树并返回树中最近的Theme。如果我们的Widget之上有一个单独的Theme定义,则返回该值。如果不是,则返回App主题。 事实上,FloatingActionButton真是通过这种方式找到accentColor的!
ListView的构造函数需要一次创建所有项目,但ListView.builder的构造函数不需要,它将在列表项滚动到屏幕上时创建该列表项。
new ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
return new ListTile(
title: new Text('${items[index]}'),
);
},
);
滑动删除有直接可用的Widget;
将响应转换为自定义Dart对象;
class Post {
final int userId;
final int id;
final String title;
final String body;
Post({this.userId, this.id, this.title, this.body});
factory Post.fromJson(Map json) {
return new Post(
userId: json['userId'],
id: json['id'],
title: json['title'],
body: json['body'],
);
}
}
http package提供了一种方便的方法来为请求添加headers。您也可以使用dart:iopackage来添加。
Flutter提供各种按钮和类似的交互式widget。这些widget中的大多数实现了Material Design 指南, 它们定义了一组具有质感的UI组件。可以使用GestureDetector来给任何自定义widget添加交互性。 可以在管理状态和Flutter Gallery中找到GestureDetector的示例。
如果你要构建一个 CustomButton ,并在构造器中传入它的 label?那就组合 RaisedButton 和 label,而不是扩展 RaisedButton。
Isolates 是分离的运行线程,并且不和主线程的内存堆共享内存。这意味着你不能访问主线程中的变量,或者使用 setState() 来更新 UI。正如它们的名字一样,Isolates 不能共享内存。
在 Flutter 中,最简单的方法是使用 ListView widget。它表现得既和 iOS 中的 ScrollView 一致,也能和 TableView 一致,因为你可以给它的 widget 做垂直排布:
- 交互
参考:https://flutterchina.club/tutorials/interactive/
- 手势
要从widget层监听手势,使用 GestureDetector.
- 动画
参考动画的说明: https://flutterchina.club/animations/
在 Flutter 中,使用 AnimationController 。这是一个可以暂停、寻找、停止、反转动画的 Animation类型。它需要一个 Ticker 当 vsync 发生时来发送信号,并且在每帧运行时创建一个介于 0 和 1 之间的线性插值(interpolation)。你可以创建一个或多个的 Animation 并附加给一个 controller。
- 路由
路由的介绍:https://docs.flutter.io/flutter/widgets/Navigator-class.html
在页面之间跳转,你有几个选择:
具体指定一个由路由名构成的 Map。(MaterialApp
直接跳转到一个路由。(WidgetApp)
Navigator 类不仅用来处理 Flutter 中的路由,还被用来获取你刚 push 到栈中的路由返回的结果。通过 await等待路由返回的结果来达到这点。
Map coordinates = await Navigator.of(context).pushNamed('/location');
Navigator.of(context).pop({"lat":43.821757,"long":-79.226392});
Flutter资源文件管理
- 图片资源
iOS 把 images 和 assets 作为不同的东西,而 Flutter 中只有 assets。被放到 iOS 中 Images.xcasset 文件夹下的资源在 Flutter 中被放到了 assets 文件夹中。assets 可以是任意类型的文件,而不仅仅是图片。
例如,你可以把 json 文件放置到 my-assets 文件夹中。在 pubspec.yaml 文件中声明 assets:`assets:
- my-assets/data.json
然后在代码中使用 AssetBundle 来访问它:
FutureloadAsset() async {
return await rootBundle.loadString('my-assets/data.json');
}`
images/my_icon.png // Base: 1.0x image
images/2.0x/my_icon.png // 2.0x image
images/3.0x/my_icon.png // 3.0x image
- 文件的读写
PathProvider 插件提供了一种平台透明的方式来访问设备文件系统上的常用位置。该类当前支持访问两个文件系统位置:
Flutter数据存储
在 Flutter 中,可以使用 Shared Preferences plugin 来达到相似的功能。它包裹了 UserDefaluts 以及 Android 上等价的 SharedPreferences 的功能。
在 iOS 中,你通过 CoreData 来存储结构化的数据。这是一个 SQL 数据库的上层封装,让查询和关联模型变得更加简单。在 Flutter 中,使用 SQFlite 插件来实现这个功能。
Flutter的网络请求
- 网络HTTP
使用dio 来发起网络请求,它是一个强大易用的dart http请求库,支持Restful API、FormData、拦截器、请求取消、Cookie管理、文件上传/下载
参见具体说明: https://flutterchina.club/networking/
- JSON序列化
使用dart:convert库可以简单解码和编码JSON;要对简单的JSON进行编码,请将简单值(字符串,布尔值或数字字面量)或包含简单值的Map,list等传给encode方法:
Flutter与原生平台
- 平台特定代码交互
交互的方式: https://flutterchina.club/platform-channels/
通过MethodChannel实现。在Flutter当中定义平台管道,定义平台需要捕获的方法名称->Appdelegate当中注册定义的管道->管道方法调用的时候,实现平台方法的调用(达到Flutter调用平台方法的目的)
- Flutter页面路由 与 原生页面之间跳转实现:
Flutter 的代码并不直接在平台之下运行,相反,Dart 代码构建的 Flutter 应用在设备上以原生的方式运行,却“侧步躲开了”平台提供的 SDK。这意味着,例如,你在 Dart 中发起一个网络请求,它就直接在 Dart 的上下文中运行。你并不会用上平常在 iOS 或 Android 上使用的原生 API。你的 Flutter 程序仍然被原生平台的 ViewController 管理作一个 view,但是你并不会直接访问 ViewController 自身,或是原生框架。
我怎么访问 GPS 传感器?使用 location 社区插件。
我怎么访问摄像头?image_picker 在访问摄像头时非常常用。
Flutter开发语言
- Dart语言一些语法特性:
所有没有初始化的变量值都是 null。
注意: 只有当名字冲突的时候才使用 this。否则的话, Dart 代码风格样式推荐忽略 this。
注意: 如果在构造函数的初始化列表中使用 super(),需要把它放到最后。 详情参考 Dart 最佳实践。
在构造函数体执行之前除了可以调用超类构造函数之外,还可以 初始化实例参数。 使用逗号分隔初始化表达式。
class Point {
num x;
num y;
Point(this.x, this.y);
// Initializer list sets instance variables before
// the constructor body runs.
Point.fromJson(Map jsonMap)
: x = jsonMap['x'],
y = jsonMap['y'] {
print('In Point.fromJson(): ($x, $y)');
}
}
有时候一个构造函数会调动类中的其他构造函数。 一个重定向构造函数是没有代码的,在构造函数声明后,使用 冒号调用其他构造函数。
Point.alongXAxis(num x) : this(x, 0);
如果一个构造函数并不总是返回一个新的对象,则使用 factory 来定义 这个构造函数。例如,一个工厂构造函数 可能从缓存中获取一个实例并返回,或者 返回一个子类型的实例。
class Logger {
final String name;
bool mute = false;
// _cache is library-private, thanks to the _ in front
// of its name.
static final Map _cache =
{};
factory Logger(String name) {
if (_cache.containsKey(name)) {
return _cache[name];
} else {
final logger = new Logger._internal(name);
_cache[name] = logger;
return logger;
}
}
Logger._internal(this.name);
void log(String msg) {
if (!mute) {
print(msg);
}
}
}
Abstract methods(抽象函数): 实例函数、 getter、和 setter 函数可以为抽象函数, 抽象函数是只定义函数接口但是没有实现的函数,由子类来 实现该函数。如果用分号来替代函数体则这个函数就是抽象函数。
abstract class Doer {
// ...Define instance variables and methods...
void doSomething(); // Define an abstract method.
}
class EffectiveDoer extends Doer {
void doSomething() {
// ...Provide an implementation, so the method is not abstract here...
}
}
操作符可以被覆写。 例如,如果你定义了一个 Vector 类, 你可以定义一个 + 函数来实现两个向量相加。
如果你使用 noSuchMethod() 函数来实现每个可能的 getter 、setter、 以及其他类型的函数,你可以使用 @proxy 注解来避免警告信息:
枚举类型通常称之为 enumerations 或者 enums, 是一种特殊的类,用来表现一个固定 数目的常量。枚举的 values 常量可以返回 所有的枚举值。
Mixins 是一种在多类继承中重用 一个类代码的方法。使用 with 关键字后面为一个或者多个 mixin 名字来使用 mixin
class Musician extends Performer with Musical {
// ...
}
class Maestro extends Person
with Musical, Aggressive, Demented {
Maestro(String maestroName) {
name = maestroName;
canConduct = true;
}
}
如果你查看 List 类型的 API 文档, 则可以看到 实际的类型定义为 List
。 这个 <…> 声明 list 是一个 泛型 (或者 参数化) 类型。 通常情况下,使用一个字母来代表类型参数, 例如 E, T, S, K, 和 V 等。T 是一个备用类型。这是一个类型占位符, 在开发者调用该接口的时候会指定具体类型。
如果你导入的两个库具有冲突的标识符, 则你可以使用库的前缀来区分。 例如,如果 library1 和 library2 都有一个名字为 Element 的类, 你可以这样使用:
import 'package:lib1/lib1.dart';
import 'package:lib2/lib2.dart' as lib2;
// ...
Element element1 = new Element(); // Uses Element from lib1.
lib2.Element element2 = new lib2.Element(); // Uses Element from lib2.
Dart 有一些语言特性来支持 异步编程。 最常见的特性是 async 方法和 await 表达式。要使用 await,其方法必须带有 async 关键字:
在一个方法上添加 async 关键字,则这个方法返回值为 Future。 例如,下面是一个返回字符串 的同步方法:
String lookUpVersionSync() => '1.0.0';
如果使用 async 关键字,则该方法 返回一个 Future,并且 认为该函数是一个耗时的操作。Future
;lookUpVersion() async => '1.0.0'
在 await expression 中, expression 的返回值通常是一个 Future; 如果返回的值不是 Future,则 Dart 会自动把该值放到 Future 中返回。 Future 对象代表返回一个对象的承诺(promise)。 await expression 执行的结果为这个返回的对象。 await expression 会阻塞住,直到需要的对象返回为止。如果 await 无法正常使用,确保是在一个 async 方法中。 例如要在 main() 方法中使用 await, 则 main() 方法的函数体必须标记为 async:
异步 for 循环具有如下的形式:使用 break 或者 return 语句可以 停止接收 stream 的数据, 这样就跳出了 for 循环并且 从 stream 上取消注册了。
await for (variable declaration in expression) {
// Executes each time the stream emits a value.
}
所有的 Dart 代码在 isolates 中运行而不是线程。 每个 isolate 都有自己的堆内存,并且确保每个 isolate 的状态都不能被其他 isolate 访问。
第三方Flutter框架Demo 及文章参考
- 基于Google Flutter的开源中国客户端
- 山寨掘金
- Flutter beta3 避坑指南1
其他
- Flutter安装包大小的问题?
在安卓的安装包当中会根据工程的不同增加不同的大小。本地打的包和发布的包大小也可能不一样。
部分团队经验:在Release模式下,安卓端的安装包大小增加约为3~4M ,iOS端的安装包大小增加约为13~14M,实际增加的大小跟业务端代码和资源大小有关系;
其他团队的经验,引入 Flutter 之前,涨乐财富通的安装包为 94MB,引入之后大小为 100MB,发现增大了 6MB,这其中主要是引入了 Flutter 的 SDK,增加的大小在可以接受的范围。
参考: Flutter Android/iOS包大小分析
- Flutter的组件化集成(将Flutter代码集成进现有的工程)?
对现有工程有侵入。不能完全按照官方的指导来集成。需要把Flutter编译产物放入主工程。
对混合栈的管理,参考闲鱼团队的开源方案。
参考:使用 Flutter 之后,我们的 CPU 占用率降了 50%
- 预研结果
采用Flutter框架,能够满足日常80%以上的基础UI展示和常见需求。如果涉及复杂动画或者特定平台特性的调用,也可以使用Flutter的管道特性,和平台进行交互实现。
Flutter的性能虽然不如官方宣称60FPS,但是在流畅性上也接近原生,而且主要能够在安卓和iOS平台上实现统一且支持部分定制,尽管安装包和内存上会有部分提升空间。
开发过程中遇到的实际问题及解决:
- 按钮做倒计时功能
- 导航的时候,如果用Navigator.push处理,新的路由页面如果用MaterailPageRoute创建,那么 main.dart中 run 的需要是一个MaterailApp
- 在编辑的时候,有的时候编辑器输入颜色的时候,会fetching Doc ,然后AS就会请求文档地址,网络阻塞造成卡死。 根据查找的资料,在mac下需要找到 ~/Library/preference/Android studio x.x下面的jav.table.xml这样的文件,替换里面JAVDOC节点下的文档来源地址url,或者有解决方式是直接删除掉这个文件。 试了一下删除,效果不是很好,但是有所改善。可以参考这个回答: Android Studio hangs at fetching documentation
- ListView自定义显示Item。 建立数据源 -> 自定义Adapter,处理Item显示的内容 -> list的内容设置给Adapter -> Adapter设置给ListView;