记录点滴,提升自我。
文章主要记录自己开发中遇到的问题以及解决办法,有同样问题或者需求的仅供参考
1,引用插件(尽量与本文中的一致)
#网络加载
dio: ^4.0.0
#系统应用版本 信息
package_info: ^2.0.2
#打开文件
open_file: ^3.2.1
#文件存储路径
path_provider: any
#权限平台
permission_handler: any
#progress_dialog弹窗
progress_dialog: ^1.2.4
#网页链接跳转
url_launcher: ^4.0.1
2.在AndroidManifest中添加权限以及provider(可以点击AS中的File-->Open-->选中你的项目下的android-->点击OK-->选择 New Window,在新打开的AS界面中编写Android原生的代码,如下图)
(1)添加如下权限
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<!-- 这个权限用于app安装 -->
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
(2)在<application>级别下添加
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileProvider"
android:exported="false"
android:grantUriPermissions="true"
tools:replace="android:authorities">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/filepaths"
tools:replace="android:resource" />
</provider>
(3)在res目录下新建xml文件夹,在xml中新建filepaths.xm.
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:tools="http://schemas.android.com/tools"
tools:ignore="ResourceName">
<root-path
name="root_path"
path="." />
</paths>
3.创建升级更新工具类,并在使用处调用
UpdateAppUtils.loadUpdateApp(context, false);
class UpdateAppUtils {
//是否更新app,isShow用于判断是否显示当前是最新版本,无需更新(一般用于个人中心--检查更新)
static loadUpdateApp(BuildContext context, bool isShow) async {
//判断平台,如果是Android走以下的自动更新。IOS跳转到appstore
if (Platform.isAndroid) {
updataForAndroid();
}
if (Platform.isIOS) {
final url =
"https://itunes.apple.com/cn/app/idxxxxxx"; // id 后面的数字换成自己的应用 id 就行了
if (await canLaunch(url)) {
await launch(url, forceSafariVC: false);
} else {
throw 'Could not launch $url';
}
}
}
}
(1)在updataForAndroid中(Utils,FileUtils工具类会在下方统一贴出)
//设置Flutter与原生交互的插件
static const platform =const MethodChannel('com.xxx.xxx.xxx/plugin');
static updataForAndroid() async{
//获取当前应用版本code
String versionCode = await Utils.getPackageInfoVersionCode();
//检查相应权限
bool approve = await checkPermission();
if (approve) {//权限通过,接口请求获取服务器版本信息
HttpGo.getInstance().get(PathUtils.getAppUpdateUrl, (result, msg) {
AppUpdate update = AppUpdate.fromJson(result);
if (update != null) {
//服务器版本大于本地版本 需要弹出更新
if (update.versionCode! > int.parse(versionCode)) {
List<ClientUpdatePatches>? list = update.clientUpdatePatches;
if (list != null && list.isNotEmpty) {
//弹窗显示更新信息
Utils.showUpdateDialog(context, () {
//立即更新点击效果
//获取创建新安装包存储地址
FileUtils.savePath().then((value) {
if (value != null) {
//判断新的安装包是否存在且完整
var savePath =
value + '/' + update.versionName.toString() + '.apk';
//服务器返回的安装包MD5值,用于判断安装目录下安装包的完整性
var sign = list[0].sign;
//判断安装目录下,安装文件是否存在
FileUtils.FileIsExisted(savePath).then((value) async {
if (value != null) {
if (value as bool) {
//存在,判断文件完整性
bool reslut =
await _getFileComplete(savePath, sign!);
if (reslut) {
//完整,直接安装
install(context, savePath);
} else {
//不完整,重新下载或继续下载
downloadApk(
context, list[0].downloadUrl!, savePath);
}
} else {
//不存在,去下载
downloadApk(
context, list[0].downloadUrl!, savePath);
}
}
});
}
});
},
update.versionName.toString(),
update.updateContent.toString(),
update.constraintType.toString());
}
} else {
if (isShow) {
Utils.showToast(context, '已是最新版本,无需更新');
}
}
}
});
}
}
判断文件存储权限
//获取权限
static Future<bool> checkPermission() async {
DeviceInfoPlugin deviceInfo = DeviceInfoPlugin();
AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo;
if (Platform.isAndroid) {
final status = await Permission.storage.status;
if (status != PermissionStatus.granted) {
final result = await Permission.storage.request();
if (result == PermissionStatus.granted) {
return true;
}
} else {
return true;
}
} else {
return true;
}
return false;
}
判断文件完整性(通过与原生的交互,利用后台返回的MD5签名,对比下载的安装包的MD5签名,如果一样则是完整的。这样也可以用于校验安装包的安全性)Android原生代码会在后面统一贴出
//判断文件完整性
Future<bool> _getFileComplete(String savePath, String sign) async {
final bool file_result = await platform.invokeMethod(
'checkFileComplete', {"savePath": savePath, "sign": sign});
if (file_result) {
return true;
} else {
return false;
}
}
安装APK
static install(BuildContext context, String savePath) async {
//TODO 弹出提示安装框
Utils.showEnsureCancelDialog(context, "安装包已下载完成,请点击确认安装",() async {
//判断是否有安卓8.0安装权限
bool reslut = await _getInstallPermission();
if (reslut) {
//有权限直接安装
OpenFile.open(savePath).then((value) {
print(value);
});
} else {
//没有权限申请权限
bool reslut = await _postInstallPermission();
if (reslut) {
//申请通过,开始安装
OpenFile.open(savePath).then((value) {
print(value);
});
} else {
//没有通过。提醒,关闭弹窗
Utils.showToast(context, '安装权限未开启,请开启相关权限');
}
}
});
}
//判断是否允许应用内安装权限
static Future<bool> _getInstallPermission() async {
final bool Permission_result =
await platform.invokeMethod('checkInstallPermission');
if (Permission_result) {
return true;
} else {
return false;
}
}
//判断允许应用内安装权限的开启与否
static Future<bool> _postInstallPermission() async {
final bool result = await platform.invokeMethod('setInstallPermission');
if (result) {
return true;
} else {
return false;
}
}
下载APK
//下载APK
static downloadApk(
BuildContext context, String downloadUrl, String savePath) async {
var pr = new ProgressDialog(
context,
type: ProgressDialogType.Download,
isDismissible: false,
showLogs: true,
);
pr.style(message: '准备下载...');
if (!pr.isShowing()) {
pr.show();
}
await DownLoadManage.download(
url: downloadUrl,
savePath: savePath,
onReceiveProgress: (received, total) {
if (total != -1) {
///当前下载的百分比例
print((received / total * 100).toStringAsFixed(0) + "%");
var s = (received / total * 100).toStringAsFixed(0);
pr.update(progress: double.parse(s.toString()), message: "下载中,请稍后…");
}
},
done: () {
print("下载完成");
if (pr.isShowing()) {
pr.hide();
}
install(context, savePath);
},
failed: (e) {
if (pr.isShowing()) {
pr.hide();
}
Utils.showToast(context, '更新下载失败,请重试');
print("下载1失败:" + e.toString());
},
);
}
文件下载downloadmanage
/*
* 文件下载
*/
class DownLoadManage {
//用于记录正在下载的url,避免重复下载(每次启动都是新建的队列,起不到记录作用,所以作罢)
// var downloadingUrls = new Map<String, CancelToken>();
/// 断点下载大文件
static Future<void> download({
required String url,
required String savePath,
ProgressCallback? onReceiveProgress,
void Function()? done,
void Function(Exception)? failed,
}) async {
CancelToken cancelToken= CancelToken();
int downloadStart = 0;
// if(downloadingUrls.containsKey(url)){//判断下载任务是否在下载队列,如果存在说明下载过程中断,从断点处继续下载。
// print("此链接已在下载队列,断点续传");
//判断需要下载的文件在本地是否存在
File f = File(savePath);
if (await f.exists()as bool) {
print("下载文件存在,断点续传");
//将本地文件的大小作为下载任务的开头,断点续传上
downloadStart = f.lengthSync();
}else{
//文件不存在了,就重新下载
print("下载文件不存在,重新下载");
downloadStart = 0;
}
// }else{//没在下载队列表明此为新的下载任务,从头开始
// print("此链接没有在下载队列,添加进去,从头下载");
// downloadStart = 0;
// //并将下载任务加入到下载队列,并对应相应的取消令牌
// downloadingUrls[url] = cancelToken;
// }
print("开始位置:$downloadStart");
var dio = Dio();
try {
var response = await dio.get<ResponseBody>(
url,
options: Options(
/// 以流的方式接收响应数据
responseType: ResponseType.stream,
followRedirects: false,
headers: {
/// 分段下载重点位置
"range": "bytes=$downloadStart-",
},
),
);
print("下载接口返回结果:$response");
File file = File(savePath);
RandomAccessFile raf = file.openSync(mode: FileMode.append);
int received = downloadStart;
int total = await _getContentLength(response);
Stream<Uint8List> stream = response.data!.stream;
StreamSubscription<Uint8List>? subscription;
subscription = stream.listen(
(data) {
/// 写入文件必须同步
raf.writeFromSync(data);
received += data.length;
onReceiveProgress?.call(received, total);
},
onDone: () async {
//下载完毕,关闭文件流,并将下载任务移除
print("文件下载完成了");
await raf.close();
// downloadingUrls.remove(url);
done?.call();
},
onError: (e) async {
//下载失败,关闭文件流
print("文件下载失败了");
await raf.close();
// downloadingUrls.remove(url);
failed?.call(e);
},
cancelOnError: true,
);
cancelToken.whenCancel.then((_) async {
print("下载中断了");
await subscription?.cancel();
await raf.close();
});
} on DioError catch (error) {
print("下载出问题了");
/// 请求已发出,服务器用状态代码响应它不在200的范围内
if (CancelToken.isCancel(error)) {
print("下载取消");
} else {
failed?.call(error);
}
//下载任务没有响应就从队列中移除
// downloadingUrls.remove(url);
}
}
/// 获取下载的文件大小
static Future<int> _getContentLength(Response<ResponseBody> response) async {
try {
var headerContent =
response.headers.value(HttpHeaders.contentRangeHeader);
print("下载文件$headerContent");
if (headerContent != null) {
return int.parse(headerContent.split('/').last);
} else {
return 0;
}
} catch (e) {
return 0;
}
}
}
flieUtils工具类
class FileUtils {
// 获取存储路径
static Future<String> findLocalPath() async {
// 因为Apple没有外置存储,所以第一步我们需要先对所在平台进行判断
// 如果是android,使用getExternalStorageDirectory
// 如果是iOS,使用getApplicationSupportDirectory
final directory = Platform.isAndroid
? await getExternalStorageDirectory()
: await getApplicationSupportDirectory();
return directory.path;
}
// 获取app存储路径
static Future<String> savePath() async {
// 获取存储路径
var _localPath =
(await findLocalPath()) + Platform.pathSeparator + 'Download';
final savedDir = Directory(_localPath);
// 判断下载路径是否存在
bool hasExisted = await savedDir.exists();
// 不存在就新建路径
if (!hasExisted) {
savedDir.create();
}
return _localPath;
}
//判断文件是否存在
static Future<bool> FileIsExisted(String path) async {
File file = File(path);
var dir_bool = await file.exists(); //返回真假
return dir_bool;
}
}
utils工具类
class Utils {
//获取系统 版本号
static Future<String> getPackageInfoVersionCode() async {
final PackageInfo info = await PackageInfo.fromPlatform();
return info.buildNumber;
}
//升级app 弹窗 // 是否强制更新 1是 2否
static Future showUpdateDialog(BuildContext context,Function ensure,String versionName,String updateContent, String constraintType) async {
var result = await showDialog(
barrierDismissible: false,
context: context,
builder: (BuildContext context) {
return WillPopScope(
onWillPop: () async => !(constraintType=='1'),
child:UpdateDialog(ensure,versionName,updateContent,constraintType),
);
});
return result;
}
//toast 信息
static void showToast(BuildContext context, String msg) {
FToast fToast = new FToast();
fToast.init(context);
fToast.showToast(
child: Container(
padding: EdgeInsets.fromLTRB(15.w, 13.h, 15.w, 13.h),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(6.r),
color: MyColors.FF333B49,
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Expanded(
child: Text(
msg,
textAlign: TextAlign.center,
style: TextStyle(fontSize: 16.sp, color: MyColors.FFFFFFFF),
),
),
],
),
),
gravity: ToastGravity.CENTER,
toastDuration: Duration(seconds: 2),
);
}
}
Android原生代码
private val INSTALL_PERMISSION = "com.xxx.xxx.xxx/plugin"
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
//各种Android权限的交互
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, INSTALL_PERMISSION)
.setMethodCallHandler(
MethodChannel.MethodCallHandler { call, result ->
run {
Log.i("kotlin 收到 flutter 请求:", "${call.method}");
//检查是否具有安装安装权限
if (call.method.contentEquals("checkInstallPermission")) {
result.success(canRequestPackageInstalls(activity));
//跳转设置应用安装权限
} else if (call.method.contentEquals("setInstallPermission")) {
RequestPackageInstalls(activity, result);
//检查文件完整性
} else if (call.method.contentEquals("checkFileComplete")) {
val path = call.argument<String>("savePath");
val sign = call.argument<String>("sign");
Log.i("传递过来的文件签名:", sign.toString())
if (path.toString().isNotEmpty()) {
Log.i("传递过来的文件路径不为空,继续检查", "")
FileIsComplete(path.toString(), result, activity, sign.toString())
} else {
Log.i("传递过来的文件路径为空直接返回", "")
result.success(false);
};
}
}
}
)
}
//检测是否有安装权限
private fun canRequestPackageInstalls(activity: Activity): Boolean {
return Build.VERSION.SDK_INT <= Build.VERSION_CODES.O || activity.packageManager.canRequestPackageInstalls()
}
//检测是否有安装权限
private fun RequestPackageInstalls(activity: Activity, result: MethodChannel.Result) {
//注意这个是8.0新API
val packageURI: Uri = Uri.parse("package:$packageName")
val intent = Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES, packageURI)
activity.startActivityForResult(intent, 10086)
this.setOnPermissionChangeListener(object : OnPermissionChangeListener {
override fun onChange(groupPosition: Boolean) {
result.success(groupPosition)
}
})
}
//通过MD5值检查文件是否完整
private fun FileIsComplete(savePath: String, result: MethodChannel.Result,
activity: Activity, sign: String) {
Log.i("进来检查方法了", "")
Thread {
try {
var msgDigest: MessageDigest? = null
msgDigest = MessageDigest.getInstance("MD5")
var bytes: ByteArray = ByteArray(1024)
var byteCount: Int = 0;
val fis = FileInputStream(File(savePath))
while (fis.read(bytes).also({ byteCount = it }) > 0) {
msgDigest.update(bytes, 0, byteCount)
}
val bi = BigInteger(1, msgDigest.digest())
val sha: String = bi.toString(16)
fis.close()
Log.i("文件的MD5值:", sha);
// if ("74:2B:83:46:8D:74:93:9C:96:42:38:39:04:65:E7:6B:D3:30:71:94".equals(sha)) {
if (sign.equals(sha)) {
Log.i("对比结果:", "true")
activity.runOnUiThread(Runnable {
result.success(true);
})
} else {
Log.i("对比结果:", "false")
activity.runOnUiThread(Runnable {
result.success(false);
})
}
} catch (e: Exception) {
Log.i("对比失败:", e.toString());
activity.runOnUiThread(Runnable {
result.success(false);
})
}
}.start()
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (resultCode == RESULT_OK) {
Log.d("开启结果:", "true")
if (requestCode == 10086) {
onPermissionChangeListener?.onChange(true)
} else if (requestCode == 10010) {
onPermissionChangeListener?.onChange(true)
}else if(requestCode==10099){
onPermissionChangeListener?.onChange(true)
}
} else {
Log.d("开启结果:", "false")
if (requestCode == 10086) {
onPermissionChangeListener?.onChange(false)
} else if (requestCode == 10010) {
onPermissionChangeListener?.onChange(false)
}else if(requestCode==10099){
onPermissionChangeListener?.onChange(false)
}
}
}
//设置各种点击事件
private var onPermissionChangeListener: OnPermissionChangeListener? = null
fun setOnPermissionChangeListener(onPermissionChangeListener: OnPermissionChangeListener?) {
this.onPermissionChangeListener = onPermissionChangeListener
}
interface OnPermissionChangeListener {
fun onChange(groupPosition: Boolean)
}