如何启动
启用安卓模拟器
$ emulator -list-avds // 返回可用安卓模拟器列表
$ emulator @AVD_name // 启动某个模拟设备
启用 ios 模拟器
$ open -a simulator // 直接打开xcode 模拟器
启动项目
$ flutter devices // 返回可用模拟器列表
$ flutter run -d 模拟器xxx // 启用id为xxx的模拟器
热更新启动方式
$ flutter run -d 模拟器xxx --hot // 启用id为xxx的模拟器
不能使用 Hot Reload 的场景
- 代码出现编译错误的不能使用 Hot Reload
- 代码更改会影响 APP 状态的不能使用 Hot Reload
- 全局变量( global variables)和静态字段(static fields)的更改不能使用 Hot Reload
// 如下的代码:
final sampleTable = [
Table("T3"),
];
/// 运行 App 之后,如果做了如下的更改:
final sampleTable = [
Table("T3"),
Table("T10"), // 修改这里的值
];
- main() 方法里的更改不能使用 Hot Reload
- 枚举类型更改为常规的类或者常规的类变为枚举类型也不能使用 HotReload
- 修改通用类型声明也不能使用 HotReload
// 例如,如下的例子:
class A {
T i;
}
// 改为:
class A {
T i;
V v;
}
如何定义 model(6 步)
JSON 和序列化
// 1.导入库
import 'package:json_annotation/json_annotation.dart';
// 在Dart中,可以在同一库中访问私有成员。 使用import可以导入库,并且只能访问其公共成员。 使用part/part of,可以将一个库分成几个文件,并且私有成员可访问这些文件中的所有代码。
// 2. part xx.g.dart库,将在我们运行生成命令后自动生成
part 'tag.g.dart';
/// 3.装饰器引用,这个标注是告诉生成器,这个类是需要生成Model类的
@JsonSerializable()
/// 4.定义类
class Tag {
String id;
String tagName;
Tag(
this.id,
this.tagName,
);
/// 5.定义 方法名为 类名.fromJson和_$类名ToJson 方法,格式如下
factory Tag.fromJson(Map json) => _$TagFromJson(json);
Map toJson() => _$TagToJson(this);
/// 6.执行命令生成json序列化代码
/// $ flutter packages pub run build_runner build
}
flutter packages pub run build_runner watch
在项目根目录下运行来启动watcher。只需启动一次观察器,然后并让它在后台运行,这是安全的
相关部件(Widget)使用说明
一、Scaffold 使用
Scaffold({
this.appBar, // 顶部导航
this.body, // 主体
this.drawer, // 页面左抽屉,从左往右抽出
this.endDrawer, // 页面右抽屉,从右往左抽出
this.bottomNavigationBar, // 底部导航
this.backgroundColor, // 背景色
})
二、AppBar 使用及扩展
AppBar 设计上涵盖了左中右下四个方向的占位,对应的属性分别是 leading, title, actions, bottom,leading,actions 带有默认 padding 和 margin 值
AppBar({
this.leading, // Widget
this.automaticallyImplyLeading = true, // bool 路由栈存在上一个路由的情况下,leading为返回箭头
this.title, // Widget,通常是Text
this.actions, // List
this.bottom, // PreferredSizeWidget,通常是TabBar
this.elevation, // double 高度阴影
this.backgroundColor, // Color
this.centerTitle, // bool 居中标题
...
})
定制 AppBar
首页 AppBar 模块 bottom 部分组成是 Tabbar + 频道选择,非 PreferredSizeWidget 类型的部件,实现方式是定制一个 PreferredSizeWidget,实现抽象类未实现的方法,在实现 build 的部分复合 Tabbar 和定制化的部分
class ZnTabBar extends StatefulWidget implements PreferredSizeWidget {
/// Creates a [Size] with the given [height] and an infinite [width].
@override
Size get preferredSize {
return Size.fromHeight(/* ZnTabBar所依赖的高度 */);
}
/// ...
@override
Widget build(BuildContext context) {
return Row(
children: [
Expanded(child: TabBar()),
CustomPart() // 定制化部分
)
}
三、使用 GestureDetector
GestureDetector 是一个用于手势识别的功能性组件,通过它可以识别各种手势。GestureDetector 实际上是指针事件的语义化封装;
使用场景
1、类似 Text、TextField 等部件没有监听点击的事件;
2、扁平化交互,Material 组件库中提供了多种按钮组件如 RaisedButton、FlatButton、OutlineButton 等,它们都是直接或间接对 RawMaterialButton 组件的包装定制,Material 库中的按钮都有如下相同点:
- 按下时都会有“水波动画”
- 有一个 onPressed 属性来设置点击回调,当按钮按下时会执行该回调,如果不提供该回调则按钮会处于禁用状态,禁用状态不响应用户点击;
GestureDetector(
child: Text('点击'),
onTap: () {
// 处理点击
}
)
其它,如实现双击、长按、拖动、滑动等
GestureDetector({
this.onDoubleTap, // 双击
this.onLongPress, // 长按
this.onVerticalDragStart,// 垂直拖动
this.onHorizontalDragStart, // 水平搬动
this.onPanStart, // 滑动
this.onScaleStart, // 缩放
...
四、使用 SingleChildScrollView
接收一个子组件,当显示内容超过屏幕且不超出太多的情况下使用,解决内容超出可视区域产生边界溢出的情况。
使用场景
1、线性布局下,Column 接收一组部件,但内容超出屏高产生垂直边界溢出的情况下;
2、流式布局下,Row 接收一组部件,当内容超出屏宽且不折行的情况下;(可使用 Wrap 产生折行)
3、期望的内容不会超过屏幕太多时,视口不会包含超出屏幕尺寸太多的内容时;(可使用 ListView 或 CustomScrollView 代替)
五、使用 GridView
GridView.count
GridView.count 是 GridView 的命名构造函数,内部使用了 SliverGridDelegateWithFixedCrossAxisCount,通过它可以快速的创建横轴固定数量子元素的 GridView
GridView.count(
crossAxisCount: 2, // 横轴子元素的数量
crossAxisSpacing: 1.0, // 横轴方向子元素的间距
mainAxisSpacing: 1.0, // 主轴方向的间距
children: buildGridList(),
primary: true // 如果为 true,即使滚动视图没有足够的内容滚动视图也是可滚动的,默认为 false。
shrinkWrap: true, // shrinkWrap 常用于内容大小不确定情况,如果是无界约束,则 shrinkWrap 必须为 true。
)
六、CustomScrollView
CustomScrollView 是可以使用 Sliver 来自定义滚动模型(效果)的组件,这里 Sliver 通常指可滚动组件子元素。
使用场景
1、包含 类似 GridView 和 ListView 交互,同时要求整个页面的滑动效果是统一的;
2、需要结合 FlexibleSpaceBar 实现 Material Design 中头部伸缩;
CustomScrollView(
controller: _scrollController,
slivers: [
// AppBar,包含一个导航栏
SliverAppBar(
title: Text(...) // Widget
),
// GridView
SliverGrid(
/// 配置同GridView
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 250.0,
mainAxisSpacing: 1.0,
crossAxisSpacing: 1.0,
childAspectRatio: 1.0,
),
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
// 返回Widget
},
childCount: ..., // 列表长度
),
),
//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个列表项
),
),
)
七、动画
1、继承 SingleTickerProviderStateMixin 和 TickerProviderStateMixin,通过将 SingleTickerProviderStateMixin 添加到类定义中,可以将 stateful 对象作为 vsync 的值;
2、两个对象:Animation 和 AnimationController;
3、Animation 生成指导动画的值,AnimationController 管理 Animation;
4、当创建一个 AnimationController 时,需要传递一个 vsync 参数,存在 vsync 时会防止屏幕外动画消耗不必要的资源;
5、使用 Listeners 和 StatusListeners 监听动画状态改变;
6、释放资源
vsync 做了什么事?
vsync 对象会绑定动画的定时器到一个可视的 widget,所以当 widget 不显示时,动画定时器将会暂停,当 widget 再次显示时,动画定时器重新恢复执行,这样就可以避免动画相关 UI 不在当前屏幕时消耗资源。
使用
/// 1.定义类
class ZNAnimation extends StatefulWidget {
static final String routeName = 'animaton';
@override
_ZNAnimation createState() => _ZNAnimation();
}
class _ZNAnimation extends State with SingleTickerProviderStateMixin {
}
/// 2.声明
AnimationController controller;
Animation animation;
/// 3.初始化并设置动画监听
@override
initState() {
super.initState();
controller = AnimationController(
duration: const Duration(milliseconds: 100), vsync: this);
// 图片宽高从0变到300
animation = Tween(begin: 30.0, end: 25.0).animate(controller);
animation.addStatusListener((status) {
if (status == AnimationStatus.completed) {
// 动画执行结束时反向执行动画
controller.reverse();
} else if (status == AnimationStatus.dismissed) {
// 动画恢复到初始状态时执行动画(正向)
setState(() {
...
});
}
});
}
/// 4. 使用
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: animation,
builder: (BuildContext context, Widget child) {
return new Container(
height: animation.value,
width: animation.value,
child: xxx, // 接收外部child再由AnimatedBuilder返回并插入widget树中
);
},
child: xxx // 外部Widget对象
)
}
/// 5.释放资源
```dart
@override
dispose() {
//路由销毁时需要释放动画资源
controller.dispose();
super.dispose();
}
八、其它部件
1、底部弹窗 showModalBottomSheet
,
2、尺寸限制类容器 ConstrainedBox
,
3、装饰容器 DecorateBox
可以在其子组件绘制前(或后)绘制一些装饰(Decoration),如背景、边框、渐变等。
4、剪裁 ClipRRect
将子组件剪裁为圆角矩形
Effective Dart 及项目开发约定
Effective Dart: 最佳实践
一、dart 规范补充
1、要 尽可能的使用集合字面量来定义集合。
// 好的范例
var points = [];
var addresses = {};
// 坏的范例
var points = new List();
var addresses = new Map();
如果有必要还可以提供泛型类型。
// 好的范例
var points = [];
var addresses = {};
// 坏的范例
var points = new List();
var addresses = new Map();
2、不要 使用 .length 来判断集合是否为空。
Dart 提供了更加高效率和易用的 getter 函数: .isEmpty 和.isNotEmpty。使用这些函数并不需要对结果再次取非。
/// 好的范例
if (lunchBox.isEmpty) return 'so hungry...';
if (words.isNotEmpty) return words.join(' ');
/// 坏的范例
if (lunchBox.length == 0) return 'so hungry...';
if (!words.isEmpty) return words.join(' ');
3、使用 map().toList() 或者 map().toSet() 来 强制立刻执行 map 的方法
map() 函数返回的对象也是一个 Iterable,该对象是懒求值(lazily evaluated) 的,只有当访问里面的值的时候, map 的方法才被调用。
4、普通方法使用位置参数定义默认值
queryCommentList(String videoId, [int pageSize = 15, int pageNo = 1]) {
// ...
}
// 调用
queryCommentList('videoId');
5、类构造方法使用命名参数
class ZNVideoPlay extends StatefulWidget {
final Video video;
ZNVideoPlay({this.video});
@override
State createState() => _VideoPlay();
}
// 调用
ZNVideoPlay(video: widget.video),
二、文件或库引入方式
直接使用库导出对象,Flutter 编辑器插件自动导入库绝对路径;
组件通信的两种方式:
一、InheritedWidget
使用(3 步)
// 1、定义数据存储部件
class ShareDataWidget extends InheritedWidget {
ShareDataWidget({@required this.data, Widget child}) : super(child: child);
final String data; //需要在子树中共享的数据,保存点击次数
//定义一个便捷方法,方便子树中的widget获取共享数据
static ShareDataWidget of(BuildContext context) {
return context.inheritFromWidgetOfExactType(ShareDataWidget);
}
//该回调决定当data发生变化时,是否通知子树中依赖data的Widget
@override
bool updateShouldNotify(ShareDataWidget old) {
//如果返回true,则子树中依赖(build函数中有调用)本widget
//的子widget的`state.didChangeDependencies`会被调用
return old.data != data;
}
}
// 2、父Widget使用ShareDataWidget包装子Widget
ShareDataWidget(
data: tagId,
child: VideoList(),
))
// 3、子Widget使用并监听
@override
void didChangeDependencies() {
_initData();
super.didChangeDependencies();
}
_initData() async {
var tagId = ShareDataWidget.of(context).data;
// Fetch data with tagId
}