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" }
:
追記後、『Sync Now』で内容をプロジェクトに反映させます。
:core:dataモジュールのbuild.gradle.ktsにWebkitモジュールへの参照を追記します。
:
dependencies {
:
implementation(libs.webkit)
}
追記後、『同期アイコン』で内容をプロジェクトに反映させます。
リポジトリ層にWebViewのインターフェースを作成します。
ファイル名をWebViewHtmlRepository.ktにしています。
package jp.co.example.android01.core.data.repository
interface WebViewHtmlRepository {
suspend fun getHtmlContent(url: String): Result<String>
}
リポジトリ層の実装クラスを作成します。
ファイル名を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)
}
}
リポジトリを追加したので、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)
}
: