Android WebView and Native communication summary

Android WebView and Native communication summary

Many current mobile app developments require embedded WebView to facilitate rapid business development, especially in e-commerce apps, where business changes quickly and there are many activities. Relying only on the native development method is difficult to meet the rapid business development, so the hybrid development model appears. Currently, the more well-known ones are

Cordova
,
Ionic
, There are domestic
Appcan
,
APICloud
Development platform, these types are all dependent on the realization of WebView. And Facebook
React Native
And Ali's
Weex
Is another realization of hybrid development,
React Native
with
Weex
Allow native developers to write front-end code like H5 development, and then render into native components through their own SDK, independent of
WebView
. This article mainly summarizes the current
WebView
The way to interact with native.

Android

WebView
with
JavaScript
The interaction is actually the Android native and the web page
Javascript
The interaction between them, so you can understand how the data is transferred between them. The following introduces from two aspects:

Native sends data to Javascript

There are two ways for Native to send data to JavaScript, one is

evaluateJavascript
The other is
loadUrl
. The difference is that
evaluateJavascript
ratio
loadUrl
More efficient,
evaluateJavascript
Can be used after android 4.4, the execution of this method will not refresh the page, but
loadUrl
Then. So usually we use as follows:

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { evaluateJavascript(jsCommand, null ); } else { loadUrl(jsCommand); } Copy code

Of course, if we want to get the execution result of javascript code directly, we can write like this:

String command = "ABC" ; webView.evaluateJavascript( "(function() {return " + command + "; })();" , new ValueCallback< String >() { @Override public void onReceiveValue ( String result ) { //The result here is ABC } }); Copy code

Javascript sends data to Native

There are 4 ways to send data from Javascript to Native. The first way is to use

webChromClient
middle
onJsAlert()
,
onJsPromot()
Method to get Javascript related data. The second way is to use coverage
shouldOverrideUrlLoading
Method to intercept the url protocol. The third is the most convenient, which is
@JavascriptInterface
Solution, most apps now use this method, which will be described in detail later. The last one is to use
webView
Embedded in
iframe
Way through the update
iframe
Url. Well-known hybrid framework
JsBridge
This method was used before, but it has now been changed to use
@JavascriptInterface
This way. The following briefly introduces the use of various methods.

onJsPrompt

webChromeClient
Provided in
onJsAlert
,
onJsPrompt
Method to facilitate developers to rewrite in Javascript
alert
,
prompt
The corresponding behavior of the method. We can choose one of these two methods as a bridge between native and js. Usually we resort to
onJsPrompt
Method to achieve, because in js, this method is usually less used. And for
onJsAlert()
, When calling in js
alrt()
It will be triggered when we rewrite this method to achieve a custom prompt View

However, this method has restrictions on the amount of incoming data, which is related to the WebView version of the mobile phone. Take my test machine as an example.

oppo reno
On the mobile phone android 10, the data can only be transferred up to 10k. And use
@JavascriptInterface
Solution, the data can be transferred up to 20-30M

Let's look at the writing of the front-end web page, call it directly

prompt
function

var Data = prompt ( "Native:?//ID = getUserInfo. 1" ); Console .log ( 'Data:' + Data); duplicated code

Setting up for WebView

WebChromeClient
Rewrite
onJsPrompt
Methods as below:

@Override public boolean onJsPrompt (WebView view, String url, String message, String defaultValue, JsPromptResult result) { Uri uri = Uri.parse(message); //If it is to transfer nativeAPI. if (url.startsWith( "native://" )) { result.confirm( "call natvie api success" ); return true ; } return super .onJsPrompt(view, url, message, defaultValue, result); } Copy code

shouldOverrideUrlLoading

The Js code of the front-end page:

document .location= "native://getUserInfo?id=1" ; Copy code

The native level is setting for WebView

WebViewClient
Object, we need to rewrite
shouldOverrideUrlLoading
method. have to be aware of is,
WebViewClient
There are two of them
shouldOverrideUrlLoading
Method definition:

  • public boolean shouldOverrideUrlLoading(WebView view, String url)
  • public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request)

The above one has been marked in the sdk

Deprecated
, The following one was introduced in android 7.0, so in order to avoid compatibility issues. When using, it is recommended to rewrite both methods.

@Override public boolean shouldOverrideUrlLoading (WebView view, String url) { //If it is to call nativeAPI. if (url.startsWith( "native://" )) { Log.i( "CommonWebViewClient" , "shouldOverrideUrlLoading execute------>" ) return true ; } return super .shouldOverrideUrlLoading(view, url); } Copy code

@JavascriptInterface

There are security vulnerabilities under Android 4.2, but most of the minimum supported versions of our app have been upgraded to 5.0. This can be ignored. Of course, you can search for it if you are interested.

At the native level, we need to inject an object into the WebView to handle the data interaction on both sides. The injection method is as follows:

  • First define a class to handle the interaction between the two sides:
public class HybridAPI { public static final String TAG = "HybridAPI" ; @JavascriptInterface public void sendToNative ( final String message) { Log.i(TAG, "get data from js------------>" + message); } } Copy code
  • in
    WebView
    Inject an instance of this class in
HybridAPI hybridAPI = new HybridAPI(); webview.addJavascriptInterface(hybridAPI, "HybridAPI" ) Copy code

Use the following code directly in the web page to send the data to the native side

HybridAPI.sendToNative( 'Hello' ); Copy code

iframe

We can also use

iframe
Request forgery to send data to the native side. The idea is to add a
iframe
Control, by modifying its
src
Property, trigger the native side
shouldOverrideUrlLoading
The execution of the method, similarly, the native side rewrites the method to get the data passed by the js side. The specific operation method is as follows:

var iframe = document .createElement( 'iframe' ); iframe.style.display = 'none' ; document .documentElement.appendChild(iframe); iframe.src= "native://getUserInfo?id=1" ; Copy code

After the operation is complete, we then remove this component from the current dom structure.

setTimeout ( function () { iframe && iframe.parentNode && iframe.parentNode.removeChild(iframe); }, 100 ); Copy code

Concrete practice

In the previous section, several scenarios for the interaction between WebView and Native are summarized. But there is still a long way to go before the actual project use. There are still many issues to consider in actual project development. Such as:

  • How to define the rules of interaction
  • How the data is transmitted
  • After the call, how to get the result of the callback
  • For Javascript requests, how should the native side be designed?
  • ....

The native side sends a message to JavaScript only

loadUrl
,
evaluateJavascript
These two ways. Javascript can be used to send information to the native
onJsPrompt
,
@JavascriptInterface
,
shouldOverrideUrlLoading
Waiting for several options, we will adopt
@JavascriptInterface
Take this approach (that is, the annotation scheme that everyone usually refers to) as an example to see how to solve the problems encountered in actual project development.

Rules of interaction

1. let's define the interaction rules at both ends.

Javascript sends data to native:

We agreed to adopt in H5

HybridAPI.sendToNative
The method sends data to the native side, so we need to do the following support on the native side:

  • Define a
    HybridAPI
    Class and register to WebView
HybridAPI hybridAPI = new HybridAPI( this ); webview.addJavascriptInterface(hybridAPI, "HybridAPI" ); Copy code
  • in
    HybridAPI
    Define a method in the class
    sendToNative
    , This method is exposed to Javascript to send data to native
@JavascriptInterface public void sendToNative ( final String message) { Log.i(TAG, "get data from js------------>" + message); } Copy code

The native layer sends data to Javascript:

public final String TO_JAVASCRIPT_PREFIX = "javascript:HybridAPI.onReceiveData('%s')" ; public void sendToJavaScript (Map<String, Object> message) { String str = new Gson().toJson(message); final String jsCommand = String.format(TO_JAVASCRIPT_PREFIX, escapeString(str)); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { evaluateJavascript(jsCommand, null ); } else { loadUrl(jsCommand); } } Copy code

In H5, we write like this, when native sends data to Javascript, it will trigger the Javascript in

Hybrid.onReceiveData
Method, this method can receive the data from the native layer

HybridAPI.onReceiveData = function ( message ) { console .log( '[response from native]' + message); } Copy code

Definition of data structure

In the above we have based

@JavascriptInterface
The solution completes the realization of the communication mechanism between native and WebView. Both parties can exchange data, but more issues need to be considered during development. For example, if Javascript sends data to native, you need to convert the data into a string, and then send the string to native, native to parse the string, find the corresponding processing method, extract the relevant business parameters, and then Proceed accordingly. So we need to define the data structure of this string.

We have agreed on the above, the H5 end can be used

HybridAPI.sendToNative
Send data to native, this method has only one string parameter, with
Get user information
Take this business function as an example, our string parameter is
native://getUserInfo?id=1
, In this string
getUserInfo
Indicates the purpose or behavior of the current communication (in order to obtain user information),
?
Back
id=1
It means the parameter (user id is 1), if there are more parameters, the string will be longer, and if the transcoding of Chinese is involved, its readability will be greatly reduced, so this kind of interaction is not intuitive and friendly , We expect users to use the following method to communicate with native:

HybridAPI.invoke(methodName, params, callbackFun)

  • methodName
    : Current communication behavior
  • params
    : Passed parameters
  • callbackFun
    : Receive the return data from the native side

Therefore, we carry out a layer of encapsulation at the js level

var callbackId = 0 ; var callbackFunList = {} HybridAPI.invoke = function ( method, params, callbackFun ) { var message = { method, params } if (callbackFun) { callbackId = callbackId + 1 ; message.id = 'Hybrid_CB_' + callbackId; callbackFunList[callbackId] = callbackFun } HybridAPI.sendToNative( JSON .stringify(message)); } Copy code

In the end, it was called

sendToNative
Communicate with the native layer, but use
HybridAPI.invoke
The method is more friendly to developers.

Because the callback function needs to be called after the execution is successful. For this reason, when sending a message, first put

callbackFun
Save it and respond after the execution is successful. When a Javascript request is sent to the native layer, it will trigger
sendToNative
Method, in this method, we will parse the front-end data:

@JavascriptInterface public void sendToNative ( final String message) { JSONObject object = DataUtil.str2JSONObject(message); if (object == null ) { return ; } final String callbackId = DataUtil.getStrInJSONObject(object, "id" ); final String method = DataUtil.getStrInJSONObject(object, "method" ); final String params = DataUtil.getStrInJSONObject(object, "params" ); handleAPI(method, params, callbackId); } private void handleAPI (String method, String params, String callbackId) { if ( "getDeviceInfo" .equals (method)) { getDeviceInfo(); } else if ( "getUserInfo" .equals (method)) { getUserInfo(); } else if ( 'login' .equals(method)) { login(); } .... } Copy code

After the native side is processed, call

evaluateJavascript
or
loadUrl
Method, feedback to the front end. Example of operation process:

//Specify the receiving entrance on the js side public final String TO_JAVASCRIPT_PREFIX = "javascript:HybridAPI.onReceiveData('%s')" ; public void callJs () { Map<String, Object> responseData = new HashMap<>(); responseData.put( "error" , error); responseData.put( "data" , result); //The id of the callback function is returned to js, so that the corresponding callback function can be found responseData.put( "id" , callbackId); sendToJavaScript(responseData); } public void sendToJavaScript (Map<String, Object> message) { String str = new Gson().toJson(message); final String jsCommand = String.format(TO_JAVASCRIPT_PREFIX, escapeString(str)); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { evaluateJavascript(jsCommand, null ); } else { loadUrl(jsCommand); } } //escape private String escapeString (String javascript) { String result; result = javascript.replace( "\\" , "\\\\" ); result = result.replace( "\"" , "\\\"" ); result = result.replace( "\'" , "\\\'" ); result = result.replace( "\n" , "\\n" ); result = result.replace( "\r" , "\\r" ); result = result.replace( "\f" , "\\f" ); return result; } Copy code

above

callJs
Organize relevant data in the method, and then use
Gson
Perform serialization, then transfer to escape the string, and finally call
evaluateJavascript
or
loadUrl
To pass to js. So the js side can be used
HybridAPI.onReceiveData
Come and receive.

Remember the definition in this code

callbackFunList
? When the above native returns data to js, it will bring one
id
, We can find the callback function of this communication according to this id, and then call back the data.

var callbackId = 0 ; var callbackFunList = {} //Look here HybridAPI.invoke = function ( method, params, callbackFun ) { var message = { method, params } if (callbackFun) { callbackId = callbackId + 1 ; message.id = 'Hybrid_CB_' + callbackId; callbackFunList[callbackId] = callbackFun } HybridAPI.sendToNative( JSON .stringify(message)); } Copy code

Therefore, our js end receives data, which may look like this:

HybridAPI.onReceiveData = function ( message ) { var callbackFun = this .callbackFunList[message.id]; if (callbackFun) { callbackFun(message.error || null , message.data); } delete this .callbackFunList[message.id]; } Copy code

Back to our above

Get user information
This business function, our way of writing will look like this:

HybridAPI.invoke( 'getUserInfo' , { "id" : "1" }, function ( error, data ) { if (error) { console .log( 'Failed to obtain user information' ); } else { console .log( 'username:' + data.username + ', age:' + data.age); } }); Copy code

At this point, we have implemented a complete data communication process, which is used by the js terminal

HybridAPI.invoke(method, params, callbackFun)
To send data to the native side. After the native processing is completed, the js side passes
callbackFun
To receive data.

Improve

In the above java code, we can see that the entrance of the native layer is

sendToNative
Method, in which the incoming string is parsed and handed over to
handleAPI
Method to deal with

@JavascriptInterface public void sendToNative ( final String message) { JSONObject object = DataUtil.str2JSONObject(message); if (object == null ) { return ; } final String callbackId = DataUtil.getStrInJSONObject(object, "id" ); final String method = DataUtil.getStrInJSONObject(object, "method" ); final String params = DataUtil.getStrInJSONObject(object, "params" ); handleAPI(method, params, callbackId); } private void handleAPI (String method, String params, String callbackId) { if ( "getDeviceInfo" .equals (method)) { getDeviceInfo(); } else if ( "getUserInfo" .equals (method)) { getUserInfo(); } else if ( 'login' .equals(method)) { login(); } .... } Copy code

We will find that with the development of the business and the iteration of the project, the js side may need native to provide more and more capabilities, so our

handleAPI
There will be more and more in the method
if...else if...
Up.

Therefore, we can divide by business and create a new one

UserController
Class to handle
getUserInfo
,
login
,
logout
This native interface related to the user. Create a new one
DeviceController
To deal with something like
getDeviceInfo
,
getDeviceXXX
,... and other interfaces related to device information. Then we maintain a controller list, and each time we call the js api, we will look for the method in the corresponding controller from this list.

In this way, specific business processing methods can be extracted. However, even so, it is still unavoidable to write a paragraph of this in each Controller

if...else if ...
This kind of code. So, in fact, we can naturally think of doing something with reflection.

We have agreed with the H5 development, if we need to get user information, we call

getUserInfo
Method, the name of this method is always the same. At the same time, we define it like this on the Java side
UserController
:

public class UserController implements IController { private volatile static UserController instance; private UserController () {} public static UserController getInstance () { if (instance == null ) { synchronized (UserController.class) { if (instance == null ) { instance = new UserController(); } } } return instance; } @APIMethod public UserInfo getUserInfo (Map<String, Object> params, String callbackId) { //TODO } @APIMethod public void login (Map<String, Object> params, INativeCallback callback) { //TODO } @APIMethod public boolean logout (Map<String, Object> params, INativeCallback callback) { //TODO } } Copy code

We will

UserController
Add to the controller list mentioned above, and then we in the handleAPI method:

private void handleNativeAPI (String methodName, String params, String callback) { for (IController controller: controllerList) { Method[] methods = controller.getClass().getDeclaredMethods(); for (Method method: methods) { Annotation[] annotations = method.getAnnotations(); for (Annotation annotation: annotations) { //Get the specific type of annotation Class<? extends Annotation> annotationType = annotation.annotationType(); if (method.getName().equals(methodName) && APIMethod.class == annotationType) { try { Map<String, Object> map = DataUtil.jsonStr2Map(params); method.invoke(controller, map, callback); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } return ; } } } } } Copy code

Later, whenever a new interactive method is added, we only need to write a method in the corresponding java class and use

@APIMethod
The logo will do.

Above we have summarized several ways of communicating between WebView and native, and combined with specific practices to give the corresponding implementation ideas. Of course, because of space reasons, there is not everything here. such as:

  • How to realize the function of monitoring an event on the native side on the H5 side?
  • After H5 listens to native events and performs corresponding operations, how to return the result of the operation to native?
  • If the js side adjusts a native method that does not exist, what should be done?
  • ...

If you carefully understand the communication methods between the two ends introduced above, it should not be a problem to implement the above functions. But if you want to better encapsulate the code and make it more comfortable for developers, it takes a little effort.