AndroidバージョンごとのWebView仕様に振り回されないためにCrosswalkのXWalkViewを使ってみたよ

android-icon Android
crosswalk Crosswalk

Androidは4.4からWebViewがWebKitベースのものからChromeベースのものに変わりました。
もちろん一部仕様も変更されています。
例えばjavascriptの実行方法がloadUrlメソッドからevaluateJavascriptメソッドに変わってしまっているので、両方対応しようとするとちょいとばかしダサいことになります。

if(Build.VERSION.SDK_INT < 19){
    webView.loadUrl("javascript:alert('ダサい')");
}else{
    webView.evaluateJavascript("alert('ダサい')", null);
}

また4.3以前の脆弱性サポートは既に打ち切られています。恐いのなんのって。

そこでいろいろ調べて見つけたのがCrosswalkというWebViewエンジン。
Androidアプリに組み込むことでバージョンに依存しないWebviewの実装・動作が可能になる。
素敵だからこいつを使ってみたよ。
そしてバッチリまとまった参考記事が少なかったのでまとめてみたよ。

1.XWalkViewをインストール

Mavenリポジトリを使うためbuild.gradleに以下2つを追加する。

// 追加①
repositories {
    maven {
        url 'https://download.01.org/crosswalk/releases/crosswalk/android/maven2'
    }
}
// 追加②
compile 'org.xwalk:xwalk_core_library:20.50.533.12'

追加後のbuild.gradle

build.gradle
apply plugin: 'com.android.application'

android {
    compileSdkVersion 24
    buildToolsVersion "24.0.0"

    defaultConfig {
        applicationId "com.sample.hello"
        minSdkVersion 23
        targetSdkVersion 24
        versionCode 1
        versionName "1.0"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

// 追加①
repositories {
    maven {
        url 'https://download.01.org/crosswalk/releases/crosswalk/android/maven2'
    }
}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    testCompile 'junit:junit:4.12'
    compile 'com.android.support:appcompat-v7:24.0.0'
    // 追加②
    compile 'org.xwalk:xwalk_core_library:20.50.533.12'
}

とっても簡単

2.XWalkViewを配置

WebViewの代わりにorg.xwalk.core.XWalkViewを対象のレイアウトに配置する

activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context="com.sample.hello.MainActivity">

    <org.xwalk.core.XWalkView
            xmlns:android="http://schemas.android.com/apk/res/android"
            android:id="@+id/webView"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
    />
</RelativeLayout>

とっても簡単

5.AndroidManifestにpermissionを追加

<uses-permission android:name="android.permission.INTERNET"/>

追加後のAndroidManifest.xml

AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="com.sample.hello">

    <uses-permission android:name="android.permission.INTERNET"/>
    <application
            android:allowBackup="true"
            android:icon="@mipmap/ic_launcher"
            android:label="@string/app_name"
            android:supportsRtl="true"
            android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>

                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>
    </application>

</manifest>

4.ActivityからXWalkViewを操作

MainActivity.java
package com.sample.hello;

import android.content.Intent;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import org.xwalk.core.XWalkResourceClient;
import org.xwalk.core.XWalkView;

import java.util.HashMap;
import java.util.Map;

public class MainActivity extends AppCompatActivity {

    // XWalkViewに登録するResourceClient
    private class ResourceClient extends XWalkResourceClient {
        ResourceClient(XWalkView view) {
            super(view);
        }

        // XWalkViewのロード完了通知
        @Override
        public void onLoadFinished(XWalkView view, String url) {
            super.onLoadFinished(view, url);
            // 描画後にグローバルに定義されたjavascript関数をコール
            xWalkView.evaluateJavascript("onLoaded();", null);
        }

        // Location変更通知
        @Override
        public boolean shouldOverrideUrlLoading(XWalkView view, String url) {
            // URLを使って値の受け渡しを行う
            if (url.contains("app-api://")) {
                // 'true'を返した場合Locationの変更がキャンセルされる
                return true;
            }

            // 'false'を返した場合Locationの変更が続行される
            return false;
        }
    }

    private XWalkView xWalkView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // XWalkViewを取得
        xWalkView = (XWalkView) findViewById(R.id.webView);

        // XWalkViewから通知を受け取るためのXWalkResourceClient継承クラスをset
        xWalkView.setResourceClient(new ResourceClient(xWalkView));

        // loadメソッドに'Map<String, String>'を渡すことでリクエストヘッダーにフィールドを追加することができる
        Map<String, String> additionalHttpHeaders = new HashMap<>();
        additionalHttpHeaders.put("mobileAppAuth", "3f7d22402f6ac375bf1168c3ef5d8f08");

        //localhostはアクセスできないのでローカルサーバーで開発中は自分のIPアドレス,ポートを指定してね
        xWalkView.load("http://192.168.0.0:8000/gacha", null, additionalHttpHeaders);

        //***********************************************************
        // ここでグローバルに定義されたjavascript関数をコールしても 
        // `is not defined エラーで怒られます`
        //***********************************************************
        // xWalkView.evaluateJavascript("onLoaded();", null);
    }

    @Override
    protected void onPause() {
        super.onPause();
        if (xWalkView != null) {
            xWalkView.pauseTimers();
            xWalkView.onHide();
        }
    }

    @Override
    protected void onResume() {
        super.onResume();
        if (xWalkView != null) {
            xWalkView.resumeTimers();
            xWalkView.onShow();
        }
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (xWalkView != null) {
            xWalkView.onDestroy();
        }
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        if (xWalkView != null) {
            xWalkView.onActivityResult(requestCode, resultCode, data);
        }
    }

    @Override
    protected void onNewIntent(Intent intent) {
        if (xWalkView != null) {
            xWalkView.onNewIntent(intent);
        }
    }
}

XWalkResourceClientにはこれだけOverride可能なメソッドがあるのでプログレスバー出したり等いろいろな拡張ができそう。

return メソッド名
void onLoadFinished(XWalkViewInternal view, String url)
void onLoadStarted(XWalkViewInternal view, String url)
void onProgressChanged(XWalkViewInternal view, int progressInPercent)
void onReceivedLoadError(XWalkViewInternal view, int errorCode, String description, String failingUrl)
WebResourceResponse shouldInterceptLoadRequest(XWalkView view, String url)
WebResourceResponse shouldInterceptLoadRequest(XWalkViewInternal view, String url)

5.実行

はい、出ました。

最後に

XWalkView、WebView関連の記事を探していたらevaluateJavascriptメソッドでグローバルに定義した関数をコールすることができますよという旨の記載が多々出てくるが、騙されてはいけない。
実際はWebViewのロード完了後以降でないとコールできないのだが、その点には一切触れていないものが多かったので注意されたし。これだけで3時間はハマる(ハマった)。