Flutter 实战进阶

Flutter 在实际开发中遇到的一些问题及解决方案,作为笔记记录。

1、container width、height 100%

1
2
3
4
5
FractionallySizedBox(
widthFactor: 1,
heightFactor: 1,
child: ,
)
1
2
double width = MediaQuery.of(context).size.width
double height = MediaQuery.of(context).size.height

2、沉浸式背景图片

背景图片铺满Appbar及状态栏

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Widget build(BuildContext context) {
return new Stack(
children: <Widget>[
Container(
child: Image.network('https://www.bing.com/az/hprichbg/rb/Punakaiki_DE-DE0884339574_1920x1080.jpg'),
color: Colors.lightGreen,
),
Scaffold(
backgroundColor: Colors.transparent,
appBar: AppBar(
backgroundColor: Colors.transparent,
title: Text('Coin'),
),
body: MyCoinPage(),
)
],
);
}

3、APPBar no shadow

AppBar默认底部有阴影。去掉只需要配置参数即可:

1
2
3
AppBar(
elevation: 0.0,
)

4、自定义字体

1
2
3
4
5
6
7
8
9
10
flutter:
# The following line ensures that the Material Icons font is
# included with your application, so that you can use the icons in
# the material Icons class.
uses-material-design: true

fonts:
- family: Kphi
fonts:
- asset: assets/fonts/Komet-Pro-Heavy-Italic.otf

5、Tab切换动画

Tab切换需加入SingleTickerProviderStateMixin

通过 with SingleTickerProviderStateMixin 实现Tab页的切换动画效果。因为初始化animationController的时候需要一个TickerProvider类型的参数Vsync参数,所以我们混入了TickerProvider的子类SingleTickerProviderStateMixin

6、 逐帧动画 FrameAnimationImage

Flutter 逐帧动画 FrameAnimationImage。 逐帧动画: 依次顺序循环播放多张图片组成一个动画。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
/// Flutter FrameAnimation 逐帧动画
import 'package:flutter/material.dart';

class FrameAnimationImage extends StatefulWidget {
final List<String> assetList;
final double width;
final double height;
final int interval;

FrameAnimationImage(
{this.assetList, this.width, this.height, this.interval = 200});

@override
State<StatefulWidget> createState() {
return _FrameAnimationImageState();
}
}

class _FrameAnimationImageState extends State<FrameAnimationImage>
with SingleTickerProviderStateMixin {
// 动画控制
Animation<double> _animation;
AnimationController _controller;
int interval = 200;

@override
void initState() {
super.initState();

if (widget.interval != null) {
interval = widget.interval;
}
final int imageCount = widget.assetList.length;
final int maxTime = interval * imageCount;

// 启动动画controller
_controller = new AnimationController(
duration: Duration(milliseconds: maxTime), vsync: this);
_controller.addStatusListener((AnimationStatus status) {
if (status == AnimationStatus.completed) {
_controller.forward(from: 0.0); // 完成后重新开始
}
});

_animation = new Tween<double>(begin: 0, end: imageCount.toDouble())
.animate(_controller)
..addListener(() {
setState(() {
// the state that has changed here is the animation object’s value
});
});

_controller.forward();
}

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

@override
Widget build(BuildContext context) {
int ix = _animation.value.floor() % widget.assetList.length;

List<Widget> images = [];
// 把所有图片都加载进内容,否则每一帧加载时会卡顿
for (int i = 0; i < widget.assetList.length; ++i) {
if (i != ix) {
images.add(Image.asset(
widget.assetList[i],
width: 0,
height: 0,
));
}
}

images.add(Image.asset(
widget.assetList[ix],
width: widget.width,
height: widget.height,
));

return Stack(alignment: AlignmentDirectional.center, children: images);
}
}

7、 时间格式化库 intl

8、 时间格式化库intltextDirection冲突

报错如下:

1
The getter 'rtl' isn't defined for the class 'TextDirection'

参考: Stack Overflow

解决方案:

1
import 'package:intl/intl.dart' as intl;

9、 convert string to date

dart将时间字符串转为date库:

参考: Stack Overflow

1
2
var parsedDate = DateTime.parse('1974-03-20 00:00:00.000');
print('dateTime: $parsedDate');

10、 tabcontroller.addListener

Tab切换时,可以通过tabcontroller.addListener监听切换事件。在实际开发中发现如下问题,可以根据需求选择不同的判断方式:

1
2
3
4
5
6
7
_tabController = new TabController(length: 2, vsync: this);
_tabController.addListener(() {
// _tabController.addListener...
if (_tabController.indexIsChanging) {
// _tabController.indexIsChanging...
}
});

当点击切换时:

  • _tabController.addListener 触发两次
  • _tabController.indexIsChanging 触发一次

当滑动切换时:

  • _tabController.addListener 触发一次
  • _tabController.indexIsChanging 不触发

11、 FutureBuilder => Future widget

参考: FutureBuilder 延迟加载组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@override
Widget build(BuildContext context) {
return new FutureBuilder(
future: getTextFromFile(),
initialData: "Loading text..",
builder: (BuildContext context, AsyncSnapshot<String> text) {
return new SingleChildScrollView(
padding: new EdgeInsets.all(8.0),
child: new Text(
text.data,
style: new TextStyle(
fontWeight: FontWeight.bold,
fontSize: 19.0,
),
));
});
}

Future<String> getFileData(String path) async {
return await new Future(() => "test text");
}

Future<String> getTextFromFile() async {
return getFileData("test.txt");
}
}

12、获取页面生命周期

使用 with WidgetsBindingObserver后,可以对页面的生命周期进行监听。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class _LifecycleWatcherState extends State<LifecycleWatcher> with WidgetsBindingObserver {
AppLifecycleState _lastLifecycleState;

@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}

@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}

@override
void didChangeAppLifecycleState(AppLifecycleState state) {
print('current state: $state');
setState(() {
_lastLifecycleState = state;
});
}

}

13、页面重构

