Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions docs/en/apis/sparkling-sdk-android.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ val baseInfoConfig = BaseInfoConfig(isDebug = BuildConfig.DEBUG)
val lynxConfig = SparklingLynxConfig.build(this) {
// optional: add global Lynx behaviors/modules, template provider, etc.
// setSharedProcessDensityOverride(2.0f)
setDefaultThreadStrategy(SparklingThreadStrategy.MULTI_THREADS)
}
val hybridConfig = SparklingHybridConfig.build(baseInfoConfig) {
setLynxConfig(lynxConfig)
Expand Down Expand Up @@ -78,6 +79,7 @@ Configuration object passed to both container types.
|----------|-------------|
| `scheme` | The `hybrid://...` URL to load. |
| `sparklingUIProvider` | Implements `SparklingUIProvider` for custom loading/error/toolbar views. |
| `threadStrategy` | Optional per-container `SparklingThreadStrategy`. Overrides the global default. |
| `hybridSchemeParam` | Parsed scheme parameters (auto-populated from `scheme`). |
| `lynxViewport` | Optional `SparklingLynxViewport(widthPx, heightPx)` fixed viewport in physical pixels. Programmatic configuration overrides parsed scheme dimensions. |
| `containerId` | Unique container identifier (auto-generated). |
Expand All @@ -86,6 +88,16 @@ For advanced hosts that already provide `LynxKitInitParams`, set its `lynxViewpo
params take precedence over `SparklingContext.lynxViewport`, which takes precedence over canonical
scheme `width` and `height`. All three paths require a complete positive width/height pair.

## Thread strategy

`SparklingThreadStrategy` provides typed mappings for the Lynx rendering
strategies `ALL_ON_UI`, `MOST_ON_TASM`, `PART_ON_LAYOUT`, and `MULTI_THREADS`.
Set an optional global default with
`SparklingLynxConfig.Builder.setDefaultThreadStrategy(...)`, or override it for
one container with `SparklingContext.threadStrategy`. The per-container value
takes precedence. If neither value is set, Sparkling leaves the Lynx SDK
default unchanged.

## SparklingUIProvider

Interface for customizing container UI. Applies to both full-page and embedded containers.
Expand Down
11 changes: 11 additions & 0 deletions docs/zh/apis/sparkling-sdk-android.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ val baseInfoConfig = BaseInfoConfig(isDebug = BuildConfig.DEBUG)
val lynxConfig = SparklingLynxConfig.build(this) {
// 可选:添加全局 Lynx 行为/模块、模板提供者等
// setSharedProcessDensityOverride(2.0f)
setDefaultThreadStrategy(SparklingThreadStrategy.MULTI_THREADS)
}
val hybridConfig = SparklingHybridConfig.build(baseInfoConfig) {
setLynxConfig(lynxConfig)
Expand Down Expand Up @@ -78,6 +79,7 @@ Sparkling 创建的 `LynxView` 也必须使用完全相同的 density 构造**
|------|------|
| `scheme` | 要加载的 `hybrid://...` URL。 |
| `sparklingUIProvider` | 实现 `SparklingUIProvider` 以自定义加载/错误/工具栏视图。 |
| `threadStrategy` | 可选的容器级 `SparklingThreadStrategy`,优先于全局默认值。 |
| `hybridSchemeParam` | 解析后的 scheme 参数(从 `scheme` 自动填充)。 |
| `lynxViewport` | 可选的 `SparklingLynxViewport(widthPx, heightPx)`,以物理像素指定固定 viewport。程序化配置会覆盖 scheme 中解析的尺寸。 |
| `containerId` | 唯一的容器标识符(自动生成)。 |
Expand All @@ -86,6 +88,15 @@ Sparkling 创建的 `LynxView` 也必须使用完全相同的 density 构造**
init params、`SparklingContext.lynxViewport`、canonical scheme 的 `width` 和 `height`。
三种入口都只接受完整的正数宽高组合。

## 线程策略

`SparklingThreadStrategy` 为 Lynx 的 `ALL_ON_UI`、`MOST_ON_TASM`、
`PART_ON_LAYOUT` 和 `MULTI_THREADS` 渲染线程策略提供类型安全的映射。
可以通过 `SparklingLynxConfig.Builder.setDefaultThreadStrategy(...)`
设置可选的全局默认值,也可以通过 `SparklingContext.threadStrategy`
为单个容器覆盖。容器级配置优先;两者都未设置时,Sparkling 不改变
Lynx SDK 的默认策略。

## SparklingUIProvider

自定义容器 UI 的接口。适用于全页和嵌入式容器。
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,4 +104,5 @@ class SparklingContext : HybridContext() {
var lifecycleDelegate: SparklingLifecycleDelegate? = null
var lynxViewCreatedListener: SparklingLynxViewCreatedListener? = null
var lynxViewport: SparklingLynxViewport? = null
var threadStrategy: SparklingThreadStrategy? = null
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Copyright (c) 2026 TikTok Pte. Ltd.
// Licensed under the Apache License Version 2.0 that can be found in the
// LICENSE file in the root directory of this source tree.
package com.tiktok.sparkling

import com.lynx.tasm.ThreadStrategyForRendering

/**
* Controls how Lynx rendering work is distributed across threads.
*/
enum class SparklingThreadStrategy {
ALL_ON_UI,
MOST_ON_TASM,
PART_ON_LAYOUT,
MULTI_THREADS,
;

internal fun toLynxThreadStrategy(): ThreadStrategyForRendering =
when (this) {
ALL_ON_UI -> ThreadStrategyForRendering.ALL_ON_UI
MOST_ON_TASM -> ThreadStrategyForRendering.MOST_ON_TASM
PART_ON_LAYOUT -> ThreadStrategyForRendering.PART_ON_LAYOUT
MULTI_THREADS -> ThreadStrategyForRendering.MULTI_THREADS
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import com.lynx.tasm.LynxEnv
import com.lynx.tasm.base.LLog
import com.lynx.tasm.behavior.Behavior
import com.lynx.tasm.provider.AbsTemplateProvider
import com.tiktok.sparkling.SparklingThreadStrategy
import com.tiktok.sparkling.hybridkit.lynx.SparklingLynxModuleWrapper

class SparklingLynxConfig private constructor(
Expand All @@ -20,6 +21,7 @@ class SparklingLynxConfig private constructor(
val globalModules: MutableMap<String, SparklingLynxModuleWrapper>,
val additionInit: LynxEnv.() -> Unit,
val sharedProcessDensityOverride: Float?,
val defaultThreadStrategy: SparklingThreadStrategy?,
val logLevel: Int = LLog.INFO,
) : ILynxConfig {
companion object {
Expand All @@ -39,6 +41,7 @@ class SparklingLynxConfig private constructor(
private val globalModules = mutableMapOf<String, SparklingLynxModuleWrapper>()
private var additionInit: LynxEnv.() -> Unit = {}
private var sharedProcessDensityOverride: Float? = null
private var defaultThreadStrategy: SparklingThreadStrategy? = null

fun setCheckPropsSetter(checkPropsSetter: Boolean) {
isCheckPropsSetter = checkPropsSetter
Expand Down Expand Up @@ -77,6 +80,10 @@ class SparklingLynxConfig private constructor(
sharedProcessDensityOverride = density
}

fun setDefaultThreadStrategy(threadStrategy: SparklingThreadStrategy?) {
defaultThreadStrategy = threadStrategy
}

fun build() =
SparklingLynxConfig(
context,
Expand All @@ -87,6 +94,7 @@ class SparklingLynxConfig private constructor(
globalModules,
additionInit,
sharedProcessDensityOverride,
defaultThreadStrategy,
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import com.lynx.tasm.behavior.Behavior
import com.lynx.tasm.behavior.BehaviorBundle
import com.lynx.tasm.service.LynxServiceCenter
import com.tiktok.sparkling.SparklingContext
import com.tiktok.sparkling.SparklingThreadStrategy
import com.tiktok.sparkling.applyLynxViewport
import com.tiktok.sparkling.resolveLynxViewport
import com.tiktok.sparkling.hybridkit.HybridCommon
Expand Down Expand Up @@ -99,9 +100,11 @@ object HybridLynxKit {
}

val viewBuilder = createLynxViewBuilder(lynxConfig)
(hybridContext as? SparklingContext)?.resolveLynxViewport()?.let { viewport ->
val sparklingContext = hybridContext as? SparklingContext
sparklingContext?.resolveLynxViewport()?.let { viewport ->
viewBuilder.applyLynxViewport(viewport)
}
applyThreadStrategy(viewBuilder, sparklingContext?.threadStrategy, lynxConfig?.defaultThreadStrategy)
var lynxViewRef: SimpleLynxKitView? = null
(lynxConfig?.templateProvider ?: LynxEnv.inst().templateProvider)?.let { templateProvider ->
viewBuilder.setTemplateProvider(
Expand All @@ -112,7 +115,6 @@ object HybridLynxKit {
}
val bridge = SparklingBridge()
bridge.registerLynxModule(viewBuilder, hybridContext.containerId)
val sparklingContext = hybridContext as? SparklingContext
val lynxView =
SimpleLynxKitView(context, hybridContext, viewBuilder, kitInitParams, lifeCycle)
lynxViewRef = lynxView
Expand All @@ -132,4 +134,14 @@ object HybridLynxKit {
LynxViewBuilder().apply {
lynxConfig?.sharedProcessDensityOverride?.let(::setDensity)
}

internal fun applyThreadStrategy(
viewBuilder: LynxViewBuilder,
pageThreadStrategy: SparklingThreadStrategy?,
defaultThreadStrategy: SparklingThreadStrategy?,
) {
(pageThreadStrategy ?: defaultThreadStrategy)?.let {
viewBuilder.setThreadStrategyForRendering(it.toLynxThreadStrategy())
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Copyright (c) 2026 TikTok Pte. Ltd.
// Licensed under the Apache License Version 2.0 that can be found in the
// LICENSE file in the root directory of this source tree.
package com.tiktok.sparkling

import com.lynx.tasm.ThreadStrategyForRendering
import org.junit.Assert.assertEquals
import org.junit.Test

class SparklingThreadStrategyTest {
@Test
fun allStrategiesMapToTheirLynxCounterparts() {
val expected =
mapOf(
SparklingThreadStrategy.ALL_ON_UI to ThreadStrategyForRendering.ALL_ON_UI,
SparklingThreadStrategy.MOST_ON_TASM to ThreadStrategyForRendering.MOST_ON_TASM,
SparklingThreadStrategy.PART_ON_LAYOUT to ThreadStrategyForRendering.PART_ON_LAYOUT,
SparklingThreadStrategy.MULTI_THREADS to ThreadStrategyForRendering.MULTI_THREADS,
)

assertEquals(expected.keys, SparklingThreadStrategy.values().toSet())
expected.forEach { (strategy, lynxStrategy) ->
assertEquals(lynxStrategy, strategy.toLynxThreadStrategy())
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@ import android.view.WindowManager
import android.view.Display
import android.view.accessibility.AccessibilityManager
import com.lynx.tasm.LynxView
import com.lynx.tasm.ThreadStrategyForRendering
import com.tiktok.sparkling.SparklingContext
import com.tiktok.sparkling.SparklingLynxViewCreatedListener
import com.tiktok.sparkling.SparklingThreadStrategy
import com.tiktok.sparkling.hybridkit.HybridContext
import com.tiktok.sparkling.hybridkit.HybridKit
import com.tiktok.sparkling.hybridkit.HybridCommon
Expand All @@ -24,6 +26,7 @@ import com.tiktok.sparkling.hybridkit.base.HybridLoadSession
import com.tiktok.sparkling.hybridkit.base.Theme
import com.tiktok.sparkling.hybridkit.config.BaseInfoConfig
import com.tiktok.sparkling.hybridkit.config.SparklingHybridConfig
import com.tiktok.sparkling.hybridkit.config.SparklingLynxConfig
import com.tiktok.sparkling.hybridkit.scheme.HybridSchemeParam
import io.mockk.*
import org.json.JSONObject
Expand Down Expand Up @@ -178,6 +181,43 @@ class HybridKitTest {
assertNull(sparklingContext.lynxViewCreatedListener)
assertNotNull(HybridKit.createKitView(scheme, sparklingContext, mockContext))
}

@Test
fun testPageThreadStrategyIsAppliedDuringLynxViewCreation() {
val lynxConfig =
SparklingLynxConfig.build(mockApplication) {
setDefaultThreadStrategy(SparklingThreadStrategy.PART_ON_LAYOUT)
}
HybridCommon.setHybridConfig(
SparklingHybridConfig.build(BaseInfoConfig(isDebug = false)) {
setLynxConfig(lynxConfig)
},
mockApplication,
)
val scheme =
HybridSchemeParam(
engineType = HybridKitType.LYNX,
bundle = "https://example.com/main.lynx.bundle",
)
var strategyAtCreation: ThreadStrategyForRendering? = null
val sparklingContext =
SparklingContext().apply {
hybridSchemeParam = scheme
threadStrategy = SparklingThreadStrategy.MULTI_THREADS
lynxViewCreatedListener =
SparklingLynxViewCreatedListener { lynxView ->
strategyAtCreation = lynxView.threadStrategyForRendering
}
}

val result = HybridKit.createKitView(scheme, sparklingContext, mockContext)

assertEquals(ThreadStrategyForRendering.MULTI_THREADS, strategyAtCreation)
assertEquals(
ThreadStrategyForRendering.MULTI_THREADS,
(result as LynxView).threadStrategyForRendering,
)
}
}

@RunWith(RobolectricTestRunner::class)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.tiktok.sparkling.hybridkit.config

import android.app.Application
import com.lynx.tasm.LynxEnv
import com.tiktok.sparkling.SparklingThreadStrategy
import com.tiktok.sparkling.hybridkit.lynx.SparklingLynxModuleWrapper
import io.mockk.mockk
import org.junit.Assert.assertEquals
Expand Down Expand Up @@ -108,6 +109,7 @@ class HybridConfigTest {
assertTrue(empty.globalModules.isEmpty())
assertNotNull(empty.additionInit)
assertNull(empty.sharedProcessDensityOverride)
assertNull(empty.defaultThreadStrategy)

val moduleWrapper = mockk<SparklingLynxModuleWrapper>(relaxed = true)
var initCalled = false
Expand All @@ -118,11 +120,13 @@ class HybridConfigTest {
setTemplateProvider(null)
addLynxModules(mapOf("foo" to moduleWrapper))
setAdditionInit { initCalled = true }
setDefaultThreadStrategy(SparklingThreadStrategy.MULTI_THREADS)
}

assertFalse(cfg.isCheckPropsSetter)
assertEquals(1, cfg.globalModules.size)
assertSame(moduleWrapper, cfg.globalModules["foo"])
assertEquals(SparklingThreadStrategy.MULTI_THREADS, cfg.defaultThreadStrategy)
// exercise additionInit lambda
cfg.additionInit.invoke(mockk<LynxEnv>(relaxed = true))
assertTrue(initCalled)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// Copyright (c) 2026 TikTok Pte. Ltd.
// Licensed under the Apache License Version 2.0 that can be found in the
// LICENSE file in the root directory of this source tree.
package com.tiktok.sparkling.hybridkit.lynx

import com.lynx.tasm.LynxViewBuilder
import com.lynx.tasm.ThreadStrategyForRendering
import com.tiktok.sparkling.SparklingThreadStrategy
import io.mockk.mockk
import io.mockk.verify
import org.junit.Assert.assertEquals
import org.junit.Test

class HybridLynxKitThreadStrategyTest {
@Test
fun pageStrategyOverridesGlobalDefault() {
val builder = LynxViewBuilder()

HybridLynxKit.applyThreadStrategy(
builder,
SparklingThreadStrategy.MULTI_THREADS,
SparklingThreadStrategy.PART_ON_LAYOUT,
)

assertEquals(ThreadStrategyForRendering.MULTI_THREADS, builder.threadStrategy)
}

@Test
fun globalDefaultAppliesWhenPageStrategyIsUnset() {
val builder = LynxViewBuilder()

HybridLynxKit.applyThreadStrategy(
builder,
null,
SparklingThreadStrategy.MOST_ON_TASM,
)

assertEquals(ThreadStrategyForRendering.MOST_ON_TASM, builder.threadStrategy)
}

@Test
fun lynxDefaultRemainsUntouchedWhenBothStrategiesAreUnset() {
val builder = mockk<LynxViewBuilder>(relaxed = true)

HybridLynxKit.applyThreadStrategy(builder, null, null)

verify(exactly = 0) {
builder.setThreadStrategyForRendering(any())
}
}
}
Loading