Added compose module

commit_hash:929b87b6271554e0fc8433fd817424d45077af19
This commit is contained in:
pkurchatov
2026-02-27 13:55:23 +03:00
parent 5d72ba0e66
commit 0e906745b2
18 changed files with 430 additions and 3 deletions
+13
View File
@@ -596,6 +596,19 @@
"client/android/compose-interop/proguard-rules.pro":"divkit/public/client/android/compose-interop/proguard-rules.pro",
"client/android/compose-interop/src/main/java/com/yandex/div/compose/interop/DivContexts.kt":"divkit/public/client/android/compose-interop/src/main/java/com/yandex/div/compose/interop/DivContexts.kt",
"client/android/compose-interop/src/main/java/com/yandex/div/compose/interop/DivViewInterop.kt":"divkit/public/client/android/compose-interop/src/main/java/com/yandex/div/compose/interop/DivViewInterop.kt",
"client/android/compose/build.gradle.kts":"divkit/public/client/android/compose/build.gradle.kts",
"client/android/compose/proguard-rules.pro":"divkit/public/client/android/compose/proguard-rules.pro",
"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/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/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",
"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/Utils.kt":"divkit/public/client/android/compose/src/main/kotlin/com/yandex/div/compose/views/Utils.kt",
"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/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",
+2
View File
@@ -30,6 +30,7 @@ buildscript {
plugins {
alias(libs.plugins.android.kotlin.multiplatform.library) apply false
alias(libs.plugins.android.library) apply false
alias(libs.plugins.kotlin.multiplatform) apply false
}
@@ -124,6 +125,7 @@ buildTimeTracker {
apiValidation {
ignoredProjects += [
"api-generator-test",
"compose",
"divkit-demo-app",
"divkit-perftests",
"divkit-regression-testing",
+1 -1
View File
@@ -1,7 +1,7 @@
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
alias(libs.plugins.compose.compiler)
alias(libs.plugins.kotlin.compose)
}
apply from: "${project.projectDir}/../div-library.gradle"
+23
View File
@@ -0,0 +1,23 @@
plugins {
alias(libs.plugins.android.library)
alias(libs.plugins.kotlin.compose)
}
apply(from = "../div-library.gradle")
android {
namespace = "com.yandex.div.compose"
}
dependencies {
implementation(project(":div-data"))
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.compose.material3)
implementation(libs.androidx.compose.ui.tooling.preview)
implementation(libs.androidx.coreKtx)
implementation(libs.coil.compose)
implementation(libs.coil.network)
debugImplementation(libs.androidx.compose.ui.tooling)
}
View File
@@ -0,0 +1,11 @@
package com.yandex.div.compose
import android.content.Context
import android.content.ContextWrapper
import androidx.annotation.MainThread
import com.yandex.div.json.expressions.ExpressionResolver
class DivContext @MainThread internal constructor(
baseContext: Context,
internal val expressionResolver: ExpressionResolver
) : ContextWrapper(baseContext)
@@ -0,0 +1,17 @@
package com.yandex.div.compose
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.yandex.div.compose.views.DivBlockView
import com.yandex.div2.DivData
@Composable
fun DivView(
data: DivData,
modifier: Modifier = Modifier
) {
DivBlockView(
data = data.states.first().div,
modifier = modifier
)
}
@@ -0,0 +1,88 @@
package com.yandex.div.compose
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.tooling.preview.Preview
import androidx.core.net.toUri
import com.yandex.div.json.expressions.Expression
import com.yandex.div.json.expressions.ExpressionResolver
import com.yandex.div2.Div
import com.yandex.div2.DivBackground
import com.yandex.div2.DivContainer
import com.yandex.div2.DivData
import com.yandex.div2.DivEdgeInsets
import com.yandex.div2.DivFixedSize
import com.yandex.div2.DivImage
import com.yandex.div2.DivSize
import com.yandex.div2.DivSolidBackground
import com.yandex.div2.DivText
@Preview
@Composable
private fun DivViewPreview() {
val divContext = DivContext(
baseContext = LocalContext.current,
expressionResolver = ExpressionResolver.EMPTY
)
CompositionLocalProvider(LocalContext provides divContext) {
DivView(data = testData)
}
}
private val testData = DivData(
logId = "preview",
states = listOf(
DivData.State(
stateId = 0,
div = Div.Container(
value = DivContainer(
orientation = constant(DivContainer.Orientation.VERTICAL),
paddings = DivEdgeInsets(
start = constant(10),
end = constant(10),
top = constant(20),
bottom = constant(20)
),
margins = DivEdgeInsets(
top = constant(10),
bottom = constant(10)
),
background = listOf(
DivBackground.Solid(
value = DivSolidBackground(
color = constant(0xFF909090.toInt()),
)
)
),
items = listOf(
Div.Image(
value = DivImage(
width = fixedSize(48),
height = fixedSize(48),
imageUrl = constant("https://yastatic.net/s3/home/divkit/logo.png".toUri())
)
),
Div.Text(
value = DivText(
fontSize = constant(36),
text = constant("Hello!"),
textColor = constant(0xFFBF0000.toInt()),
)
)
)
)
)
)
)
)
private fun <T : Any> constant(value: T): Expression<T> {
return Expression.ConstantExpression(value)
}
private fun fixedSize(value: Long): DivSize {
return DivSize.Fixed(
value = DivFixedSize(value = constant(value))
)
}
@@ -0,0 +1,20 @@
package com.yandex.div.compose.views
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.yandex.div.compose.views.modifiers.apply
import com.yandex.div2.Div
@Composable
internal fun DivBlockView(
data: Div,
modifier: Modifier = Modifier
) {
val modifier = modifier.apply(data.value())
when (data) {
is Div.Container -> DivContainerView(modifier, data.value)
is Div.Image -> DivImageView(modifier, data.value)
is Div.Text -> DivTextView(modifier, data.value)
else -> TODO()
}
}
@@ -0,0 +1,71 @@
package com.yandex.div.compose.views
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.yandex.div2.Div
import com.yandex.div2.DivContainer
@Composable
internal fun DivContainerView(
modifier: Modifier,
data: DivContainer
) {
when (data.orientation.evaluate()) {
DivContainer.Orientation.HORIZONTAL ->
HorizontalView(
modifier = modifier,
items = data.items.orEmpty()
)
DivContainer.Orientation.VERTICAL ->
VerticalView(
modifier = modifier,
items = data.items.orEmpty()
)
DivContainer.Orientation.OVERLAP ->
OverlapView(
modifier = modifier,
items = data.items.orEmpty()
)
}
}
@Composable
private fun HorizontalView(
modifier: Modifier,
items: List<Div>
) {
Row(modifier = modifier) {
items.forEach {
DivBlockView(data = it)
}
}
}
@Composable
private fun VerticalView(
modifier: Modifier,
items: List<Div>
) {
Column(modifier = modifier) {
items.forEach {
DivBlockView(data = it)
}
}
}
@Composable
private fun OverlapView(
modifier: Modifier,
items: List<Div>
) {
Box(modifier = modifier) {
items.forEach {
DivBlockView(data = it)
}
}
}
@@ -0,0 +1,18 @@
package com.yandex.div.compose.views
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import coil3.compose.AsyncImage
import com.yandex.div2.DivImage
@Composable
internal fun DivImageView(
modifier: Modifier,
data: DivImage
) {
AsyncImage(
modifier = modifier,
model = data.imageUrl.evaluate(),
contentDescription = null
)
}
@@ -0,0 +1,20 @@
package com.yandex.div.compose.views
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.sp
import com.yandex.div2.DivText
@Composable
internal fun DivTextView(
modifier: Modifier,
data: DivText
) {
Text(
text = data.text.evaluate(expressionResolver),
modifier = modifier,
color = data.textColor.evaluate().toColor(),
fontSize = data.fontSize.evaluate().toFloat().sp,
)
}
@@ -0,0 +1,31 @@
package com.yandex.div.compose.views
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
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.json.expressions.Expression
import com.yandex.div.json.expressions.ExpressionResolver
internal fun Int.toColor(): Color {
return Color(this)
}
internal fun Long.toDp(): Dp {
return toFloat().dp
}
internal val divContext: DivContext
@Composable
get() = LocalContext.current as DivContext
internal val expressionResolver: ExpressionResolver
@Composable
get() = divContext.expressionResolver
@Composable
internal fun <T : Any> Expression<T>.evaluate(): T {
return evaluate(expressionResolver)
}
@@ -0,0 +1,33 @@
package com.yandex.div.compose.views.modifiers
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.toColor
import com.yandex.div2.DivBackground
import com.yandex.div2.DivSolidBackground
@Composable
internal fun Modifier.backgrounds(value: List<DivBackground>): Modifier {
var modifier = this
value.forEach { background ->
when (background) {
is DivBackground.Solid ->
modifier = modifier.solidBackground(background.value)
is DivBackground.Image -> TODO()
is DivBackground.LinearGradient -> TODO()
is DivBackground.NinePatch -> TODO()
is DivBackground.RadialGradient -> TODO()
}
}
return modifier
}
@Composable
private fun Modifier.solidBackground(value: DivSolidBackground): Modifier {
return background(
color = value.color.evaluate().toColor()
)
}
@@ -0,0 +1,45 @@
package com.yandex.div.compose.views.modifiers
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import com.yandex.div.compose.views.evaluate
import com.yandex.div.compose.views.toDp
import com.yandex.div2.DivBase
import com.yandex.div2.DivEdgeInsets
@Composable
internal fun Modifier.apply(data: DivBase): Modifier {
var modifier = this
.width(data.width)
.height(data.height)
data.margins?.let {
modifier = modifier.padding(it)
}
data.background?.let {
modifier = modifier.backgrounds(it)
}
data.paddings?.let {
modifier = modifier.padding(it)
}
data.id?.let {
modifier = modifier.testTag(it)
}
return modifier
}
@Composable
private fun Modifier.padding(value: DivEdgeInsets): Modifier {
return padding(
start = (value.start ?: value.left).evaluate().toDp(),
end = (value.end ?: value.right).evaluate().toDp(),
top = value.top.evaluate().toDp(),
bottom = value.bottom.evaluate().toDp()
)
}
@@ -0,0 +1,31 @@
package com.yandex.div.compose.views.modifiers
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.yandex.div.compose.views.evaluate
import com.yandex.div.compose.views.toDp
import com.yandex.div2.DivSize
@Composable
internal fun Modifier.height(height: DivSize): Modifier {
return when (height) {
is DivSize.MatchParent -> fillMaxHeight()
is DivSize.WrapContent -> wrapContentHeight()
is DivSize.Fixed -> height(height.value.value.evaluate().toDp())
}
}
@Composable
internal fun Modifier.width(width: DivSize): Modifier {
return when (width) {
is DivSize.MatchParent -> fillMaxWidth()
is DivSize.WrapContent -> wrapContentWidth()
is DivSize.Fixed -> width(width.value.value.evaluate().toDp())
}
}
+4 -1
View File
@@ -88,6 +88,7 @@ androidx-work = { module = "androidx.work:work-runtime-ktx", version.ref = "andr
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "androidx-compose-bom" }
androidx-compose-foundation = { module = "androidx.compose.foundation:foundation" }
androidx-compose-material3 = { module = "androidx.compose.material3:material3" }
androidx-compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling" }
androidx-compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" }
@@ -119,6 +120,7 @@ appmetrica-plugin = { module = "io.appmetrica.analytics:gradle", version.ref = "
buildTimeTracker = { module = "com.asarkar.gradle:build-time-tracker", version.ref = "buildTimeTracker" }
coil = { module = "io.coil-kt.coil3:coil", version.ref = "coil" }
coil-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coil" }
coil-gif = { module = "io.coil-kt.coil3:coil-gif", version.ref = "coil" }
coil-network = { module = "io.coil-kt.coil3:coil-network-okhttp", version.ref = "coil" }
coil-network-cachecontrol = { module = "io.coil-kt.coil3:coil-network-cache-control", version.ref = "coil" }
@@ -194,8 +196,9 @@ zxing-embedded = { module = "com.journeyapps:zxing-android-embedded", version =
[plugins]
apiGenerator = { id = "com.yandex.divkit.api-generator", version = "unspecified" }
android-kotlin-multiplatform-library = { id = "com.android.kotlin.multiplatform.library", version.ref = "agp" }
android-library = { id = "com.android.library", version.ref = "agp" }
buildkonfig = { id = "com.codingfeline.buildkonfig", version = "0.17.1" }
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
kotlin-ksp = { id = "com.google.devtools.ksp", version.ref = "kotlin-ksp" }
kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
+2 -1
View File
@@ -25,6 +25,7 @@ include ':api-generator-test'
include ':assertion'
include ':beacon'
include ':coil'
include ':compose'
include ':compose-interop'
include ':div'
include ':div-core'
@@ -45,6 +46,7 @@ include ':div-states'
include ':div-storage'
include ':div-svg'
include ':div-video'
include ':div-video-m3'
include ':divkit-demo-app'
include ':divkit-perftests'
include ':divkit-regression-testing'
@@ -59,7 +61,6 @@ include ':screenshot-test-runtime'
include ':ui-test-common'
include ':utils'
include ':video-custom'
include ':div-video-m3'
def internalSettings = file("settings.internal.gradle")
if (internalSettings.exists()) {