1、TabView + pull_to_refresh 如何优雅共存
当页面中有Tab切换,且每个Tab页都是可以下拉刷新、上拉加载的组件时。如果每个TabView使用的是同一个组件,只是根据不同的标识显示不同的内容。在实际开发中发现,这么做虽然可以减少部分代码量,但是出现的问题是,Tab切换时每次都等于是重新渲染公共的组件,导致比较卡顿,且切换时会发生多个TabView间的数据混乱等一些列问题。而且多个TabView公用一个scrollController的话也会出很多问题。

正确的做法是,每个TabView应该是独立组件,包括互相独立的scrollController。只不过其中的一些子组件是可以抽出来共用的,这是没问题的。

2、listView显示占位图。

空列表的一般需要显示占位图,开始的做法是,判断列表为空时,直接返回一个占位图的Container,代替ListView。这样做之后,发现数据为空时就不能下拉刷新了,因为下拉刷新和ListView绑定在一起。于是改为下述方案,当数据为空时任然可以下拉刷新,实时更新数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
child: new SmartRefresher(
headerConfig: const RefreshConfig(completeDuration: 300),
enablePullDown: true,
enablePullUp: true,
onRefresh: _onRefresh,
headerBuilder: LoadingIndicator.buildHeader,
footerBuilder: LoadingIndicator.buildFooter,
controller: _refreshController,
child: (!loading && _payList.length == 0)
? ListView.builder(
itemCount: 1,
itemBuilder: (context, i) {
return _emptyTip();
},
)
: _listView(),
),

14、网络图片加载组件

默认的图片加载组件 networkImage,当图片链接错误、为空时,会报exception导致Crash。FadeInImage同理。

参考: GitHub

试了好几款组件都没有做错误处理,最终找到一个可用的组件:Flutter_image ,同时配合FadeInImage设置默认占位图,完美。

1
2
3
4
5
6
adeInImage(
fit: fit,
placeholder: AssetImage(placeHolderImage),
image: NetworkImageWithRetry(imgUrl),
),

15、主题颜色 ThemeData

16、网络图片加载失败Exception

使用 flutter_imageNetworkImageWithRetry 代替 NetworkImage 后,当图片404或者错误时,确实没有报Exception奔溃,但是安卓端却显示crash。

换了很多其他的插件都不好使,最终查明,NetworkImageWithRetry虽然可以catch exception不让应用奔溃,但是会触发Flutter.onError,而安卓端在检测到onError之后,当做crash处理,直接将页面关闭….

方案: 修改组件源码,图片加载失败时,不要触发Flutter.onError。

17、ActionSheet 点击隐藏

点击 ActionSheet 选择某项后,隐藏之。

1
Navigator.of(context, rootNavigator: true).pop("Discard");

18、阻止软键盘顶起页面

当软键盘弹起时,会将页面高度缩减,导致页面中的内容随着父级高度变化而变化。即软键盘弹起时将内容顶起,导致诸多问题。

解决方案:

1
2
3
4
5
6
return Scaffold(
appBar: AppBar(
title: new Text("通讯录"),
),
resizeToAvoidBottomPadding: false, //输入框抵住键盘 内容不随键盘滚动
);

19、dart正则

1
2
3
4
5
6
7
8
9
10
RegExp regExp = new RegExp(
r'(ludis)',
caseSensitive: false, // 是否大小写敏感
multiLine: false, // 待匹配文字是否是多行的
);

// 正则
var d = regExp.allMatches(text);
d.forEach((f) => print(f.group(0)));
print('match result, ${d.toString()}, ${d.toList()}');

20、键盘确认按钮样式

键盘的确认按钮会显示为“搜索”,点击后触发onSubmitted事件。

1
2
3
4
5
TextFormField( 
...
textInputAction: TextInputAction.done,
...
)

21、Map类型错误

json转换时报错如下:

1
'_InternalLinkedHashMap<dynamic, dynamic>' is not a subtype of type 'Map<String, dynamic>'

原因: 类型为Map的其实际类型为Map<dynamic, dynamic>而非Map<String, dynamic>

解决方法: 进行转换

1
2
Map json; // Map<dynamic, dynamic>
Map<String, dynamic> = new Map<String, dynamic>.from(json); // Map<String, dynamic>

22、GestureDetector 点击区域小

使用GestureDetector包裹Container,发现在Container内容为空的区域点击时,捕捉不到onTap点击事件。

参考: GitHub

解决方法:

1
2
3
4
5
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: (){},
child: Container()
)

23、TextField优化:

TextField decoration 可使用以下两种:

  • 使用 InputDecoration.collapsed() 。其高度不能和父级同高,导致可点击区域变小,用户体验不好。优势在于InputDecoration.collapsed()为无边框的输入框。
  • 使用InputDecoration。可使输入框高度和父级同高,扩大可点击区域。去掉边框即可。

解决方法:

1
2
3
InputDecoration(
border: InputBorder.none, // 去除输入框边框
)

24、Flutter hide keyboard

调用方法隐藏键盘。

1
FocusScope.of(context).requestFocus(new FocusNode());

25、去除TextFiled水纹效果 ripple effect

参考: how-to-disable-default-widget-splash-effect-in-flutter

解决方案:

  1. 使用自定义主体,覆盖默认主题
  2. 给组件包裹Theme容器,设置 splashFactoryColors.transparent。✅
1
2
3
4
5
child: new Theme(
data: new ThemeData(splashFactory: Colors.transparent),
child: new TextField(...),
),

Dart 数组、字符串常用方法:

Flutter组件合集

Element

Form

1、Input

  • TextField 最常用的文本输入组件。
    • 用户修改文本时,可通过Onchange获取最新的文本信息。
    • onSubmitted可获取到软键盘的确认按钮。
    • 默认下方有一个横线,decoration属性可以设置图标、padding等更多属性

2、Checkbox

  • CheckboxListTile
    下拉复选框,带有复选框的ListTile,带有标签的ListTile
    整个列表的图块是交互的,点击图块中的任意位置可切换复选框。

  • Checkbox

