provide default DivDownloader and DivRequestExecutor

commit_hash:8a4f6a1b6d06ce3f68c380b8f71f0cc2b9c1f79b
This commit is contained in:
bakalskayas
2025-08-29 19:16:55 +03:00
parent 4e4157597a
commit a7336d9d4d
17 changed files with 300 additions and 82 deletions
+7 -2
View File
@@ -667,6 +667,7 @@
"client/android/div-data/src/main/java/com/yandex/div/internal/util/JsonUtils.kt":"divkit/public/client/android/div-data/src/main/java/com/yandex/div/internal/util/JsonUtils.kt",
"client/android/div-data/src/main/java/com/yandex/div/json/JSONSerializable.java":"divkit/public/client/android/div-data/src/main/java/com/yandex/div/json/JSONSerializable.java",
"client/android/div-data/src/main/java/com/yandex/div/json/JsonTemplate.kt":"divkit/public/client/android/div-data/src/main/java/com/yandex/div/json/JsonTemplate.kt",
"client/android/div-data/src/main/java/com/yandex/div/json/LoadingErrorLogger.kt":"divkit/public/client/android/div-data/src/main/java/com/yandex/div/json/LoadingErrorLogger.kt",
"client/android/div-data/src/main/java/com/yandex/div/json/ParsingEnvironment.kt":"divkit/public/client/android/div-data/src/main/java/com/yandex/div/json/ParsingEnvironment.kt",
"client/android/div-data/src/main/java/com/yandex/div/json/ParsingEnvironmentExtensions.kt":"divkit/public/client/android/div-data/src/main/java/com/yandex/div/json/ParsingEnvironmentExtensions.kt",
"client/android/div-data/src/main/java/com/yandex/div/json/ParsingErrorLogger.java":"divkit/public/client/android/div-data/src/main/java/com/yandex/div/json/ParsingErrorLogger.java",
@@ -847,6 +848,12 @@
"client/android/div-markdown/jacoco.excludes":"divkit/public/client/android/div-markdown/jacoco.excludes",
"client/android/div-markdown/proguard-rules.pro":"divkit/public/client/android/div-markdown/proguard-rules.pro",
"client/android/div-markdown/src/main/java/com/yandex/div/markdown/DivMarkdownExtensionHandler.kt":"divkit/public/client/android/div-markdown/src/main/java/com/yandex/div/markdown/DivMarkdownExtensionHandler.kt",
"client/android/div-network/build.gradle":"divkit/public/client/android/div-network/build.gradle",
"client/android/div-network/jacoco.excludes":"divkit/public/client/android/div-network/jacoco.excludes",
"client/android/div-network/proguard-rules.pro":"divkit/public/client/android/div-network/proguard-rules.pro",
"client/android/div-network/src/main/java/com/yandex/div/network/DefaultDivDownloader.kt":"divkit/public/client/android/div-network/src/main/java/com/yandex/div/network/DefaultDivDownloader.kt",
"client/android/div-network/src/main/java/com/yandex/div/network/DefaultDivRequestExecutor.kt":"divkit/public/client/android/div-network/src/main/java/com/yandex/div/network/DefaultDivRequestExecutor.kt",
"client/android/div-network/src/main/java/com/yandex/div/network/DivParsingExtensions.kt":"divkit/public/client/android/div-network/src/main/java/com/yandex/div/network/DivParsingExtensions.kt",
"client/android/div-pinch-to-zoom/build.gradle":"divkit/public/client/android/div-pinch-to-zoom/build.gradle",
"client/android/div-pinch-to-zoom/jacoco.excludes":"divkit/public/client/android/div-pinch-to-zoom/jacoco.excludes",
"client/android/div-pinch-to-zoom/proguard-rules.pro":"divkit/public/client/android/div-pinch-to-zoom/proguard-rules.pro",
@@ -1823,9 +1830,7 @@
"client/android/divkit-demo-app/src/main/java/com/yandex/divkit/demo/div/DemoCustomContainerAdapter.kt":"divkit/public/client/android/divkit-demo-app/src/main/java/com/yandex/divkit/demo/div/DemoCustomContainerAdapter.kt",
"client/android/divkit-demo-app/src/main/java/com/yandex/divkit/demo/div/DemoDiv2Logger.kt":"divkit/public/client/android/divkit-demo-app/src/main/java/com/yandex/divkit/demo/div/DemoDiv2Logger.kt",
"client/android/divkit-demo-app/src/main/java/com/yandex/divkit/demo/div/DemoDivCustomViewAdapter.kt":"divkit/public/client/android/divkit-demo-app/src/main/java/com/yandex/divkit/demo/div/DemoDivCustomViewAdapter.kt",
"client/android/divkit-demo-app/src/main/java/com/yandex/divkit/demo/div/DemoDivDownloader.kt":"divkit/public/client/android/divkit-demo-app/src/main/java/com/yandex/divkit/demo/div/DemoDivDownloader.kt",
"client/android/divkit-demo-app/src/main/java/com/yandex/divkit/demo/div/DemoDivLottieRawResProvider.kt":"divkit/public/client/android/divkit-demo-app/src/main/java/com/yandex/divkit/demo/div/DemoDivLottieRawResProvider.kt",
"client/android/divkit-demo-app/src/main/java/com/yandex/divkit/demo/div/DemoDivRequestExecutor.kt":"divkit/public/client/android/divkit-demo-app/src/main/java/com/yandex/divkit/demo/div/DemoDivRequestExecutor.kt",
"client/android/divkit-demo-app/src/main/java/com/yandex/divkit/demo/div/DemoGlobalVariablesController.kt":"divkit/public/client/android/divkit-demo-app/src/main/java/com/yandex/divkit/demo/div/DemoGlobalVariablesController.kt",
"client/android/divkit-demo-app/src/main/java/com/yandex/divkit/demo/div/DemoNestedScrollView.kt":"divkit/public/client/android/divkit-demo-app/src/main/java/com/yandex/divkit/demo/div/DemoNestedScrollView.kt",
"client/android/divkit-demo-app/src/main/java/com/yandex/divkit/demo/div/Div2Activity.kt":"divkit/public/client/android/divkit-demo-app/src/main/java/com/yandex/divkit/demo/div/Div2Activity.kt",
@@ -0,0 +1,21 @@
package com.yandex.div.json
import com.yandex.div.internal.Assert
import com.yandex.div.internal.KLog
interface LoadingErrorLogger {
fun logError(e: Exception)
companion object {
val ASSERT: LoadingErrorLogger = object : LoadingErrorLogger {
override fun logError(e: Exception) {
Assert.fail(e.message, e)
}
}
val LOG: LoadingErrorLogger = object : LoadingErrorLogger {
override fun logError(e: Exception) {
KLog.e("LoadingErrorLogger", e) { "An error occurred during loading process" }
}
}
}
}
+12
View File
@@ -0,0 +1,12 @@
apply from: "${project.projectDir}/../div-library.gradle"
apply from: "${project.projectDir}/../div-tests.gradle"
apply from: "${project.projectDir}/../publish-android.gradle"
android {
namespace 'com.yandex.div.network'
}
dependencies {
implementation project(path: ':div')
implementation libs.okhttp
}
@@ -0,0 +1,7 @@
#####################
# Generated classes #
#####################
**/R.class
**/R$*.class
**/BuildConfig.*
**/Manifest*.*
View File
@@ -0,0 +1,118 @@
package com.yandex.div.network
import com.yandex.div.core.downloader.DivDownloader
import com.yandex.div.core.downloader.DivPatchDownloadCallback
import com.yandex.div.core.images.LoadReference
import com.yandex.div.core.view2.Div2View
import com.yandex.div.histogram.DivParsingHistogramReporter
import com.yandex.div.json.LoadingErrorLogger
import com.yandex.div.json.ParsingErrorLogger
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import kotlinx.coroutines.cancel
import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient
import okhttp3.Request
import org.json.JSONException
import org.json.JSONObject
import java.io.IOException
/**
* The [downloadJson] function defines how raw JSON is fetched from a URL,
* allowing integration with different HTTP clients.
*/
class DefaultDivDownloader(
private val downloadJson: suspend (url: String) -> Result<String>,
private val scope: CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Default),
private val histogramReporter: DivParsingHistogramReporter = DivParsingHistogramReporter.DEFAULT,
private val parsingErrorLogger: ParsingErrorLogger = ParsingErrorLogger.LOG,
private val loadingErrorLogger: LoadingErrorLogger = LoadingErrorLogger.LOG,
) : DivDownloader {
@JvmOverloads
constructor(
client: OkHttpClient,
scope: CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Default),
histogramReporter: DivParsingHistogramReporter = DivParsingHistogramReporter.DEFAULT,
parsingErrorLogger: ParsingErrorLogger = ParsingErrorLogger.LOG,
loadingErrorLogger: LoadingErrorLogger = LoadingErrorLogger.LOG
) : this(
downloadJson = { url -> client.downloadJson(url) },
scope = scope,
histogramReporter = histogramReporter,
parsingErrorLogger = parsingErrorLogger,
loadingErrorLogger = loadingErrorLogger,
)
override fun downloadPatch(
divView: Div2View,
downloadUrl: String,
callback: DivPatchDownloadCallback
): LoadReference {
val job = scope.launch {
val json: String = try {
val result = downloadJson(downloadUrl)
result.getOrElse { t ->
logFailAndNotify(downloadUrl, t, callback)
return@launch
}
} catch (e: Exception) {
logFailAndNotify(downloadUrl, e, callback)
return@launch
}
val patch = try {
JSONObject(json).asDivPatchWithTemplates(histogramReporter, parsingErrorLogger)
} catch (e: JSONException) {
parsingErrorLogger.logError(
IllegalArgumentException("Failed to parse patch JSON from $downloadUrl", e)
)
notifyMain { callback.onFail() }
return@launch
}
notifyMain { callback.onSuccess(patch) }
}
return LoadReference { job.cancel("cancel all downloads") }
}
private suspend fun notifyMain(action: suspend () -> Unit) {
withContext(Dispatchers.Main) { action() }
}
private suspend fun logFailAndNotify(
downloadUrl: String,
cause: Throwable,
callback: DivPatchDownloadCallback
) {
loadingErrorLogger.logError(
IOException("Failed to download patch from $downloadUrl", cause)
)
notifyMain { callback.onFail() }
}
}
private suspend fun OkHttpClient.downloadJson(uri: String): Result<String> {
return withContext(Dispatchers.IO) {
val request = Request.Builder()
.url(uri)
.get()
.build()
newCall(request).execute().use { resp ->
if (!resp.isSuccessful) {
return@use Result.failure(
IOException("HTTP ${resp.code} ${resp.message} for $uri")
)
}
val body = resp.body?.string()
?: return@use Result.failure(
IllegalStateException("Empty response body for $uri")
)
Result.success(body)
}
}
}
@@ -0,0 +1,91 @@
package com.yandex.div.network
import com.yandex.div.core.DivRequestExecutor
import com.yandex.div.core.images.LoadReference
import com.yandex.div.json.LoadingErrorLogger
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import java.io.IOException
/**
* The [executeRequest] function — network request handler, pluggable for different HTTP clients.
*/
class DefaultDivRequestExecutor(
private val executeRequest: suspend (DivRequestExecutor.Request) -> Result<Unit>,
private val scope: CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO),
private val loadingErrorLogger: LoadingErrorLogger = LoadingErrorLogger.LOG,
) : DivRequestExecutor {
@JvmOverloads
constructor(
client: OkHttpClient,
scope: CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO),
loadingErrorLogger: LoadingErrorLogger = LoadingErrorLogger.LOG,
) : this(
executeRequest = { request -> client.executeRequest(request) },
scope = scope,
loadingErrorLogger = loadingErrorLogger,
)
override fun execute(
request: DivRequestExecutor.Request,
callback: DivRequestExecutor.Callback?
): LoadReference {
val job = scope.launch(Dispatchers.Main) {
try {
val result = withContext(Dispatchers.IO) {
executeRequest(request)
}
result
.onSuccess { callback?.onSuccess() }
.onFailure { err ->
logFailAndNotify(request, err, callback)
}
} catch (e: Exception) {
logFailAndNotify(request, e, callback)
}
}
return LoadReference { job.cancel("Cancel submit action") }
}
private fun logFailAndNotify(
request: DivRequestExecutor.Request,
cause: Throwable,
callback: DivRequestExecutor.Callback?
) {
loadingErrorLogger.logError(
IllegalStateException(
"Error while executing request [${request.method} ${request.url}]", cause
)
)
callback?.onFail()
}
}
private suspend fun OkHttpClient.executeRequest(
request: DivRequestExecutor.Request
): Result<Unit> {
val httpRequest = Request.Builder()
.url(request.url.toString())
.method(request.method, request.body.toRequestBody())
.apply { request.headers?.forEach { addHeader(it.name, it.value) } }
.build()
newCall(httpRequest).execute().use { resp ->
return if (resp.isSuccessful) {
Result.success(Unit)
} else {
Result.failure(
IOException("HTTP request failed ${resp.code} ${resp.message} [${request.method} ${request.url}]")
)
}
}
}
@@ -0,0 +1,31 @@
package com.yandex.div.network
import com.yandex.div.data.DivParsingEnvironment
import com.yandex.div.histogram.DivParsingHistogramReporter
import com.yandex.div.json.ParsingErrorLogger
import com.yandex.div2.DivPatch
import org.json.JSONObject
internal fun JSONObject.asDivPatchWithTemplates(
histogramReporter: DivParsingHistogramReporter,
errorLogger: ParsingErrorLogger,
): DivPatch {
val templates = optJSONObject("templates")
val card = getJSONObject("patch")
val environment = DivParsingEnvironment(errorLogger)
templates?.let {
environment.parseTemplatesWithHistograms(it, histogramReporter)
}
return DivPatch(environment, card)
}
private fun DivParsingEnvironment.parseTemplatesWithHistograms(
templates: JSONObject,
histogramReporter: DivParsingHistogramReporter,
) {
histogramReporter.measureTemplatesParsing(templates, null) {
parseTemplates(templates)
}
}
@@ -582,6 +582,9 @@ public class DivConfiguration {
return this;
}
/**
* A default implementation, [DefaultDivDownloader], is available.
*/
@NonNull
public Builder divDownloader(@NonNull DivDownloader divDownloader) {
mDivDownloader = divDownloader;
@@ -3,6 +3,9 @@ package com.yandex.div.core
import android.net.Uri
import com.yandex.div.core.images.LoadReference
/**
* A default implementation, [DefaultDivRequestExecutor], is available.
*/
interface DivRequestExecutor {
class Header(val name: String, val value: String)
@@ -7,6 +7,7 @@ import com.yandex.div.core.view2.Div2View;
/**
* Downloads patches for Divs
* A default implementation, [DefaultDivDownloader], is provided.
*/
@PublicApi
public interface DivDownloader {
@@ -88,6 +88,7 @@ dependencies {
implementation project(path: ':div-json')
implementation project(path: ':div-lottie')
implementation project(path: ':div-markdown')
implementation project(path: ':div-network')
implementation project(path: ':div-pinch-to-zoom')
implementation project(path: ':div-rive')
implementation project(path: ':div-shimmer')
@@ -8,9 +8,9 @@ import com.yandex.android.beacon.SendBeaconPerWorkerLogger
import com.yandex.div.core.DivKit
import com.yandex.div.core.DivKitConfiguration
import com.yandex.div.internal.Assert
import com.yandex.div.network.DefaultDivRequestExecutor
import com.yandex.divkit.demo.beacon.SendBeaconRequestExecutorImpl
import com.yandex.divkit.demo.beacon.SendBeaconWorkerSchedulerImpl
import com.yandex.divkit.demo.div.DemoDivRequestExecutor
import com.yandex.divkit.demo.utils.VisualAssertionErrorHandler
import com.yandex.divkit.regression.di.HasRegressionTesting
import com.yandex.divkit.regression.di.RegressionComponent
@@ -67,7 +67,7 @@ class DivkitApplication : Application(), HasRegressionTesting {
DivKitConfiguration.Builder()
.sendBeaconConfiguration(configureSendBeacon(okHttpClient))
.histogramConfiguration(Container::histogramConfiguration)
.divRequestExecutor { DemoDivRequestExecutor(okHttpClient) }
.divRequestExecutor { DefaultDivRequestExecutor(Container.httpClient)}
.build()
)
@@ -1,35 +0,0 @@
package com.yandex.divkit.demo.div
import com.yandex.div.core.downloader.DivDownloader
import com.yandex.div.core.downloader.DivPatchDownloadCallback
import com.yandex.div.core.images.LoadReference
import com.yandex.div.core.view2.Div2View
import com.yandex.divkit.demo.Container
import com.yandex.divkit.demo.utils.loadText
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import org.json.JSONException
import org.json.JSONObject
class DemoDivDownloader : DivDownloader {
override fun downloadPatch(divView: Div2View, downloadUrl: String, callback: DivPatchDownloadCallback): LoadReference {
val job = GlobalScope.launch(Dispatchers.Main) {
val json = Container.httpClient.loadText(downloadUrl)
if (json != null) {
try {
callback.onSuccess(JSONObject(json).asDivPatchWithTemplates())
} catch (e: JSONException) {
callback.onFail()
}
} else {
callback.onFail()
}
}
return LoadReference {
job.cancel("cancel all downloads")
}
}
}
@@ -1,42 +0,0 @@
package com.yandex.divkit.demo.div
import com.yandex.div.core.DivRequestExecutor
import com.yandex.div.core.images.LoadReference
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
class DemoDivRequestExecutor(private val okHttpClient: OkHttpClient) : DivRequestExecutor {
private val scope = MainScope()
override fun execute(request: DivRequestExecutor.Request, callback: DivRequestExecutor.Callback?): LoadReference {
val job = scope.launch(Dispatchers.Main) {
runCatching {
withContext(Dispatchers.IO) {
okHttpClient.newCall(request.toHttpRequest()).execute()
}
}.getOrNull() ?: run {
callback?.onFail()
return@launch
}
callback?.onSuccess()
}
return LoadReference { job.cancel("Cancel submit action") }
}
private fun DivRequestExecutor.Request.toHttpRequest(): Request {
return Request.Builder()
.url(url.toString())
.method(method, body.toRequestBody())
.apply {
headers?.forEach { addHeader(it.name, it.value) }
}
.build()
}
}
@@ -26,6 +26,7 @@ import com.yandex.div.json.templates.CachingTemplateProvider
import com.yandex.div.json.templates.InMemoryTemplateProvider
import com.yandex.div.json.templates.TemplateProvider
import com.yandex.div.markdown.DivMarkdownExtensionHandler
import com.yandex.div.network.DefaultDivDownloader
import com.yandex.div.shimmer.DivShimmerExtensionHandler
import com.yandex.div.shine.DivShineExtensionHandler
import com.yandex.div.shine.DivShineLogger
@@ -79,7 +80,7 @@ fun divConfiguration(
flagPreferenceProvider.getExperimentFlag(Experiment.RENDER_EFFECT_ENABLED)
)
.tooltipRestrictor { _, _, _, _ -> true }
.divDownloader(DemoDivDownloader())
.divDownloader(DefaultDivDownloader(Container.httpClient))
.typefaceProvider(YandexSansDivTypefaceProvider(activity))
.additionalTypefaceProviders(
mapOf(
+1
View File
@@ -35,6 +35,7 @@ include ':div-histogram'
include ':div-json'
include ':div-lottie'
include ':div-markdown'
include ':div-network'
include ':div-pinch-to-zoom'
include ':div-rive'
include ':div-shimmer'