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
6 changes: 6 additions & 0 deletions docs/en/apis/scheme.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,12 @@ screen size or density.
The pair is atomic. If either parameter is missing, non-integer, zero, negative, or too large for an
Android measure spec, Sparkling ignores both and keeps the default full-size behavior.

A valid fixed viewport cannot be used with the effective Android
`SparklingThreadStrategy.MULTI_THREADS` rendering strategy. Sparkling resolves
the page strategy before the global default and rejects that unsafe
combination with a typed `SparklingLynxConfigurationException` before Lynx
view construction.

```
hybrid://lynxview_page?bundle=main.lynx.bundle&width=720&height=1280
```
Expand Down
43 changes: 40 additions & 3 deletions docs/en/apis/sparkling-sdk-android.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +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)
setDefaultThreadStrategy(SparklingThreadStrategy.PART_ON_LAYOUT)
setResourceFetcherFactory { sparklingContext ->
SparklingResourceFetcherConfig.builder()
.setGenericResourceFetcher(createGenericFetcher(sparklingContext))
Expand Down Expand Up @@ -58,8 +58,8 @@ Entry point for creating containers. See [Containers](../guide/containers.md) fo
| Method | Description |
|--------|-------------|
| `Sparkling.build(context, sparklingContext)` | Creates a `Sparkling` instance from an Android `Context` and a `SparklingContext`. |
| `navigate()` | Starts `SparklingActivity` (full-page container). Returns `true` on success. |
| `createView(withoutPrepare)` | Creates a `SparklingView` (embedded container). Returns `null` on failure. |
| `navigate()` | Starts `SparklingActivity` (full-page container). Returns `true` on success and throws a typed exception for incompatible Lynx configuration. |
| `createView(withoutPrepare)` | Creates a `SparklingView` (embedded container). Returns `null` for other creation failures and throws a typed exception for incompatible Lynx configuration when prepare is enabled. |

## SparklingView

Expand Down Expand Up @@ -162,6 +162,43 @@ one container with `SparklingContext.threadStrategy`. The per-container value
takes precedence. If neither value is set, Sparkling leaves the Lynx SDK
default unchanged.

### Fixed viewport compatibility

Do not combine an effective fixed viewport with the effective
`SparklingThreadStrategy.MULTI_THREADS` strategy. This combination can crash
inside the native Lynx SDK. Sparkling rejects it before constructing the
`LynxView` and throws `SparklingLynxConfigurationException` with
`SparklingLynxConfigurationError.FIXED_VIEWPORT_WITH_MULTI_THREADS`.
Both `navigate()` and prepared `createView(false)` calls fail synchronously
before starting or constructing a container.

Sparkling resolves all typed configuration before validating it:

1. viewport: `LynxKitInitParams.lynxViewport`, then
`SparklingContext.lynxViewport`, then canonical scheme `width`/`height`;
2. thread strategy: `SparklingContext.threadStrategy`, then
`SparklingLynxConfig.defaultThreadStrategy`, then the unchanged Lynx
default.

Validation therefore does not depend on whether the viewport or strategy
setter ran first. A safe page strategy can override a global
`MULTI_THREADS` default. `MULTI_THREADS` without a fixed viewport and fixed
viewports with `ALL_ON_UI`, `MOST_ON_TASM`, `PART_ON_LAYOUT`, or no explicit
strategy keep their existing behavior.

Java callers can catch and inspect the typed failure:

```java
try {
SparklingView view = Sparkling.build(context, sparklingContext).createView(false);
} catch (SparklingLynxConfigurationException exception) {
if (exception.getError()
== SparklingLynxConfigurationError.FIXED_VIEWPORT_WITH_MULTI_THREADS) {
// Select a safe strategy or remove the fixed viewport.
}
}
```

## SparklingUIProvider

Interface for customizing container UI. Applies to both full-page and embedded containers.
Expand Down
5 changes: 5 additions & 0 deletions docs/zh/apis/scheme.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,11 @@ Sparkling 会使用精确的 preset measure spec 创建 LynxView,并以完全
这两个参数是原子配置。如果任意一个缺失、不是整数、为零、为负数,或超出 Android
measure spec 的安全范围,Sparkling 会同时忽略二者并保留默认的全尺寸行为。

有效的固定 viewport 不能与 Android 最终生效的
`SparklingThreadStrategy.MULTI_THREADS` 渲染策略同时使用。Sparkling 会先按
页面级配置优先于全局默认值的规则解析线程策略,再在构造 LynxView 前通过类型安全的
`SparklingLynxConfigurationException` 拒绝该危险组合。

```
hybrid://lynxview_page?bundle=main.lynx.bundle&width=720&height=1280
```
Expand Down
41 changes: 38 additions & 3 deletions docs/zh/apis/sparkling-sdk-android.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ val baseInfoConfig = BaseInfoConfig(isDebug = BuildConfig.DEBUG)
val lynxConfig = SparklingLynxConfig.build(this) {
// 可选:添加全局 Lynx 行为/模块、模板提供者等
// setSharedProcessDensityOverride(2.0f)
setDefaultThreadStrategy(SparklingThreadStrategy.MULTI_THREADS)
setDefaultThreadStrategy(SparklingThreadStrategy.PART_ON_LAYOUT)
}
val hybridConfig = SparklingHybridConfig.build(baseInfoConfig) {
setLynxConfig(lynxConfig)
Expand Down Expand Up @@ -51,8 +51,8 @@ Sparkling 创建的 `LynxView` 也必须使用完全相同的 density 构造**
| 方法 | 说明 |
|------|------|
| `Sparkling.build(context, sparklingContext)` | 通过 Android `Context` 和 `SparklingContext` 创建 `Sparkling` 实例。 |
| `navigate()` | 启动 `SparklingActivity`(全页容器)。成功返回 `true`。 |
| `createView(withoutPrepare)` | 创建 `SparklingView`(嵌入式容器)。失败返回 `null`。 |
| `navigate()` | 启动 `SparklingActivity`(全页容器)。成功返回 `true`;Lynx 配置不兼容时抛出类型安全异常。 |
| `createView(withoutPrepare)` | 创建 `SparklingView`(嵌入式容器)。其他创建失败返回 `null`;启用 prepare 时遇到不兼容 Lynx 配置会抛出类型安全异常。 |

## SparklingView

Expand Down Expand Up @@ -126,6 +126,41 @@ init params、`SparklingContext.lynxViewport`、canonical scheme 的 `width` 和
为单个容器覆盖。容器级配置优先;两者都未设置时,Sparkling 不改变
Lynx SDK 的默认策略。

### 固定 viewport 兼容性

不要同时使用最终生效的固定 viewport 和
`SparklingThreadStrategy.MULTI_THREADS`。这个组合可能在 Lynx SDK
native 内部触发崩溃。Sparkling 会在构造 `LynxView` 前拒绝该组合,并抛出
`SparklingLynxConfigurationException`;其 `error` 为
`SparklingLynxConfigurationError.FIXED_VIEWPORT_WITH_MULTI_THREADS`。
`navigate()` 和会执行 prepare 的 `createView(false)` 都会在启动或构造容器前同步失败。

Sparkling 会先解析所有类型安全配置,再执行校验:

1. viewport:`LynxKitInitParams.lynxViewport`、其次
`SparklingContext.lynxViewport`、最后 canonical scheme 的
`width`/`height`;
2. 线程策略:`SparklingContext.threadStrategy`、其次
`SparklingLynxConfig.defaultThreadStrategy`、最后保持 Lynx 默认值不变。

因此,无论先设置 viewport 还是线程策略,校验结果都相同。安全的页面级策略
可以覆盖全局 `MULTI_THREADS` 默认值。未使用固定 viewport 的
`MULTI_THREADS`,以及搭配 `ALL_ON_UI`、`MOST_ON_TASM`、
`PART_ON_LAYOUT` 或未显式设置线程策略的固定 viewport,均保持原有行为。

Java 调用方可以捕获并检查类型安全异常:

```java
try {
SparklingView view = Sparkling.build(context, sparklingContext).createView(false);
} catch (SparklingLynxConfigurationException exception) {
if (exception.getError()
== SparklingLynxConfigurationError.FIXED_VIEWPORT_WITH_MULTI_THREADS) {
// 改用安全的线程策略,或移除固定 viewport。
}
}
```

## SparklingUIProvider

自定义容器 UI 的接口。适用于全页和嵌入式容器。
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ package com.tiktok.sparkling
import android.content.Context
import android.content.Intent
import android.util.Log
import com.tiktok.sparkling.hybridkit.HybridCommon
import com.tiktok.sparkling.hybridkit.config.SparklingLynxConfig
import com.tiktok.sparkling.utils.SchemeParser

class Sparkling private constructor(
Expand Down Expand Up @@ -42,16 +44,22 @@ class Sparkling private constructor(
/**
* Navigate to a Sparkling activity.
* @return true if navigation was successful, false otherwise
* @throws SparklingLynxConfigurationException if the resolved Lynx options are incompatible
*/
fun navigate(): Boolean =
try {
processSparklingContext(sparklingContext)
sparklingContext.validateLynxConfiguration(
(HybridCommon.hybridConfig?.lynxConfig as? SparklingLynxConfig)?.defaultThreadStrategy,
)
val intent = Intent(context, SparklingActivity::class.java)
intent.putExtra(SPARKLING_CONTEXT_CONTAINER_ID, sparklingContext.containerId)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
SparklingContextTransferStation.saveSparklingContext(sparklingContext)
context.startActivity(intent)
true
} catch (e: SparklingLynxConfigurationException) {
throw e
} catch (e: Exception) {
Log.e(TAG, "Failed to navigate: ${e.message}")
false
Expand All @@ -75,7 +83,8 @@ class Sparkling private constructor(
/**
* Create a SparklingView.
* @param withoutPrepare If true, skip the prepare step
* @return SparklingView instance, or null if creation fails
* @return SparklingView instance, or null for non-configuration creation failures
* @throws SparklingLynxConfigurationException if the resolved Lynx options are incompatible
*/
fun createView(withoutPrepare: Boolean = false): SparklingView? =
try {
Expand All @@ -84,6 +93,8 @@ class Sparkling private constructor(
view.prepare(sparklingContext)
}
view
} catch (e: SparklingLynxConfigurationException) {
throw e
} catch (e: Exception) {
Log.e(TAG, "Failed to create view: ${e.message}")
null
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// 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

/**
* Identifies an unsupported combination of typed Sparkling Lynx options.
*/
enum class SparklingLynxConfigurationError {
FIXED_VIEWPORT_WITH_MULTI_THREADS,
}

/**
* Thrown before Lynx view construction when typed Sparkling options cannot be used together.
*/
class SparklingLynxConfigurationException(
val error: SparklingLynxConfigurationError,
) : IllegalArgumentException(error.message())

internal fun SparklingContext.validateLynxConfiguration(defaultThreadStrategy: SparklingThreadStrategy?) {
val lynxViewport = resolveLynxViewport()
val threadStrategy = threadStrategy ?: defaultThreadStrategy
if (lynxViewport != null && threadStrategy == SparklingThreadStrategy.MULTI_THREADS) {
throw SparklingLynxConfigurationException(
SparklingLynxConfigurationError.FIXED_VIEWPORT_WITH_MULTI_THREADS,
)
}
}

private fun SparklingLynxConfigurationError.message(): String =
when (this) {
SparklingLynxConfigurationError.FIXED_VIEWPORT_WITH_MULTI_THREADS -> {
"A fixed Lynx viewport cannot be used with the MULTI_THREADS rendering strategy."
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import com.tiktok.sparkling.SparklingContext
import com.tiktok.sparkling.SparklingThreadStrategy
import com.tiktok.sparkling.applyLynxViewport
import com.tiktok.sparkling.resolveLynxViewport
import com.tiktok.sparkling.validateLynxConfiguration
import com.tiktok.sparkling.hybridkit.HybridCommon
import com.tiktok.sparkling.hybridkit.HybridContext
import com.tiktok.sparkling.hybridkit.base.IHybridKitLifeCycle
Expand Down Expand Up @@ -99,12 +100,16 @@ object HybridLynxKit {
kitInitParams.loadUri = hybridContext.resolveFullScheme()?.toUri()
}

val viewBuilder = createLynxViewBuilder(lynxConfig)
val sparklingContext = hybridContext as? SparklingContext
sparklingContext?.resolveLynxViewport()?.let { viewport ->
val lynxViewport = sparklingContext?.resolveLynxViewport()
val threadStrategy = resolveThreadStrategy(sparklingContext?.threadStrategy, lynxConfig?.defaultThreadStrategy)
sparklingContext?.validateLynxConfiguration(lynxConfig?.defaultThreadStrategy)

val viewBuilder = createLynxViewBuilder(lynxConfig)
lynxViewport?.let { viewport ->
viewBuilder.applyLynxViewport(viewport)
}
applyThreadStrategy(viewBuilder, sparklingContext?.threadStrategy, lynxConfig?.defaultThreadStrategy)
applyThreadStrategy(viewBuilder, threadStrategy)
var lynxViewRef: SimpleLynxKitView? = null
val resourceFetcherConfig =
SparklingResourceFetcherConfigurator.resolve(sparklingContext, lynxConfig)
Expand Down Expand Up @@ -140,11 +145,15 @@ object HybridLynxKit {

internal fun applyThreadStrategy(
viewBuilder: LynxViewBuilder,
pageThreadStrategy: SparklingThreadStrategy?,
defaultThreadStrategy: SparklingThreadStrategy?,
threadStrategy: SparklingThreadStrategy?,
) {
(pageThreadStrategy ?: defaultThreadStrategy)?.let {
threadStrategy?.let {
viewBuilder.setThreadStrategyForRendering(it.toLynxThreadStrategy())
}
}

internal fun resolveThreadStrategy(
pageThreadStrategy: SparklingThreadStrategy?,
defaultThreadStrategy: SparklingThreadStrategy?,
): SparklingThreadStrategy? = pageThreadStrategy ?: defaultThreadStrategy
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// 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 static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;

import android.app.Application;
import com.tiktok.sparkling.hybridkit.HybridKit;
import com.tiktok.sparkling.hybridkit.base.HybridKitType;
import com.tiktok.sparkling.hybridkit.config.BaseInfoConfig;
import com.tiktok.sparkling.hybridkit.config.SparklingHybridConfig;
import com.tiktok.sparkling.hybridkit.scheme.HybridSchemeParam;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Config;

@RunWith(RobolectricTestRunner.class)
@Config(sdk = 33, packageName = "com.tiktok.sparkling")
public class SparklingLynxConfigurationJavaApiTest {
@Test
public void javaReceivesTypedFailureForUnsafeCombination() {
Application application = RuntimeEnvironment.getApplication();
HybridKit.INSTANCE.init(application);
HybridKit.INSTANCE.setHybridConfig(
new SparklingHybridConfig.Builder(new BaseInfoConfig(false)).build(), application);
SparklingContext sparklingContext = new SparklingContext();
HybridSchemeParam scheme = new HybridSchemeParam();
scheme.setEngineType(HybridKitType.LYNX);
scheme.setBundle("main.lynx.bundle");
sparklingContext.setHybridSchemeParam(scheme);
sparklingContext.setLynxViewport(new SparklingLynxViewport(320, 480));
sparklingContext.setThreadStrategy(SparklingThreadStrategy.MULTI_THREADS);
Sparkling sparkling = Sparkling.build(application, sparklingContext);

SparklingLynxConfigurationException exception =
assertThrows(
SparklingLynxConfigurationException.class,
() -> sparkling.createView(false));

assertEquals(
SparklingLynxConfigurationError.FIXED_VIEWPORT_WITH_MULTI_THREADS,
exception.getError());
assertTrue(exception instanceof IllegalArgumentException);
}
}
Loading
Loading