Flutter开发XAPK安装器:如何正确安装XAPK中的所有APK?
如何在Flutter中正确安装XAPK文件中的所有APK?
我用Flutter开发XAPK安装器,借助open_file包能唤起普通APK的安装对话框,但XAPK包含base APK、OBB文件和多个附加APK。解压后仅安装base APK时,应用无法正常运行,因为未完成所有附加APK的配套安装。请问如何正确安装XAPK文件中的所有APK?
现有Dart代码如下:
import 'dart:io'; import 'package:archive/archive.dart'; import 'package:package_archive_info/package_archive_info.dart'; import 'package:path_provider/path_provider.dart'; abstract class XapkInstaller { static install({ required String apkPath }) async { late List<FileSystemEntity> allFiles, apkFiles; late PackageArchiveInfo appInfo; late String appPackageName; Directory tempDir = await getTemporaryDirectory(); String tempPath = tempDir.path; String appName = apkPath.split("/").last.replaceAll(".xapk", ""); String zipFilePath = tempDir.path + "/${appName}_zip"; // 转换XAPK为Zip并移动到指定目录 _moveFile(File(apkPath), zipFilePath, appName); final bytes = File(zipFilePath + "/$appName" + ".zip").readAsBytesSync(); final archive = ZipDecoder().decodeBytes(bytes); // 解压Zip内容到临时目录 for (final file in archive) { final String filename = file.name; if (file.isFile) { final data = file.content as List<int>; File(tempDir.path + "/$appName" + "/$filename") ..createSync(recursive: true) ..writeAsBytesSync(data); } else { Directory(tempPath).create(recursive: true); } } final Directory myDir = Directory(tempDir.path + "/$appName"); allFiles = myDir.listSync(recursive: true, followLinks: true); apkFiles = allFiles.where((element) => element.path.endsWith('.apk')).toList(); for (int x = 0; x < apkFiles.length; x++) { final String filePath = apkFiles[x].path; try { appInfo = await PackageArchiveInfo.fromPath(filePath); appPackageName = appInfo.packageName; } catch (e) { appInfo = PackageArchiveInfo( appName: "", packageName: "", version: "", buildNumber: ""); } if (appInfo.appName.isNotEmpty && appPackageName == App.apkName(apkPath: filePath)) { try { // 将base APK复制到指定目录 File(filePath) .copySync(await App.getApksDirectory() + "/$appName.apk"); // 移动OBB文件到Android/obb目录 _moveObbToAndroidDir(allFiles, appPackageName); // 唤起安装对话框(仅安装base APK) await OpenFile.open(filePath); // 删除原XAPK文件 File(await App.getApksDirectory() + "/$appName" + ".xapk").delete(); } catch (e) { // 安装错误处理 } } } // 180秒后清理临时文件 Future.delayed(const Duration(seconds: 180), () { tempDir.deleteSync(recursive: true); tempDir.create(); }); } static _moveFile(File sourceFile, String newPath, String appname) async { if (!Directory(newPath).existsSync()) Directory(newPath).createSync(); final String zipFilePath = "$newPath/" + appname + ".zip"; try { return sourceFile.copySync(zipFilePath); } on FileSystemException { // 复制并删除原文件 await sourceFile.copy(zipFilePath); await sourceFile.delete(); } } static _moveObbToAndroidDir( List<FileSystemEntity> allFiles, String appPackageName) async { for (int x = 0; x < allFiles.length; x++) { final fileExtension = allFiles[x].path.split("/").last.split(".").last; if (fileExtension == "obb") { String filepath = allFiles[x].path; String obbFileName = filepath.split("/").last.split(".").first; String obbDirPath = await App.internalStoragePath() + "/Android" + "/obb" + "/$appPackageName"; // 创建OBB目录 if (!Directory(obbDirPath).existsSync()) { Directory(obbDirPath).createSync(); } // 目标OBB文件路径 final String renamePath = obbDirPath + "/" + obbFileName + ".obb"; try { // 同步复制 File(filepath).copySync(renamePath); } on FileSystemException { // 异步复制 await File(filepath).copy(renamePath); } } } } }
问题分析
当前代码的核心问题是:仅调用OpenFile.open()安装base APK,但XAPK中的**split APK(附加APK)**未被处理。Android系统对包含split APK的应用,要求一次性安装所有关联APK文件,单独安装base APK会导致应用缺失必要组件,无法正常运行。
解决方案
要正确安装XAPK中的所有APK,需使用Android原生PackageInstaller API批量安装多个APK文件。Flutter本身无直接支持,需通过MethodChannel调用Android原生代码实现。
步骤1:修改Dart代码,收集APK路径并调用原生方法
保留原有解压、OBB移动逻辑,新增MethodChannel调用原生安装方法:
import 'dart:io'; import 'package:flutter/services.dart'; import 'package:archive/archive.dart'; import 'package:package_archive_info/package_archive_info.dart'; import 'package:path_provider/path_provider.dart'; abstract class XapkInstaller { static const MethodChannel _channel = MethodChannel('xapk_installer'); static install({ required String apkPath }) async { late List<FileSystemEntity> allFiles, apkFiles; late String appPackageName; Directory tempDir = await getTemporaryDirectory(); String appName = apkPath.split("/").last.replaceAll(".xapk", ""); String zipFilePath = tempDir.path + "/${appName}_zip"; // 转换XAPK为Zip并解压 _moveFile(File(apkPath), zipFilePath, appName); final bytes = File(zipFilePath + "/$appName" + ".zip").readAsBytesSync(); final archive = ZipDecoder().decodeBytes(bytes); for (final file in archive) { final String filename = file.name; if (file.isFile) { final data = file.content as List<int>; File(tempDir.path + "/$appName" + "/$filename") ..createSync(recursive: true) ..writeAsBytesSync(data); } else { Directory(tempDir.path).createSync(recursive: true); } } // 收集所有APK路径 final Directory myDir = Directory(tempDir.path + "/$appName"); allFiles = myDir.listSync(recursive: true, followLinks: true); apkFiles = allFiles.where((element) => element.path.endsWith('.apk')).toList(); List<String> apkPaths = apkFiles.map((file) => file.path).toList(); try { // 获取应用包名 PackageArchiveInfo info = await PackageArchiveInfo.fromPath(apkPaths.first); appPackageName = info.packageName; // 移动OBB文件 _moveObbToAndroidDir(allFiles, appPackageName); // 调用原生批量安装方法 bool installSuccess = await _channel.invokeMethod('installMultipleApks', { 'apkPaths': apkPaths, 'packageName': appPackageName, }); if (installSuccess) { // 安装成功后删除原XAPK File(await App.getApksDirectory() + "/$appName.xapk").delete(); } } catch (e) { // 错误处理 } // 180秒后清理临时文件 Future.delayed(const Duration(seconds: 180), () { tempDir.deleteSync(recursive: true); tempDir.create(); }); } // 保留原有_moveFile、_moveObbToAndroidDir方法 static _moveFile(File sourceFile, String newPath, String appname) async { if (!Directory(newPath).existsSync()) Directory(newPath).createSync(); final String zipFilePath = "$newPath/$appname.zip"; try { return sourceFile.copySync(zipFilePath); } on FileSystemException { await sourceFile.copy(zipFilePath); await sourceFile.delete(); } } static _moveObbToAndroidDir( List<FileSystemEntity> allFiles, String appPackageName) async { for (var file in allFiles) { final fileExtension = file.path.split("/").last.split(".").last; if (fileExtension == "obb") { String filepath = file.path; String obbFileName = filepath.split("/").last; String obbDirPath = await App.internalStoragePath() + "/Android/obb/$appPackageName"; if (!Directory(obbDirPath).existsSync()) { Directory(obbDirPath).createSync(); } final String targetPath = obbDirPath + "/$obbFileName"; try { File(filepath).copySync(targetPath); } on FileSystemException { await File(filepath).copy(targetPath); } } } } }
步骤2:实现Android原生安装逻辑
在Android项目中创建XapkInstallerPlugin类,处理MethodChannel调用:
package com.your.package.name; import android.app.PendingIntent; import android.content.Intent; import android.content.pm.PackageInstaller; import android.os.Build; import android.util.Log; import androidx.core.content.FileProvider; import io.flutter.embedding.engine.plugins.FlutterPlugin; import io.flutter.plugin.common.MethodCall; import io.flutter.plugin.common.MethodChannel; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.List; public class XapkInstallerPlugin implements FlutterPlugin, MethodChannel.MethodCallHandler { private static final String CHANNEL_NAME = "xapk_installer"; private MethodChannel channel; @Override public void onAttachedToEngine(FlutterPluginBinding flutterPluginBinding) { channel = new MethodChannel(flutterPluginBinding.getBinaryMessenger(), CHANNEL_NAME); channel.setMethodCallHandler(this); } @Override public void onMethodCall(MethodCall call, MethodChannel.Result result) { if (call.method.equals("installMultipleApks")) { List<String> apkPaths = call.<List<String>>argument("apkPaths"); String packageName = call.<String>argument("packageName"); installMultipleApks(apkPaths, packageName, result); } else { result.notImplemented(); } } private void installMultipleApks(List<String> apkPaths, String packageName, MethodChannel.Result result) { PackageInstaller packageInstaller = channel.getFlutterView().getContext().getPackageManager().getPackageInstaller(); PackageInstaller.SessionParams params = new PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL); params.setAppPackageName(packageName); int sessionId = 0; try { sessionId = packageInstaller.createSession(params); PackageInstaller.Session session = packageInstaller.openSession(sessionId); // 将所有APK写入安装会话 for (String apkPath : apkPaths) { addApkToSession(session, apkPath); } // 创建安装回调Intent Intent intent = new Intent(channel.getFlutterView().getContext(), InstallResultReceiver.class); PendingIntent pendingIntent = PendingIntent.getBroadcast( channel.getFlutterView().getContext(), sessionId, intent, Build.VERSION.SDK_INT >= Build.VERSION_CODES.S ? PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE : PendingIntent.FLAG_UPDATE_CURRENT ); // 提交安装会话 session.commit(pendingIntent.getIntentSender()); session.close(); result.success(true); } catch (IOException e) { result.error("INSTALL_ERROR", e.getMessage(), null); if (sessionId != 0) { packageInstaller.abandonSession(sessionId); } } } private void addApkToSession(PackageInstaller.Session session, String apkPath) throws IOException { File apkFile = new File(apkPath); try (InputStream inputStream = new FileInputStream(apkFile)) { OutputStream outputStream = session.openWrite(apkFile.getName(), 0, apkFile.length()); byte[] buffer = new byte[1024 * 1024]; int length; while ((length = inputStream.read(buffer)) != -1) { outputStream.write(buffer, 0, length); } session.fsync(outputStream); outputStream.close(); } } @Override public void onDetachedFromEngine(FlutterPluginBinding binding) { channel.setMethodCallHandler(null); } }
步骤3:创建安装结果广播接收器
创建InstallResultReceiver类接收安装状态:
package com.your.package.name; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.pm.PackageInstaller; import android.widget.Toast; public class InstallResultReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { int status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_FAILURE); String message = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE); switch (status) { case PackageInstaller.STATUS_SUCCESS: Toast.makeText(context, "安装成功", Toast.LENGTH_LONG).show(); break; case PackageInstaller.STATUS_FAILURE: case PackageInstaller.STATUS_FAILURE_ABORTED: case PackageInstaller.STATUS_FAILURE_BLOCKED: case PackageInstaller.STATUS_FAILURE_CONFLICT: case PackageInstaller.STATUS_FAILURE_INCOMPATIBLE: case PackageInstaller.STATUS_FAILURE_INVALID: case PackageInstaller.STATUS_FAILURE_STORAGE: Toast.makeText(context, "安装失败: " + message, Toast.LENGTH_LONG).show(); break; } } }
步骤4:配置AndroidManifest.xml
添加必要权限和组件声明:
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="32" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" /> <application ...> <!-- 注册广播接收器 --> <receiver android:name=".InstallResultReceiver" /> <!-- 配置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/file_paths" /> </provider> </application>
在res/xml目录创建file_paths.xml:
<?xml version="1.0" encoding="utf-8"?> <paths> <external-cache-path name="external_cache" path="." /> <cache-path name="cache" path="." /> </paths>
关键注意事项
- 权限处理:需动态申请
REQUEST_INSTALL_PACKAGES权限(Android 8.0+),以及存储权限(Android 13以下)。 - FileProvider配置:必须正确配置,否则Android 7.0+无法访问APK文件。
- 会话提交:所有APK需写入同一个安装会话后一次性提交,才能保证base和split APK被正确关联安装。
内容的提问来源于stack exchange,提问作者Sonu Lodha
相关产品推荐
相关产品推荐

