Flutter实战开发

1.1 冷启动优化

冷启动分为安卓和iOS两个平台,我们主要针对安卓平台的冷启动优化,优化方式主要是通过在启动主题进行设置

打开安卓项目下的清单文件会看到默认的启动主题

1
android:theme="@style/LaunchTheme"
1
2
3
4
5
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
Flutter draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>

launch_background.xml

1
2
3
4
5
6
7
8
9
10
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />

<!-- You can insert your own image assets here -->
<!-- <item>-->
<!-- <bitmap-->
<!-- android:gravity="center"-->
<!-- android:src="@mipmap/launch_image" />-->
<!-- </item> -->
</layer-list>

将注释打开,我们在这里放我们的启动图片,即可避免启动过程中的白屏现象,我这里的修改如下:

1
2
3
4
5
6
7
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/splash_bg"/>
<item android:bottom="20dp">
<bitmap android:src="@drawable/iv_splash" android:gravity="center"/>
</item>
</layer-list>

1.2 动态权限

使用插件permission_handler

1
2
# 权限请求
permission_handler: 5.0.1

将整个权限申请封装到一个透明的StatefulWidget中,为什么呢,因为一般情况下,如果用户拒绝权限,我们会友好的提示,权限被拒绝,需要去设置中心开启权限,开启权限,回到APP中,我们需要监听AP生命周期,如何监听(使用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
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
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_tingche/common/app_function.dart';
import 'package:permission_handler/permission_handler.dart';

///权限请求弹窗封装
class PermissionRequestWidget extends StatefulWidget {
final Permission permission;
final List<String> permissionList;
final bool isCloseApp;
final String leftButtonText;

const PermissionRequestWidget(
{Key key,
@required this.permission,
@required this.permissionList,
this.isCloseApp = false,
this.leftButtonText = '再考虑一下'})
: super(key: key);

@override
_PermissionRequestWidgetState createState() =>
_PermissionRequestWidgetState();
}

class _PermissionRequestWidgetState extends State<PermissionRequestWidget>
with WidgetsBindingObserver {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
checkPermission();
}

@override
void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state);
if (state == AppLifecycleState.resumed && _isGoSetting) {
checkPermission();
}
}

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

@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.transparent,
);
}

void checkPermission({PermissionStatus status}) async {
//请求权限
Permission permission = widget.permission;
//拿到权限状态
if (status == null) {
status = await permission.status;
}
//判断当前权限
if (status.isUndetermined) {
//第一次申请
showPermissionAlert(widget.permissionList[0], "同意", permission);
} else if (status.isDenied) {
//用户第一次申请拒绝
showPermissionAlert(widget.permissionList[1], "重试", permission);
} else if (status.isPermanentlyDenied) {
//第二次申请 用户拒绝
showPermissionAlert(widget.permissionList[2], "去设置中心", permission,
isSetting: true);
} else {
//权限通过
Navigator.of(context).pop(true);
}
}

//是否去设置中心
bool _isGoSetting = false;

void showPermissionAlert(
String message, String rightStr, Permission permission,
{bool isSetting = false}) {
showCupertinoDialog(
context: context,
builder: (context) {
return CupertinoAlertDialog(
title: Text('温馨提示'),
content: Container(
padding: EdgeInsets.all(12),
child: Text(message),
),
actions: <Widget>[
//左边的按钮
CupertinoDialogAction(
child: Text(widget.leftButtonText),
onPressed: () {
if (widget.isCloseApp) {
closeApp();
} else {
Navigator.of(context).pop();
}
},
),
//右边的按钮
CupertinoDialogAction(
child: Text(rightStr),
onPressed: () {
//关闭弹窗
Navigator.of(context).pop();
if (isSetting) {
_isGoSetting = true;
openAppSettings();
} else {
//申请权限
requestPermission(permission);
}
},
),
],
);
});
}

void requestPermission(Permission permission) async {
//发起权限申请
PermissionStatus status = await permission.request();
//校验权限
checkPermission(status: status);
}
}

如何使用?在SplashPage中,我们需要弹出权限申请弹窗,所以,在splashPage的initState()方法中,打开权限申请,这里对页面跳转封装了一个utils简化代码,因为一般的materialPageRoute不是透明的,我们需要打开的是PageRoute,在PageRouteBuilder中,设置opaque为false

