【flutter】webview下载文件方法集锦

说明:android的webview是不支持下载的!!!

所以我们需要监听下载接口 然后手动执行下载操作,分为三种类型

  1. 直接打开浏览器下载(最简单),但是一些下载接口需要cookie信息时不能满足
java 复制代码
     /**
         * 下载文件选择指定应用下载
         */
        webView.setDownloadListener(new DownloadListener() {
            @Override
            public void onDownloadStart(String url, String userAgent, String contentDisposition, String mimetype, long contentLength) {
                Uri uri = Uri.parse(url);
                Intent intent = new Intent(Intent.ACTION_VIEW, uri);
                activity.startActivity(intent);
            }
        });
  1. 在后台内置下载(此时还是在监听里面执行下载操作,还不算复杂)
java 复制代码
//在webview属性设置处添加
        webView.setDownloadListener(new DownloadListener() {
            @RequiresApi(api = Build.VERSION_CODES.KITKAT)
            @Override
            public void onDownloadStart(String url, String userAgent, String contentDisposition, String mimetype, long contentLength) {
              if(URLUtil.isNetworkUrl(url)){
                    // 解析 contentDisposition 以获取文件名
                    String fileName = contentDisposition.split("filename=")[1].trim().replace("\"", "");
                    try {
                        fileName = URLDecoder.decode(fileName, "UTF-8");
                    } catch (UnsupportedEncodingException e) {
                        e.printStackTrace();
                    }
                    downloadUrl(url, fileName);
                } 
            }

        });
        //下载方法
      private void downloadUrl(String url, final String fileName) {
      //获取cookie
        CookieManager cookieManager = CookieManager.getInstance();
        String cookie = cookieManager.getCookie(url);
        DownloadManager.Request request = new DownloadManager.Request(Uri.parse(url));
        request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
        request.setTitle(fileName); // 使用文件名作为标题
        request.setDescription("Downloading " + fileName + "...");
        request.setVisibleInDownloadsUi(true); // 在下载管理器中显示下载
        if (cookie != null) {
            // 处理获取到的 Cookie
//            Log.d("Cookie", "Cookie for URL " + url + ": " + cookie);
            request.addRequestHeader("Cookie", cookie);
        } else {
            Log.d("Cookie", "No cookie found for URL " + url);
        }
        request.setMimeType("application/octet-stream");
        // 设置下载路径和文件名
        request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, fileName);
        // 获取下载管理器并开始下载
        DownloadManager downloadManager = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
        downloadManager.enqueue(request);
        final Handler handler = new Handler(Looper.getMainLooper());
        handler.post(new Runnable() {
            @Override
            public void run() {
                Toast.makeText(context, "下载完成,请到通知栏点击安装或打开文件", Toast.LENGTH_SHORT).show();
            }
        });
    }
        
  1. 监听下载已经无法满足下载:如果浏览器是以下载接口请求后生成 blob:https:下载地址,去获取浏览器存储的文件,在监听里便无法直接下载原文件,因为无法获取到响应头信息,因此无法获取文件名来保存文件。当然如果知道下载的文件类型 可以 通过自定义文件名方式来下载blob流文件。

    1. 下载blob流文件:有固定的文件类型 或者通过魔法字符获取文件类型,仍在下载监听内下载
      实现思路:
      (1)判断为blob下载
java 复制代码
      webView.setDownloadListener(new DownloadListener() {
            @RequiresApi(api = Build.VERSION_CODES.KITKAT)
            @Override
            public void onDownloadStart(String url, String userAgent, String contentDisposition, String mimetype, long contentLength) {
                if (url.startsWith("blob:")) {
                    // 获取文件名
                    String fileName = URLUtil.guessFileName(url, contentDisposition, mimetype);

                    // 获取 blob 数据并写入文件 已经在拦截地方下载
                    downloadBlobFile(url, fileName);
                }
            }

        });

(2)定义下载事件,通过调用js请求 blob接口,获取返回数据 回传给android (需要先设置监听才能收到返回数据)