3、Button

  • FlatButton
    默认无边框,无背景色的按钮
    FlatButton.icon 为带图标的

  • RaiseButton
    凸起的按钮——带有shadow阴影的质感按钮
    RaiseButton.icon 为带图标的

  • IconButton
    纯图标按钮,无边框无背景色

  • PopupMenuButton
    弹出菜单栏的图标。
    PopupMenuButton 和 popupMenuItem 配合使用。
    选择菜单项时,触发onSelected方法。

  • FloatingActionButton
    默认的圆形悬浮按钮,每个页面最多一个。Scaffold.floatingActionButton

  • RawMateriaButton
    不使用当前Theme或ButtonTheme,高度自定义的Materail button

  • DropdownButton
    从项目列表进行选择的按钮。

  • OutlineButton
    RaisedButton 和 FlatButton的交叉,有边框。高程最初为0,背景透明。按下按钮时,背景变不透明,高程增加

4、Text

  • Text
    一行/多行文本显示
  • RichText

5、Radio

  • Radio
  • RadioListTile
    点击按钮附加的文字时,同样可以触发点击效果

6、Slider

  • Slider 范围选择滑块
  • SliderTheme 自定义Slider的样式
  • SliderThemeData SliderTheme的data修饰属性

7、Switch

  • Switch
    Switch、Switch.adaptive(ios风格)
    可自定义背景图、背景颜色等

  • SwitchListTile
    衍生组件,可以通过点击关联的文字标签切换switch

  • AnimatedSwitcher
    一个在新旧组件之间做渐变切换的组件,有一定的动画效果

Frame

1、Align
Alignment.center/centerLeft/bottomLeft(yx)

2、Stack

  • Stack
    绝对布局,依次叠加。

  • IndexedStack
    显示一个子项列表中的单个子项(只显示指定的一个子选项)

3、Layout

  • Row
  • Column
  • Container
    最小空间原则
  • Center

4、Box

  • ContainerBox
    添加额外的限制条件到child上。比如最大/最小高度/宽度

  • OverflowBox
    对子组件添加的约束,不同于子组件从其父组件获得的约束,允许child溢出父控件的空间

  • DecoratedBox
    在child绘制前或绘制后,添加额外的限制条件到child上,可以用来绘制一个圆形、方形、padding等。

  • FittedBox
    根据需求对child进行缩放、定位。contain/fill/cover/fitHeight/fitWidth

  • LimitedBox
    对最大宽高进行限制(前提是LimitedBox不受约束),即将child限制在指定的最大宽高中。

  • RoraredBox
    可以将子组件旋转整数的四分之一

  • SizedOverflowBox
    特定大小的窗口组件,将其约束传递给其子组件,可能会溢出。

  • UnconstrainedBox
    不限制子组件的大小,让子组件尽可能的扩展

5、Expanded

  • Expanded
    撑开flex布局子组件空间

6、Spacing

  • Padding
    内边距

  • SliverPadding
    列表缩进,应用于每个子元素

  • AnimatedPadding
    缩进变化时的动画

7、Table

  • Table

Media

1、Image

  • AssetImage
    从AssetBundle中获取图像

  • DecorationImage
    修饰Box的图片,一般配合BoxDecoration的img属性使用

  • ExactAssetImage
    带有scale属性的AssetImage

  • FadeInImage
    提供placeholder image到目标图片的过度效果

  • FileImage
    展示本地的文件,将给定的File对象解码为图片。Image.file是ImageFile的简写形式

  • NetworkImage
    网络图片

  • RawImage
    显示dart:ui.Image的组件,很少使用,推荐使用Image

  • MemoryImage
    将给定的Unit8List缓冲区解码为图像的组件

  • Image
    Image.asset 加载项目资源目录的图片,相对路径
    Image.network网络资源图片
    Image.file加载本地图片
    Image.memory 加载Unit8List资源图片

2、Icon

  • Icon

  • ImageIcon
    来自ImageProvider的图标,如AssetImage

  • IconTheme
    用于应用栏图标的言责、不透明度和大小

  • IconData

  • IconThemeData

3、Canvas

  • Canvas
    用于操作图形的界面
  • PainterSketch
    操作图形的界面
  • PainterPath
    在canvas上绘制的团
  • CircleProgressBarPainter
    操作图形的界面

Components

1、BottomNavigationBarItem
2、BottomNavigationbar
底部导航

List

1、AnimatedList
新增、删除选项时可以设置动画
2、ListView
最常用
3、ListBody
不常用,按照主轴方向排列子节点

Card

1、Card
卡片用于表示一些相关信息,例如相册,地理位置,用餐,联系方式等。

Bar

1、AppBar
应用栏——顶部导航显示的工具栏
2、BottomAppBar
底部应用栏

3、BottomNavigationBarItem
底部导航应用栏的子项
4、SnackBar
屏幕底部弹出的提示信息
5、Sliverbar
SliderApperBar可随内容滚动,一般在scroll滑动视图中。与NestedScrollView配合可实现上提到顶的悬停。
6、ScrollbarPainter
7、FlexibleSpaceBar
类似 Sliverbar, ”扩展和折叠的应用栏“,APPBar的一部风,可以扩展和折叠;
8、ButtonBar
末端对齐的按钮容器,向左向右看齐的按钮。
9、SnackBarAction
SnackBar,底部消息右侧有无可操作的行为
10、TabBar
实现并行界面的横向滑动展示

Dialog

1、AlertDoalog
弹出对话框,可自定义操作选项
2、Dialog
无可操作选项的弹出层
3、AboutDialog
通常用于展示信息
4、SimpleDialog
可以为用户提供多个选项选择,有一个可选的标题,显示在选项上方

Scaffold

1、Scaffold
基本的布局结构。Scaffold支持顶栏、侧边栏、底部导航栏等常用布局
2、ScaffoldState
通常用来控制SnackBars和BottomSheets和Drawer的显示和隐藏。

Grid

1、GridTile
GridList中的一种瓷片组合,包含header、body、footer三部分
2、GridView
常见的滚动列表,会占满给出的空间区域。
3、GridPaper
会在GridView的item上层浮现一层网格
4、SliverGrid
可以将多个item以每行两个的形式进行排列
5、GridTileBar
通常用来做GridTile的header与footer组件。

Scroll

