アプリ内コンテンツを読み込む

HTML、JavaScript、CSS などのウェブベースのコンテンツをアプリに提供して、インターネット経由で取得するのではなく、それをアプリに静的にコンパイルできます。

アプリ内コンテンツでは、インターネット アクセスは必要なく、ユーザーの帯域幅を消費しません。コンテンツが WebView 専用に設計されている(つまり、ネイティブ アプリとの通信に依存している)場合は、ユーザーが誤ってウェブブラウザでそのコンテンツを読み込むことはありません。

ただし、アプリ内コンテンツにはいくつかのデメリットがあります。ウェブベースのコンテンツを更新するには、新しいアプリのアップデートを送信する必要があります。ユーザーが古いバージョンのアプリを使用している場合、ウェブサイトの内容とデバイス上のアプリの内容の間でコンテンツが一致しなくなる可能性があります。

WebViewAssetLoader

WebViewAssetLoader は、アプリ内コンテンツを WebView オブジェクトに読み込む、柔軟かつ効率的な方法です。このクラスでは、以下がサポートされます。

  • 同一生成元ポリシーとの互換性を確保するために、HTTP(S) URL を使用してコンテンツを読み込む。
  • JavaScript、CSS、画像、iframe などのサブリソースを読み込む。

メイン アクティビティ ファイルに WebViewAssetLoader を含めます。アセット フォルダからシンプルなウェブ コンテンツを読み込む例を次に示します。

Kotlin

private class LocalContentWebViewClient(private val assetLoader: WebViewAssetLoader) : WebViewClientCompat() {
    @RequiresApi(21)
    override fun shouldInterceptRequest(
        view: WebView,
        request: WebResourceRequest
    ): WebResourceResponse? {
        return assetLoader.shouldInterceptRequest(request.url)
    }

    // To support API < 21.
    override fun shouldInterceptRequest(
        view: WebView,
        url: String
    ): WebResourceResponse? {
        return assetLoader.shouldInterceptRequest(Uri.parse(url))
    }
}

Java

private static class LocalContentWebViewClient extends WebViewClientCompat {

    private final WebViewAssetLoader mAssetLoader;

    LocalContentWebViewClient(WebViewAssetLoader assetLoader) {
        mAssetLoader = assetLoader;
    }

    @Override
    @RequiresApi(21)
    public WebResourceResponse shouldInterceptRequest(WebView view,
                                     WebResourceRequest request) {
        return mAssetLoader.shouldInterceptRequest(request.getUrl());
    }

    @Override
    @SuppressWarnings("deprecation") // To support API < 21.
    public WebResourceResponse shouldInterceptRequest(WebView view,
                                     String url) {
        return mAssetLoader.shouldInterceptRequest(Uri.parse(url));
    }
}

アプリは、ニーズに合わせて WebViewAssetLoader インスタンスを構成する必要があります。次のセクションで例を示します。

アプリ内アセットとリソースを作成する

WebViewAssetLoader は、特定のリソースパスに対応するリソースを読み込むために、PathHandler インスタンスを使用します。このインターフェースを実装してアプリに必要なリソースを取得することもできますが、Webkit ライブラリには Android のアセットとリソースを読み込むための AssetsPathHandlerResourcesPathHandler がバンドルされています。

まず、アプリのアセットとリソースを作成します。通常、以下が適用されます。

  • HTML、JavaScript、CSS などのテキスト ファイルはアセットに含まれます。
  • イメージなどのバイナリ ファイルはリソースに属します。

プロジェクトにテキストベースのウェブ ファイルを追加する手順は次のとおりです。

  1. Android Studio で、[app] > [src] > [main] フォルダを右クリックし、[New] > [Directory] を選択します。
    Android Studio のディレクトリ作成メニューを示す画像
    図 1.プロジェクトのアセット フォルダを作成します。
  2. フォルダに「assets」という名前を付けます。
    アセット フォルダを示す画像
    図 2. アセット フォルダに名前を付けます。
  3. assets フォルダを右クリックし、[New] > [File] をクリックします。 「index.html」と入力して、Return キーまたは Enter キーを押します。
  4. 前の手順を繰り返して、stylesheet.css に空のファイルを作成します。
  5. 作成した空のファイルに、次の 2 つのコードサンプルの内容を追加します。
```html
<!-- index.html content -->

<html>
  <head>
    <!-- Tip: Use relative URLs when referring to other in-app content to give
              your app code the flexibility to change the scheme or domain as
              necessary. -->
    <link rel="stylesheet" href="/assets/stylesheet.css">
  </head>
  <body>
    <p>This file is loaded from in-app content.</p>
    <p><img src="/res/drawable/android_robot.png" alt="Android robot" width="100"></p>
  </body>
</html>
```

```css
<!-- stylesheet.css content -->

body {
  background-color: lightblue;
}
```

画像ベースのウェブ ファイルをプロジェクトに追加する方法は次のとおりです。

  1. Android_symbol_green_RGB.png ファイルをローカルマシンにダウンロードします。

  2. ファイル名を android_robot.png に変更します。

  3. ファイルをハードドライブ上のプロジェクトの main/res/drawable ディレクトリに手動で移動します。

図 4 は、追加した画像と上記のコードサンプルのテキストがアプリに表示された状態を示しています。

アプリでレンダリングされた出力を示す画像
図 4. アプリ内でレンダリングされたアプリ内 HTML ファイルと画像ファイル

アプリを完成させるには、次の手順を行います。

  1. 次のコードを onCreate() メソッドに追加して、ハンドラを登録し AssetLoader を構成します。

    Kotlin

    val assetLoader = WebViewAssetLoader.Builder()
                           .addPathHandler("/assets/", AssetsPathHandler(this))
                           .addPathHandler("/res/", ResourcesPathHandler(this))
                           .build()
    webView.webViewClient = LocalContentWebViewClient(assetLoader)
    

    Java

    final WebViewAssetLoader assetLoader = new WebViewAssetLoader.Builder()
             .addPathHandler("/assets/", new WebViewAssetLoader.AssetsPathHandler(this))
             .addPathHandler("/res/", new WebViewAssetLoader.ResourcesPathHandler(this))
             .build();
    mWebView.setWebViewClient(new LocalContentWebViewClient(assetLoader));
    
  2. onCreate() メソッドに次のコードを追加して、コンテンツを読み込みます。

    Kotlin

    webView.loadUrl("https://appassets.androidplatform.net/assets/index.html")
    

    Java

    mWebView.loadUrl("https://appassets.androidplatform.net/assets/index.html");
    

アプリ内コンテンツとウェブサイトのリソースを組み合わせる

アプリ内コンテンツとインターネットのコンテンツ(ウェブサイトの CSS によってスタイル設定されたアプリ内 HTML ページなど)を組み合わせて読み込む必要が生じることがあります。WebViewAssetLoader は、このユースケースをサポートしています。登録された PathHandler インスタンスが、指定されたパスのリソースを検出できない場合、WebView はインターネットからのコンテンツの読み込みにフォールバックします。アプリ内コンテンツとウェブサイトのリソースを混在させる場合は、アプリ内リソース用に /assets//resources/ などのディレクトリ パスを予約します。これらの場所にウェブサイトのリソースを保存しないようにしてください。

Kotlin

val assetLoader = WebViewAssetLoader.Builder()
                        .setDomain("example.com") // Replace this with your website's domain.
                        .addPathHandler("/assets/", AssetsPathHandler(this))
                        .build()

webView.webViewClient = LocalContentWebViewClient(assetLoader)
val inAppHtmlUrl = "https://example.com/assets/index.html"
webView.loadUrl(inAppHtmlUrl)
val websiteUrl = "https://example.com/website/data.json"

// JavaScript code to fetch() content from the same origin.
val jsCode = "fetch('$websiteUrl')" +
        ".then(resp => resp.json())" +
        ".then(data => console.log(data));"

webView.evaluateJavascript(jsCode, null)

Java

final WebViewAssetLoader assetLoader = new WebViewAssetLoader.Builder()
           .setDomain("example.com") // Replace this with your website's domain.
           .addPathHandler("/assets/", new AssetsPathHandler(this))
           .build();

mWebView.setWebViewClient(new LocalContentWebViewClient(assetLoader));
String inAppHtmlUrl = "https://example.com/assets/index.html";
mWebView.loadUrl(inAppHtmlUrl);
String websiteUrl = "https://example.com/website/data.json";

// JavaScript code to fetch() content from the same origin.
String jsCode = "fetch('" + websiteUrl + "')" +
      ".then(resp => resp.json())" +
      ".then(data => console.log(data));";

mWebView.evaluateJavascript(jsCode, null);

ウェブでホストされている JSON データを取得するアプリ内 HTML ページの例については、GitHub の WebView デモをご覧ください。

loadDataWithBaseURL

アプリで HTML ページを読み込むだけでサブリソースをインターセプトする必要がない場合は、アプリアセットを必要としない loadDataWithBaseURL() の使用を検討してください。これは、次のコードサンプルで示すように使用できます。

Kotlin

val html = "<html><body><p>Hello world</p></body></html>"
val baseUrl = "https://example.com/"

webView.loadDataWithBaseURL(baseUrl, html, "text/html", null, baseUrl)

Java

String html = "<html><body><p>Hello world</p></body></html>";
String baseUrl = "https://example.com/";

mWebView.loadDataWithBaseURL(baseUrl, html, "text/html", null, baseUrl);

引数の値は慎重に選択してください。以下の点を考慮してください。

  • baseUrl: HTML コンテンツを読み込む URL。これは HTTP(S) URL である必要があります。
  • data: 文字列として表示する HTML コンテンツです。
  • mimeType: 通常は text/html に設定する必要があります。
  • encoding: baseUrl が HTTP(S) URL の場合、これは使用されないため、null に設定できます。
  • historyUrl: baseUrl と同じ値に設定されます。

アプリにおいて同一オリジン ポリシーに準拠するため、baseUrl には HTTP(S) URL を使用することを強くおすすめします。

コンテンツに適した baseUrl が見つからず、loadData() を使用する場合は、パーセント エンコードまたは Base64 エンコードを使用してコンテンツをエンコードする必要があります。次のコードサンプルに示すように、Base64 エンコードを選択し、Android API を使用してプログラムでエンコードすることを強くおすすめします。

Kotlin

val encodedHtml: String = Base64.encodeToString(html.toByteArray(), Base64.NO_PADDING)

webView.loadData(encodedHtml, mimeType, "base64")

Java

String encodedHtml = Base64.encodeToString(html.getBytes(), Base64.NO_PADDING);

mWebView.loadData(encodedHtml, mimeType, "base64");

非推奨事項

アプリ内コンテンツを読み込む方法は他にもいくつかありますが、使用しないことを強くおすすめします。

  • file:// URL と data: URL は不透明オリジンと見なされるため、fetch()XMLHttpRequest などの強力なウェブ API を利用できません。loadData() は内部で data: URL を使用するため、代わりに WebViewAssetLoader または loadDataWithBaseURL() を使用することをおすすめします。
  • file:// URL に関する問題は WebSettings.setAllowFileAccessFromFileURLs()WebSettings.setAllowUniversalAccessFromFileURLs() で回避できますが、true に設定するとアプリがファイルベースの攻撃に対して脆弱になるため、設定しないことをおすすめします。セキュリティを強化するため、すべての API レベルで明示的に false に設定することをおすすめします。
  • 同じ理由で、file://android_assets/ クラスと file://android_res/ URL はおすすめしません。AssetsHandler クラスと ResourcesHandler クラスは、ドロップイン代替品です。
  • MIXED_CONTENT_ALWAYS_ALLOW は使用しないでください。一般的に、この設定は必須ではなく、アプリのセキュリティを低下させます。アプリ内コンテンツをウェブサイトのリソースと同じスキーム(HTTP または HTTPS)で読み込み、必要に応じて MIXED_CONTENT_COMPATIBILITY_MODE または MIXED_CONTENT_NEVER_ALLOW を使用することをおすすめします。