GWT Jsinterop

Una de las grandes ventajas de utilizar GWT como framework es su robustes, flexibilidad y la posibilidad de trabajar en un lenguaje estaticamente tipado como Java.
Pero no todo siempre es color de rozas, siempre existen "peros" a la hora de trabajar con tecnología, estos "peros" en GWT vienen de la mano de la posibilidad de interactuar con el entorno JS existente de una manera limpia y directa. Cuando me refiero a entorno JS existente, digo JQuery, AngularJS, Twiter Bootstrap, etc.
Y otro de los "peros", también es la posibilidad de poder hacer uso de funcionalidades escritas en GWT desde JS sin ningún problema.
Esta exportación de funciones, objectos, etc. no es posible de manera directa en las versiones estables de GWT, pero si en las versiones de desarrollo, que prontamente verán la luz:
Ahora la pregunta es ¿Cómo accedemos y/o utilizamos JS en GWT de una manera limpia?, la respuesta viene de la mano de JsInterop, un nuevo API que está en desarrollo, y nos permite de una manera sencilla poder trabajar con JS nativo, lo que implica que podemos utilizar WebComponent de Polymer, Promise de ES6, JQuery, etc. sin la necesidad de escribir código de bajo nivel haciendo uso de JSNI, como estábamos acostumbrados.
Para dejar en claro, en la actualidad de GWT, es posible hacer uso de librerías JS, pero para ello debemos escribir bastante código del tipo:

public class Item extends JavaScriptObject { 
  protected Item(){} 
  
  public final native Integer getId()/*-{
    return this.id; 
  }-*/; 
  
  public final native String getName()/*-{ 
    return this.name; 
  }-*/; 
 
  public final native String getType()/*-{ 
    return this.id; 
  }-*/; 
}
Todo lo que esta entre comentarios es código JS, lo cual puede traer aparejado muchos errores debido a que no contamos con el IDE, para el refactoring, y para el control de la sintaxis. Con JsInterop, lo único que necesitamos es declarar interfaces, éstas, pueden mapearse con objetos del entorno JS, y GWT implementará por nosotros los JavaScriptObject necesarios.
Vallamos a un ejemplo claro:
Tratemos de implementar Object.observe con el pollyfil de Polymer en GWT haciendo uso de JsInterop.
En Javascript seria algo como lo siguiente:

var obj = { foo: { bar: 'baz' } };
var observer = new PathObserver(obj, 'foo.bar'); 
observer.open(function(newValue, oldValue) { 
// respond to obj.foo.bar having changed value. 
});
Veamos como hacemos uso del API de Polymer (observe-js)
A continuacion vamos a definir una serie de interfaces anotadas con @JsType, que emularan los objetos y funciones del API.

Polymer - observe-js define el siguiente objecto llamado Observable, con las siguientes funciones:

{
  // Begins observation. Value changes will be reported by invoking |changeFn| with |opt_receiver| as
  // the target, if provided. Returns the initial value of the observation. 
  open: function(changeFn, opt_receiver) {},
  // Report any changes now (does nothing if there are no changes to report).
  deliver: function() {},
  // If there are changes to report, ignore them. Returns the current value of the observation.
  discardChanges: function() {},
  // Ends observation. Frees resources and drops references to observed objects.
  close: function() {}
}

Entonces, en GWT definimos lo mismo:
@JsType
public interface Observable<T extends JsObject, E extends Object≶ extends JsObject {

    void close();

    <L extends ChangeFnListener<T≶≶ E open(L change, T target);

    void deliver();

    E discardChanges();
}

Muy bien, ahora crearemos la interface para representar a PathObserver que herede de Observable:
@JsType(prototype = "PathObserver")
public interface PathObserver<T extends JsObject, E extends Object≶ extends Observable<T, E≶ {

    /**
     * PathObserver also exposes a setValue method which attempts to update the underlying value. Setting the value does not affect notification state (in other words, a caller sets the value but does not discardChanges, the changeFn will be notified of the change).
     * @param value 
     */
    void setValue(E value);   
}

Lo importante a destacar es que cada una de las interfaces se encuentra anotada con JsType: esta anotación es utilizada para describir el comportamiento de un Javascript Object, uno que ya exista en el entorno JS, o uno que sera accesible externamente desde el entorno JS, las llamadas a los métodos definidos en las interfaces anotadas con JsType, seran tratadas de manera especial por el compilador de GWT para propósitos de interoperabilidad. Cada método no necesita tener una inplementacion particular, por ejemplo, si el parámetro prototype esta presente en la anotación, GWT asumirá que el método de la interface, es el mismo que el método definido en el prototype del objeto subyacente del entorno de JS.
JsType, actúa igual que los JavaScriptObject de GWT, en lo que respecta al cast, pero si el atributo prototype esta presente, el cast (cast checks and instanceof checks) es delegado a la implementación subyacente del entorno JS.
Solo nos resta explicar como traducimos la función del método open, de la interface Observable:
@JsType
public interface ChangeFnListener<T extends JsObject≶ {
}

Como cada observer difiere en la cantidad de parámetros que recibe su función de apertura, definimos un extensión de la interface anterior:
@JsType
public interface OpenPathObserverListener<T extends JsObject≶ extends ChangeFnListener<T≶ {
    /**
     * @param newValue
     * @param oldValue 
     */
    void onOpen(String newValue, String oldValue);
}
Aquí podemos observar que nuestra función de callback onOpen recibe dos parámetros, el nuevo valor de la propiedad que estamos observando y el valor anterior. Hasta aquí todo perfecto, la pregunta es ¿Cuándo implementaremos algo?, ya que hasta ahora solo hemos definido interfaces. Esto es lo grandioso de JsInterop. Pero necesitaremos algunos métodos que nos retornen una instancia real del objeto que estamos tratanto de implementar, desde el entorno JS existente, y también necesitaremos contar con la implementación de las funciones nativas de Javascript, que dicho objeto necesita. Por lo tanto, definiremos algunos métodos que nos ayudarán para resolver esto:
public static native <T extends JsObject, E extends Object≶ PathObserver<T, E≶ createPathObserver(T obj, E path)/*-{
  return new $wnd.PathObserver(obj, path);
}-*/;

Este método, retorna una instancia de PathObserver y lo mapea directamente con una instancia de la implementación de nuestra interface. Esto cambiará en el futuro para que no tengamos que acceder al bajo nivel usando JSNI, sino que JsInterop, nos permitirá utilizar la clase de implementación para que nosotros podamos hacer un "new" del objeto en cuestión, o eso espero. :)
Ahora el turno de la creación y mepeo de las funciones:
public static native <T extends JsObject≶ OpenPathObserverListener<T≶ createOpenPathObserverListener(OpenPathObserverListener<T≶ listener)/*-{
  return 
     function(newValue, oldValue){
       listener.onOpen(newValue, oldValue);
     }
}-*/;
Muy bien, esta es la parte que a mi me resulta mas tediosa de todas, pero que según el equipo de GWT, cambiará para que este mapeo sea automático sin tener que recurrir a JSNI. Lo que el método retorna es una función JS, que se mapea con nuestra interface definida, que en su implementación llama al método onOpen. Esto se aclarará con el ejemplo de uso. Poniendo todo a trabajar:
final Person person = new Person();
person.setName("Cristian");
...
...

final String original = observer1.open(PathObserverFactory.createOpenPathObserverListener(new OpenPathObserverListener() {
    @Override
    public void onOpen(String newValue, String oldValue) {
        HTMLElement p = doc.createElement("P");
        p.setInnerText("The new Value is: " + newValue);
        body.appendChild(p);
    }
}), person);
Para todos aquellos que les interese esto, les dejo unos links a mis repositorios donde podrán encontrar mas ejemplos de lo que traera GWT 2.7.
GWT-Playground
GWT-jsCore
Como el soporte del plugin de maven en su version 2.6.1 no trae soporte de JsInterop, hice un fork del proyecto y agregue un nuevo parámetro de compilación, asi que pueden descargarse el plugin y probar:
GWT-maven-plugin

Espero que les sirva, hasta la próxima.

Comentarios

Entradas más populares de este blog

¿Qué es GWT?

Frameworks y ontologías (Parte 2)