注意点:在initState中,我们无法获取到context,所以这里的技巧就是在开启一个队列,在队列中能获取到context,Future.delayed开启队列,为什么?

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
@override
void initState() {
super.initState();
Future.delayed(Duration.zero, () {
NavigatorUtils.pushPageByFade(
context: context,
//打开权限页面
targetPage: PermissionRequestWidget(
permission: Permission.storage,
permissionList: DString.permission_storage_list,
isCloseApp: true,
),
dismissCallBack: (value) async {
if (value != null && value) {
//权限通过
LogUtils.d("权限通过");
bool flag = await showProtocolDialog(context);
if (flag) {
jumpHome(context);
} else {
//退出
}
} else {
LogUtils.d("权限未通过");
}
});
});
}

以下是关于NavigatorUtils的代码封装,作用:简化页面跳转

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
import 'dart:io';

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';

class NavigatorUtils {
///普通的打开页面
///[targetPage] 目标页面
///[isReplace] 是否替换当前页面
static void pushPage(
{@required BuildContext context,
@required Widget targetPage,
bool isReplace = false,
Function(dynamic value) dismissCallback}) {
PageRoute pageRoute;
if (Platform.isAndroid) {
pageRoute = MaterialPageRoute(builder: (context) => targetPage);
} else {
pageRoute = CupertinoPageRoute(builder: (context) => targetPage);
}

if (isReplace) {
Navigator.of(context).pushReplacement(pageRoute).then((value) {
if (dismissCallback != null) {
dismissCallback(value);
}
});
} else {
Navigator.of(context).push(pageRoute).then((value) {
if (dismissCallback != null) {
dismissCallback(value);
}
});
}
}

static void pushPageByFade({
@required BuildContext context,
@required Widget targetPage,
bool isReplace = false,
bool opaque = false,
Function(dynamic value) dismissCallBack,
}) {
PageRoute pageRoute = PageRouteBuilder(
opaque: opaque,
pageBuilder: (BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation) {
return targetPage;
},
//动画
transitionsBuilder: (BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation, Widget child) {
return FadeTransition(
opacity: animation,
child: child,
);
},
);

if (isReplace) {
Navigator.of(context).pushReplacement(pageRoute).then((value) {
if (dismissCallBack != null) {
dismissCallBack(value);
}
});
} else {
Navigator.of(context).push(pageRoute).then((value) {
if (dismissCallBack != null) {
dismissCallBack(value);
}
});
}
}
}

1.3 用户协议与隐私协议弹窗

弹窗封装代码如下:

代码分析:我们需要弹窗,这里使用showCupertinoDialog,接收两个参数,contextbuiilder,在builder中去构建弹窗,我们使用了CupertinoAlertDialog来构建弹窗,主要是以下三个参数:

  • title:标题
  • content:主体内容
  • actions:数组,底部的按钮,使用CupertinoDialogAction表示
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
94
95
96
97
98
99
100
101
102
103
import 'package:flutter/cupertino.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_tingche/common/DString.dart';
import 'package:flutter_tingche/utils/log_utils.dart';
import 'package:flutter_tingche/utils/navigator_utils.dart';
import 'package:flutter_tingche/widget/common_webview.dart';

///此类介绍:隐私协议
class ProtocolMixin {
///点击事件
TapGestureRecognizer _registerProtocolRecognizer;
TapGestureRecognizer _privacyProtocolRecognizer;

///打开隐私协议与用户协议弹窗,使用IOS风格的弹窗
Future<bool> showProtocolDialog(BuildContext context) async {
_registerProtocolRecognizer = TapGestureRecognizer();
_privacyProtocolRecognizer = TapGestureRecognizer();

bool value = await showCupertinoDialog(
context: context, builder: (context) => alertDialog(context));

//销毁
_registerProtocolRecognizer.dispose();
_privacyProtocolRecognizer.dispose();
return Future.value(value);
}

///弹出苹果风格的弹窗
alertDialog(BuildContext context) {
return CupertinoAlertDialog(
title: Text('温馨提示'),
content: Container(
height: 240,
padding: EdgeInsets.all(12.0),
child: SingleChildScrollView(child: buildContent(context)),
),
actions: [
CupertinoDialogAction(
child: Text('不同意'),
onPressed: () => Navigator.of(context).pop(false),
),
CupertinoDialogAction(
child: Text('同意'),
onPressed: () => Navigator.of(context).pop(true),
)
],
);
}

///弹窗内容
Widget buildContent(BuildContext context) {
return RichText(
text: TextSpan(
text: '请您在使用本产品之前仔细阅读',
style: TextStyle(color: Colors.grey[600]),
children: [
TextSpan(
text: "《用户协议》",
style: TextStyle(color: Colors.blue),
recognizer: _registerProtocolRecognizer
..onTap = () {
//打开用户协议
openUserProtocol(context);
}),
TextSpan(text: "与", style: TextStyle(color: Colors.grey[600])),
TextSpan(
text: "《隐私协议》",
style: TextStyle(color: Colors.blue),
recognizer: _privacyProtocolRecognizer
..onTap = () {
//打开隐私协议
openPrivateProtocol(context);
}),
TextSpan(
text: DString.userPrivateProtocol,
style: TextStyle(color: Colors.grey[600])),
]));
}

//查看用户协议
void openUserProtocol(BuildContext context) {
LogUtils.d("查看用户协议");
NavigatorUtils.pushPage(
context: context,
targetPage: CommonWebViewPage(
url: "https://biglead.blog.csdn.net/article/details/93532582",
title: "用户协议",
));
}

//查看隐私协议
void openPrivateProtocol(BuildContext context) {
LogUtils.d("查看隐私协议");
NavigatorUtils.pushPage(
context: context,
targetPage: CommonWebViewPage(
url: "https://biglead.blog.csdn.net/article/details/93532582",
title: "隐私协议",
));
}
}

