Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Mobile / Android

Xamarin.Forms Android Using J2V8 to Execute JavaScript without WebView

5.00/5 (3 votes)
7 Jun 2017CPOL3 min read 17.8K   145  
This article illustrate how to execute JavaScript on Android using Xamarin.Forms.
  • Download J2V8BL.zip - 4.2 MB  [This contains the Android Bindings Library you can directly use it for Xamarin.Android. So you don't have to follow this article to build another one.]

Introduction

Recently, I need to execute JavaScript code in an Xamarin.Forms Project. Due to the WebView is taking more resources from the mobile device. I was seeking an approach to achieve my requirement. And I found the J2V8 was good to do so. When I build my code and use the J2V8, I faced some problems. But finally, I solved these problems and you may get the errors when you are trying to use it.

Go

In the Xamrin.Forms solution, build an Android Binding Library(BL). In the BL project, add the "j2v8_android-3.0.5.aar" file to the "Jars" folder. Set the Build Action for j2v8_android-3.0.5.aar" to LibraryProjectZip. Now build the BL project, you may get 3 errors and others warnings. Obviously, we don't have to take care of the warnings at this time.

1>..\obj\Debug\generated\src\Com.Eclipsesource.V8.Utils.V8Map.cs(10,99,10,121): error CS0738: 'V8Map' does not implement interface member 'IMap.EntrySet()'. 'V8Map.EntrySet()' cannot implement 'IMap.EntrySet()' because it does not have the matching return type of 'ICollection'.

1>..\obj\Debug\generated\src\Com.Eclipsesource.V8.Utils.V8Map.cs(10,99,10,121): error CS0738: 'V8Map' does not implement interface member 'IMap.KeySet()'. 'V8Map.KeySet()' cannot implement 'IMap.KeySet()' because it does not have the matching return type of 'ICollection'.

1>..\obj\Debug\generated\src\Com.Eclipsesource.V8.Utils.V8Map.cs(10,99,10,121): error CS0535: 'V8Map' does not implement interface member 'IMap.Put(Object, Object)'

These errors are Problem: Class does not implement interface method. To fix these problems, I followed the documentation from here. But it may take sometime to figure it out since there is no demo project.

Let's fix the errors.

1).

In the output window from Visual Studio, find the first error and double click it. Now it should open the generated cs file "..\obj\Debug\generated\src\Com.Eclipsesource.V8.Utils.V8Map.cs". Find the "EntrySet()" and the "KeySet ()" method.

C#
static IntPtr id_entrySet;
// Metadata.xml XPath method reference: path="/api/package[@name='com.eclipsesource.v8.utils']/class[@name='V8Map']/method[@name='entrySet' and count(parameter)=0]"
[Register ("entrySet", "()Ljava/util/Set;", "GetEntrySetHandler")]
public virtual unsafe global::System.Collections.Generic.ICollection<global::Java.Util.IMapEntry> EntrySet ()
{
    if (id_entrySet == IntPtr.Zero)
        id_entrySet = JNIEnv.GetMethodID (class_ref, "entrySet", "()Ljava/util/Set;");
    try {

        if (((object) this).GetType () == ThresholdType)
             return global::Android.Runtime.JavaSet<global::Java.Util.IMapEntry>.FromJniHandle (JNIEnv.CallObjectMethod (((global::Java.Lang.Object) this).Handle, id_entrySet), JniHandleOwnership.TransferLocalRef);
        else
             return global::Android.Runtime.JavaSet<global::Java.Util.IMapEntry>.FromJniHandle (JNIEnv.CallNonvirtualObjectMethod (((global::Java.Lang.Object) this).Handle, ThresholdClass, JNIEnv.GetMethodID (ThresholdClass, "entrySet", "()Ljava/util/Set;")), JniHandleOwnership.TransferLocalRef);
    } finally {
    }
}

//...............................................................................//