1、ScrollView
滚动视图,属于抽象类,不能直接实例化。
ListView: 常用的ScrollView
PageView: 每个子widget都是视口窗口大小
GridView: 一个现实二维子widget的ScrollView
CustomScrollView: 自定义滚动效果的ScrollView
SingleChildScrollView: 只有一个子Widget的ScrollView
ScrollNotification、NotificationListener: 可以用于在不使用ScrollController的情况下查看滚动位置的widget

2、Scrollable
一个可以使内容滚动的Widget
3、ScrollbarPainter
用户绘制滚定条的CustomPainter。
4、ScrollMetrics
包含当前ViewPort及滚动位置等信息,抽象类,不可被实例化。
5、ScrollPhysics
确定滚动组件的物理属性
6、BoxScrollView
使用单个子布局模型的ScrollView
ListView、GridView、CustomScrollView
7、CustomScrollView
自定义滚动效果的ScrollView
8、NestedScrollView (⭐️)
可以嵌在另一个滚动视图中的ScrollView

Tab

1、Tab
Tab切换,如果同时传给Tab icon和text,text会展示在icon下面

1、CheckedPopupMenuItem
带有选中标记的弹出菜单,配合PopupMenuButton使用
2、DropdownMenuItem
DropdownButton创建的一个菜单项,配合DropdownButton使用
3、PopupMenuButton
一个提供菜单栏弹出对话框的按钮
4、PopupMenuDivider
菜单栏弹出对话框中每一项的水平线
5、PopupMenuEntry
弹出菜单的一个基类
6、PopupMenuItem

Pick

1、DayPicker
显示给定月份的日期,并可以选择一天
2、MonthPicker
选择一个月的可滚动月份列表,同上
3、YearPicker
同上
4、ShowdatePicker
日期选择器的对话框
5、CityPicker
显示中国的省市县区,通过函数showCityPicker调用,在容器窗口上弹出遮罩层

Chip

1、Chip
chip是表示属性,文本,实体或动作的元素。

2、ChipTheme
描述chip的颜色,形状和文本样式
3、ChipThemeData
保存chip主题的颜色,形状和文本样式
4、ChoiceChip
允许从一组选项中进行单一的选择
5、FilterChip
通过使用标签或者描述性词语来过滤内容
6、InputChip
输入型chip
7、RawChip

Panel

1、ExpansionPanelList
扩展面板,包含一个标题和一个正文,可以展开或者折叠。面板展开,主体可见。

Progress

1、LinearProgressIndicator
一个线性进度条
2、CircularProhressIndicator
循环进度条,旋转表示进度

Themes

Materail

1、MaterialApp
代表Material设计风格的应用
2、MaterailColor
颜色值
3、MaterailButton
4、MaterailPageRoute
页面跳转携带参数替换整个屏幕的页面路由
5、MaterailAccentColor
用来定义单一的强调色,以及四种色调的色系
6、MergeableMaterialItem

Cupertino(IOS style)

1、CupertinoApp
Cupertino苹果设计风格的应用,用于创IOS风格应用的顶层组件(Cupertino苹果电脑的全球总公司所在地,位于美国旧金山)。
包含了iOS应用程序通常需要的许多widget
2、CupertinoButton
iOS风格的button
3、CupertinoColors
iOS平台常用的颜色
4、CupertinoIcons
Cupertino图标的标识符
5、CupertinoNavigationBar
iOS风格的导航栏
6、CupertinoPageRoute
iOS风格全屏切换路由的滑动动画
7、CupertinoPageScaffold
实现单个iOS应用程序页的页面布局
8、CupertinoPicker
iOS风格的选择器
9、CupertinoPopupSurface
像iOS弹出式表面,快速实现一个圆角弹框
10、CupertinoScrollbar
iOS风格的滚动条
11、CupertinoSlider
iOS风格的Slider,选择连续性或非连续性的数据
12、CupertinoSegmentedControl

展示一些用户可以选择的选项
13、CupertinoSliverNavigationBar
iOS-11风格下拥有较大标题块的导航栏目
14、CupertinoSwitch
iOS风格的switch开关
15、CupertinoTabBar
iOS风格的底部导航栏
16、CupertinotabScaffold
iOS应用程序的选项卡的根布局结构
17、CupertinoTabView
具有自己的Navigator状态与历史记录的选项卡试图组件。
该组件有自己的路由体系,有自己的导航体系,并且他自身内部的导航系统并不与任何父母元素共享
18、CupertinoTimerPicker
iOS风格下的时间选择组件

Flutter学习之旅——实用入坑指南

开篇: 一如前端深似海,从此节操是路人从此再无安宁日,从此红尘是路人。要说技术更迭速度,还有比前端更快的么😂根本停不下来。这不,Google刚发布Flutter不到一年时间,1.0正式版发布不到两个月。阿里系的闲鱼老大哥,已经率先用Flutter重构了闲鱼,虽然没完全重构,但高频的重度页面都是Flutter的了。这一幕似曾相识,当初RN出来的时候不也是闲鱼团队先吃的螃蟹吗,在这里向闲鱼团队的老哥们致敬🐣。

既然老大哥都出动了,也侧面验证了这项技术的可行性。当小弟的也不能落后嘛,每天抽时间断断续续的学了两周时间,仿部分知乎的客户端,撸了一套客户端出来。前一周主要是熟悉Dart语言和常规的客户端布局方式,后一周主要是掌握使用HTTP的请求、下拉上拉、左滑右滑、长按等常用手势、相机调用、video播放等进阶用法。 两周下来,基本上可以开发80%以上常见的客户端需求。

前期一直在用simulator开发,略有卡顿,心中难免有些疑惑。结果最后release打包到手机后,竟然如丝般顺滑!!!简直喜出望外,完全可以睥睨原生开发,在这一点上的确要优于目前的RN。最重要的是作为Materail Design极简又有质感风格的狗血粉丝,Flutter造出来的界面简直倍爽。至此正式入坑Flutter开发。Google万岁!

Flutter是谷歌的移动UI框架,可以快速在iOS和Android上构建高质量的原生用户界面。 Flutter可以与现有的代码一起工作。在全世界,Flutter正在被越来越多的开发者和组织使用,并且Flutter是完全免费、开源的。

Beta1版本于2018年2月27日在2018 世界移动大会公布。
Beta2版本2018年3月6日发布。
1.0版本于2018年12月5日(北京时间)发布