如何使用?在启动页混入我们封装的类

1
class _SplashPageState extends State<SplashPage> with ProtocolMixin

就可以直接调用弹窗的方法了

1
2
3
4
5
6
bool flag = await showProtocolDialog(context);
if (flag) {
jumpHome(context);
} else {
//退出
}

弹窗内容

内容部分由于隐私协议与用户协议几个字需要蓝色表示,所以整体使用RichText+TextSpan来实现富文本,

TextSpan有个children属性,可以在children中放多个TextSpan来实现部分文字变色

1.4 SharedPreferences

使用插件shared_preferences

pub地址:https://pub.dev/packages/shared_preferences/

添加依赖:

1
shared_preferences: ^0.5.12+4

获取sp:

1
var sp = await SharedPreferences.getInstance();

获取值:

1
bool isAgreement = sp.getBool(DString.SP_IS_AGREEMENT);

设置值:

1
sp.setBool(DString.SP_IS_AGREEMENT, true);

1.5 图片选择

使用官方库:image_picker

仓库地址:https://pub.flutter-io.cn/packages/image_picker

1
2
3
4
5
6
7
8
9
10
11
Future _getImage(bool isTakePhoto) async {
Navigator.pop(context);
var image = await ImagePicker().getImage(
source: isTakePhoto ? ImageSource.camera : ImageSource.gallery,
);
if (image != null) {
setState(() {
_images.add(File(image.path));
});
}
}

使用案例:https://gitee.com/evancola/flutter_study/blob/master/lib/page/image_picker_page.dart

1.6 渐变导航栏

要实现这种滑动渐变,那么我们需要去监听列表的滚动,使用NotificationListener去监听滚动

1
2
3
4
5
6
7
8
9
10
NotificationListener(
onNotification:(state){
if (state is ScrollUpdateNotification &&state.depth == 0) {
///如果是在滚动中,并且是第一个元素,即listview
_onScroll(state.metrics.pixels);
}
//如果使用RefreshIndicator做下拉刷新,NotificationListener 中的onNotification方法 返回值不能设置为true 设置为false 或者 不写return
return false;
}
)

onScroll

1
2
3
4
5
6
7
8
9
10
11
12
13
14
///滚动的最大距离
const APPBAR_SCROLL_OFFSET = 100;

_onScroll(double pixels) {
double alpha = pixels / APPBAR_SCROLL_OFFSET;
if (alpha < 0) {
alpha = 0;
} else if (alpha > 1) {
alpha = 1;
}
setState(() {
appBarAlpha = alpha;
});
}

Flutter技巧

1.1 Charles抓包

必须在代码中手动设置代理,charles才能抓包,仅在模拟器或手机设置代理,flutter APP也不会走模拟器或手机的系统代理

注意:上线前必须关闭代理,否则将造成严重后果(用户将无法访问网络)

如何设置代理?如果是Dio

1
2
3
4
5
6
7
8
Dio dio = Dio();
// 设置代理用来调试应用
(dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate = (client) {
client.findProxy = (Uri) {
// 用1个开关设置是否开启代理
return AppConstant.isDebug ? 'PROXY 192.168.5.5:8888' : 'DIRECT';
};
};

如果是HttpClient

1
2
3
4
5
HttpClient client = HttpClient();
client.findProxy = (uri) {
// 用1个开关设置是否开启代理
return AppConstant.isDebug ? "PROXY 192.168.5.5:8888" : 'DIRECT';
};

参考博客:https://blog.csdn.net/haha223545/article/details/91541452