java 复制代码
	//下载方法定义
 	@RequiresApi(api = Build.VERSION_CODES.KITKAT)
    private void downloadBlobFile(String blobUrl, final String fileName) {
        // 由于 WebView 不能直接处理 blob URL,我们需要使用 JavaScript 接口或其他方法来获取 blob 数据
        // 以下代码是一个示例,展示了如何使用 WebView 获取 blob 数据并写入文件
        webView.evaluateJavascript("console.log('开始请求blob...');" +
                "var xhr = new XMLHttpRequest();" +
                "xhr.open('GET', '"+blobUrl+"', true);" +
                "xhr.responseType = 'blob';" +
                "xhr.onload = function() {" +
                "  if (this.status === 200) {" +
                "    const blob = this.response;" +
                "    var reader = new FileReader();" +
                "    reader.readAsDataURL(blob);" +
                "    reader.onloadend = function() {" +
                "      var base64data = reader.result.split(',')[1];" +
                "      console.log('base64data:'+base64data);" +
                "      Android.androidCallback( base64data);" +
                "    };" +
                "  } else {" +
                "    console.error('Failed to fetch blob data');" +
                "  }" +
                "};" +
                "xhr.onerror = function() {" +
                "  console.error('Error fetching blob data');" +
                "};" +
                "xhr.send();", null);
    }

(3)保存文件

java 复制代码
	//在webview设置处设置监听
	webView.addJavascriptInterface(new WebAppInterface(context), "Android");
	
	//定义APP监听接口 获取传回的数据
	public class WebAppInterface {
        Context mContext;

        WebAppInterface(Context c) {
            mContext = c;
        }

        @RequiresApi(api = Build.VERSION_CODES.O)
        @JavascriptInterface
        public void androidCallback(String message) {
            // 在这里处理从 JavaScript 传递过来的数据
            Toast.makeText(mContext, "Received message: " + message, Toast.LENGTH_SHORT).show();
            String base64Data = message;
            //保存成文件
            saveBase64ToFile(base64Data, "downloaded_file");
        }
    }
    
	//保存文件方法定义
	@RequiresApi(api = Build.VERSION_CODES.O)
    private void saveBase64ToFile(String base64Data, String fileName) {
        try {
            byte[] decodedBytes = Base64.decode(base64Data, Base64.DEFAULT);
            File file = new File(context.getFilesDir(), fileName);
            FileOutputStream outputStream = new FileOutputStream(file);
            outputStream.write(decodedBytes);
            outputStream.close();
            //在通知栏显示下载内容方便用户打开文件
            showDownloadNotification(file);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

(4)显示下载通知

java 复制代码
    //通知方法定义
	  @RequiresApi(api = Build.VERSION_CODES.O)
    private void showDownloadNotification(File file) throws IOException {
        NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            NotificationChannel channel = new NotificationChannel("file_open", "File Open", NotificationManager.IMPORTANCE_DEFAULT);
            notificationManager.createNotificationChannel(channel);
        }

// 创建一个 Intent 来打开文件
        Intent intent = new Intent(Intent.ACTION_VIEW);
        String mimeType = Files.probeContentType(Paths.get(file.getPath()));
// 创建一个文件提供者 URI
        Uri fileUri = FileProvider.getUriForFile(context, context.getPackageName() + ".flutterWebviewPluginProvider", file);
        intent.setDataAndType(fileUri, mimeType); // 设置文件的 MIME 类型
        intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); // 授予临时权限

// 创建 PendingIntent
        PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);

        Uri defaultSoundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION);
        long[] pattern = {0, 100, 200, 300};
        Notification notification = new NotificationCompat.Builder(context, "file_open")
                .setSmallIcon(R.drawable.ic_download)
                .setContentTitle(file.getName())
                .setContentText("下载完成,点击打开文件")
                .setPriority(NotificationCompat.PRIORITY_DEFAULT)
                .setContentIntent(pendingIntent) // 设置点击通知后执行的操作
                .setAutoCancel(true) // 点击后自动取消通知
                .build();

        notificationManager.notify(1, notification);
    }
	

2、 监听所有接口请求,过滤出下载接口,自定义下载

(1)设置setWebViewClientwebView.setWebViewClient(webViewClient);

(2)定义webViewClient

(3)过滤出下载接口后,先发起请求获取文件名信息,再进行下载操作

