本节目标

  • app 升级策略
  • android 动态授权
  • android 设备目录
  • ios 支持 swift 语言
  • 快速提示框

视频

代码

https://github.com/ducafecat/flutter_learn_news/releases/tag/v1.0.11

正文

ios 支持 swift 语言

  • 出发点

社区第三方包都在用 swift 开发,打包的时候需要加入 swift 语言包。

  • 操作

创建一个支持 swift 的新项目,然后把 lib assets pubspec.yaml 覆盖即可。

app 升级策略

代码实现

定义接口

  • post /app/update

加入依赖包

  • pubspec.yaml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
dependencies:
# 设备信息
device_info: ^0.4.2+3

# 包信息
package_info: ^0.4.0+18

# 路径查询
path_provider: ^1.6.8

# permission 权限
permission_handler: ^5.0.0+hotfix.6

# 安装
install_plugin: ^2.0.1

# 对话框
easy_dialog: ^1.0.5

升级工具类

  • lib/common/utils/update.dart
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 'dart:io';

import 'package:dio/dio.dart';
import 'package:easy_dialog/easy_dialog.dart';
import 'package:flutter/material.dart';
import 'package:flutter_ducafecat_news/common/apis/app.dart';
import 'package:flutter_ducafecat_news/common/entitys/entitys.dart';
import 'package:flutter_ducafecat_news/common/widgets/toast.dart';
import 'package:flutter_ducafecat_news/global.dart';
import 'package:install_plugin/install_plugin.dart';
import 'package:path_provider/path_provider.dart';

/// app 升级
class AppUpdateUtil {
static AppUpdateUtil _instance = AppUpdateUtil._internal();
factory AppUpdateUtil() => _instance;

BuildContext _context;
AppUpdateResponseEntity _appUpdateInfo;

AppUpdateUtil._internal();

/// 获取更新信息
Future run(BuildContext context) async {
_context = context;

// 提交 设备类型、发行渠道、架构、机型
AppUpdateRequestEntity requestDeviceInfo = AppUpdateRequestEntity(
device: Global.isIOS == true ? "ios" : "android",
channel: Global.channel,
architecture: Global.isIOS == true
? Global.iosDeviceInfo.utsname.machine
: Global.androidDeviceInfo.device,
model: Global.isIOS == true
? Global.iosDeviceInfo.name
: Global.androidDeviceInfo.brand,
);
_appUpdateInfo =
await AppApi.update(context: context, params: requestDeviceInfo);

_runAppUpdate();
}

/// 检查是否有新版
Future _runAppUpdate() async {
// 比较版本
final isNewVersion =
(_appUpdateInfo.latestVersion.compareTo(Global.packageInfo.version) ==
1);

// 安装
if (isNewVersion == true) {
_appUpdateConformDialog(() {
Navigator.of(_context).pop();
if (Global.isIOS == true) {
// 去苹果店
InstallPlugin.gotoAppStore(_appUpdateInfo.shopUrl);
} else {
// apk 下载安装
toastInfo(msg: "开始下载升级包");
_downloadAPKAndSetup(_appUpdateInfo.fileUrl);
}
});
}
}

/// 下载文件 & 安装
Future _downloadAPKAndSetup(String fileUrl) async {
// 下载
Directory externalDir = await getExternalStorageDirectory();
String fullPath = externalDir.path + "/release.apk";

Dio dio = Dio(BaseOptions(
responseType: ResponseType.bytes,
followRedirects: false,
validateStatus: (status) {
return status < 500;
}));
Response response = await dio.get(
fileUrl,
);

File file = File(fullPath);
var raf = file.openSync(mode: FileMode.write);
raf.writeFromSync(response.data);
await raf.close();

// 安装
await InstallPlugin.installApk(fullPath, Global.packageInfo.packageName);
}

/// 升级确认对话框
void _appUpdateConformDialog(VoidCallback onPressed) {
EasyDialog(
title: Text(
"发现新版本 ${_appUpdateInfo.latestVersion}",
style: TextStyle(fontWeight: FontWeight.bold),
textScaleFactor: 1.2,
),
description: Text(
_appUpdateInfo.latestDescription,
textScaleFactor: 1.1,
textAlign: TextAlign.center,
),
height: 220,
contentList: [
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
new FlatButton(
padding: const EdgeInsets.only(top: 8.0),
textColor: Colors.lightBlue,
onPressed: onPressed,
child: new Text(
"同意",
textScaleFactor: 1.2,
),
),
new FlatButton(
padding: const EdgeInsets.only(top: 8.0),
textColor: Colors.lightBlue,
onPressed: () {
Navigator.of(_context).pop();
},
child: new Text(
"取消",
textScaleFactor: 1.2,
),
),
],
)
]).show(_context);
}
}

