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 数组、字符串常用方法:

Author

Ludis

Posted on

2019-03-19

Updated on

2019-03-19

Licensed under

Comments