webview_flutter插件如何支持文件上传?
在WEB应用开发中,我们不可避免的会遇到文件上传的场景,由于国泰基金的APP采用的是Flutter + H5的模式进行构建,也就是传统的Hybrid App的开发模式,内部的H5会存在诸如上传身份证图片这样的场景,而内部H5的包裹采用的是flutter_webview这个官方维护的插件,至于为什么会采用flutter_webview这个插件而不是flutter_webview_plugin也会渲染
在WEB应用开发中,我们不可避免的会遇到文件上传的场景,由于国泰基金的APP采用的是Flutter + H5的模式进行构建,也就是传统的Hybrid App的开发模式,内部的H5会存在诸如上传身份证图片这样的场景,而内部H5的包裹采用的是flutter_webview这个官方维护的插件,至于为什么会采用flutter_webview这个插件而不是flutter_webview_plugin也会渲染H5,在未来的文章我会逐步提到~
flutter_webview地址:https://pub.flutter-io.cn/packages/webview_flutter
flutter_webview_plugin地址:https://pub.flutter-io.cn/packages/flutter_webview_plugin
我们可以看到,截止到目前为止,flutter_webview这个插件已经升级到1.0.3这个版本,在1.0.0这个生产环境版本之前,大多数DEMO使用的是0.3这个开发预览版,也就在今年10月左右的时候推出了1.0.0这个相对来说比较稳定的生产版本,也算是众望所归:
不过遗憾的是,目前为止的版本,android端是没有办法支持H5文件上传的,也就是对下方这段H5代码的支持,不过毕竟这是个非常常见的场景,我相信在未来的官方升级版本中会将这个issue考虑进去,不过,目前的话,咱们如果使用还是得自己去修改这个插件的JAVA源码以适应这样的场景,害,只能说一句「淦」!
<input type="file">
那么,我们应该如何做呢?
1.1
我们不能再直接通过从pub端下载的方式去集成我们的webview插件,我们需要将这个插件的源码移植到我们自己的工程中去包裹,笔者使用的是1.0.1这个版本,不过截止到文章的更新时间,已经更新到1.0.3了,不过这并不影响我们集成上传文件的能力:
flutter_webview插件源码:https://github.com/flutter/plugins/tree/master/packages/webview_flutter
基于这样的方式,我一般都会在工程根目录创建一个plugins这样的一个文件夹去包裹类似这样的插件源码:
那么在pubspec.yaml中集成的方式也变成如下:
1.2
也就是需要在源码的Android工程中,找到具体的代码段将文件上传的回调补充进去,这其实对于Android的原生开发者来说是非常easy的,这也就是像我之前提到的,尽管Flutter能够解决大多数跨端开发的场景,但是你依旧需要比较熟悉Android或者IOS其中一个或者所有的原生开发细节,这在你遇到插件所不能解决的场景中,受益匪浅,所以,并不是只会写H5或者Dart就够了!
那么接下来,我就来说一下需要改哪些代码段!
具体Android段的代码内容在如下路径:
1.3
修改WebviewFactory.java,这是webview工厂的创建者
这里其实创建了一个flutterwebview句柄,也是为了在其他对象中能够获得当前创建的webview对象,当然我们看到这是一个私有变量,所以我们同时需要创建一个获取器,也就是getFlutterWebView方法!
1.4
修改WebViewFlutterPlugin.java,这是flutter插件的适配器,这里规定了一个flutter插件是如何和Android的Activity生命周期以及其上下文相关联的:
public class WebViewFlutterPlugin implements FlutterPlugin, PluginRegistry.ActivityResultListener, ActivityAware {
private static final String TAG = "WebViewFlutterPlugin";
private FlutterCookieManager flutterCookieManager;
public static Activity activity;
private WebViewFactory factory;
/**
* Add an instance of this to {@link io.flutter.embedding.engine.plugins.PluginRegistry} to
* register it.
*
* <p>THIS PLUGIN CODE PATH DEPENDS ON A NEWER VERSION OF FLUTTER THAN THE ONE DEFINED IN THE
* PUBSPEC.YAML. Text input will fail on some Android devices unless this is used with at least
* flutter/flutter@1d4d63ace1f801a022ea9ec737bf8c15395588b9. Use the V1 embedding with {@link
* #registerWith(Registrar)} to use this plugin with older Flutter versions.
*
* <p>Registration should eventually be handled automatically by v2 of the
* GeneratedPluginRegistrant. https://github.com/flutter/flutter/issues/42694
*/
public WebViewFlutterPlugin() {
Log.v(TAG,"WebViewFlutterPlugin");
}
/**
* Registers a plugin implementation that uses the stable {@code io.flutter.plugin.common}
* package.
*
* <p>Calling this automatically initializes the plugin. However plugins initialized this way
* won't react to changes in activity or context, unlike {@link CameraPlugin}.
*/
@SuppressWarnings("deprecation")
public static void registerWith(Registrar registrar) {
Log.v(TAG,"registerWith");
registrar
.platformViewRegistry()
.registerViewFactory(
"plugins.flutter.io/webview",
new WebViewFactory(registrar.messenger(), registrar.view()));
new FlutterCookieManager(registrar.messenger());
}
@Override
public void onAttachedToEngine(FlutterPluginBinding binding) {
Log.v(TAG,"onAttachedToEngine");
BinaryMessenger messenger = binding.getBinaryMessenger();
factory = new WebViewFactory(messenger, null);
binding
.getFlutterEngine()
.getPlatformViewsController()
.getRegistry()
.registerViewFactory(
"plugins.flutter.io/webview", factory);
flutterCookieManager = new FlutterCookieManager(messenger);
Context appContext = binding.getApplicationContext();
if (appContext instanceof FlutterApplication) {
Activity currentActivity = ((FlutterApplication) appContext).getCurrentActivity();
if (currentActivity != null) {
activity = currentActivity;
}
}
}
@Override
public void onDetachedFromEngine(FlutterPluginBinding binding) {
Log.v(TAG,"onDetachedFromEngine");
if (flutterCookieManager == null) {
return;
}
activity = null;
flutterCookieManager.dispose();
flutterCookieManager = null;
}
@Override
public boolean onActivityResult(int requestCode, int resultCode, Intent data) {
Log.v(TAG,"onActivityResult");
if (factory != null && factory.getFlutterWebView() != null){
return factory.getFlutterWebView().activityResult(requestCode, resultCode, data);
}
return false;
}
@Override
public void onAttachedToActivity(ActivityPluginBinding binding) {
Log.v(TAG,"onAttachedToActivity");
activity = binding.getActivity();
binding.addActivityResultListener(this);
}
@Override
public void onDetachedFromActivityForConfigChanges() {
Log.v(TAG,"onDetachedFromActivityForConfigChanges");
}
@Override
public void onReattachedToActivityForConfigChanges(ActivityPluginBinding binding) {
Log.v(TAG,"onReattachedToActivityForConfigChanges");
}
@Override
public void onDetachedFromActivity() {
Log.v(TAG,"onDetachedFromActivity");
}
}
1.5
修改FlutterWebView.java,在这里我们通过WebChromeClient监听h5选择文件的操作并拦截,然后打开文件管理选择要上传的文件,最后将文件返回给H5,这里其实是一个老生常谈的问题,也就是原生的Android Webview是如何拦截文件上传操作的:
private class FlutterWebChromeClient extends WebChromeClient {
@Override
public boolean onCreateWindow(
final WebView view, boolean isDialog, boolean isUserGesture, Message resultMsg) {
final WebViewClient webViewClient =
new WebViewClient() {
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
@Override
public boolean shouldOverrideUrlLoading(
@NonNull WebView view, @NonNull WebResourceRequest request) {
final String url = request.getUrl().toString();
if (!flutterWebViewClient.shouldOverrideUrlLoading(
FlutterWebView.this.webView, request)) {
webView.loadUrl(url);
}
return true;
}
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
if (!flutterWebViewClient.shouldOverrideUrlLoading(
FlutterWebView.this.webView, url)) {
webView.loadUrl(url);
}
return true;
}
};
final WebView newWebView = new WebView(view.getContext());
newWebView.setWebViewClient(webViewClient);
final WebView.WebViewTransport transport = (WebView.WebViewTransport) resultMsg.obj;
transport.setWebView(newWebView);
resultMsg.sendToTarget();
return true;
}
// For Android < 3.0
public void openFileChooser(ValueCallback<Uri> valueCallback) {
Log.v(TAG, "openFileChooser Android < 3.0");
uploadMessage = valueCallback;
openImageChooserActivity();
}
// For Android >= 3.0
public void openFileChooser(ValueCallback valueCallback, String acceptType) {
Log.v(TAG, "openFileChooser Android >= 3.0");
uploadMessage = valueCallback;
openImageChooserActivity();
}
//For Android >= 4.1
public void openFileChooser(ValueCallback<Uri> valueCallback, String acceptType, String capture) {
Log.v(TAG, "openFileChooser Android >= 4.1");
uploadMessage = valueCallback;
openImageChooserActivity();
}
// For Android >= 5.0
@Override
public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback, FileChooserParams fileChooserParams) {
Log.v(TAG, "openFileChooser Android >= 5.0");
uploadMessageAboveL = filePathCallback;
openImageChooserActivity();
return true;
}
}
我们可以看下其中的openImageChooserActivity的具体实现:
private void openImageChooserActivity() {
Log.v(TAG, "openImageChooserActivity");
Intent intent1 = new Intent(Intent.ACTION_PICK, null);
intent1.setDataAndType(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI, "image/*");
Intent chooser = new Intent(Intent.ACTION_CHOOSER);
chooser.putExtra(Intent.EXTRA_TITLE, "选择图片");
chooser.putExtra(Intent.EXTRA_INTENT,intent1);
if (WebViewFlutterPlugin.activity != null){
WebViewFlutterPlugin.activity.startActivityForResult(chooser, FILE_CHOOSER_RESULT_CODE);
} else {
Log.v(TAG, "activity is null");
}
}
那么当获取到图片后进行的回调是如何处理的呢,也就是startActivityForResult对应的处理方式,当然在不同的Android版本中这是不一致的:
public boolean activityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == FILE_CHOOSER_RESULT_CODE) {
if (null == uploadMessage && null == uploadMessageAboveL) {
return false;
}
Uri result = data == null || resultCode != RESULT_OK ? null : data.getData();
if (uploadMessageAboveL != null) {
onActivityResultAboveL(requestCode, resultCode, data);
} else if (uploadMessage != null && result != null) {
uploadMessage.onReceiveValue(result);
uploadMessage = null;
}
}
return false;
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
private void onActivityResultAboveL(int requestCode, int resultCode, Intent intent) {
if (requestCode != FILE_CHOOSER_RESULT_CODE || uploadMessageAboveL == null)
{
return;
}
Uri[] results = null;
if (resultCode == Activity.RESULT_OK) {
if (intent != null) {
String dataString = intent.getDataString();
ClipData clipData = intent.getClipData();
if (clipData != null) {
results = new Uri[clipData.getItemCount()];
for (int i = 0; i < clipData.getItemCount(); i++) {
ClipData.Item item = clipData.getItemAt(i);
results[i] = item.getUri();
}
}
if (dataString != null)
{
results = new Uri[]{Uri.parse(dataString)};
}
}
}
uploadMessageAboveL.onReceiveValue(results);
uploadMessageAboveL = null;
}
最后大功告成,一个属于你自己可上传文件可维护的flutter_webview插件就可以正常使用了:
最后放上源码的地址,大家可以直接下载使用:https://github.com/xueenze/flutter_webview_with_file_upload.git
希望大家对上述内容有好的建议能够与我及时沟通,小编的邮箱还是那个:kameleon@126.com,欢迎大家多多交流!
更多推荐
所有评论(0)