一、思路
- 1.使用dio下载apk包存放到应用的根目录下
- 2.使用channel通讯的方式调用安卓原生方法,使用FileProvider安装应用
PS:注意需要添加相应的权限 和 配置FileProvider
二、实施
- 1.dart端创建
DownloadAndUpdatePage
类 实现下载apk
功能,并调用通道方法安装apk
import 'package:app_update_plugin/app_update_plugin.dart';
import 'package:flutter/material.dart';
import 'package:dio/dio.dart';
import 'package:path_provider/path_provider.dart';
import 'dart:io';
class DownloadAndUpdatePage extends StatefulWidget {
final String updateUrl;// 替换为实际的APK下载链接
DownloadAndUpdatePage({required this.updateUrl});
@override
_DownloadAndUpdatePageState createState() => _DownloadAndUpdatePageState();
}
class _DownloadAndUpdatePageState extends State<DownloadAndUpdatePage> {
late String apkPath;
void _downloadApk() async {
String downloadUrl = widget.updateUrl;
Dio dio = Dio();
try {
// 获取APP的文件目录路径
Directory appDocDir = await getApplicationDocumentsDirectory();
apkPath = '${appDocDir.path}/update.apk';
// 下载文件
await dio.download(downloadUrl, apkPath, onReceiveProgress: (received, total) {
if (total != -1) {
// 下载进度
print((received / total * 100).toStringAsFixed(0) + "%");
}
});
// 下载完成,调用安装APK函数
_installApk(apkPath);
} catch (e) {
print("下载失败:$e");
}
}
Future<void> _installApk(String apkPath) async {
final plugin = AppUpdatePlugin();
// AppInstaller.installApk(apkPath);
print('******************* apkPath ************************\n $apkPath');
plugin.installApk(apkPath);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('下载并安装APK'),
),
body: Center(
child: ElevatedButton(
onPressed: _downloadApk,
child: Text('下载并安装APK'),
),
),
);
}
}
调用方式
Navigator.push(context, MaterialPageRoute(builder: (context){
return DownloadAndUpdatePage(updateUrl: 'http://xxxxxx.apk',);//替换为实际生产上的apk地址
}));
通讯代码
@override
void installApk(String appPath){
methodChannel.invokeMethod('installApk',{'apkPath' : appPath});
}
- 2.安卓端代码
创建ApkInstaller
类用于安装apk
package com.example.app_update_plugin;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.os.StrictMode;
import androidx.core.content.FileProvider;
import java.io.File;
public class ApkInstaller {
/**
* 安装APK文件
*
* @param context 上下文
* @param apkPath APK文件的绝对路径
*/
public static void installApk(Context context, String apkPath) {
// 创建File对象
File apkFile = new File(apkPath);
// 设置文件Uri访问权限
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
StrictMode.VmPolicy.Builder builder = new StrictMode.VmPolicy.Builder();
StrictMode.setVmPolicy(builder.build());
}
// 安装APK
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
Uri apkUri = FileProvider.getUriForFile(context, context.getPackageName() + ".fileprovider", apkFile);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.setDataAndType(apkUri, "application/vnd.android.package-archive");
} else {
intent.setDataAndType(Uri.fromFile(apkFile), "application/vnd.android.package-archive");
}
context.startActivity(intent);
}
}
PS:其中Uri apkUri = FileProvider.getUriForFile(context, context.getPackageName() + ".fileprovider", apkFile);
中的context.getPackageName() + ".fileprovider"
要和配置文件中的authorities
对应上(后面会提到)
通讯方法实现
package com.example.app_update_plugin;
import androidx.annotation.NonNull;
import io.flutter.embedding.engine.plugins.FlutterPlugin;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugin.common.MethodChannel.MethodCallHandler;
import io.flutter.plugin.common.MethodChannel.Result;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.StrictMode;
import androidx.core.content.FileProvider;
//import androidx.appcompat.app.AppCompatActivity;
import java.io.File;
import android.text.TextUtils;
import android.content.Context;
import android.util.Log;
import com.example.app_update_plugin.ApkInstaller;
// 其他自定义类的导入语句...
/** AppUpdatePlugin */
public class AppUpdatePlugin implements FlutterPlugin, MethodCallHandler {
/// The MethodChannel that will the communication between Flutter and native Android
///
/// This local reference serves to register the plugin with the Flutter Engine and unregister it
/// when the Flutter Engine is detached from the Activity
private MethodChannel channel;
private Context context; // 用于存储Context
@Override
public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) {
channel = new MethodChannel(flutterPluginBinding.getBinaryMessenger(), "app_update_plugin");
channel.setMethodCallHandler(this);
// 获取ApplicationContext并存储在变量中
context = flutterPluginBinding.getApplicationContext();
}
@Override
public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) {
Log.e("11111","2222222");
if (call.method.equals("getPlatformVersion")) {
result.success("Android " + android.os.Build.VERSION.RELEASE);
} else if (call.method.equals("installApk")) {
String filePath = call.argument("apkPath");
if (!TextUtils.isEmpty(filePath)) {
String apkPath = filePath; // APK文件路径
ApkInstaller.installApk(context, apkPath);
} else {
result.error("installApk", "apkPath is null", null);
}
} else {
result.notImplemented();
}
}
@Override
public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {
channel.setMethodCallHandler(null);
}
}
添加权限
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
添加FileProvider配置
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths" />
</provider>
PS:其中authorities的内容要和安卓端代码ApkInstaller类中的Uri apkUri = FileProvider.getUriForFile(context, context.getPackageName() + ".fileprovider", apkFile);
这行中的context.getPackageName() + ".fileprovider"
严格对应上否则会报错配置找不到。
在xml/路径下添加provider_paths.xml文件配置文件访问路径 如果没有这个路径手动创建一个内容如下
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-files-path
name="img"
path="app_flutter" />
<root-path
name="root"
path="app_flutter" />
<external-path
name="external"
path="app_flutter" />
<!--代表外部存储区域的根目录下的文件 Environment.getExternalStorageDirectory()/DCIM/camerademo目录-->
<external-path
name="mq_DCIM"
path="app_flutter" />
<!--代表外部存储区域的根目录下的文件 Environment.getExternalStorageDirectory()/Pictures/camerademo目录-->
<external-path
name="mq_Pictures"
path="app_flutter" />
<!--代表app 私有的存储区域 Context.getFilesDir()目录下的images目录 /data/user/0/com.hm.camerademo/files/images-->
<files-path
name="mq_private_files"
path="app_flutter" />
<!--代表app 私有的存储区域 Context.getCacheDir()目录下的images目录 /data/user/0/com.hm.camerademo/cache/images-->
<cache-path
name="mq_private_cache"
path="app_flutter" />
<!--代表app 外部存储区域根目录下的文件 Context.getExternalFilesDir(Environment.DIRECTORY_PICTURES)目录下的Pictures目录-->
<external-files-path
name="mq_external_files"
path="app_flutter" />
<!--代表app 外部存储区域根目录下的文件 Context.getExternalCacheDir目录下的images目录-->
<external-cache-path
name="mq_external_cache"
path="app_flutter" />
<root-path
name="mq_external_cache"
path="" />
</paths>
</PreferenceScreen>