
Приветствую пользователей Хабра. Наверное, многие из более менее опытных пользователей слышали про JavaScriptInterface — «мостик» между Java и JavaScript, при помощи которого можно вызывать Java методы. У JavaScriptInterface есть несколько довольно значимых недостатков:
1) Методы вызываются не в UI-потоке, а в специальном потоке Java Bridge, который нельзя забивать, иначе WebView перестанет отвечать.
2) При обращении к UI из методов, вызванных при помощи JavaScriptInterface, ничего не происходит, что может привести к нескольким часам дебага у незнающих разработчиков. Как решение, приходится использовать метод runOnUi или хендлеры.
3) Невозможно передавать пользовательские типы данных
Вызов JS-функций стандартным способом происходит так:
myWebView.loadUrl("myFunction('Hello World!')");
Минус данного подхода в том, что вызов функции — это, фактически, строка, и при передаче аргументов всех их нужно конвертировать в String.
Столкнувшись с этими проблемами в одном из своих проектов, в котором Java и JavaScript взаимодействуют очень тесно, я решил написать библиотеку облегчающую вызовы JS из Java и наоборот.
Внимание: данной статье описывается работа с библиотекой версии 0.5.3.
Основной идеей библиотеки стало то, что пользователь (читайте программист) вызывает Java-методы, а библиотека сама вызывает JavaScript-функции и передает ей аргументы. Также можно вызывать функции с коллбеками. Все это работает и в обратном направлении — из JS в Java.
Вот основные преимущества библиотеки:
— удобный вызов JS-функций с параметрами
— вызов с коллбеками и обработкой ошибок при исполнении JS
— возможность передачи собственных типов данных
— перенос выполнения кода в UI поток
При написании библиотеки я ориентировался на библиотеку Retrofit и даже использовал некоторые куски кода из ее исходников.
В Scripto есть два типа сущностей:
Script — служит для вызова JS-функций из Java.
Interface — предназначен для вызова Java-методов из JS.
Перед дальнейшим прочтением статьи советую быстро пробежаться по Readme библиотеки для полного понимания сути происходящего.
Итак, условия задачи:
Есть HTML-документ с формой ввода пользовательских данных. После ввода данных пользователя и нажатия кнопки «Save» приложение должно сохранить данные в SharedPreferences. При закрытии и повторном открытии приложения данные в форме восстанавливаются из настроек. Задача полностью выдуманная и не несет в себе никакого смысла.
Итак, первое, что нам нужно сделать — это создать форму:
<!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
Пять полей с разными типами значений: строка, целое число, вещественное число, булево значение.

Ниже представлен код скрипта test.js, который сохраняет и восстанавливает данные пользователя:
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);
JS-скрипт android_interface.js, вызывающий наши Java-методы:
function PreferencesInterface() {} PreferencesInterface.saveUserData = function(user) { Scripto.call('Preferences', arguments); }; PreferencesInterface.getUserData = function(callback) { Scripto.callWithCallback('Preferences', arguments); };
В интерфейсе мы вызываем специальную функцию call нашей библиотеки, а также передаем ей аргументы. Благодаря этому библиотека сможет получить имя функции, вызвавшей ее и вызвать одноименный Java-метод, передав ему аргументы.
Давайте создадим модель для нашего пользователя:
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); } }
Т. к. библиотека использует GSON для конвертации пользовательских типов данных, мы используем аннотацию SerializedName.
Теперь создадим Java-интерфейс настроек для сохранения данных:
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); } }
Все готово, осталось только связать интерфейсы и скрипты между собой.
Scripto scripto = new Scripto.Builder(webView).build(); scripto.addInterface("Preferences", new PreferencesInterface(this));
Для того, чтобы узнать, что библиотека готова к работе нам нужно установить слушатель. После того, как библиотека готова мы вызываем функцию для восстановления данных:
scripto.onPrepared(new ScriptoPrepareListener() { @Override public void onScriptoPrepared() { userInfoScript.loadUserData(); } });
Загружаем нашу HTML-страницу:
String html = AssetsReader.readFileAsText(this, "test.html"); webView.loadDataWithBaseURL("file:///android_asset/", html, "text/html", "utf-8", null);
Готово. Теперь при нажатии на кнопку «Save» мы сохраним наши данные в SharedPreferences, а при следующем запуске приложения они восстановятся.
Давайте еще сделаем вывод информации о пользователе в Toast при нажатии на кнопку «Show user info»:
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(); }
В методе onResponse мы получаем уже сконвертированный из JSON объект. Если при выполнении скрипта произошла ошибка мы получим исключение в метод onError. Если не прописывать метод onError, библиотека выбросит исключение JavaScriptException.
Результат:

Библиотека на Github: Scripto.
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Будете ли вы использовать Scripto в своих проектах вместо JavaScriptInterface?
32.61%Да15
67.39%Нет31
Проголосовали 46 пользователей. Воздержались 45 пользователей.