static IntPtr id_keySet;
// Metadata.xml XPath method reference: path="/api/package[@name='com.eclipsesource.v8.utils']/class[@name='V8Map']/method[@name='keySet' and count(parameter)=0]"
[Register ("keySet", "()Ljava/util/Set;", "GetKeySetHandler")]
public virtual unsafe global::System.Collections.ICollection<global::Java.Util.IMapEntry> KeySet ()
{
    if (id_keySet == IntPtr.Zero)
        id_keySet = JNIEnv.GetMethodID (class_ref, "keySet", "()Ljava/util/Set;");
    try {

        if (((object) this).GetType () == ThresholdType)
            return global::Android.Runtime.JavaSet<global::Com.Eclipsesource.V8.V8Value>.FromJniHandle (JNIEnv.CallObjectMethod (((global::Java.Lang.Object) this).Handle, id_keySet), JniHandleOwnership.TransferLocalRef);
        else
            return global::Android.Runtime.JavaSet<global::Com.Eclipsesource.V8.V8Value>.FromJniHandle (JNIEnv.CallNonvirtualObjectMethod (((global::Java.Lang.Object) this).Handle, ThresholdClass, JNIEnv.GetMethodID (ThresholdClass, "keySet", "()Ljava/util/Set;")), JniHandleOwnership.TransferLocalRef);
        } finally {
    }
}

2). 

In the BL project, expand the "Transforms" folder and open the Metadata.xml file. Add attr nodes inside the xml file. The XPaths are from the above methods.

XML
<metadata>
  <attr
    path="/api/package[@name='com.eclipsesource.v8.utils']/class[@name='V8Map']/method[@name='entrySet' and count(parameter)=0]"
    name="managedReturn">
    System.Collections.ICollection
  </attr>

  <attr
    path="/api/package[@name='com.eclipsesource.v8.utils']/class[@name='V8Map']/method[@name='keySet' and count(parameter)=0]"
    name="managedReturn">
    System.Collections.ICollection
  </attr>
</metadata>

Now build the BL project. You will get the last error.

3).

In the generated Com.Eclipsesource.V8.Utils.V8Map.cs file, Add the V8Map partial class inside the Com.Eclipsesource.V8.Utils namespace. And implemente the IMap.Put interface.

C++
public partial class V8Map[...]


public partial class V8Map : global::Java.Lang.Object, global::Com.Eclipsesource.V8.IReleasable, global::Java.Util.IMap
{
    //Metadata.xml XPath method reference: path="/api/package[@name='com.eclipsesource.v8.utils']/class[@name='V8Map']/method[@name='put' and count(parameter)=2 and parameter[1][@type='com.eclipsesource.v8.V8Value'] and parameter[2][@type='V']]"
    [Register("put", "(Lcom/eclipsesource/v8/V8Value;Ljava/lang/Object;)Ljava/lang/Object;", "GetPut_Lcom_eclipsesource_v8_V8Value_Ljava_lang_Object_Handler")]
    Java.Lang.Object global::Java.Util.IMap.Put(Java.Lang.Object key, Java.Lang.Object value)
    {
        return Put((global::Com.Eclipsesource.V8.V8Value)key, value);
    }
}

Now build the BL project. In the output window from Visual Studio, you will get 4 errors.

C++
1>..\obj\Debug\generated\src\Com.Eclipsesource.V8.Utils.V8Map.cs(228,13,228,206): error CS0266: Cannot implicitly convert type 'System.Collections.Generic.ICollection<Java.Util.IMapEntry>' to 'System.Collections.ICollection'. An explicit conversion exists (are you missing a cast?)

1>..\obj\Debug\generated\src\Com.Eclipsesource.V8.Utils.V8Map.cs(230,13,230,289): error CS0266: Cannot implicitly convert type 'System.Collections.Generic.ICollection<Java.Util.IMapEntry>' to 'System.Collections.ICollection'. An explicit conversion exists (are you missing a cast?)

1>..\obj\Debug\generated\src\Com.Eclipsesource.V8.Utils.V8Map.cs(300,13,300,213): error CS0266: Cannot implicitly convert type 'System.Collections.Generic.ICollection<Com.Eclipsesource.V8.V8Value>' to 'System.Collections.ICollection'. An explicit conversion exists (are you missing a cast?)

1>..\obj\Debug\generated\src\Com.Eclipsesource.V8.Utils.V8Map.cs(302,13,302,296): error CS0266: Cannot implicitly convert type 'System.Collections.Generic.ICollection<Com.Eclipsesource.V8.V8Value>' to 'System.Collections.ICollection'. An explicit conversion exists (are you missing a cast?)

