💭

accompanist で使用する WebView の指定サイズには気をつけよう

2024/12/09に公開

ポート株式会社 サービス開発部 Advent Calendar 2024 の9日目です。

初めに

ポート株式会社でAndroid開発をしている@shxun6934 です。

弊社のAndroidアプリでは、WebViewをComposeで表現する際、accompanist.webviewを使用しています。

accompanist.webviewを使用していたそんなある日の出来事です。

ユーザーからのお問い合わせから

ユーザーさんのお問い合わせから意見をいただきました。

ある画面のボタンを押しても、何も表示されない画面になり、操作できませんでした

その画面は、WebViewになっていて、accompanistwebviewを使用していました。

なぜ問い合わせ内容の挙動になっていたのか、その理由を見つけるべく、accompanist.webviewの挙動を調べることにしました。

accompanist.webviewとは

話に入る前に、まず、accompanist.webviewについて紹介します。

accompanist.webviewは、WebView をラップしたCompose を提供してくれるライブラリーです。

https://google.github.io/accompanist/web

使い方

基本的な使い方は、至ってシンプル。

rememberWebViewStateにWebViewで表示したいURLを渡して、WebViewに渡すだけです。

val state = rememberWebViewState(
    url = "https://example.com"
)

WebView(
    state = state
)

(アプリがインターネット通信できるようしておかないといけません。)

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <!-- インターネット通信できるようにする -->
    <uses-permission android:name="android.permission.INTERNET"/>

    <application>

    <!-- 中略 -->

    </application>
</manifest>

簡単そうに見えて...

では、例としてGitHubを開いてみましょう。

val state = rememberWebViewState(
    url = "https://github.com"
)

WebView(
    state = state,
    onCreated = {
        it.settings.apply {
            // JSを許可
            javaScriptEnabled = true
        }
    }
)

(画質荒くて申し訳ないです。。。)

一見よさそうだけど...

アプリの画面全体にWebViewが表示されていて、スクロールもできている、ハンバーガメニューをタップすると、メニューも開けているように見えます。

ですが、、、

「Sign Up」や「Sign In」が画面全体ではなかったり、ハンバーガメニューの検索欄がちょっと違和感があります。

なぜこうなっているのか

普段、UIをCompose で書いている人からすれば、当たり前のことなのですが、Modifierでサイズを指定してないとこうなってしまいます。

内部のコードを見てみる

accompanist.webviewの内部コードを見てみましょう。

https://github.com/google/accompanist/blob/v0.36.0/web/src/main/java/com/google/accompanist/web/WebView.kt より

BoxWithConstraints(modifier) {
    // WebView changes it's layout strategy based on
    // it's layoutParams. We convert from Compose Modifier to
    // layout params here.
    val width =
        if (constraints.hasFixedWidth)
            LayoutParams.MATCH_PARENT
        else
            LayoutParams.WRAP_CONTENT
    val height =
        if (constraints.hasFixedHeight)
            LayoutParams.MATCH_PARENT
        else
            LayoutParams.WRAP_CONTENT

    val layoutParams = FrameLayout.LayoutParams(
        width,
        height
    )

    /* 中略 */
}

BoxWithConstraints内では、親のレイアウトの制約情報(== Constraints)を取得できます。

minWidthmaxWidthminHeightmaxHeightといったような情報ですね。

accompanist.webviewは、親のレイアウトの制約情報を取得し、WidthおよびHeightの制約がFixed、つまりレスポンシブであるかどうかでレイアウトのパラメータを決めています。

レスポンシブである場合は親のレイアウトの制約に合わせるMATCH_PARENT、そうでない場合は自身のレイアウトのサイズに合わせるWRAP_CONTENTになります。

原因

例に挙げたGitHubのような、スクロールできるぐらいのHeightが大きいWebページだと、Androidの端末によっては、WebViewのレイアウトサイズが画面全体になります。

そのため、内部のコード的にはWRAP_CONTENTの制約になっていても、MATCH_PARENTの制約がかかっているように見えてしまいます。

画面遷移

Webページ内で画面遷移をした際に、WebページのHeightが画面以内に収まっているとWRAP_CONTENT本来の制御になり、WebページのHeight分しか表示されなくなります。

ダイアログ・モーダル

Webページ内のコンテンツをタップした際にダイアログやモーダルを表示する挙動になっている場合は、ダイアログやモーダルが画面内に正常に表示されなくなります。

詳しいレンダリングは自分も深く理解できていないのですが、おそらく、WRAP_CONTENTの制約だと、ダイアログやモーダルを表示するための領域をあらかじめ確保していないと思われます。

(ご存じの方いたらコメントやX等で教えてくださるとうれしいです!)

サイズ指定を行うと

こうしないためには、MATCH_PARENTの制約をWebViewにかけるようにしてあげます。
ということで、Modifier.fillMaxHeight()を指定するとちゃんと動くようになります。

val state = rememberWebViewState(
    url = "https://github.com"
)

WebView(
    state = state,
+    modifier = Modifier.fillMaxHeight(),
    onCreated = {
        it.settings.apply {
            // JSを許可
            javaScriptEnabled = true
        }
    }
)

最後に

一見実装できているように見えて、実際に動作確認してみるとできていないことがあると思います。

そういう時は、やはり、コードを見て仕組みがどうなっているかを理解するのが一番だと改めて感じました。

時間がある時に、ライブラリーを使用する際は内部のコード見ておくのもいいかもしれないですね。

10日目は、ogom さんの記事です!

余談

accompanist.webviewはDeprecatedなので、今後使うことはないと思います。

ただし、公式から、移行する場合はForkすることをお勧めされているので、しっかりと内部コードを理解しておくと特に不具合なく移行できると思います。

参考

ポート株式会社 エンジニアブログ

Discussion