ユーザ用ツール

サイト用ツール


サイドバー

プログレス合同会社

広告

android:studio:application:webview

31.WebViewの導入

OkHttpでは、JavaScriptでコンテンツを生成する(動的コンテンツ)サイトからデータを取得することができません。
動的コンテンツサイトからデータを取得するため、WebViewを導入します。

レガシーなAndroidでも動作するように、WebViewのラッパーandroidx.webkitを使用します。

バージョンカタログファイルにandroidx.webkitのバージョン定義を追記します。

[versions]
   :
webkit = "1.14.0"                   # https://mvnrepository.com/artifact/androidx.webkit/webkit

[libraries]
   :
webkit = { module = "androidx.webkit:webkit", version.ref = "webkit" }
   :

3行目[versions]
コメント部分のURLを参照して、最新安定バージョンを指定します。
7行目[libraries]
ライブラリモジュールとバージョンを関連付けします。

追記後、『Sync Now』で内容をプロジェクトに反映させます。

Webkitモジュールの参照

:core:dataモジュールのbuild.gradle.ktsWebkitモジュールへの参照を追記します。

   :
dependencies {
   :
  implementation(libs.webkit)
}

4行目
Webkitモジュールを参照します。

追記後、『同期アイコン』で内容をプロジェクトに反映させます。

リポジトリ層の作成

リポジトリ層にWebViewのインターフェースを作成します。
ファイル名をWebViewHtmlRepository.ktにしています。

package jp.co.example.android01.core.data.repository

interface WebViewHtmlRepository {
  suspend fun getHtmlContent(url: String): Result<String>
}

4行目
urlのHTMLを取得します。

リポジトリ層の実装クラスを作成します。
ファイル名をWebViewHtmlRepositoryImpl.ktにしています。

package jp.co.example.android01.core.data.repository

import android.annotation.SuppressLint
import javax.inject.Singleton
import javax.inject.Inject
import android.content.Context
import kotlinx.coroutines.withContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withTimeoutOrNull
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import android.webkit.WebView
import androidx.webkit.WebViewFeature
import androidx.webkit.WebSettingsCompat
import androidx.webkit.WebViewClientCompat

/**
 * WebViewによるHTMLコンテンツ取得用のリポジトリ実装
 */
@Singleton
class WebViewHtmlRepositoryImpl @Inject constructor(
  private val context: Context
) : WebViewHtmlRepository {
  override suspend fun getHtmlContent(url: String): Result<String> = withContext(Dispatchers.Main) {
    try {
      val htmlContent = withTimeoutOrNull(30000) { // 30秒のタイムアウト
        loadUrlWithWebView(url)
      }
      if (htmlContent != null) {
        Result.success(htmlContent)
      } else {
        Result.failure(Exception("WebViewでのHTML取得がタイムアウトしました"))
      }
    } catch (e: Exception) {
      Result.failure(e)
    }
  }

  @SuppressLint( "SetJavaScriptEnabled" )
  private suspend fun loadUrlWithWebView(url: String): String = suspendCancellableCoroutine { continuation ->
    val webView = WebView(context).apply {
      // メモリ効率のための設定
      setLayerType(WebView.LAYER_TYPE_HARDWARE, null)
      // ヘッドレス用のサイズ設定(最小限)
      layoutParams = android.view.ViewGroup.LayoutParams(1, 1)
      // androidx.webkitを使用したヘッドレス設定
      settings.apply {
        javaScriptEnabled = true
        domStorageEnabled = true
        loadWithOverviewMode = true
        useWideViewPort = true
        builtInZoomControls = false
        displayZoomControls = false
        setSupportZoom(false)
        userAgentString = "Mozilla/5.0 (Linux; Android 10; SM-G975F) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.120 Mobile Safari/537.36"
        // ヘッドレス用の最適化設定
        cacheMode = android.webkit.WebSettings.LOAD_DEFAULT
        setGeolocationEnabled(false)
        mediaPlaybackRequiresUserGesture = false
        // androidx.webkitの機能を使用
        if (WebViewFeature.isFeatureSupported(WebViewFeature.ALGORITHMIC_DARKENING)) {
          WebSettingsCompat.setAlgorithmicDarkeningAllowed(settings, false)
        }
        // 画像読み込みを無効化してパフォーマンス向上(必要に応じて)
        blockNetworkImage = false
        blockNetworkLoads = false
      }
      // WebViewClientCompatを使用
      webViewClient = object : WebViewClientCompat() {
        override fun onPageFinished(view: WebView?, url: String?) {
          super.onPageFinished(view, url)
          // JavaScript実行後にHTMLを取得
          view?.evaluateJavascript(
            "(function() { return document.documentElement.outerHTML; })();"
          ) { html ->
            // JavaScriptの結果からHTMLを抽出
            val cleanHtml = html
              ?.removeSurrounding("\"")
              ?.replace("\\\"", "\"")
              ?.replace("\\n", "\n")
              ?.replace("\\/", "/")
              ?.replace("\\u003C", "<")
              ?.replace("\\u003E", ">")
              ?.replace("\\u003D", "=")
              ?.replace("\\u0026", "&")
            if (continuation.isActive) {
              continuation.resume(cleanHtml ?: "")
            }
          }
        }
        override fun onReceivedHttpError(view: WebView, request: android.webkit.WebResourceRequest, errorResponse: android.webkit.WebResourceResponse) {
          super.onReceivedHttpError(view, request, errorResponse)
          if (errorResponse.statusCode >= 400 && continuation.isActive) {
            continuation.resumeWithException(Exception("HTTP Error: ${errorResponse.statusCode}"))
          }
        }
      }
    }

    // キャンセル時の処理
    continuation.invokeOnCancellation {
      try {
        webView.stopLoading()
        webView.clearHistory()
        webView.clearCache(true)
        webView.loadUrl("about:blank")
        webView.destroy()
      } catch (_: Exception) {
        // エラーを無視してクリーンアップを続行
      }
    }

    // URLを読み込み開始
    webView.loadUrl(url)
  }
}

22行目
@Inject constructorHiltにクラスの生成方法を伝えます。
77行目~86行目
取得したHTMLはXSS対策のためエスケープされているのでデコードします。

DI(Hilt)モジュールの追加

リポジトリを追加したので、DI(Hilt)モジュールを追加します。

   :
import jp.co.example.android01.core.data.repository.WebViewHtmlRepository
import jp.co.example.android01.core.data.repository.WebViewHtmlRepositoryImpl
   :
object HiltModule {
   :
  @Provides
  @Singleton
  fun provideWebViewHtmlRepository(
    @ApplicationContext context: Context
  ): WebViewHtmlRepository {
    return WebViewHtmlRepositoryImpl(context)
  }
   :

2行目~3行目
必要なimportを追加します。
7行目~13行目
WebViewHtmlRepositoryの実装はWebViewHtmlRepositoryImplであることをHiltに伝えています。

android/studio/application/webview.txt · 最終更新: by プログレス合同会社