Added DivContextComponent

commit_hash:abb1eac222c9e3e86cc2d1eb2fdac23edb091fab
This commit is contained in:
pkurchatov
2026-03-11 01:39:08 +03:00
parent 61077774fe
commit b03334bd2e
15 changed files with 462 additions and 15 deletions
+7
View File
@@ -600,8 +600,14 @@
"client/android/compose/proguard-rules.pro":"divkit/public/client/android/compose/proguard-rules.pro",
"client/android/compose/src/main/kotlin/com/yandex/div/compose/DivComposeConfiguration.kt":"divkit/public/client/android/compose/src/main/kotlin/com/yandex/div/compose/DivComposeConfiguration.kt",
"client/android/compose/src/main/kotlin/com/yandex/div/compose/DivContext.kt":"divkit/public/client/android/compose/src/main/kotlin/com/yandex/div/compose/DivContext.kt",
"client/android/compose/src/main/kotlin/com/yandex/div/compose/DivReporter.kt":"divkit/public/client/android/compose/src/main/kotlin/com/yandex/div/compose/DivReporter.kt",
"client/android/compose/src/main/kotlin/com/yandex/div/compose/DivView.kt":"divkit/public/client/android/compose/src/main/kotlin/com/yandex/div/compose/DivView.kt",
"client/android/compose/src/main/kotlin/com/yandex/div/compose/DivViewPreview.kt":"divkit/public/client/android/compose/src/main/kotlin/com/yandex/div/compose/DivViewPreview.kt",
"client/android/compose/src/main/kotlin/com/yandex/div/compose/dagger/DivContextComponent.kt":"divkit/public/client/android/compose/src/main/kotlin/com/yandex/div/compose/dagger/DivContextComponent.kt",
"client/android/compose/src/main/kotlin/com/yandex/div/compose/dagger/DivContextModule.kt":"divkit/public/client/android/compose/src/main/kotlin/com/yandex/div/compose/dagger/DivContextModule.kt",
"client/android/compose/src/main/kotlin/com/yandex/div/compose/dagger/DivContextScope.kt":"divkit/public/client/android/compose/src/main/kotlin/com/yandex/div/compose/dagger/DivContextScope.kt",
"client/android/compose/src/main/kotlin/com/yandex/div/compose/expressions/DivComposeExpressionResolver.kt":"divkit/public/client/android/compose/src/main/kotlin/com/yandex/div/compose/expressions/DivComposeExpressionResolver.kt",
"client/android/compose/src/main/kotlin/com/yandex/div/compose/expressions/EvaluatorWarningSender.kt":"divkit/public/client/android/compose/src/main/kotlin/com/yandex/div/compose/expressions/EvaluatorWarningSender.kt",
"client/android/compose/src/main/kotlin/com/yandex/div/compose/views/DivBlockView.kt":"divkit/public/client/android/compose/src/main/kotlin/com/yandex/div/compose/views/DivBlockView.kt",
"client/android/compose/src/main/kotlin/com/yandex/div/compose/views/DivContainerView.kt":"divkit/public/client/android/compose/src/main/kotlin/com/yandex/div/compose/views/DivContainerView.kt",
"client/android/compose/src/main/kotlin/com/yandex/div/compose/views/DivImageView.kt":"divkit/public/client/android/compose/src/main/kotlin/com/yandex/div/compose/views/DivImageView.kt",
@@ -610,6 +616,7 @@
"client/android/compose/src/main/kotlin/com/yandex/div/compose/views/modifiers/BackgroundModifiers.kt":"divkit/public/client/android/compose/src/main/kotlin/com/yandex/div/compose/views/modifiers/BackgroundModifiers.kt",
"client/android/compose/src/main/kotlin/com/yandex/div/compose/views/modifiers/Modifiers.kt":"divkit/public/client/android/compose/src/main/kotlin/com/yandex/div/compose/views/modifiers/Modifiers.kt",
"client/android/compose/src/main/kotlin/com/yandex/div/compose/views/modifiers/SizeModifiers.kt":"divkit/public/client/android/compose/src/main/kotlin/com/yandex/div/compose/views/modifiers/SizeModifiers.kt",
"client/android/compose/src/test/kotlin/com/yandex/div/compose/expressions/DivComposeExpressionResolverTest.kt":"divkit/public/client/android/compose/src/test/kotlin/com/yandex/div/compose/expressions/DivComposeExpressionResolverTest.kt",
"client/android/div-common.gradle":"divkit/public/client/android/div-common.gradle",
"client/android/div-core/build.gradle":"divkit/public/client/android/div-core/build.gradle",
"client/android/div-core/proguard-rules.pro":"divkit/public/client/android/div-core/proguard-rules.pro",
+8
View File
@@ -1,16 +1,21 @@
plugins {
alias(libs.plugins.android.library)
alias(libs.plugins.kotlin.compose)
alias(libs.plugins.kotlin.ksp)
}
apply(from = "../div-library.gradle")
apply(from = "../div-tests.gradle")
android {
namespace = "com.yandex.div.compose"
}
dependencies {
implementation(project(":div-core"))
implementation(project(":div-data"))
implementation(project(":div-evaluable"))
implementation(project(":logging"))
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.compose.material3)
@@ -18,6 +23,9 @@ dependencies {
implementation(libs.androidx.coreKtx)
implementation(libs.coil.compose)
implementation(libs.coil.network)
implementation(libs.yatagan.api.compiled)
ksp(libs.yatagan.processor.ksp)
debugImplementation(libs.androidx.compose.ui.tooling)
}
@@ -1,13 +1,21 @@
package com.yandex.div.compose
import android.content.Context
import com.yandex.div.json.expressions.ExpressionResolver
import com.yandex.div.compose.dagger.`Yatagan$DivContextComponent`
import com.yandex.div.core.annotations.PublicApi
import com.yandex.yatagan.Module
import com.yandex.yatagan.Provides
class DivComposeConfiguration {
fun createContext(baseContext: Context): DivContext {
return DivContext(
baseContext = baseContext,
expressionResolver = ExpressionResolver.EMPTY
)
}
@Module
@PublicApi
class DivComposeConfiguration(
@get:Provides val reporter: DivReporter = DivReporter()
)
fun DivComposeConfiguration.createContext(baseContext: Context): DivContext {
return `Yatagan$DivContextComponent`.builder()
.baseContext(baseContext)
.configuration(this)
.build()
.context
}
@@ -3,9 +3,13 @@ package com.yandex.div.compose
import android.content.Context
import android.content.ContextWrapper
import androidx.annotation.MainThread
import com.yandex.div.core.annotations.PublicApi
import com.yandex.div.json.expressions.ExpressionResolver
import javax.inject.Inject
class DivContext @MainThread internal constructor(
@PublicApi
class DivContext @Inject @MainThread internal constructor(
baseContext: Context,
internal val expressionResolver: ExpressionResolver
internal val expressionResolver: ExpressionResolver,
internal val reporter: DivReporter
) : ContextWrapper(baseContext)
@@ -0,0 +1,22 @@
package com.yandex.div.compose
import com.yandex.div.core.annotations.PublicApi
import com.yandex.div.internal.Log
@PublicApi
open class DivReporter {
open fun reportError(message: String) {
Log.e(TAG, message)
}
open fun reportError(message: String, e: Throwable) {
Log.e(TAG, message, e)
}
open fun reportWarning(message: String) {
Log.w(TAG, message)
}
}
private const val TAG = "DivReporter"
@@ -0,0 +1,31 @@
package com.yandex.div.compose.dagger
import android.content.Context
import com.yandex.div.compose.DivComposeConfiguration
import com.yandex.div.compose.DivContext
import com.yandex.yatagan.BindsInstance
import com.yandex.yatagan.Component
@DivContextScope
@Component(
modules = [
DivComposeConfiguration::class,
DivContextModule::class
]
)
internal interface DivContextComponent {
val context: DivContext
@Component.Builder
interface Builder {
@BindsInstance
fun baseContext(baseContext: Context): Builder
@BindsInstance
fun configuration(configuration: DivComposeConfiguration): Builder
fun build(): DivContextComponent
}
}
@@ -0,0 +1,35 @@
package com.yandex.div.compose.dagger
import com.yandex.div.compose.expressions.DivComposeExpressionResolver
import com.yandex.div.compose.expressions.EvaluatorWarningSender
import com.yandex.div.evaluable.EvaluationContext
import com.yandex.div.evaluable.Evaluator
import com.yandex.div.evaluable.function.GeneratedBuiltinFunctionProvider
import com.yandex.div.json.expressions.ExpressionResolver
import com.yandex.yatagan.Binds
import com.yandex.yatagan.Module
import com.yandex.yatagan.Provides
@Module
internal interface DivContextModule {
@Binds
fun bindExpressionResolver(impl: DivComposeExpressionResolver): ExpressionResolver
companion object {
@DivContextScope
@Provides
fun providerEvaluator(
warningSender: EvaluatorWarningSender
): Evaluator {
val evaluationContext = EvaluationContext(
variableProvider = { _ -> null },
storedValueProvider = { _ -> null },
functionProvider = GeneratedBuiltinFunctionProvider,
warningSender = warningSender
)
return Evaluator(evaluationContext)
}
}
}
@@ -0,0 +1,7 @@
package com.yandex.div.compose.dagger
import javax.inject.Scope
@Scope
@Retention(AnnotationRetention.RUNTIME)
internal annotation class DivContextScope
@@ -0,0 +1,187 @@
package com.yandex.div.compose.expressions
import com.yandex.div.compose.DivReporter
import com.yandex.div.compose.dagger.DivContextScope
import com.yandex.div.core.Disposable
import com.yandex.div.core.ObserverList
import com.yandex.div.evaluable.Evaluable
import com.yandex.div.evaluable.EvaluableException
import com.yandex.div.evaluable.Evaluator
import com.yandex.div.evaluable.MissingVariableException
import com.yandex.div.internal.parser.Converter
import com.yandex.div.internal.parser.TypeHelper
import com.yandex.div.internal.parser.ValueValidator
import com.yandex.div.json.ParsingErrorLogger
import com.yandex.div.json.expressions.ExpressionResolver
import com.yandex.div.json.invalidValue
import com.yandex.div.json.missingVariable
import com.yandex.div.json.resolveFailed
import com.yandex.div.json.typeMismatch
import javax.inject.Inject
@DivContextScope
internal class DivComposeExpressionResolver @Inject constructor(
private val evaluator: Evaluator,
private val reporter: DivReporter
) : ExpressionResolver {
private val evaluationsCache = mutableMapOf<String, Any>()
private val varToExpressions = mutableMapOf<String, MutableSet<String>>()
private val expressionObservers = mutableMapOf<String, ObserverList<() -> Unit>>()
override fun <R, T : Any> get(
expressionKey: String,
rawExpression: String,
evaluable: Evaluable,
converter: Converter<R, T?>?,
validator: ValueValidator<T>,
fieldType: TypeHelper<T>,
logger: ParsingErrorLogger
): T {
return try {
tryResolve(expressionKey, rawExpression, evaluable, converter, validator, fieldType)
} catch (e: Exception) {
reporter.reportError("Failed to resolve expression", e)
throw e
}
}
override fun subscribeToExpression(
rawExpression: String,
variableNames: List<String>,
callback: () -> Unit
): Disposable {
variableNames.forEach {
val expressions = varToExpressions.getOrPut(it) { mutableSetOf() }
expressions.add(rawExpression)
}
val observers = expressionObservers.getOrPut(rawExpression) { ObserverList() }
observers.addObserver(callback)
return Disposable { expressionObservers[rawExpression]?.removeObserver(callback) }
}
private fun <R : Any, T : Any> tryResolve(
expressionKey: String,
rawExpression: String,
evaluable: Evaluable,
converter: Converter<R, T?>?,
validator: ValueValidator<T>,
fieldType: TypeHelper<T>,
): T {
val result = try {
getEvaluationResult<R>(rawExpression, evaluable)
} catch (e: EvaluableException) {
val variableName = tryGetMissingVariableName(e)
if (variableName != null) {
throw missingVariable(rawExpression, variableName, e)
}
throw resolveFailed(expressionKey, rawExpression, e)
}
val convertedValue = if (fieldType.isTypeValid(result)) {
result as T
} else {
safeConvert(expressionKey, rawExpression, converter, result, fieldType)
?: throw invalidValue(expressionKey, rawExpression, result)
}
safeValidate(expressionKey, rawExpression, validator, convertedValue)
return convertedValue
}
private fun <R : Any> getEvaluationResult(
rawExpression: String,
evaluable: Evaluable
): R {
val cachedValue = evaluationsCache[rawExpression]
if (cachedValue != null) {
return cachedValue as R
}
val value = evaluator.eval<R>(evaluable)
if (evaluable.checkIsCacheable()) {
evaluable.variables.forEach {
val expressions = varToExpressions.getOrPut(it) { mutableSetOf() }
expressions.add(rawExpression)
}
evaluationsCache[rawExpression] = value
}
return value
}
private fun tryGetMissingVariableName(e: EvaluableException): String? {
return if (e is MissingVariableException) e.variableName else null
}
private fun <R, T> safeConvert(
expressionKey: String,
rawExpression: String,
converter: Converter<R, T?>?,
rawValue: R,
fieldType: TypeHelper<T>
): T? {
val convertedValue = if (converter == null) {
// cast is unsafe but we hope that safeValidate() will check this type later
rawValue as? T
} else {
try {
// We need to guard with try..catch because at this point we're
// not sure that resolved expression type is same type that converter accepts.
converter.invoke(rawValue)
} catch (e: ClassCastException) {
throw typeMismatch(
expressionKey = expressionKey,
rawExpression = rawExpression,
wrongTypeValue = rawValue,
cause = e,
)
} catch (e: Exception) {
// okay to catch exceptions here cause convertors may
// throw different types of errors from common ClassCastException
// up to specific NumberFormatException.
throw invalidValue(
expressionKey = expressionKey,
rawExpression = rawExpression,
wrongValue = rawValue,
cause = e,
)
}
}
fun fieldAwaitsStringButValueNotConverted(value: T?): Boolean {
if (value == null) {
return false
}
return fieldType.typeDefault is String && !fieldType.isTypeValid(value)
}
return if (fieldAwaitsStringButValueNotConverted(convertedValue)) {
convertedValue.toString() as T
} else {
convertedValue
}
}
private fun <T> safeValidate(
expressionKey: String,
rawExpression: String,
validator: ValueValidator<T>,
convertedValue: T
) {
try {
// We need to guard with try..catch because at this point we're
// not sure that resolved expression type is same type that validator checks.
if (!validator.isValid(convertedValue)) {
throw invalidValue(rawExpression, convertedValue)
}
} catch (e: ClassCastException) {
throw typeMismatch(
expressionKey = expressionKey,
rawExpression = rawExpression,
wrongTypeValue = convertedValue,
cause = e,
)
}
}
}
@@ -0,0 +1,20 @@
package com.yandex.div.compose.expressions
import com.yandex.div.compose.DivReporter
import com.yandex.div.compose.dagger.DivContextScope
import com.yandex.div.evaluable.ExpressionContext
import com.yandex.div.evaluable.WarningSender
import javax.inject.Inject
@DivContextScope
internal class EvaluatorWarningSender @Inject constructor(
private val reporter: DivReporter
): WarningSender {
override fun send(
expressionContext: ExpressionContext,
message: String
) {
reporter.reportWarning(message)
}
}
@@ -15,6 +15,6 @@ internal fun DivBlockView(
is Div.Container -> DivContainerView(modifier, data.value)
is Div.Image -> DivImageView(modifier, data.value)
is Div.Text -> DivTextView(modifier, data.value)
else -> TODO()
else -> reporter.reportError("Element not supported")
}
}
@@ -6,6 +6,7 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.yandex.div.compose.DivContext
import com.yandex.div.compose.DivReporter
import com.yandex.div.json.expressions.Expression
import com.yandex.div.json.expressions.ExpressionResolver
@@ -25,6 +26,10 @@ internal val expressionResolver: ExpressionResolver
@Composable
get() = divContext.expressionResolver
internal val reporter: DivReporter
@Composable
get() = divContext.reporter
@Composable
internal fun <T : Any> Expression<T>.evaluate(): T {
return evaluate(expressionResolver)
@@ -4,8 +4,13 @@ import androidx.compose.foundation.background
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.yandex.div.compose.views.evaluate
import com.yandex.div.compose.views.reporter
import com.yandex.div.compose.views.toColor
import com.yandex.div2.DivBackground
import com.yandex.div2.DivImageBackground
import com.yandex.div2.DivLinearGradient
import com.yandex.div2.DivNinePatchBackground
import com.yandex.div2.DivRadialGradient
import com.yandex.div2.DivSolidBackground
@Composable
@@ -16,10 +21,17 @@ internal fun Modifier.backgrounds(value: List<DivBackground>): Modifier {
is DivBackground.Solid ->
modifier = modifier.solidBackground(background.value)
is DivBackground.Image -> TODO()
is DivBackground.LinearGradient -> TODO()
is DivBackground.NinePatch -> TODO()
is DivBackground.RadialGradient -> TODO()
is DivBackground.Image ->
reporter.reportError("Background not supported: ${DivImageBackground.TYPE}")
is DivBackground.LinearGradient ->
reporter.reportError("Background not supported: ${DivLinearGradient.TYPE}")
is DivBackground.NinePatch ->
reporter.reportError("Background not supported: ${DivNinePatchBackground.TYPE}")
is DivBackground.RadialGradient ->
reporter.reportError("Background not supported: ${DivRadialGradient.TYPE}")
}
}
return modifier
@@ -0,0 +1,100 @@
package com.yandex.div.compose.expressions
import com.yandex.div.compose.DivReporter
import com.yandex.div.data.Variable
import com.yandex.div.evaluable.EvaluationContext
import com.yandex.div.evaluable.Evaluator
import com.yandex.div.evaluable.function.GeneratedBuiltinFunctionProvider
import com.yandex.div.internal.parser.TYPE_HELPER_STRING
import com.yandex.div.internal.parser.TypeHelper
import com.yandex.div.json.expressions.Expression
import org.junit.Assert.assertEquals
import org.junit.Assert.fail
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.mock
import org.robolectric.RobolectricTestRunner
@RunWith(RobolectricTestRunner::class)
class DivComposeExpressionResolverTest {
private val variables = mutableMapOf<String, Variable>()
private val evaluationContext = EvaluationContext(
variableProvider = { variables[it]?.getValue() },
storedValueProvider = mock(),
functionProvider = GeneratedBuiltinFunctionProvider,
warningSender = { _, _ -> }
)
private val reporter = object : DivReporter() {
override fun reportError(message: String) {
fail(message)
}
override fun reportError(message: String, e: Throwable) {
fail(message)
}
}
private val expressionResolver = DivComposeExpressionResolver(
Evaluator(evaluationContext),
reporter
)
@Test
fun `expression with string function`() {
assertEquals(
"length = 9",
evaluate("length = @{len('123456789')}")
)
}
@Test
fun `expression with string variables`() {
variables["host"] = Variable.StringVariable("host", "test.abc")
variables["path"] = Variable.StringVariable("path", "path")
assertEquals(
"https://test.abc/path",
evaluate("https://@{host}/@{path}")
)
}
@Test
fun `expression with integer variable`() {
variables["value"] = Variable.IntegerVariable("value", 100)
assertEquals(
"value + 10 = 110",
evaluate("value + 10 = @{value + 10}")
)
}
@Test
fun `expression with double variable`() {
variables["value"] = Variable.DoubleVariable("value", 123.45)
assertEquals(
"value + 10 = 133.45",
evaluate("value + 10 = @{value + 10}")
)
}
private fun evaluate(expression: String): String {
return expression(expression, TYPE_HELPER_STRING).evaluate(expressionResolver)
}
private fun <T : Any> expression(
rawExpression: String,
typeHelper: TypeHelper<T>,
validator: (T) -> Boolean = { true },
) = Expression.MutableExpression<T, T>(
expressionKey = "test",
rawExpression = rawExpression,
validator = validator,
converter = { it },
logger = { fail(it.message) },
typeHelper = typeHelper,
)
}
@@ -6,6 +6,7 @@ import androidx.activity.ComponentActivity
import androidx.compose.ui.platform.ComposeView
import com.yandex.div.compose.DivComposeConfiguration
import com.yandex.div.compose.DivView
import com.yandex.div.compose.createContext
import com.yandex.div.data.DivParsingEnvironment
import com.yandex.div.json.ParsingErrorLogger
import com.yandex.div2.DivData