Now we need to add cast into the code. Double click the error and open the generated code. Add "as global::System.Collections.ICollection" to do the cast inside the "EntrySet()" and the "KeySet ()" method.

C++
static IntPtr id_entrySet;
// Metadata.xml XPath method reference: path="/api/package[@name='com.eclipsesource.v8.utils']/class[@name='V8Map']/method[@name='entrySet' and count(parameter)=0]"
[Register ("entrySet", "()Ljava/util/Set;", "GetEntrySetHandler")]
public virtual unsafe global::System.Collections.ICollection EntrySet ()
{
    if (id_entrySet == IntPtr.Zero)
        id_entrySet = JNIEnv.GetMethodID (class_ref, "entrySet", "()Ljava/util/Set;");
    try {

    if (((object) this).GetType () == ThresholdType)
        return global::Android.Runtime.JavaSet<global::Java.Util.IMapEntry>.FromJniHandle (JNIEnv.CallObjectMethod (((global::Java.Lang.Object) this).Handle, id_entrySet), JniHandleOwnership.TransferLocalRef) as global::System.Collections.ICollection;
    else
        return global::Android.Runtime.JavaSet<global::Java.Util.IMapEntry>.FromJniHandle (JNIEnv.CallNonvirtualObjectMethod (((global::Java.Lang.Object) this).Handle, ThresholdClass, JNIEnv.GetMethodID (ThresholdClass, "entrySet", "()Ljava/util/Set;")), JniHandleOwnership.TransferLocalRef) as global::System.Collections.ICollection;
    } finally {
    }
}


//........................................................................//

static IntPtr id_keySet;
// Metadata.xml XPath method reference: path="/api/package[@name='com.eclipsesource.v8.utils']/class[@name='V8Map']/method[@name='keySet' and count(parameter)=0]"
[Register ("keySet", "()Ljava/util/Set;", "GetKeySetHandler")]
public virtual unsafe global::System.Collections.ICollection<global::Java.Util.IMapEntry> KeySet ()
{
    if (id_keySet == IntPtr.Zero)
        id_keySet = JNIEnv.GetMethodID (class_ref, "keySet", "()Ljava/util/Set;");
    try {

        if (((object) this).GetType () == ThresholdType)
            return global::Android.Runtime.JavaSet<global::Com.Eclipsesource.V8.V8Value>.FromJniHandle (JNIEnv.CallObjectMethod (((global::Java.Lang.Object) this).Handle, id_keySet), JniHandleOwnership.TransferLocalRef) as global::System.Collections.ICollection;
        else
            return global::Android.Runtime.JavaSet<global::Com.Eclipsesource.V8.V8Value>.FromJniHandle (JNIEnv.CallNonvirtualObjectMethod (((global::Java.Lang.Object) this).Handle, ThresholdClass, JNIEnv.GetMethodID (ThresholdClass, "keySet", "()Ljava/util/Set;")), JniHandleOwnership.TransferLocalRef) as global::System.Collections.ICollection;
        } finally {
    }
}

Now build the BL project. It should build successfully. Keep in mind that backup the generated cs file because if you clean the project, it will regenerate the cs files.

Add the reference BL project from Android project. Add the following code in the MainActivity.cs to execute the JS as a demo.

C++
       
       ......

       LoadApplication(new App());

       JS.ExecuteJS();   
    }
}
    
    //..................
    public class JS
    {
        public static void ExecuteJS()
        {
            Com.Eclipsesource.V8.V8 runtime = Com.Eclipsesource.V8.V8.CreateV8Runtime();
            int result = runtime.ExecuteIntegerScript(""
                  + "var hello = 'hello, ';\n"
                  + "var world = 'world!';\n"
                  + "hello.concat(world).length;\n");

            runtime.Release();
        }
    }

For more infomation about how to execute JS, please see this. You can add IScriptEngine into the PCL, and implement it from the Android project as a Dependency. Then you can use it from the PCL.

Points of Interest

For UWP project, you may use the Jurassic ScriptEngine. Actually I used it in my Xamarin.Forms project at first, it is working good on UWP. But it is working on  Android only the Linker is None, this leads that the apk package is very large(double). I tried different ways to make it work, but failed. If you can make it work successfully, please let me know.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)