diff --git a/.mapping.json b/.mapping.json index 8cfbccafa..76712afc2 100644 --- a/.mapping.json +++ b/.mapping.json @@ -613,6 +613,10 @@ "client/android/compose/src/main/kotlin/com/yandex/div/compose/actions/DivActionHandlingContext.kt":"divkit/public/client/android/compose/src/main/kotlin/com/yandex/div/compose/actions/DivActionHandlingContext.kt", "client/android/compose/src/main/kotlin/com/yandex/div/compose/actions/DivCustomActionHandler.kt":"divkit/public/client/android/compose/src/main/kotlin/com/yandex/div/compose/actions/DivCustomActionHandler.kt", "client/android/compose/src/main/kotlin/com/yandex/div/compose/actions/SetVariableActionHandler.kt":"divkit/public/client/android/compose/src/main/kotlin/com/yandex/div/compose/actions/SetVariableActionHandler.kt", + "client/android/compose/src/main/kotlin/com/yandex/div/compose/context/DivLocalContext.kt":"divkit/public/client/android/compose/src/main/kotlin/com/yandex/div/compose/context/DivLocalContext.kt", + "client/android/compose/src/main/kotlin/com/yandex/div/compose/context/DivLocalContextStorage.kt":"divkit/public/client/android/compose/src/main/kotlin/com/yandex/div/compose/context/DivLocalContextStorage.kt", + "client/android/compose/src/main/kotlin/com/yandex/div/compose/context/DivViewContext.kt":"divkit/public/client/android/compose/src/main/kotlin/com/yandex/div/compose/context/DivViewContext.kt", + "client/android/compose/src/main/kotlin/com/yandex/div/compose/context/DivViewContextStorage.kt":"divkit/public/client/android/compose/src/main/kotlin/com/yandex/div/compose/context/DivViewContextStorage.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", @@ -634,7 +638,6 @@ "client/android/compose/src/main/kotlin/com/yandex/div/compose/utils/UnitConversions.kt":"divkit/public/client/android/compose/src/main/kotlin/com/yandex/div/compose/utils/UnitConversions.kt", "client/android/compose/src/main/kotlin/com/yandex/div/compose/variables/DivVariableAdapter.kt":"divkit/public/client/android/compose/src/main/kotlin/com/yandex/div/compose/variables/DivVariableAdapter.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/DivLocalContext.kt":"divkit/public/client/android/compose/src/main/kotlin/com/yandex/div/compose/views/DivLocalContext.kt", "client/android/compose/src/main/kotlin/com/yandex/div/compose/views/DivSeparatorView.kt":"divkit/public/client/android/compose/src/main/kotlin/com/yandex/div/compose/views/DivSeparatorView.kt", "client/android/compose/src/main/kotlin/com/yandex/div/compose/views/DivTextView.kt":"divkit/public/client/android/compose/src/main/kotlin/com/yandex/div/compose/views/DivTextView.kt", "client/android/compose/src/main/kotlin/com/yandex/div/compose/views/container/ContainerAlignment.kt":"divkit/public/client/android/compose/src/main/kotlin/com/yandex/div/compose/views/container/ContainerAlignment.kt", @@ -659,6 +662,7 @@ "client/android/compose/src/main/kotlin/com/yandex/div/compose/views/modifiers/BorderModifiers.kt":"divkit/public/client/android/compose/src/main/kotlin/com/yandex/div/compose/views/modifiers/BorderModifiers.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/DivViewRecompositionTest.kt":"divkit/public/client/android/compose/src/test/kotlin/com/yandex/div/compose/DivViewRecompositionTest.kt", "client/android/compose/src/test/kotlin/com/yandex/div/compose/DivViewTest.kt":"divkit/public/client/android/compose/src/test/kotlin/com/yandex/div/compose/DivViewTest.kt", "client/android/compose/src/test/kotlin/com/yandex/div/compose/DivViewWithFunctionsTest.kt":"divkit/public/client/android/compose/src/test/kotlin/com/yandex/div/compose/DivViewWithFunctionsTest.kt", "client/android/compose/src/test/kotlin/com/yandex/div/compose/DivViewWithTriggersTest.kt":"divkit/public/client/android/compose/src/test/kotlin/com/yandex/div/compose/DivViewWithTriggersTest.kt", diff --git a/client/android/compose/src/main/kotlin/com/yandex/div/compose/DivContext.kt b/client/android/compose/src/main/kotlin/com/yandex/div/compose/DivContext.kt index 28b4eafc8..64a862e28 100644 --- a/client/android/compose/src/main/kotlin/com/yandex/div/compose/DivContext.kt +++ b/client/android/compose/src/main/kotlin/com/yandex/div/compose/DivContext.kt @@ -3,13 +3,18 @@ package com.yandex.div.compose import android.content.ContextWrapper import androidx.annotation.MainThread import androidx.annotation.VisibleForTesting +import com.yandex.div.compose.context.DivLocalContext +import com.yandex.div.compose.context.DivViewContext import com.yandex.div.compose.dagger.DivContextComponent import com.yandex.div.compose.internal.DivDebugFeatures -import com.yandex.div.compose.views.DivLocalContext import com.yandex.div.core.annotations.InternalApi import com.yandex.div.core.annotations.PublicApi import com.yandex.div.core.expression.variables.DivVariableController +import com.yandex.div.evaluable.function.GeneratedBuiltinFunctionProvider import com.yandex.div.internal.expressions.FunctionProviderDecorator +import com.yandex.div.internal.expressions.toLocalFunctions +import com.yandex.div2.DivBase +import com.yandex.div2.DivData import com.yandex.div2.DivTrigger import com.yandex.div2.DivVariable import javax.inject.Inject @@ -32,7 +37,58 @@ class DivContext @Inject @MainThread internal constructor( val debugFeatures: DivDebugFeatures get() = component.debugFeatures - internal fun createLocalContext( + /** + * Removes [DivView] context associated with the given [com.yandex.div2.DivData]. + */ + fun clearViewContext(data: DivData) { + component.viewContextStorage.remove(data) + } + + internal fun getViewContext(data: DivData): DivViewContext { + component.viewContextStorage.get(data)?.let { + return it + } + + val baseFunctionProvider = FunctionProviderDecorator(GeneratedBuiltinFunctionProvider) + val functions = data.functions.orEmpty().toLocalFunctions() + return DivViewContext( + rootLocalContext = createLocalContext( + variableController = DivVariableController(component.variableController), + functionProvider = baseFunctionProvider + functions, + triggers = data.variableTriggers.orEmpty(), + variables = data.variables.orEmpty() + ) + ).also { + component.viewContextStorage.put(data, it) + } + } + + internal fun getLocalContext( + data: DivBase, + viewContext: DivViewContext, + parentContext: DivLocalContext + ): DivLocalContext { + viewContext.localContextStorage.get(data)?.let { + return it + } + + val functions = data.functions.orEmpty().toLocalFunctions() + val variables = data.variables.orEmpty() + return createLocalContext( + variableController = if (variables.isEmpty()) { + parentContext.variableController + } else { + DivVariableController(parentContext.variableController) + }, + functionProvider = parentContext.functionProvider + functions, + triggers = data.variableTriggers.orEmpty(), + variables = variables + ).also { + viewContext.localContextStorage.put(data, it) + } + } + + private fun createLocalContext( variableController: DivVariableController, functionProvider: FunctionProviderDecorator, triggers: List, diff --git a/client/android/compose/src/main/kotlin/com/yandex/div/compose/DivView.kt b/client/android/compose/src/main/kotlin/com/yandex/div/compose/DivView.kt index e84a72698..f62e8a241 100644 --- a/client/android/compose/src/main/kotlin/com/yandex/div/compose/DivView.kt +++ b/client/android/compose/src/main/kotlin/com/yandex/div/compose/DivView.kt @@ -1,10 +1,14 @@ package com.yandex.div.compose import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.Modifier +import com.yandex.div.compose.context.LocalDivContext +import com.yandex.div.compose.context.LocalDivViewContext +import com.yandex.div.compose.triggers.observe +import com.yandex.div.compose.utils.divContext import com.yandex.div.compose.utils.reporter import com.yandex.div.compose.views.DivBlockView -import com.yandex.div.compose.views.WithLocalDivContext import com.yandex.div2.DivData /** @@ -39,7 +43,12 @@ fun DivView( return } - WithLocalDivContext(data) { + val viewContext = divContext.getViewContext(data) + viewContext.rootLocalContext.triggerStorage.observe() + CompositionLocalProvider( + LocalDivViewContext provides viewContext, + LocalDivContext provides viewContext.rootLocalContext + ) { DivBlockView( data = div, modifier = modifier diff --git a/client/android/compose/src/main/kotlin/com/yandex/div/compose/context/DivLocalContext.kt b/client/android/compose/src/main/kotlin/com/yandex/div/compose/context/DivLocalContext.kt new file mode 100644 index 000000000..6615224cc --- /dev/null +++ b/client/android/compose/src/main/kotlin/com/yandex/div/compose/context/DivLocalContext.kt @@ -0,0 +1,47 @@ +package com.yandex.div.compose.context + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.compositionLocalOf +import com.yandex.div.compose.DivException +import com.yandex.div.compose.actions.DivActionHandlingContext +import com.yandex.div.compose.dagger.DivLocalScope +import com.yandex.div.compose.triggers.DivTriggerStorage +import com.yandex.div.compose.triggers.observe +import com.yandex.div.compose.utils.divContext +import com.yandex.div.core.expression.variables.DivVariableController +import com.yandex.div.internal.expressions.FunctionProviderDecorator +import com.yandex.div.json.expressions.ExpressionResolver +import com.yandex.div2.DivBase +import javax.inject.Inject + +@DivLocalScope +internal class DivLocalContext @Inject constructor( + val actionHandlingContext: DivActionHandlingContext, + val expressionResolver: ExpressionResolver, + val functionProvider: FunctionProviderDecorator, + val triggerStorage: DivTriggerStorage, + val variableController: DivVariableController +) + +internal val LocalDivContext = compositionLocalOf { + throw DivException("DivLocalContext not provided") +} + +@Composable +internal fun WithLocalDivContext(data: DivBase, content: @Composable () -> Unit) { + val functions = data.functions.orEmpty() + val variables = data.variables.orEmpty() + val triggers = data.variableTriggers.orEmpty() + if (functions.isEmpty() && variables.isEmpty() && triggers.isEmpty()) { + return content() + } + + val localContext = divContext.getLocalContext( + data, + viewContext = LocalDivViewContext.current, + parentContext = LocalDivContext.current + ) + localContext.triggerStorage.observe() + CompositionLocalProvider(LocalDivContext provides localContext, content) +} diff --git a/client/android/compose/src/main/kotlin/com/yandex/div/compose/context/DivLocalContextStorage.kt b/client/android/compose/src/main/kotlin/com/yandex/div/compose/context/DivLocalContextStorage.kt new file mode 100644 index 000000000..e29991c19 --- /dev/null +++ b/client/android/compose/src/main/kotlin/com/yandex/div/compose/context/DivLocalContextStorage.kt @@ -0,0 +1,15 @@ +package com.yandex.div.compose.context + +import com.yandex.div2.DivBase + +internal class DivLocalContextStorage() { + private val items = mutableMapOf() + + fun get(div: DivBase): DivLocalContext? { + return items[div] + } + + fun put(div: DivBase, context: DivLocalContext) { + items[div] = context + } +} diff --git a/client/android/compose/src/main/kotlin/com/yandex/div/compose/context/DivViewContext.kt b/client/android/compose/src/main/kotlin/com/yandex/div/compose/context/DivViewContext.kt new file mode 100644 index 000000000..d03481657 --- /dev/null +++ b/client/android/compose/src/main/kotlin/com/yandex/div/compose/context/DivViewContext.kt @@ -0,0 +1,17 @@ +package com.yandex.div.compose.context + +import androidx.compose.runtime.compositionLocalOf +import com.yandex.div.compose.DivException + +internal class DivViewContext( + val rootLocalContext: DivLocalContext +) { + /** + * Stores [DivLocalContext]s for view elements. + */ + val localContextStorage = DivLocalContextStorage() +} + +internal val LocalDivViewContext = compositionLocalOf { + throw DivException("DivViewContext not provided") +} diff --git a/client/android/compose/src/main/kotlin/com/yandex/div/compose/context/DivViewContextStorage.kt b/client/android/compose/src/main/kotlin/com/yandex/div/compose/context/DivViewContextStorage.kt new file mode 100644 index 000000000..b52a18b91 --- /dev/null +++ b/client/android/compose/src/main/kotlin/com/yandex/div/compose/context/DivViewContextStorage.kt @@ -0,0 +1,22 @@ +package com.yandex.div.compose.context + +import com.yandex.div.compose.dagger.DivContextScope +import com.yandex.div2.DivData +import javax.inject.Inject + +@DivContextScope +internal class DivViewContextStorage @Inject constructor() { + private val items = mutableMapOf() + + fun get(data: DivData): DivViewContext? { + return items[data] + } + + fun put(data: DivData, context: DivViewContext) { + items[data] = context + } + + fun remove(data: DivData) { + items.remove(data) + } +} diff --git a/client/android/compose/src/main/kotlin/com/yandex/div/compose/dagger/DivContextComponent.kt b/client/android/compose/src/main/kotlin/com/yandex/div/compose/dagger/DivContextComponent.kt index ecf7d5a3d..3ff3bd567 100644 --- a/client/android/compose/src/main/kotlin/com/yandex/div/compose/dagger/DivContextComponent.kt +++ b/client/android/compose/src/main/kotlin/com/yandex/div/compose/dagger/DivContextComponent.kt @@ -5,8 +5,9 @@ import coil3.ImageLoader import com.yandex.div.compose.DivComposeConfiguration import com.yandex.div.compose.DivReporter import com.yandex.div.compose.actions.DivActionHandler -import com.yandex.div.core.expression.variables.DivVariableController +import com.yandex.div.compose.context.DivViewContextStorage import com.yandex.div.compose.internal.DivDebugFeatures +import com.yandex.div.core.expression.variables.DivVariableController import com.yandex.yatagan.BindsInstance import com.yandex.yatagan.Component import javax.inject.Named @@ -25,6 +26,7 @@ internal interface DivContextComponent { val debugFeatures: DivDebugFeatures val imageLoader: ImageLoader val reporter: DivReporter + val viewContextStorage: DivViewContextStorage @get:Named(Names.HOST_VARIABLES) val variableController: DivVariableController diff --git a/client/android/compose/src/main/kotlin/com/yandex/div/compose/dagger/DivLocalComponent.kt b/client/android/compose/src/main/kotlin/com/yandex/div/compose/dagger/DivLocalComponent.kt index d009beaf0..3cf436aa7 100644 --- a/client/android/compose/src/main/kotlin/com/yandex/div/compose/dagger/DivLocalComponent.kt +++ b/client/android/compose/src/main/kotlin/com/yandex/div/compose/dagger/DivLocalComponent.kt @@ -2,7 +2,7 @@ package com.yandex.div.compose.dagger import com.yandex.div.compose.triggers.DivTriggerStorage import com.yandex.div.compose.variables.DivVariableAdapter -import com.yandex.div.compose.views.DivLocalContext +import com.yandex.div.compose.context.DivLocalContext import com.yandex.div.core.expression.variables.DivVariableController import com.yandex.div.internal.expressions.FunctionProviderDecorator import com.yandex.yatagan.BindsInstance diff --git a/client/android/compose/src/main/kotlin/com/yandex/div/compose/internal/DivDebugFeatures.kt b/client/android/compose/src/main/kotlin/com/yandex/div/compose/internal/DivDebugFeatures.kt index 2163539a6..0838be812 100644 --- a/client/android/compose/src/main/kotlin/com/yandex/div/compose/internal/DivDebugFeatures.kt +++ b/client/android/compose/src/main/kotlin/com/yandex/div/compose/internal/DivDebugFeatures.kt @@ -1,26 +1,41 @@ package com.yandex.div.compose.internal import com.yandex.div.compose.actions.DivActionHandler +import com.yandex.div.compose.context.DivViewContextStorage import com.yandex.div.compose.dagger.DivContextScope -import com.yandex.div.compose.views.DivLocalContext import com.yandex.div.core.annotations.InternalApi import com.yandex.div.json.expressions.ExpressionResolver import com.yandex.div2.DivAction +import com.yandex.div2.DivData import javax.inject.Inject +/** + * Provides access to the features not intended to be used in production environment. + */ @DivContextScope @InternalApi class DivDebugFeatures @Inject internal constructor( - private val actionHandler: DivActionHandler + private val actionHandler: DivActionHandler, + private val viewContextStorage: DivViewContextStorage ) { - internal var lastViewLocalContext: DivLocalContext? = null - val expressionResolver: ExpressionResolver? - get() = lastViewLocalContext?.expressionResolver + /** + * Returns [ExpressionResolver] associated with the given [DivData]. + */ + fun getExpressionResolver(data: DivData): ExpressionResolver? { + return viewContextStorage.get(data)?.rootLocalContext?.expressionResolver + } - fun performAction(action: DivAction) { - lastViewLocalContext?.let { - actionHandler.handle(context = it.actionHandlingContext, action = action) + /** + * Performs action in the context of [com.yandex.div.compose.DivView] associated with + * the given [DivData]. + */ + fun performAction(data: DivData, action: DivAction) { + viewContextStorage.get(data)?.let { + actionHandler.handle( + context = it.rootLocalContext.actionHandlingContext, + action = action + ) } } } diff --git a/client/android/compose/src/main/kotlin/com/yandex/div/compose/triggers/DivTriggerStorage.kt b/client/android/compose/src/main/kotlin/com/yandex/div/compose/triggers/DivTriggerStorage.kt index 4146822f5..9914ea13a 100644 --- a/client/android/compose/src/main/kotlin/com/yandex/div/compose/triggers/DivTriggerStorage.kt +++ b/client/android/compose/src/main/kotlin/com/yandex/div/compose/triggers/DivTriggerStorage.kt @@ -1,5 +1,7 @@ package com.yandex.div.compose.triggers +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect import com.yandex.div.compose.actions.DivActionHandler import com.yandex.div.compose.actions.DivActionHandlingContext import com.yandex.div.compose.dagger.DivLocalScope @@ -19,18 +21,13 @@ internal class DivTriggerStorage @Inject constructor( private class Item( private val trigger: DivTrigger, - expressionResolver: ExpressionResolver, + private val expressionResolver: ExpressionResolver, private val handleActions: (actions: List) -> Unit ) { private val mode = trigger.mode.evaluate(expressionResolver) - private val conditionSubscription: Disposable private var lastCondition: Boolean = false - - init { - conditionSubscription = trigger.condition - .observeAndGet(expressionResolver, this::onConditionChanged) - } + private var conditionSubscription: Disposable? = null private fun onConditionChanged(condition: Boolean) { when (mode) { @@ -47,12 +44,42 @@ internal class DivTriggerStorage @Inject constructor( } } } + + fun startObserving() { + conditionSubscription = trigger.condition + .observeAndGet(expressionResolver, this::onConditionChanged) + } + + fun stopObserving() { + conditionSubscription?.close() + conditionSubscription = null + } } private val items = mutableListOf() + private var isActive = true fun add(trigger: DivTrigger) { - items.add(Item(trigger, expressionResolver, this::handle)) + val item = Item(trigger, expressionResolver, this::handle) + items.add(item) + + if (isActive) { + item.startObserving() + } + } + + fun startObserving() { + if (!isActive) { + items.forEach { it.startObserving() } + isActive = true + } + } + + fun stopObserving() { + if (isActive) { + items.forEach { it.stopObserving() } + isActive = false + } } private fun handle(actions: List) { @@ -61,3 +88,13 @@ internal class DivTriggerStorage @Inject constructor( } } } + +@Composable +internal fun DivTriggerStorage.observe() { + DisposableEffect(this) { + startObserving() + onDispose { + stopObserving() + } + } +} diff --git a/client/android/compose/src/main/kotlin/com/yandex/div/compose/utils/Context.kt b/client/android/compose/src/main/kotlin/com/yandex/div/compose/utils/Context.kt index 4a59145a7..1ba8d0d0a 100644 --- a/client/android/compose/src/main/kotlin/com/yandex/div/compose/utils/Context.kt +++ b/client/android/compose/src/main/kotlin/com/yandex/div/compose/utils/Context.kt @@ -6,7 +6,7 @@ import coil3.ImageLoader import com.yandex.div.compose.DivContext import com.yandex.div.compose.DivReporter import com.yandex.div.compose.actions.DivActionHandler -import com.yandex.div.compose.views.LocalDivContext +import com.yandex.div.compose.context.LocalDivContext import com.yandex.div.json.expressions.ExpressionResolver internal val divContext: DivContext diff --git a/client/android/compose/src/main/kotlin/com/yandex/div/compose/views/DivBlockView.kt b/client/android/compose/src/main/kotlin/com/yandex/div/compose/views/DivBlockView.kt index 2dafdac69..05a22a1e7 100644 --- a/client/android/compose/src/main/kotlin/com/yandex/div/compose/views/DivBlockView.kt +++ b/client/android/compose/src/main/kotlin/com/yandex/div/compose/views/DivBlockView.kt @@ -2,6 +2,7 @@ package com.yandex.div.compose.views import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import com.yandex.div.compose.context.WithLocalDivContext import com.yandex.div.compose.views.container.DivContainerView import com.yandex.div.compose.views.gallery.DivGalleryView import com.yandex.div.compose.views.image.DivImageView diff --git a/client/android/compose/src/main/kotlin/com/yandex/div/compose/views/DivLocalContext.kt b/client/android/compose/src/main/kotlin/com/yandex/div/compose/views/DivLocalContext.kt deleted file mode 100644 index 9e1b09fb7..000000000 --- a/client/android/compose/src/main/kotlin/com/yandex/div/compose/views/DivLocalContext.kt +++ /dev/null @@ -1,75 +0,0 @@ -package com.yandex.div.compose.views - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.runtime.compositionLocalOf -import androidx.compose.runtime.remember -import com.yandex.div.compose.DivException -import com.yandex.div.compose.actions.DivActionHandlingContext -import com.yandex.div.compose.dagger.DivLocalScope -import com.yandex.div.compose.utils.divContext -import com.yandex.div.core.expression.variables.DivVariableController -import com.yandex.div.evaluable.function.GeneratedBuiltinFunctionProvider -import com.yandex.div.internal.expressions.FunctionProviderDecorator -import com.yandex.div.internal.expressions.toLocalFunctions -import com.yandex.div.json.expressions.ExpressionResolver -import com.yandex.div2.DivBase -import com.yandex.div2.DivData -import javax.inject.Inject - -@DivLocalScope -internal class DivLocalContext @Inject constructor( - val actionHandlingContext: DivActionHandlingContext, - val expressionResolver: ExpressionResolver, - val functionProvider: FunctionProviderDecorator, - val variableController: DivVariableController -) - -internal val LocalDivContext = compositionLocalOf { - throw DivException("DivLocalContext not provided") -} - -@Composable -internal fun WithLocalDivContext(data: DivData, content: @Composable () -> Unit) { - val divContext = divContext - val localContext = remember(data) { - val baseFunctionProvider = FunctionProviderDecorator(GeneratedBuiltinFunctionProvider) - val functions = data.functions.orEmpty().toLocalFunctions() - divContext.createLocalContext( - variableController = DivVariableController(divContext.component.variableController), - functionProvider = baseFunctionProvider + functions, - triggers = data.variableTriggers.orEmpty(), - variables = data.variables.orEmpty() - ).also { - divContext.debugFeatures.lastViewLocalContext = it - } - } - CompositionLocalProvider(LocalDivContext provides localContext, content) -} - -@Composable -internal fun WithLocalDivContext(data: DivBase, content: @Composable () -> Unit) { - val functions = data.functions.orEmpty() - val variables = data.variables.orEmpty() - val triggers = data.variableTriggers.orEmpty() - if (functions.isEmpty() && variables.isEmpty() && triggers.isEmpty()) { - return content() - } - - val divContext = divContext - val localContext = LocalDivContext.current - val newLocalContext = remember(data) { - val variableController = if (variables.isEmpty()) { - localContext.variableController - } else { - DivVariableController(localContext.variableController) - } - divContext.createLocalContext( - variableController = variableController, - functionProvider = localContext.functionProvider + functions.toLocalFunctions(), - triggers = triggers, - variables = variables - ) - } - CompositionLocalProvider(LocalDivContext provides newLocalContext, content) -} diff --git a/client/android/compose/src/main/kotlin/com/yandex/div/compose/views/modifiers/ActionModifiers.kt b/client/android/compose/src/main/kotlin/com/yandex/div/compose/views/modifiers/ActionModifiers.kt index 64b3ae0ec..d8764a67a 100644 --- a/client/android/compose/src/main/kotlin/com/yandex/div/compose/views/modifiers/ActionModifiers.kt +++ b/client/android/compose/src/main/kotlin/com/yandex/div/compose/views/modifiers/ActionModifiers.kt @@ -16,7 +16,7 @@ import com.yandex.div.compose.utils.observedFloatValue import com.yandex.div.compose.utils.observedIntValue import com.yandex.div.compose.utils.observedValue import com.yandex.div.compose.utils.reporter -import com.yandex.div.compose.views.LocalDivContext +import com.yandex.div.compose.context.LocalDivContext import com.yandex.div2.Div import com.yandex.div2.DivAction import com.yandex.div2.DivAnimation diff --git a/client/android/compose/src/test/kotlin/com/yandex/div/compose/DivViewRecompositionTest.kt b/client/android/compose/src/test/kotlin/com/yandex/div/compose/DivViewRecompositionTest.kt new file mode 100644 index 000000000..c97d57f79 --- /dev/null +++ b/client/android/compose/src/test/kotlin/com/yandex/div/compose/DivViewRecompositionTest.kt @@ -0,0 +1,177 @@ +package com.yandex.div.compose + +import android.view.View +import androidx.activity.ComponentActivity +import androidx.compose.runtime.State +import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.assertTextEquals +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.yandex.div.test.data.action +import com.yandex.div.test.data.data +import com.yandex.div.test.data.expression +import com.yandex.div.test.data.setVariableAction +import com.yandex.div.test.data.text +import com.yandex.div.test.data.typedValue +import com.yandex.div.test.data.variable +import com.yandex.div2.DivData +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class DivViewRecompositionTest { + + @get:Rule + val rule = createAndroidComposeRule() + + private val mutableData = mutableStateOf(data(text(text = ""))) + + private val activity: ComponentActivity + get() = rule.activity + + private lateinit var divContext: DivContext + + @Before + fun setup() { + divContext = DivComposeConfiguration( + reporter = TestReporter() + ).createContext(baseContext = activity) + } + + @Test + fun `content changes when data is updated`() { + mutableData.value = data(text(text = "Data 1")) + + setContent(mutableData) + + rule.onNodeWithText("Data 1").assertIsDisplayed() + + mutableData.value = data(text(text = "Data 2")) + + rule.onNodeWithText("Data 2").assertIsDisplayed() + } + + @Test + fun `new variables are used when data is updated`() { + mutableData.value = data( + text(text = expression("Data @{counter}")), + variables = listOf(variable("counter", 1)) + ) + + setContent(mutableData) + + rule.onNodeWithText("Data 1").assertIsDisplayed() + + mutableData.value = data( + text(text = expression("Data @{counter}")), + variables = listOf(variable("counter", 2)) + ) + + rule.onNodeWithText("Data 2").assertIsDisplayed() + } + + @Test + fun `variable values are restored when data is switched back`() { + val data = data( + text( + action = action( + typed = setVariableAction(name = "counter", value = typedValue(5)) + ), + id = "button", + text = expression("counter = @{counter}") + ), + variables = listOf(variable("counter", 1)) + ) + mutableData.value = data + + setContent(mutableData) + + rule.onNodeWithTag("button").apply { + assertTextEquals("counter = 1") + performClick() + assertTextEquals("counter = 5") + } + + mutableData.value = data(text(text = "New Data")) + + rule.onNodeWithText("New Data").assertIsDisplayed() + + mutableData.value = data + + rule.onNodeWithTag("button").assertTextEquals("counter = 5") + } + + @Test + fun `variable values are restored when ComposeView is recreated with the same context`() { + val data = data( + text( + action = action( + typed = setVariableAction(name = "counter", value = typedValue(5)) + ), + id = "button", + text = expression("counter = @{counter}") + ), + variables = listOf(variable("counter", 1)) + ) + + setContent(data) + + rule.onNodeWithTag("button").apply { + assertTextEquals("counter = 1") + performClick() + assertTextEquals("counter = 5") + } + + activity.setContentView(View(activity)) + setContent(data) + + rule.onNodeWithTag("button").assertTextEquals("counter = 5") + } + + @Test + fun `variable values are not restored when ComposeView is recreated and context is cleared`() { + val data = data( + text( + action = action( + typed = setVariableAction(name = "counter", value = typedValue(5)) + ), + id = "button", + text = expression("counter = @{counter}") + ), + variables = listOf(variable("counter", 1)) + ) + + setContent(data) + + rule.onNodeWithTag("button").apply { + assertTextEquals("counter = 1") + performClick() + assertTextEquals("counter = 5") + } + + activity.setContentView(View(activity)) + divContext.clearViewContext(data) + setContent(data) + + rule.onNodeWithTag("button").assertTextEquals("counter = 1") + } + + private fun setContent(data: DivData) = setContent(mutableStateOf(data)) + + private fun setContent(data: State) { + activity.setContentView( + ComposeView(divContext).apply { + setContent { + DivView(data.value) + } + } + ) + } +} diff --git a/client/android/compose/src/test/kotlin/com/yandex/div/compose/DivViewTest.kt b/client/android/compose/src/test/kotlin/com/yandex/div/compose/DivViewTest.kt index bbd9fea03..6f0ced53f 100644 --- a/client/android/compose/src/test/kotlin/com/yandex/div/compose/DivViewTest.kt +++ b/client/android/compose/src/test/kotlin/com/yandex/div/compose/DivViewTest.kt @@ -1,7 +1,5 @@ package com.yandex.div.compose -import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.assertTextEquals import androidx.compose.ui.test.junit4.createComposeRule @@ -150,11 +148,9 @@ class DivViewTest { content: Div, variables: List? = null ) { - rule.setContent { - val divContext = configuration.createContext(baseContext = LocalContext.current) - CompositionLocalProvider(LocalContext provides divContext) { - DivView(data = data(content, variables = variables)) - } - } + rule.setContent( + configuration = configuration, + data = data(content, variables = variables) + ) } } diff --git a/client/android/compose/src/test/kotlin/com/yandex/div/compose/DivViewWithTriggersTest.kt b/client/android/compose/src/test/kotlin/com/yandex/div/compose/DivViewWithTriggersTest.kt index 6e146e202..c68208dda 100644 --- a/client/android/compose/src/test/kotlin/com/yandex/div/compose/DivViewWithTriggersTest.kt +++ b/client/android/compose/src/test/kotlin/com/yandex/div/compose/DivViewWithTriggersTest.kt @@ -1,7 +1,10 @@ package com.yandex.div.compose +import android.view.View +import androidx.activity.ComponentActivity +import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.test.assertTextEquals -import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.onNodeWithTag import androidx.test.ext.junit.runners.AndroidJUnit4 import com.yandex.div.core.expression.variables.DivVariableController @@ -9,12 +12,18 @@ import com.yandex.div.data.Variable import com.yandex.div.test.data.action import com.yandex.div.test.data.data import com.yandex.div.test.data.expression +import com.yandex.div.test.data.intExpression +import com.yandex.div.test.data.setVariableAction import com.yandex.div.test.data.text import com.yandex.div.test.data.trigger +import com.yandex.div.test.data.typedValue import com.yandex.div.test.data.variable import com.yandex.div2.Div +import com.yandex.div2.DivData import com.yandex.div2.DivTrigger import com.yandex.div2.DivVariable +import org.junit.Assert.assertEquals +import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @@ -23,14 +32,22 @@ import org.junit.runner.RunWith class DivViewWithTriggersTest { @get:Rule - val rule = createComposeRule() + val rule = createAndroidComposeRule() private val variableController = DivVariableController() - private val configuration = DivComposeConfiguration( - reporter = TestReporter(), - variableController = variableController - ) + private val activity: ComponentActivity + get() = rule.activity + + private lateinit var divContext: DivContext + + @Before + fun setup() { + divContext = DivComposeConfiguration( + reporter = TestReporter(), + variableController = variableController + ).createContext(baseContext = activity) + } @Test fun `text changes when trigger is triggered`() { @@ -84,14 +101,115 @@ class DivViewWithTriggersTest { rule.onNodeWithTag("title").assertTextEquals("new text") } + @Test + fun `does not trigger after leaving composition`() { + val condition = Variable.BooleanVariable("condition", false) + val counter = Variable.IntegerVariable("counter", 1) + variableController.declare(condition, counter) + + setContent( + text( + id = "title", + text = expression("counter = @{counter}") + ), + triggers = listOf( + trigger( + action = action( + typed = setVariableAction( + name = "counter", + value = typedValue(intExpression("@{counter + 1}")) + ) + ), + condition = "@{condition}" + ) + ) + ) + + activity.setContentView(View(activity)) + condition.set(true) + + assertEquals(1L, counter.getValue()) + } + + @Test + fun `does not trigger (local) after leaving composition`() { + val condition = Variable.BooleanVariable("condition", false) + val counter = Variable.IntegerVariable("counter", 1) + variableController.declare(condition, counter) + + setContent( + text( + id = "title", + text = expression("counter = @{counter}"), + triggers = listOf( + trigger( + action = action( + typed = setVariableAction( + name = "counter", + value = typedValue(intExpression("@{counter + 1}")) + ) + ), + condition = "@{condition}" + ) + ) + ) + ) + + activity.setContentView(View(activity)) + condition.set(true) + + assertEquals(1L, counter.getValue()) + } + + @Test + fun `does not trigger when reentering composition and condition is not changed`() { + val condition = Variable.BooleanVariable("condition", false) + val counter = Variable.IntegerVariable("counter", 1) + variableController.declare(condition, counter) + + val data = data( + text( + id = "title", + text = expression("counter = @{counter}") + ), + triggers = listOf( + trigger( + action = action( + typed = setVariableAction( + name = "counter", + value = typedValue(intExpression("@{counter + 1}")) + ) + ), + condition = "@{condition}" + ) + ) + ) + setContent(data) + + condition.set(true) + assertEquals(2L, counter.getValue()) + + activity.setContentView(View(activity)) + setContent(data) + + assertEquals(2L, counter.getValue()) + } + private fun setContent( content: Div, triggers: List? = null, variables: List? = null ) { - rule.setContent( - configuration = configuration, - data = data(content, triggers = triggers, variables = variables) + setContent(data(content, triggers = triggers, variables = variables)) + } + + private fun setContent(data: DivData) { + activity.setContentView( + ComposeView(divContext).apply { + setContent { + DivView(data) + } + } ) } } diff --git a/client/android/compose/src/test/kotlin/com/yandex/div/compose/IntegrationTest.kt b/client/android/compose/src/test/kotlin/com/yandex/div/compose/IntegrationTest.kt index 8d60fd59e..054c09085 100644 --- a/client/android/compose/src/test/kotlin/com/yandex/div/compose/IntegrationTest.kt +++ b/client/android/compose/src/test/kotlin/com/yandex/div/compose/IntegrationTest.kt @@ -45,11 +45,11 @@ class IntegrationTest(testCaseParsingResult: ParsingResult) } testCase.parseActions().forEach { - divContext.debugFeatures.performAction(it) + divContext.debugFeatures.performAction(data = divData, action = it) } testCase.checkResult( - expressionResolver = divContext.debugFeatures.expressionResolver!! + expressionResolver = divContext.debugFeatures.getExpressionResolver(divData)!! ) } diff --git a/client/android/compose/src/test/kotlin/com/yandex/div/compose/triggers/DivTriggerStorageTest.kt b/client/android/compose/src/test/kotlin/com/yandex/div/compose/triggers/DivTriggerStorageTest.kt index 8232e4a5c..b2573431f 100644 --- a/client/android/compose/src/test/kotlin/com/yandex/div/compose/triggers/DivTriggerStorageTest.kt +++ b/client/android/compose/src/test/kotlin/com/yandex/div/compose/triggers/DivTriggerStorageTest.kt @@ -118,6 +118,38 @@ class DivTriggerStorageTest { verifyTriggered() } + @Test + fun `not triggered when added after stopObserving() is called`() { + triggerStorage.stopObserving() + triggerStorage.add(trigger(action = action, condition = "@{true}")) + + verifyNoInteractions(actionHandler) + } + + @Test + fun `triggered when startObserving() is called`() { + triggerStorage.stopObserving() + triggerStorage.add(trigger(action = action, condition = "@{true}")) + + verifyNoInteractions(actionHandler) + + triggerStorage.startObserving() + + verifyTriggered() + } + + @Test + fun `not triggered when startObserving() is called and was triggered before`() { + triggerStorage.add(trigger(action = action, condition = "@{true}")) + + verifyTriggered() + + triggerStorage.stopObserving() + triggerStorage.startObserving() + + verifyNoInteractions(actionHandler) + } + private fun verifyTriggered() { verify(actionHandler).handle(actionHandlingContext, action) clearInvocations(actionHandler) diff --git a/client/android/compose/src/test/kotlin/com/yandex/div/compose/utils/ExpressionUtilsTest.kt b/client/android/compose/src/test/kotlin/com/yandex/div/compose/utils/ExpressionUtilsTest.kt index 295bd99aa..efe2897cf 100644 --- a/client/android/compose/src/test/kotlin/com/yandex/div/compose/utils/ExpressionUtilsTest.kt +++ b/client/android/compose/src/test/kotlin/com/yandex/div/compose/utils/ExpressionUtilsTest.kt @@ -10,8 +10,8 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.test.junit4.createComposeRule import androidx.test.ext.junit.runners.AndroidJUnit4 import com.yandex.div.compose.createExpressionResolver -import com.yandex.div.compose.views.DivLocalContext -import com.yandex.div.compose.views.LocalDivContext +import com.yandex.div.compose.context.DivLocalContext +import com.yandex.div.compose.context.LocalDivContext import com.yandex.div.core.expression.variables.DivVariableController import com.yandex.div.data.Variable import com.yandex.div.json.expressions.Expression @@ -36,6 +36,7 @@ class ExpressionUtilsTest { variableController = variableController ), functionProvider = mock(), + triggerStorage = mock(), variableController = variableController ) diff --git a/client/android/divkit-demo-app/src/main/java/com/yandex/divkit/demo/screenshot/DivComposeScreenshotActivity.kt b/client/android/divkit-demo-app/src/main/java/com/yandex/divkit/demo/screenshot/DivComposeScreenshotActivity.kt index eeb9aa011..b2b91b56c 100644 --- a/client/android/divkit-demo-app/src/main/java/com/yandex/divkit/demo/screenshot/DivComposeScreenshotActivity.kt +++ b/client/android/divkit-demo-app/src/main/java/com/yandex/divkit/demo/screenshot/DivComposeScreenshotActivity.kt @@ -28,6 +28,7 @@ adb shell am start -n com.yandex.divkit.demo/com.yandex.divkit.demo.screenshot.D class DivComposeScreenshotActivity : ComponentActivity() { private lateinit var divContext: DivContext + private lateinit var data: DivData val imageLoadingTracker = ComposeImageLoadingTracker() @@ -55,7 +56,7 @@ class DivComposeScreenshotActivity : ComponentActivity() { val environment = DivParsingEnvironment(ParsingErrorLogger.ASSERT).apply { if (templatesJson != null) parseTemplates(templatesJson) } - val data = DivData(environment, json.getJSONObject("card")) + data = DivData(environment, json.getJSONObject("card")) val view = ComposeView(divContext).apply { id = R.id.screenshot_view @@ -73,7 +74,7 @@ class DivComposeScreenshotActivity : ComponentActivity() { fun performActions(actions: List) { actions.forEach { - divContext.debugFeatures.performAction(it) + divContext.debugFeatures.performAction(data = data, action = it) } } diff --git a/client/android/test-utils/src/main/kotlin/com/yandex/div/test/data/DivTypedValueUtils.kt b/client/android/test-utils/src/main/kotlin/com/yandex/div/test/data/DivTypedValueUtils.kt index 0ac84bc35..46ae24b2c 100644 --- a/client/android/test-utils/src/main/kotlin/com/yandex/div/test/data/DivTypedValueUtils.kt +++ b/client/android/test-utils/src/main/kotlin/com/yandex/div/test/data/DivTypedValueUtils.kt @@ -1,5 +1,6 @@ package com.yandex.div.test.data +import com.yandex.div.json.expressions.Expression import com.yandex.div2.ArrayValue import com.yandex.div2.ColorValue import com.yandex.div2.DivTypedValue @@ -15,6 +16,10 @@ fun typedValue(value: Long): DivTypedValue { return DivTypedValue.Integer(IntegerValue(value = constant(value))) } +fun typedValue(value: Expression): DivTypedValue { + return DivTypedValue.Integer(IntegerValue(value = value)) +} + fun typedValue(value: JSONArray): DivTypedValue { return DivTypedValue.Array(ArrayValue(value = constant(value))) }