java 复制代码
  	 webViewClient = new BrowserClient() {
 			/*监听所有接口请求,过滤所有下载请求*/
            @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
            @Override
            public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
                String url = request.getUrl().toString();
                if (isDownloadUrl(url)) {
                 	Map<String, String> requestHeaders= request.getRequestHeaders();
                    String acceptHeaderValue = requestHeaders.get("Accept");
                    if(acceptHeaderValue != null && acceptHeaderValue.contains("image/")){//忽略页面上img地址为下载接口
                        return super.shouldInterceptRequest(view, request); // 拦截请求
                    }
                    // 处理下载请求
                    startDownloadWithDisposition(url);
                    return null; // 拦截请求
                }
                return super.shouldInterceptRequest(view, request);
            }
             private boolean isDownloadUrl(String url) {
                // 检查 URL 是否是下载 URL,例如检查文件扩展名或 MIME 类型
                return url.contains("downloadFile"); // 示例:检查接口地址
            }
            /*先请求获取响应头获取文件名后再进行下载操作*/
            private void startDownloadWithDisposition(String urlString) {
                try {
                    URL url = new URL(urlString);
                    HttpURLConnection connection = (HttpURLConnection) url.openConnection();
                    connection.setRequestMethod("GET"); // 只请求头部信息 HEAD请求的响应头数据不全 所以用GET

                    // 获取 Content-Disposition 头
                    String contentDisposition = connection.getHeaderField("Content-Disposition");
                    // 解析文件名
                    String fileName = parseFileNameFromContentDisposition(contentDisposition);
                    fileName = URLDecoder.decode(fileName, "UTF-8");
                    // 断开连接
                    connection.disconnect();
                    downloadUrl(urlString,fileName);


                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
              /*获取文件名*/
            private String parseFileNameFromContentDisposition(String contentDisposition) {
                if (contentDisposition == null) {
                    return null;
                }
                String[] parts = contentDisposition.split(";");
                for (String part : parts) {
                    if (part.trim().startsWith("filename=")) {
                        return part.substring("filename=".length()).trim().replace("\"", "");
                    }
                }
                return null;
            }
	};
注意:获取到的文件名需转码为utf8
java 复制代码
fileName = URLDecoder.decode(fileName, "UTF-8");

补充:

(1)okhttp引入报红时,需要在你的webview插件导入 okhttp包

java 复制代码
//webview_plugin\android\build.gradle中
	dependencies {
    	implementation 'com.squareup.okhttp3:okhttp:3.11.0'
    }

(2)消息通知引入 自定义图标.setSmallIcon(R.drawable.ic_download)

在webview插件的资源部中引入webview_plugin\android\src\main\res\

(3)通过请求头accept 来判断是否img等资源文件下载还是接口请求 资源文件下载要忽略掉不进行下载操作

java 复制代码
 				Map<String, String> requestHeaders= request.getRequestHeaders();
                    String acceptHeaderValue = requestHeaders.get("Accept");
                    if(acceptHeaderValue != null && acceptHeaderValue.contains("image/")){//忽略页面上img地址为下载接口
                        return super.shouldInterceptRequest(view, request); // 拦截请求
                    }

通常情况下:

  1. 加载API接口:API接口一般返回JSON、XML或其他结构化数据格式,所以Accept头部可能会包含类似application/json、application/xml等值。
  2. 加载图片:对于图片资源,Accept头部则会指向图像相关的MIME类型,如image/jpeg、image/png、image/gif等。
相关推荐
nicepainkiller3 小时前
Flutter 内嵌 unity3d for android
flutter·unity3d
恋猫de小郭3 小时前
Flutter Web 正式移除 HTML renderer,只支持 CanvasKit 和 SkWasm
前端·flutter·html
江上清风山间明月3 小时前
flutter编译e: Daemon compilation failed: null java.lang.Exception错误解决
java·flutter·exception·daemon·compilation
大G哥3 小时前
Flutter如何调用java接口如何导入java包
java·开发语言·flutter
m0_748240916 小时前
【Flutter】webview_flutter使用详解
flutter
Domain-zhuo7 小时前
React和Vue.js的相似性和差异性是什么?
前端·vue.js·flutter·react.js·前端框架
ChinaDragonDreamer18 小时前
Flutter:开发环境搭建和Android Studio创建Flutter Project
android·flutter·android studio
chengxuyuan1213_1 天前
组件如何与父组件通信
flutter
CherishTaoTao1 天前
flutter中provider的进阶用法小结(一)
前端·javascript·flutter