这里把学习过程中一些常用高频的东西总结出来,基本能满足大多数情况下的开发需求。

完整的代码: https://github.com/flute/zhihu_flutter

欢迎加入Flutter开拓交流,群聊号码:236379502

Scaffold 主要的属性说明

  • appBar:显示在界面顶部的一个 AppBar
  • body:当前界面所显示的主要内容
  • floatingActionButton: 在 Material 中定义的一个功能按钮。
  • persistentFooterButtons:固定在下方显示的按钮。https://material.google.com/components/buttons.html#buttons-persistent-footer-buttons
  • drawer:侧边栏控件
  • bottomNavigationBar:显示在底部的导航栏按钮栏。可以查看文档:Flutter学习之制作底部菜单导航
  • backgroundColor:背景颜色
  • resizeToAvoidBottomPadding: 控制界面内容 body 是否重新布局来避免底部被覆盖了,比如当键盘显示的时候,重新布局避免被键盘盖住内容。默认值为 true。

底部菜单 bottomNavigationBar,Tab栏切换 TabBar

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
TabController controller;

@override
void initState() {
super.initState();

// initialize the tab controller
// vsync ??
controller = new TabController(length: 5, vsync: this);
}

@override
void dispose() {
// dispose of tab controller
controller.dispose();
super.dispose();
}

...

body: new TabBarView(
children: <Widget>[new HomeTab(), new IdeaTab(), new ColleagueTab(), new MessageTab(), new MeTab()],
controller: controller,
),
bottomNavigationBar: new Material(
// background color of bottom navigation bar
color: Colors.white,
textStyle: new TextStyle(
color: Colors.black45
),
child: new TabBar(
unselectedLabelColor: Colors.black45,
labelColor: Colors.blue,
controller: controller,
tabs: <Tab>[
new Tab(
child: new Container(
padding: EdgeInsets.only(top: 5),
child: new Column(
children: <Widget>[
Icon(Icons.home, size: 25,),
Text('首页', style: TextStyle(fontSize: 10),)
],
),
),
),
new Tab(
child: new Container(
padding: EdgeInsets.only(top: 5),
child: new Column(
children: <Widget>[
Icon(Icons.access_alarm, size: 25,),
Text('想法', style: TextStyle(fontSize: 10),)
],
),
),
),
new Tab(
child: new Container(
padding: EdgeInsets.only(top: 5),
child: new Column(
children: <Widget>[
Icon(Icons.access_time, size: 25,),
Text('大学', style: TextStyle(fontSize: 10),)
],
),
),
),
new Tab(
child: new Container(
padding: EdgeInsets.only(top: 5),
child: new Column(
children: <Widget>[
Icon(Icons.account_balance_wallet, size: 25,),
Text('消息', style: TextStyle(fontSize: 10),)
],
),
),
),
new Tab(
child: new Container(
padding: EdgeInsets.only(top: 5),
child: new Column(
children: <Widget>[
Icon(Icons.adb, size: 25,),
Text('我的', style: TextStyle(fontSize: 10),)
],
),
),
),
],
),
),

顶栏自定义 appbar:title属性

顶部搜索栏:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: searchBar(),
backgroundColor: Colors.white,
bottom: new Text('bottom'),
),
body: new Container()
);
}

/**
* 顶部搜索栏
*/
Widget searchBar() {
return new Container(
child: new Row(
children: <Widget>[
new Expanded(
child: new FlatButton.icon(
color:Color.fromRGBO(229, 229, 229, 1.0),
onPressed: (){
Navigator.of(context).push(new MaterialPageRoute(builder: (context){
return new SearchPage();
}));
},
icon: new Icon(
Icons.search,
color: Colors.black38,
size: 16.0,
),
label: new Text(
"诺奖得主为上课推迟发布会",
style: new TextStyle(color: Colors.black38)
),
),
),
new Container(
child: new FlatButton.icon(
onPressed: (){
Navigator.of(context).push(new MaterialPageRoute(builder: (context){
return new AskPage();
}));
},
icon: new Icon(
Icons.border_color,
color: Colors.blue,
size: 14.0
),
label: new Text(
'提问',
style: new TextStyle(color: Colors.blue),
),
),
)
],
),
);
}

图片圆角

1
2
3
4
5
6
7
8
9
10
11
Container(
margin: EdgeInsets.only(right: 5),
decoration: new BoxDecoration(
shape: BoxShape.circle,
image: new DecorationImage(
image: new NetworkImage(avatarUrl),
)
),
width: 30,
height: 30,
),

数组里动态添加组件

https://github.com/flutter/flutter/issues/3783

1
2
3
4
5
6
7
8
9
10
bool notNull(Object o) => o != null;
Widget build() {
return new Column(
children: <Widget>[
new Title(),
new Body(),
shouldShowFooter ? new Footer() : null
].where(notNull).toList(),
);
}

Text显示指定行数,超出后显示省略号

1
2
3
4
5
6
Text(
content,
maxLines: 3,
overflow: TextOverflow.ellipsis,
style: new TextStyle(fontSize: 14, color: Colors.black54),
),

margin 负值

https://stackoverflow.com/questions/42257668/the-equivalent-of-wrap-content-and-match-parent-in-flutter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
return Container(
width: 40,
height:40,
// flutter中的margin没有负值的说法
// https://stackoverflow.com/questions/42257668/the-equivalent-of-wrap-content-and-match-parent-in-flutter
transform: Matrix4.translationValues(-20.0, 0.0, 0.0),
decoration: new BoxDecoration(
border: Border.all(width: 3, color: Colors.white),
color: Colors.black,
shape: BoxShape.circle,
image: new DecorationImage(
image: new NetworkImage('https://pic3.zhimg.com/50/d2af1b6b1_s.jpg')
)
),
);

图片自适应填满container

https://stackoverflow.com/questions/45745448/how-do-i-stretch-an-image-to-fit-the-whole-background-100-height-x-100-width

1
2
3
4
5
6
7
8
9
new Container(
height: 200,
decoration: new BoxDecoration(
image: new DecorationImage(
image: NetworkImage('https://pic3.zhimg.com/50/v2-f9fd4b13a46f2800a7049a5724e5969f_400x224.jpg'),
fit: BoxFit.fill
)
),
),