读取设备信息

  • 插件

https://pub.flutter-io.cn/packages/device_info

  • 全局信息

lib/global.dart

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
/// 是否 ios
static bool isIOS = Platform.isIOS;

/// android 设备信息
static AndroidDeviceInfo androidDeviceInfo;

/// ios 设备信息
static IosDeviceInfo iosDeviceInfo;

/// 包信息
static PackageInfo packageInfo;

/// init
static Future init() async {
...

// 读取设备信息
DeviceInfoPlugin deviceInfoPlugin = DeviceInfoPlugin();
if (Global.isIOS) {
Global.iosDeviceInfo = await deviceInfoPlugin.iosInfo;
} else {
Global.androidDeviceInfo = await deviceInfoPlugin.androidInfo;
}

// 包信息
Global.packageInfo = await PackageInfo.fromPlatform();

...
  • 定义升级信息 entity

lib/common/entitys/app.dart

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
class AppUpdateRequestEntity {
String device;
String channel;
String architecture;
String model;

AppUpdateRequestEntity({
this.device,
this.channel,
this.architecture,
this.model,
});

factory AppUpdateRequestEntity.fromJson(Map<String, dynamic> json) =>
AppUpdateRequestEntity(
device: json["device"],
channel: json["channel"],
architecture: json["architecture"],
model: json["model"],
);

Map<String, dynamic> toJson() => {
"device": device,
"channel": channel,
"architecture": architecture,
"model": model,
};
}

class AppUpdateResponseEntity {
String shopUrl;
String fileUrl;
String latestVersion;
String latestDescription;

AppUpdateResponseEntity({
this.shopUrl,
this.fileUrl,
this.latestVersion,
this.latestDescription,
});

factory AppUpdateResponseEntity.fromJson(Map<String, dynamic> json) =>
AppUpdateResponseEntity(
shopUrl: json["shopUrl"],
fileUrl: json["fileUrl"],
latestVersion: json["latestVersion"],
latestDescription: json["latestDescription"],
);

Map<String, dynamic> toJson() => {
"shopUrl": shopUrl,
"fileUrl": fileUrl,
"latestVersion": latestVersion,
"latestDescription": latestDescription,
};
}
  • api 请求

lib/common/apis/app.dart

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/// 系统相关
class AppApi {
/// 获取最新版本信息
static Future<AppUpdateResponseEntity> update({
@required BuildContext context,
AppUpdateRequestEntity params,
}) async {
var response = await HttpUtil().post(
'/app/update',
context: context,
params: params,
);
return AppUpdateResponseEntity.fromJson(response);
}
}
  • 提交信息 获取版本

lib/common/utils/update.dart

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
/// 获取更新信息
Future run(BuildContext context) async {
_context = context;

// 提交 设备类型、发行渠道、架构、机型
AppUpdateRequestEntity requestDeviceInfo = AppUpdateRequestEntity(
device: Global.isIOS == true ? "ios" : "android",
channel: Global.channel,
architecture: Global.isIOS == true
? Global.iosDeviceInfo.utsname.machine
: Global.androidDeviceInfo.device,
model: Global.isIOS == true
? Global.iosDeviceInfo.name
: Global.androidDeviceInfo.brand,
);
_appUpdateInfo =
await AppApi.update(context: context, params: requestDeviceInfo);

_runAppUpdate();
}

/// 检查是否有新版
Future _runAppUpdate() async {
// 比较版本
final isNewVersion =
(_appUpdateInfo.latestVersion.compareTo(Global.packageInfo.version) ==
1);

// 安装
if (isNewVersion == true) {
_appUpdateConformDialog(() {
Navigator.of(_context).pop();
if (Global.isIOS == true) {
// 去苹果店
InstallPlugin.gotoAppStore(_appUpdateInfo.shopUrl);
} else {
// apk 下载安装
toastInfo(msg: "开始下载升级包");
_downloadAPKAndSetup(_appUpdateInfo.fileUrl);
}
});
}
}

android 动态授权

  • 插件

https://pub.flutter-io.cn/packages/permission_handler

  • 官方文章

https://developer.android.com/training/permissions/requesting

https://developer.android.com/training/permissions/usage-notes

  • AndroidManifest.xml 中加入权限

android/app/src/main/AndroidManifest.xml

1
2
3
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
  • flutter 启动页中执行授权

lib/pages/index/index.dart

在 initState 是执行

延迟 3 秒,用户体验好些

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class _IndexPageState extends State<IndexPage> {
@override
void initState() {
super.initState();

if (Global.isRelease == true) {
doAppUpdate();
}
}

Future doAppUpdate() async {
await Future.delayed(Duration(seconds: 3), () async {
if (Global.isIOS == false &&
await Permission.storage.isGranted == false) {
await [Permission.storage].request();
}
if (await Permission.storage.isGranted) {
AppUpdateUtil().run(context);
}
});
}

android 目录权限

  • 插件

https://pub.flutter-io.cn/packages/path_provider
https://pub.flutter-io.cn/packages/install_plugin

  • 文章

https://developer.android.com/reference/androidx/core/content/FileProvider.html

  • lib/common/utils/update.dart
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/// 下载文件 & 安装
Future _downloadAPKAndSetup(String fileUrl) async {
// 下载
Directory externalDir = await getExternalStorageDirectory();
String fullPath = externalDir.path + "/release.apk";

Dio dio = Dio(BaseOptions(
responseType: ResponseType.bytes,
followRedirects: false,
validateStatus: (status) {
return status < 500;
}));
Response response = await dio.get(
fileUrl,
);

File file = File(fullPath);
var raf = file.openSync(mode: FileMode.write);
raf.writeFromSync(response.data);
await raf.close();

// 安装
await InstallPlugin.installApk(fullPath, Global.packageInfo.packageName);
}

EasyDialog 快速提示框

  • 插件

https://pub.flutter-io.cn/packages/easy_dialog

  • lib/common/utils/update.dart
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
/// 升级确认对话框
void _appUpdateConformDialog(VoidCallback onPressed) {
EasyDialog(
title: Text(
"发现新版本 ${_appUpdateInfo.latestVersion}",
style: TextStyle(fontWeight: FontWeight.bold),
textScaleFactor: 1.2,
),
description: Text(
_appUpdateInfo.latestDescription,
textScaleFactor: 1.1,
textAlign: TextAlign.center,
),
height: 220,
contentList: [
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
new FlatButton(
padding: const EdgeInsets.only(top: 8.0),
textColor: Colors.lightBlue,
onPressed: onPressed,
child: new Text(
"同意",
textScaleFactor: 1.2,
),
),
new FlatButton(
padding: const EdgeInsets.only(top: 8.0),
textColor: Colors.lightBlue,
onPressed: () {
Navigator.of(_context).pop();
},
child: new Text(
"取消",
textScaleFactor: 1.2,
),
),
],
)
]).show(_context);
}

资源

视频

蓝湖设计稿(加微信给授权 ducafecat)

https://lanhuapp.com/url/lYuz1
密码: gSKl

蓝湖现在收费了,所以查看标记还请自己上传 xd 设计稿
商业设计稿文件不好直接分享, 可以加微信联系 ducafecat

YAPI 接口管理

http://yapi.demo.qunar.com/

参考

  • 文章

https://developer.android.com/training/permissions/requesting
https://developer.android.com/training/permissions/usage-notes
https://developer.android.com/reference/androidx/core/content/FileProvider.html

  • flutter 插件

https://pub.flutter-io.cn/packages/device_info
https://pub.flutter-io.cn/packages/path_provider
https://pub.flutter-io.cn/packages/permission_handler
https://pub.flutter-io.cn/packages/install_plugin
https://pub.flutter-io.cn/packages/easy_dialog

VSCode 插件


© 猫哥

https://ducafecat.tech