在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,欢迎大家多多交流!

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