布局方式

justify-content: mainAxisAlignment
align-items: crossAxisAlignment

column 设置crossAxisAlignment: stretch后子元素宽度为100%,如果想让子元素宽度不为100%, 将其包裹在Row元素中即可。

flutter row and column

https://medium.com/jlouage/flutter-row-column-cheat-sheet-78c38d242041

捕捉点击事件

使用GestureDetector包裹widget即可。

1
2
3
4
5
6
7
child: new GestureDetector(
onTap: click,
child: Text(
name,
style: TextStyle(color: Colors.black87),
),
),

Dart 数组方法

https://codeburst.io/top-10-array-utility-methods-you-should-know-dart-feb2648ee3a2

PopupMenuButton 下拉弹窗菜单

https://stackoverflow.com/questions/43349013/how-to-open-a-popupmenubutton

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class DetailPage extends StatefulWidget {
@override
DetailPageState createState() => DetailPageState();
}

class DetailPageState extends State<DetailPage> {
final GlobalKey _menuKey = new GlobalKey();
....
....
....

child: new Row(
children: <Widget>[
new Container(
child: new GestureDetector(
onTap: () {
dynamic state = _menuKey.currentState;
state.showButtonMenu();
},
child: new Container(
child: new Text('默认排序'),
),
),
),
new PopupMenuButton(
icon: Icon(Icons.keyboard_arrow_down),
offset: Offset(0, 50),
key: _menuKey,
itemBuilder: (_) => <PopupMenuItem<String>>[
new PopupMenuItem<String>(
child: const Text('默认排序'), value: 'default'),
new PopupMenuItem<String>(
child: const Text('按时间排序'), value: 'timeline'),
],
onSelected: (_) {}
)

],
),

分割线

水平分割线 Divider
垂直分割线 VerticalDivider (无效???)

swiper

https://pub.dartlang.org/packages/flutter_swiper

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import 'package:flutter_swiper/flutter_swiper.dart';

...

var images = [
'https://pic3.zhimg.com/v2-5806d9e33e36fa772c8da56c931bb416_b.jpg',
'https://pic1.zhimg.com/50/v2-f355ca177e011626938b479f0e2e3e03_hd.jpg',
'https://pic2.zhimg.com/v2-d8e47ed961b93b875ad814104016bdfd_b.jpg'
];

child: new Swiper(
itemBuilder: (BuildContext context,int index){
return new Image.network(images[index], fit: BoxFit.cover,);
},
itemCount: 3,
pagination: new SwiperPagination(),
//control: new SwiperControl(),
),

floatingActionButton 浮动button

https://proandroiddev.com/a-deep-dive-into-floatingactionbutton-in-flutter-bf95bee11627

floatingActionButton 配合 Scaffold 使用最佳

1
2
3
4
5
6
7
8
9
Scaffold(
floatingActionButton: new FloatingActionButton(
onPressed: (){},
child: Icon(Icons.edit),
//mini: true,
),
// 默认右下角,可设置位置。
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
)

滑动视图

SingleChildScrollView

水平方向滑动 scrollDirection: Axis.horizontal

高斯模糊

https://stackoverflow.com/questions/43550853/how-do-i-do-the-frosted-glass-effect-in-flutter

1
2
3
4
5
6
import 'dart:ui';

new BackdropFilter(
filter: new ImageFilter.blur(sigmaX: 10.0, sigmaY: 10.0),
child: Text(desc, style: TextStyle(color: Colors.white),),
),

对话框弹窗

https://docs.flutter.io/flutter/material/AlertDialog-class.html

AlertDialog

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
void _showDialog(BuildContext context) {
// flutter defined function
showDialog(
context: context,
builder: (BuildContext context) {
// return object of type Dialog
return AlertDialog(
title: Text('Rewind and remember'),
content: SingleChildScrollView(
child: ListBody(
children: <Widget>[
Text('You will never be satisfied.'),
Text('You\’re like me. I’m never satisfied.'),
],
),
),
actions: <Widget>[
// usually buttons at the bottom of the dialog
new FlatButton(
child: new Text("Close"),
onPressed: () {
Navigator.of(context).pop();
},
),
],
);
},
);
}

// 调用
....
onPressed: (){
_showDialog(context);
},
....

HTTP 请求、JSON编码解码

https://flutterchina.club/networking/

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 加载库
import 'dart:convert';
import 'dart:io';

// 请求
try {
var request = await httpClient.getUrl(Uri.parse(url));
var response = await request.close();
if (response.statusCode == HttpStatus.OK) {
var json = await response.transform(UTF8.decoder).join();
var data = JSON.decode(json);
result = data['origin'];
} else {
result =
'Error getting IP address:\nHttp status ${response.statusCode}';
}
} catch (exception) {
result = 'Failed getting IP address';
}

// 保存返回的数据
// error: setState() called after dispose()

// If the widget was removed from the tree while the message was in flight,
// we want to discard the reply rather than calling setState to update our
// non-existent appearance.
if (!mounted) return;

setState(() {
_ipAddress = result;
});

时间控制:延时

1
2
3
4
5
6
7
8
9
10
11
import 'dart:async';
Future<Null> _onRefresh() {
Completer<Null> completer = new Completer<Null>();

new Timer(new Duration(seconds: 3), () {
print("timer complete");
completer.complete();
});

return completer.future;
}

下拉刷新 RefreshIndicator

1
2
3
4
5
6
7
8
9
new RefreshIndicator(
onRefresh: _onRefresh,
child: new SingleChildScrollView(
child: new Container(
padding: EdgeInsets.all(10),
child: new Text(_jsonData),
),
),
)

上拉加载更多

https://juejin.im/post/5b3abfc4518825622c14a6f1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
ScrollController _controller = new ScrollController();

@override
void initState() {
super.initState();
_controller.addListener((){
if(_controller.position.pixels == _controller.position.maxScrollExtent) {
print('下拉加载');
_getMoreData();
}
});
}

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

...
scroll controller: _controller
...

flutter运行模式

https://www.jianshu.com/p/4db65478aaa3

flutter学习资源

http://flutter.link/

1
2
3
4
➜  zh git:(master) ✗ flutter clean
Deleting 'build/'.
➜ zh git:(master) ✗ rm -rf ios/Flutter/App.framework ios/Flutter/Flutter.framework
➜ zh git:(master) ✗ rm -rf /Users/ludis/Library/Developer/Xcode/DerivedData/Runner-

报错解决

1、在安卓真机release后 ios simulator无法编译

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Launching lib/main.dart on iPhone X in debug mode...
Xcode build done. 1.0s
Failed to build iOS app
Error output from Xcode build:

** BUILD FAILED **
Xcode's output:

=== BUILD TARGET Runner OF PROJECT Runner WITH CONFIGURATION Debug ===
diff: /Users/ludis/Desktop/opt/flutter/zh/ios/Pods/Manifest.lock: No such file or directory
error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.
Could not build the application for the simulator.
Error launching application on iPhone X.
Exited (sigterm)

解决

1
2
cd ios
pod install

常见问题: https://www.jianshu.com/p/bf3002de6a5e

Flutter scroll animation

https://medium.com/flutter-community/scrolling-animation-in-flutter-6a6718b8e34f

布局指南

在scrollView的滚动布局中,如果使用column组件,并为其添加Expanded扩展子组件的话,这两者会存在冲突。
如果坚持要使用此布局,在column设置mainAxisSize: MainAxisSize.min,同时子组件由Expanded改为Flexible即可。

表单、校验

https://www.cnblogs.com/pengshaomin/p/8945720.html

1、单行文本输入框 TextFormField

1
2
3
4
5
6
7
8
new TextFormField(
maxLength: 32,
onSaved: (val)=> this._config = val,
validator: (v)=>(v == null || v.isEmpty)?"请选择配置": null,
decoration: new InputDecoration(
labelText: '配置',
),
),

2、多行输入框 keyboardType: TextInputType.multiline,

1
2
3
4
5
new TextField(
keyboardType: TextInputType.multiline,
maxLines: 3,
maxLength: 100,
),

2、单选Radio

1
2
3
4
5
6
7
8
9
10
11
new Radio(
groupValue: this.radio,
activeColor: Colors.blue,
value: 'aaa',
onChanged: (String val) {
// val 与 value 的类型对应
this.setState(() {
this.radio = val; // aaa
});
},
),

3、复选 CheckBox

1
2
3
4
5
6
7
8
9
new Checkbox(
value: flutter,
activeColor: Colors.blue,
onChanged: (val) {
setState(() {
flutter = val;
});
},
),

4、switch

1
2
3
4
5
6
7
8
9
new Switch(
activeColor: Colors.green,
value: flutter,
onChanged: (val) {
setState(() {
flutter = val;
});
},
),

5、slider

1
2
3
4
5
6
7
8
9
10
new Slider(
value: _slider,
min: 0.0,
max: 100.0,
onChanged: (val) {
setState(() {
_slider = val;
});
},
),

6、DateTimePicker

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 设置存储日期的变量
DateTime _dateTime = new DateTime.now();

// 显示文字Text,设置点击事件,点击后打开日期选择器
new GestureDetector(
onTap: (){
_showDatePicker();
},
child: new Container(
child: new Text(_dateTime.toLocal().toString()),
),
),

// 打开日期选择器
void _showDatePicker() {
_selectDate(context);
}

Future<Null> _selectDate(BuildContext context) async {
final DateTime _picked = await showDatePicker(
context: context,
initialDate: _dateTime,
firstDate: new DateTime(2016),
lastDate: new DateTime(2050)
);

if(_picked != null) {
print(_picked);
setState(() {
_dateTime = _picked;
});
}
}

7、TimePIcker

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
  TimeOfDay _time = new TimeOfDay.now();

// text显示当前时间
new GestureDetector(
onTap: _showTimePicker,
child: new Text(_time.format(context)),
),

// 显示timpicker
void _showTimePicker(){
_selectTime(context);
}

Future<Null> _selectTime(BuildContext context) async {
final TimeOfDay _picker = await showTimePicker(
context: context,
initialTime: _time,
);
if(_picker != null) {
print(_picker);
setState(() {
_time = _picker;
});
}
}

Toast/showSnackBar

showSnackBar:

https://material.io/design/components/snackbars.html#usage

1
2
3
4
5
6
7
8
9
10
11
12
void _showToast(BuildContext context) {
final scaffold = Scaffold.of(context);
scaffold.showSnackBar(
SnackBar(
content: const Text('Added to favorite'),
action: SnackBarAction(
label: 'UNDO',
onPressed: scaffold.hideCurrentSnackBar
),
),
);
}

Toast:

https://github.com/PonnamKarthik/FlutterToast

1
2
3
4
5
6
7
8
9
10
11
void _showToast(String title) {
Fluttertoast.showToast(
msg: title,
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.CENTER,
timeInSecForIos: 1,
backgroundColor: Color.fromRGBO(0, 0, 0, 0.85),
textColor: Colors.white
);
}

Popover/popup

popup: CupertinoActionSheet

http://flatteredwithflutter.com/actionsheet-in-flutter/

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
new MaterialButton(
onPressed: () {
_showActionSheet();
},
child: new Text('show ActionSheet', style: TextStyle(color: Colors.white),),
color: Colors.greenAccent,
),

void _showActionSheet() {
showCupertinoModalPopup(
context: context,
builder: (BuildContext context) => actionSheet(),
).then((value) {
Scaffold.of(context).showSnackBar(new SnackBar(
content: new Text('You clicked $value'),
));
});
}

Widget actionSheet(){
return new CupertinoActionSheet(
title: new Text('title'),
message: const Text('your options are'),
actions: <Widget>[
CupertinoActionSheetAction(
child: const Text('yes'),
onPressed: (){
Navigator.pop(context, 'yes');
},
),
CupertinoActionSheetAction(
child: const Text('no'),
onPressed: (){
Navigator.pop(context, 'no');
},
)
],
cancelButton: CupertinoActionSheetAction(
child: new Text('cancel'),
onPressed: () {
Navigator.pop(context, 'Cancel');
},
),
);
}

IOS风格组件

https://flutter-es.io/widgets/cupertino/

Dismissible 滑动删除

https://flutter.io/docs/cookbook/gestures/dismissible

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
new Dismissible(
// Each Dismissible must contain a Key. Keys allow Flutter to
// uniquely identify Widgets.
key: Key(item),
onDismissed: (direction) {
setState(() {
items.removeAt(index);
});

// Then show a snackbar!
Scaffold.of(context)
.showSnackBar(SnackBar(content: Text("$item dismissed")));
},
// Show a red background as the item is swiped away
background: Container(color: Colors.red),
child: ListTile(title: Text('$item')),
);

Swipe 左滑右滑删除

https://github.com/letsar/flutter_slidable

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
Widget _swipe(int i, String title, String desc) {
return new Slidable(
delegate: new SlidableDrawerDelegate(),
actionExtentRatio: 0.25,
child: new Container(
color: Colors.white,
child: new GestureDetector(
onTap: (){},
onDoubleTap: (){},
onLongPress: (){},
child: new ListTile(
leading: new CircleAvatar(
backgroundColor: Colors.grey[200],
child: new Text(
'$i',
style: TextStyle(color: Colors.orange),
),
foregroundColor: Colors.white,
),
title: new Text(
'$title',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(color: Colors.black87, fontSize: 16),
),
subtitle: new Text(
'$desc',
style: TextStyle(color: Colors.blue[300]),
),
),
)
),
actions: <Widget>[
new IconSlideAction(
caption: 'Archive',
color: Colors.blue,
icon: Icons.archive,
onTap: () => _showSnackBar('Archive'),
),
new IconSlideAction(
caption: 'Share',
color: Colors.indigo,
icon: Icons.share,
onTap: () => _showSnackBar('Share'),
),
],
secondaryActions: <Widget>[
new IconSlideAction(
caption: 'More',
color: Colors.black45,
icon: Icons.more_horiz,
onTap: () => _showSnackBar('More'),
),
new IconSlideAction(
caption: 'Delete',
color: Colors.red,
icon: Icons.delete,
onTap: () => _showSnackBar('Delete'),
),
],
);
}

常用手势 GestureDetector

1
2
3
4
5
new GestureDetector(
onTap: (){_showToast('点击: $i');},
onDoubleTap: (){_showToast('连点: $i');},
onLongPress: (){_showToast('长按: $i');},
)

flutter 常用组件

https://github.com/flutter/plugins

camera / image_picker

https://medium.com/flutter-community/implementing-camera-feature-in-flutter-f7f6a7a5e6dd

image_picker: (最常用场景,从相册选择或拍照得到照片)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
dynamic _picture;
dynamic _gallery;

new FlatButton.icon(
icon: Icon(Icons.camera),
label: Text('选择头像'),
onPressed: (){
_optionsDialogBox();
},
),

Future<void> _optionsDialogBox() {
return showDialog(context: context,
builder: (BuildContext context) {
return AlertDialog(
content: new SingleChildScrollView(
child: new ListBody(
children: <Widget>[
GestureDetector(
child: new Text('Take a picture'),
onTap: openCamera,
),
Padding(
padding: EdgeInsets.all(8.0),
),
GestureDetector(
child: new Text('Select from gallery'),
onTap: openGallery,
),
],
),
),
);
});
}

void openCamera() async {
Navigator.of(context).pop();
var picture = await ImagePicker.pickImage(
source: ImageSource.camera,
);

setState(() {
_picture = picture;
});
}
void openGallery() async {
Navigator.of(context).pop();
var gallery = await ImagePicker.pickImage(
source: ImageSource.gallery,
);
setState(() {
_gallery = gallery;
});
}




camera: (高阶用法,打开相机,实时获取相机流,可以定制拍照、录像等按钮。可用于相机扫码、实时识别、直播等场景)

https://pub.dartlang.org/packages/camera

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
camera: ^0.2.9

import 'package:camera/camera.dart';

class _CameraState extends State<CameraWidget> {
List<CameraDescription> cameras;
CameraController controller;
bool _isReady = false;

@override
void initState() {
super.initState();
_setupCameras();
}

Future<void> _setupCameras() async {
try {
// initialize cameras.
cameras = await availableCameras();
// initialize camera controllers.
controller = new CameraController(cameras[0], ResolutionPreset.medium);
await controller.initialize();
} on CameraException catch (_) {
// do something on error.
}
if (!isMounted) return;
setState(() {
_isReady = true;
});
}

Widget build(BuildContext context) {
if (!_isReady) return new Container();
return new Container(
height: 200,
child: AspectRatio(
aspectRatio: controller.value.aspectRatio,
child: CameraPreview(controller),
),
)
}
}

video player

https://github.com/flutter/plugins/tree/master/packages/video_player

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
video_player: ^0.8.0

import 'package:video_player/video_player.dart';

VideoPlayerController _controller;
bool _isPlaying = false;

@override
void initState() {
super.initState();
_controller = VideoPlayerController.network(
'https://www.quirksmode.org/html5/videos/big_buck_bunny.mp4',
)
..addListener(() {
final bool isPlaying = _controller.value.isPlaying;
if (isPlaying != _isPlaying) {
setState(() {
_isPlaying = isPlaying;
});
}
})
..initialize().then((_) {
// Ensure the first frame is shown after the video is initialized, even before the play button has been pressed.
setState(() {});
});
}

@override
void dispose() {
_controller?.dispose();
super.dispose();
}

// 显示、控制
_controller.value.initialized
? AspectRatio(
aspectRatio: _controller.value.aspectRatio,
child: new Container(
padding: EdgeInsets.all(10),
color: Colors.black,
child: VideoPlayer(_controller),

),
)
: Container(
child: new Text('视频加载中~'),
),
new FlatButton.icon(
label: Text('播放/暂停'),
icon: Icon(
_controller.value.isPlaying ? Icons.pause : Icons.play_arrow,
),
onPressed: _controller.value.isPlaying
? _controller.pause
: _controller.play,
)

AudioPlayer

https://github.com/rxlabz/audioplayer

Flutter的学习还在路上,列了个To Do List。继续加油: