BASEプロダクトチームブログ

ネットショップ作成サービス「BASE ( https://thebase.in )」、ショッピングアプリ「BASE ( https://thebase.in/sp )」のプロダクトチームによるブログです。

Android WebViewで少し厄介だった実装の紹介

この記事は BASE Advent Calendar 2023 の18日目の記事です。

はじめに

こんにちは、Pay IDアプリ開発チームでエンジニアをしている小林(@eijenson)です。

ショッピングアプリ「Pay ID」のAndroid版アプリの開発を担当しています。

本アプリでは一部機能でWebViewを使って実装しています。

そこで少し厄介だった仕様とそれをどう実装したかを紹介していこうと思います。

使用言語/ライブラリのバージョン

Kotlin 1.7.10

Jetpack Compose BOM 2023.06.01

今回の画面設計

よくあるMVVMの設計です。

UIはJetpack Composeで書かれています。

WebViewに関してはJetpack Composeでは用意されていないので、AndroidViewを使ってWebViewクラスをCompose内に定義しています。

AACのViewModelを使っていて、WebViewClient.shouldOverrideUrlLoadingやdoUpdateVisitedHistory等々のタイミングでViewModelのメソッドを叩いてタイミングに応じた処理を行い、その結果をFlowのイベントとしてUIクラスに伝えて結果を反映させます。

_blankのリンクをタップしたときだけ別ブラウザを開きたい

WebViewで開くリンクで、ブラウザで新しいタブを開きたい時に設定する <a href=”https://〇〇” target=”_blank”> といった_blankの設定は無視されます。

WebView でウェブアプリを作成する  |  Android Developers

無視されると、WebView上でこのリンクをタップした際にはWebView内で遷移します。

Pay IDアプリでは購入操作をWebViewで行い、ヘルプページ等へのリンクは購入画面を表示しているWebViewではなく別ブラウザで表示したいということになりました。

そのために実装したコードは以下のようなものです。

fun Screen(
  // target="_blank"のリンクをタップした時の画面遷移操作をActivity等で実装してListenerに設定する
  onClickTargetBlankLinkListener: (Uri) -> Unit,
){
//... 省略(ツールバーの設置とかテーマの設定とか)
AndroidView(
  factory = {
    val webView = WebView(it)
    // target="_blank"のリンクをタップしたときにonCreateWindowが反応するようにする
    webView.settings.setSupportMultipleWindows(true)
    webView.webChromeClient = createCheckoutCartWebChromeClient(onClickTargetBlankLinkListener)
  }
)
//... 省略
}

private fun createWebChromeClient(
    onClickTargetBlankLinkListener: (Uri) -> Unit,
): WebChromeClient {
    return object : WebChromeClient() {
        // target="_blank"のリンクをタップした際の挙動を実装
        override fun onCreateWindow(
            view: WebView?,
            isDialog: Boolean,
            isUserGesture: Boolean,
            resultMsg: Message?
        ): Boolean {
            view ?: return false
            val webView = WebView(view.context)
            webView.webViewClient = object : WebViewClient() {
                override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean {
                    val url = request?.url ?: return false
                    // _blankのリンク等新しいタブが作られる際だけ発火する
                    onClickTargetBlankLinkListener(url)
                    view?.destory() // メモリリーク防止/警告が出ますので後述します
                    return true
                }
            }

            val transport = resultMsg?.obj as WebView.WebViewTransport
            transport.webView = webView
            resultMsg.sendToTarget()

            return true
        }
    }
}

setSupportMultipleWindows(true)と設定してますが、これだけだと_blankのリンクは遷移せず何もしなくなってしまいます。

そこで、WebChromeClient.onCreateWindowにて一時的に使うWebViewを作成します。

そのWebViewのshouldOverrideUrlLoadingメソッドから_blankで遷移するURLを受け取り、Listenerに投げます。

そうすることでtarget=”_blank”のリンクがタップされた時だけ動く処理が描けるようになります

Pay IDアプリでは、その処理の中で別ブラウザでWebページを開くようにしました。

onCreateWindowメソッド内でgetHitTestResult() からURLを取得する方法もあるのですが、新しいタブを開く操作はWebページにあるJavaScriptが行うこともできて、その場合動かない可能性があったので今回の方法を取りました。

一つ解決していない問題として、この方法を使った際に_blankのリンクをタップしたタイミングで以下の警告が表示されます。

W  Application attempted to call on a destroyed WebView
    java.lang.Throwable
        at org.chromium.android_webview.AwContents.s(chromium-TrichromeWebViewGoogle6432.aab-stable-604519333:10)
        at WV.s8.loadingStateChanged(chromium-TrichromeWebViewGoogle6432.aab-stable-604519333:4)
        at J.N.MlAm1rvf(Native Method)
        at org.chromium.android_webview.AwContents.Q(chromium-TrichromeWebViewGoogle6432.aab-stable-604519333:56)
        at com.android.webview.chromium.WebViewChromium.b(chromium-TrichromeWebViewGoogle6432.aab-stable-604519333:21)
        at com.android.webview.chromium.N.handleMessage(chromium-TrichromeWebViewGoogle6432.aab-stable-604519333:44)
        at android.os.Handler.dispatchMessage(Handler.java:106)
        at android.os.Looper.loopOnce(Looper.java:205)
        at android.os.Looper.loop(Looper.java:294)
        at android.app.ActivityThread.main(ActivityThread.java:8194)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:552)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:971)

こちらは一時的に作成したWebViewを view?.destory()で後処理をした場合に発生します。

destroy()をしないとAndroid StudioのProfilerで確認するとメモリが_blankのリンクをタップするたびに増えていって減らないため、例外が出るのは許容しています。

一部ページが全て表示されず見切れてしまう

こちらはリリース後に一部のページが見切れて表示されるという不具合が発生しました。

調査の結果、Jetpack Compose上で.fillMaxSizeを使っていたのですが、そのほかにWebViewの設定でちゃんとmatch_parentを設定する必要がありました。

Box(
  modifier = Modifier.fillMaxSize(),
) {
  AndroidView(
    factory = {
      val webView = WebView(it)
      webView.layoutParams = ViewGroup.LayoutParams(
        ViewGroup.LayoutParams.MATCH_PARENT,
        ViewGroup.LayoutParams.MATCH_PARENT
      ) // こちらの設定を追加
      //... 省略
    }, update = {
      //... 省略
    })
}

Boxの部分にModifier.fillMaxSize()で画面サイズいっぱいにWebViewを表示しようとしていたのですが、確かにこれでWebviewは画面サイズまで広がります。

しかし、WebView側の設定もしないと、以下のようなcssが設定されている画面では見切れてしまいました

<!DOCTYPE html PUBLIC
        "-//W3C//DTD XHTML 1.0 Transitional//EN"
        "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<style type="text/css">
body {
<!-- この3つの記述のあるcssを使っているページだと見切れてしまう -->
    display: flex;
    align-items: center;
    height: 100VH;
}
</style>
</head>
<body>
<div>
ここが見切れます<br>
ここから見えるようになります
</div>
</body>
</html>
MATCH_PARENTを設定してない場合 設定した場合

WebViewで不特定多数のWebページを表示することはあまりないと思うので、特定の画面を表示するならそれらのページは事前にチェックする必要があると痛感した出来事でした。

さいごに

今回はWebViewに関することを書いていきました。

同じような所で困った人の一助になれば嬉しいです。

明日の19日目は ImazekiShota さんの記事です、お楽しみに!