📜 ⬆️ ⬇️

Scripto - replacing the standard JavaScriptInterface



Greetings to users of Habr. Probably, many less experienced users have heard about JavaScriptInterface - the “bridge” between Java and JavaScript, with which you can call Java methods. JavaScriptInterface has some pretty significant flaws:

1) Methods are not called in a UI stream, but in a special Java Bridge thread that cannot be clogged, otherwise WebView will stop responding.
2) When accessing the UI from methods called with JavaScriptInterface, nothing happens, which can lead to several hours of debugging from unknowing developers. As a solution, you have to use the runOnUi method or handlers.
3) Cannot transfer user data types
')
Calling JS functions in the standard way is as follows:

myWebView.loadUrl("myFunction('Hello World!')"); 


The disadvantage of this approach is that the function call is, in fact, a string, and when passing arguments, all of them need to be converted to a String.

Faced with these problems in one of my projects, in which Java and JavaScript interact very closely, I decided to write a library to facilitate JS calls from Java and vice versa.

Attention: this article describes the work with the library version 0.5.3 .

The main idea of ​​the library is that the user (read the programmer) calls Java methods, and the library itself calls JavaScript functions and passes arguments to it. You can also call functions with callbacks. All this works in the opposite direction - from JS to Java.

Here are the main advantages of the library:

- convenient call of JS-functions with parameters
- call with callbacks and error handling during JS execution
- the ability to transfer your own data types
- transfer of code execution to a UI stream

When writing the library, I was guided by the Retrofit library and even used some pieces of code from its sources.

There are two types of entities in Scripto:

Script - used to call JS-functions from Java.
Interface - designed to call Java-methods from JS.

Before further reading the article I advise you to quickly run through the library's Readme to fully understand the essence of what is happening.

So, the conditions of the problem:

There is an HTML document with a user data entry form. After entering user data and clicking the Save button, the application should save the data to SharedPreferences. When you close and reopen the application, the data in the form is restored from the settings. The task is completely fictional and does not carry any meaning.

So the first thing we need to do is create a form:

 <!doctype html> <html> <head> <meta charset="utf-8"> <meta name="HandheldFriendly" content="True"> <meta name="viewport" content="width=620, user-scalable=no"> <link rel="stylesheet" href="test.css"/> <script src="./scripto/scripto.js"></script> <script src="interfaces/preferences_interface.js"></script> <script src="test.js"></script> </head> <body> <label>Name:</label> <input id="name_field" type="text" size="15" maxlength="15"><br/> <label>Surname</label> <input id="surname_field" type="text" size="15" maxlength="15"><br/> <label>Age:</label> <input id="age_field" type="text" size="15" maxlength="15"><br/> <label>Height:</label> <input id="height_field" type="text" size="15" maxlength="15"><br/><br/> <label>Married:</label> <input id="married_checkbox" type="checkbox"><br/><br/> <button onclick="saveUserData()">Save</button> </body> </html 


Five fields with different types of values: string, integer, floating point number, boolean value.



Below is the script code test.js , which saves and restores user data:

 function loadUserData() { PreferencesInterface.getUserData(function(userJson) { var user = JSON.parse(userJson); document.getElementById('name_field').value = user.name; document.getElementById('surname_field').value = user.surname; document.getElementById('age_field').value = user.age; document.getElementById('height_field').value = user.height; document.getElementById('married_checkbox').checked = user.married; }); } function saveUserData() { var user = getUserData(); PreferencesInterface.saveUserData(user); } function getUserData() { var user = {}; user['name'] = document.getElementById('name_field').value; user['surname'] = document.getElementById('surname_field').value; user['age'] = document.getElementById('age_field').value; user['height'] = document.getElementById('height_field').value; user['married'] = document.getElementById('married_checkbox').checked; return JSON.stringify(user); } //   ,    document.addEventListener('DOMContentLoaded', function() { loadUserData(); }, false); 


