Scripto — замена стандартному JavaScriptInterface

в 2:16, , рубрики: android, java, Разработка под android

Scripto — замена стандартному JavaScriptInterface - 1

Приветствую пользователей Хабра. Наверное, многие из более менее опытных пользователей слышали про 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 и наоборот.

Основной идеей библиотеки стало то, что пользователь (читайте программист) вызывает 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

Пять полей с разными типами значений: строка, целое число, вещественное число, булево значение.

Scripto — замена стандартному JavaScriptInterface - 2

Ниже представлен код скрипта 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-страницу:

<source lang="java">
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.

Результат:

Scripto — замена стандартному JavaScriptInterface - 3

Библиотека на Github: Scripto.

Автор: ImangazalievM

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js