The JS script android_interface.js , which calls our Java methods:

 function PreferencesInterface() {} PreferencesInterface.saveUserData = function(user) { Scripto.call('Preferences', arguments); }; PreferencesInterface.getUserData = function(callback) { Scripto.callWithCallback('Preferences', arguments); }; 


In the interface, we call the special function call in our library, and also pass arguments to it. Thanks to this, the library will be able to get the name of the function that called it and call the Java method of the same name, passing arguments to it.

Let's create a model for our user:

 public class User { @SerializedName("name") private String name; @SerializedName("surname") private String surname; @SerializedName("age") private int age; @SerializedName("height") private float height; @SerializedName("married") private boolean married; public User() { } public User(String name, String surname, int age, float height, boolean married) { this.name = name; this.surname = surname; this.age = age; this.height = height; this.married = married; } public String getName() { return name; } public String getSurname() { return surname; } public int getAge() { return age; } public float getHeight() { return height; } public boolean isMarried() { return married; } public String getUserInfo() { return String.format("Name: %s \nSurname: %s \nAge: %d \nHeight: %s \nMarried: %s", name, surname, age, height, married); } } 


Since the library uses GSON to convert user-defined data types, we use the SerializedName annotation.

Now create a Java settings interface for saving data:

 public class PreferencesInterface { private Context context; private SharedPreferences prefs; public PreferencesInterface(Context context) { this.context = context; this.prefs = context.getSharedPreferences("MyPrefs", Context.MODE_PRIVATE); } public void saveUserData(User user) { prefs.edit().putString("user_name", user.getName()).apply(); prefs.edit().putString("user_surname", user.getSurname()).apply(); prefs.edit().putInt("user_age", user.getAge()).apply(); prefs.edit().putFloat("user_height", user.getHeight()).apply(); prefs.edit().putBoolean("user_married", user.isMarried()).apply(); Toast.makeText(context, user.getUserInfo(), Toast.LENGTH_SHORT).show(); } public User getUserData() { String userName = prefs.getString("user_name", ""); String userSurname = prefs.getString("user_surname", ""); int userAge = prefs.getInt("user_age", 0); float userHeight = prefs.getFloat("user_height", 0.0f); boolean userMarried = prefs.getBoolean("user_married", false); return new User (userName, userSurname, userAge, userHeight, userMarried); } } 


Everything is ready, it remains only to link the interfaces and scripts together.

 Scripto scripto = new Scripto.Builder(webView).build(); scripto.addInterface("Preferences", new PreferencesInterface(this)); 


In order to know that the library is ready for work, we need to install a listener. After the library is ready, we call the data recovery function:

 scripto.onPrepared(new ScriptoPrepareListener() { @Override public void onScriptoPrepared() { userInfoScript.loadUserData(); } }); 


Download our HTML page:

 String html = AssetsReader.readFileAsText(this, "test.html"); webView.loadDataWithBaseURL("file:///android_asset/", html, "text/html", "utf-8", null); 


Is done. Now, when you click on the “Save” button, we will save our data in SharedPreferences, and the next time you start the application, they will be restored.

Let's also display the user information in Toast when clicking on the “Show user info” button:

  public void getUserData(View view) { userInfoScript.getUserData() .onResponse(new ScriptoResponseCallback<User>() { @Override public void onResponse(User user) { Toast.makeText(MainActivity.this, user.getUserInfo(), Toast.LENGTH_LONG).show(); } }) .onError(new ScriptoErrorCallback() { @Override public void onError(JavaScriptException error) { Toast.makeText(MainActivity.this, error.getMessage(), Toast.LENGTH_SHORT).show(); } }).call(); } 


In the onResponse method , we get an object that has already been converted from JSON. If an error occurs while executing the script, we will get an exception in the onError method. If you do not prescribe the onError method, the library will throw a JavaScriptException exception.

Result:



Library on github : Scripto .

Source: https://habr.com/ru/post/305678/


All Articles