- Min SDK 24 and Target SDK 35.
- JDK 17 or later, Kotlin 1.8.22 or later, AGP 8.5.2 or later, and Gradle 8.7 or later.
- Java and Kotlin Broker Apps are supported.
arm64-v8aandarmeabi-v7aarchitectures are supported.- Android Studio 2025.1.3 or later is recommended.
- Landscape is not supported.
Extract LBWhaleAppSDK-XXX.zip, which contains the AARs and Demo, and place the SDK directory in the project. In the App-level Gradle file, load the delivered configuration:
// Adjust the path to the actual SDK location.
apply from: "${project.file('LBWhaleAppSDK/whale-sdk-config.gradle')}"WhaleAppSDK uses ARouter internally. Without the preprocessing plugin, init becomes substantially slower.
// Project-level build.gradle
buildscript {
dependencies {
classpath files('./../LBWhaleAppSDK/libs/lb-arouter-register-1.1.4.jar')
}
}// App-level build.gradle
plugins {
id("com.longbridge.arouter")
}apply plugin: 'kotlin-kapt'plugins {
id("org.jetbrains.kotlin.kapt")
}
android {
buildFeatures {
viewBinding = true
dataBinding = true
}
}Add this to the root gradle.properties:
android.enableR8.fullMode=falseCall init on the main thread from Application.onCreate:
LBWhaleApp.init(application, debug = false)Without ARouter preprocessing enabled, this call becomes substantially slower.
See Message-push integration for server-channel selection, cloud setup, and the standard message shape.
Initialize the push service on the main thread after LBWhaleApp.init:
val notificationConfig = NotificationConfig(
notificationIcon = R.mipmap.logo_notification,
notificationSmallIcon = R.mipmap.lb_push_small_notification,
notificationChannelId = "trading-messages"
)
val pushConfig = LBWhalePushConfig.Builder()
.appKey("<aliyun-push-app-key>")
.appSecret("<aliyun-push-app-secret>")
.disableMultiDevice(false)
.notificationConfig(notificationConfig)
.pushCallback(object : LBWhalePushCallback {
override fun onNotificationOpened(
title: String,
summary: String,
extMap: Map<String, String>
) {
if (LBWhaleApp.isStarted()) {
LBWhaleApp.pushService.handleNotificationOpened(
title, summary, extMap
)
} else {
// Start the SDK, then handle the pending click.
}
}
override fun onPushRegisterFailed(
errorCode: String?,
errorMessage: String?
) {
reportPushRegistrationFailure(errorCode, errorMessage)
}
})
.build()
LBWhaleApp.pushService.init(application, pushConfig)WhaleAppSDK’s built-in Aliyun push supports blending in third-party vendor channels to improve delivery. The following example forwards SDK messages through FCM.
Report the FCM token and pass payload-bearing messages to the SDK:
override fun onNewToken(token: String) {
LBWhaleApp.pushService.repotThirdToken(
this, LBWhalePushService.ThirdPush.FCM, token
)
}
override fun onMessageReceived(message: RemoteMessage) {
message.data["payload"]?.takeIf { it.isNotEmpty() }?.let { payload ->
LBWhaleApp.pushService.onThirdPushMsg(
this, LBWhalePushService.ThirdPush.FCM, payload
)
}
}repotThirdToken is the actual public API spelling in the current SDK.
Broker Server delivers Whale’s standard message through Broker’s own provider. Broker App may pass either parsed fields or the complete standard JSON to the SDK:
val result = LBWhaleApp.pushService.handleNotificationReceived(messageJson)
val clickResult = LBWhaleApp.pushService.handleNotificationOpened(messageJson)The field overloads accept title, summary, and the complete user_info map. Do not flatten or rename fields in user_info.
Both methods return LBWhaleAppResult<Unit>:
when (val result = LBWhaleApp.pushService.handleNotificationOpened(messageJson)) {
is LBWhaleAppResult.Success -> Unit
is LBWhaleAppResult.Error -> reportSanitized(result.cause)
}Possible causes include NotInitialized, UnrecognizedPushMessage, MissingPushLink, and InternalError.
In-app notifications:
- Whale-managed channel: handled internally by the SDK.
- Broker-managed channel: call
LBWhaleApp.pushService.handleNotificationReceivedafter receiving a message to parse and display it. This only takes effect after the SDK has started.
Out-of-app (system tray) notifications:
- Whale-managed channel: handled internally by the SDK.
- Broker-managed channel: handled by Broker App.
In-app notification tapped:
- Whale-managed channel: handled internally by the SDK.
- Broker-managed channel: if
LBWhaleApp.pushService.handleNotificationReceivedwas used to display the message, the SDK also handles the tap; otherwise Broker App must handle it.
Out-of-app (system tray) notification tapped:
- Whale-managed channel: delivered through
LBWhalePushCallback. Broker App may handle it directly, or callLBWhaleApp.pushService.handleNotificationOpened, which parses the notification and navigates to the matching page. This only takes effect after the SDK has started. - Broker-managed channel: call
LBWhaleApp.pushService.handleNotificationOpenedto parse the notification and navigate to the matching page. This only takes effect after the SDK has started.
Start the SDK before opening any SDK page:
val config = LBWhaleAppConfig(
appName = LBWhaleAppConfigName(
en = "My Trading App",
zhCN = "我的交易应用",
zhHK = "我的交易應用"
),
appKey = "<app-key>",
appSecret = "<app-secret>",
appId = "<app-id>",
token = "<client-token>",
refreshToken = "<client-refresh-token>",
theme = LBWhaleAppConfigTheme.AUTO,
language = LBWhaleAppConfigLanguage.EN,
priceColor = LBWhaleAppConfigPriceColor.FOLLOW_USER_SETTING,
defaultAccountChannel = "<account-channel>",
webDomainPrefix = "<web-domain-prefix>",
env = LBWhaleAppConfigEnv.PROD
)
LBWhaleApp.startWithConfig(application, config)Startup is asynchronous. The result is delivered through the callback registered in Implement the callback.
Use only the environment and credentials delivered for the project. Do not hard-code real appSecret, token, or refresh-token values in the repository.
Register LBWhaleApp.callback before calling startWithConfig:
LBWhaleApp.callback = object : LBWhaleAppCallback {
override fun onLBWhaleAppStarted() {
// SDK-dependent UI may now be opened.
}
override fun onLBWhaleAppStartFailed(error: Exception) {
reportSanitized(error)
when (error) {
is LBWhaleAppError.InvalidToken,
is LBWhaleAppError.TokenExpired,
is LBWhaleAppError.RefreshTokenExpired -> redirectToLogin()
is LBWhaleAppError.InvalidAppKey,
is LBWhaleAppError.InvalidAppSecret,
is LBWhaleAppError.InvalidConfig -> showIntegrationError(error)
is LBWhaleAppError.SyncAccountFailed,
is LBWhaleAppError.RequestFailed -> showRetryableError(error)
else -> showRetryableError(error)
}
}
override fun onLBWhaleAppRunInError(error: Exception) {
if (error is LBWhaleAppError.RefreshTokenExpired) {
LBWhaleApp.logoutAndDestroy()
redirectToLogin()
}
}
override fun onLBWhaleAppTokenChanged(token: String, refreshToken: String) {
// Notification only. The SDK continues to manage validity and renewal.
}
override fun onWhaleAppSdkOpenUrlRequested(url: String) {
brokerRouter.openValidated(url)
}
override fun onLBWhaleAppRefreshTokenExpired(
resolver: LBWhaleAppRefreshTokenResolver
) {
resolver.resolve("", "")
}
}WhaleAppSDK validates and renews credentials internally. Broker App supplies the initial token and refreshToken; it does not call check-token or refresh-token APIs.
onLBWhaleAppRefreshTokenExpired is an exceptional fallback after normal renewal fails. The default implementation above abandons retry, after which the runtime error callback reports RefreshTokenExpired so Broker App can destroy the SDK and return to the logged-out state. Only resolve with a completely new credential pair when the project has explicitly agreed on a separate way to obtain one.
Under normal operation, WhaleAppSDK completes token validation and renewal automatically, and Broker App never calls a check-token or refresh-token API directly. onLBWhaleAppRefreshTokenExpired is a fallback extension point for a rare failure case, not a channel to implement a regular refresh flow.
LBWhaleAppUICallback reports when the Client enters or exits SDK pages. It is independent of callback; register it only if needed.
- Enter SDK: fires when the first SDK page opens (live SDK page count 0 → 1).
- Exit SDK: fires when the last SDK page closes (live SDK page count 1 → 0).
- Determined by whether a page exists (
onCreate/onDestroy), not by foreground/background visibility — moving Broker App to the background while an SDK page is still alive does not count as an exit. - Destroying the SDK (
logoutAndDestroy) emits one more exit callback if an SDK page is still alive at that point. - Both callbacks run on the main thread and may update UI directly. Each method has a default empty implementation, so override only the ones needed.
LBWhaleApp.uiCallback = object : LBWhaleAppUICallback {
override fun onLBWhaleAppEnterSdkPage() {
// First live SDK page opened (0 → 1).
}
override fun onLBWhaleAppExitSdkPage() {
// Last live SDK page closed (1 → 0).
}
}Call these methods only after the SDK has started successfully.
Use the router to open SDK pages:
LBWhaleApp.pushUrl("lb://page/main")
val bundle = Bundle().apply { putString("id", "ST/US/AAPL") }
LBWhaleApp.pushUrl(
"lb://page/stock/detail",
bundle,
Intent.FLAG_ACTIVITY_NEW_TASK
)Validate any URL received by onWhaleAppSdkOpenUrlRequested before opening it in Broker App.
LBWhaleApp.changeTheme(LBWhaleAppConfigTheme.AUTO)Related: switch the price-up/price-down color scheme with changePriceColor:
LBWhaleApp.changePriceColor(LBWhaleAppConfigPriceColor.FOLLOW_USER_SETTING)LBWhaleApp.changeLanguage(LBWhaleAppConfigLanguage.EN)LBWhaleApp.logoutAndDestroy()Call this when the Client signs out, switches accounts, or the SDK reports unrecoverable authentication failure. It closes SDK pages, stops SDK activity, and clears the current startup configuration, releasing memory and network requests.
LBWhaleAppError |
When it occurs | Handling |
|---|---|---|
InvalidConfig(field) |
A required startup field is empty | Correct integration configuration |
NotInitialized |
A state-dependent API is called before startup succeeds | Wait for onLBWhaleAppStarted |
SyncAccountFailed |
Account synchronization fails during startup | Check network and retry startup |
InvalidAppKey / InvalidAppSecret |
Project credentials are rejected | Verify the delivered configuration |
InvalidToken / TokenExpired |
Initial Client credential is invalid | Obtain a new sign-in session |
RefreshTokenExpired |
The session cannot be renewed | Destroy the SDK and return to logged-out state |
RequestFailed(message, code) |
Another network request fails | Log sanitized code and message and apply project retry policy |
UnrecognizedPushMessage |
Message is not a Whale message | Let Broker App handle it |
MissingPushLink |
Recognized message has no route | Do not navigate; report sanitized metadata |
InternalError(cause) |
Unexpected routing or SDK operation failure | Report the sanitized stack through project monitoring |
The delivered SDK customizes, patches, or upgrades several open-source third-party libraries. If Broker App depends on the original version of one of these, a code conflict can occur.
The following libraries may conflict:
| Package | Upstream project |
|---|---|
com.github.zhpanvip:bannerviewpager |
BannerViewPager ↗ |
com.liulishuo.filedownloader:library |
FileDownloader ↗ |
com.github.barteksc:android-pdf-viewer |
AndroidPdfViewerV2 ↗, PdfiumAndroid ↗ |
com.zhihu.android:matisse |
Matisse ↗ |
com.contrarywind:Android-PickerView |
Android-PickerView ↗ |
skin.support:skin-support |
Android-skin-support ↗ |
com.github.tbruyelle:rxpermissions |
RxPermissions ↗ |
com.github.gzu-liyujiang |
Android_CN_OAID ↗ |
If the project depends on the original library directly, remove that dependency and use the SDK’s customized version instead. If another third-party library pulls it in transitively, exclude it in Gradle:
implementation("xxxxxxxxxx") {
exclude group: "xxxxx", module: "xxxxxx"
}var callback: LBWhaleAppCallback?
var uiCallback: LBWhaleAppUICallback?
val pushService: LBWhalePushService
fun init(application: Application, debug: Boolean = false)
fun startWithConfig(application: Application, config: LBWhaleAppConfig)
fun logoutAndDestroy()
fun pushUrl(uriString: String, bundle: Bundle? = null, flags: Int? = null)
fun changeTheme(theme: LBWhaleAppConfigTheme)
fun changeLanguage(language: LBWhaleAppConfigLanguage)
fun changePriceColor(priceColor: LBWhaleAppConfigPriceColor)
fun isStarted(): Boolean
fun getStatus(): Status
fun getVersion(): String
fun getAppId(): StringStatus values are IDLE, STARTING, STARTED, and RELEASING.
Available through LBWhaleApp.pushService.
fun init(application: Application, pushConfig: LBWhalePushConfig)
fun repotThirdToken(context: Context, thirdPush: ThirdPush, token: String)
fun onThirdPushMsg(context: Context, thirdPush: ThirdPush, msg: String?)
fun getPushDeviceId(): String
fun handleNotificationReceived(
title: String,
summary: String,
extraMap: Map<String, String>,
callback: NotificationRouterCallback? = null
): LBWhaleAppResult<Unit>
fun handleNotificationReceived(
json: String,
callback: NotificationRouterCallback? = null
): LBWhaleAppResult<Unit>
fun handleNotificationOpened(
title: String,
summary: String,
extMap: Map<String, String>,
callback: NotificationRouterCallback? = null
): LBWhaleAppResult<Unit>
fun handleNotificationOpened(
json: String,
callback: NotificationRouterCallback? = null
): LBWhaleAppResult<Unit>
fun interface NotificationRouterCallback {
fun onRouter(url: String)
}
enum class ThirdPush { FCM }The complete-JSON overloads read title, body, and user_info, then call the field overloads. Both field overloads are generated for Java through @JvmOverloads.
handleNotificationReceived and handleNotificationOpened return LBWhaleAppResult<Unit>: LBWhaleAppResult.Success on success, or LBWhaleAppResult.Error on failure — read the specific LBWhaleAppError through cause. Broker-managed channels should prefer passing the standard message shape rather than reconstructing user_info by hand.
sealed class LBWhaleAppResult<out T> {
data class Success<T>(val value: T) : LBWhaleAppResult<T>()
data class Error(val cause: Throwable) : LBWhaleAppResult<Nothing>()
val isSuccess: Boolean
val isError: Boolean
companion object {
@JvmField val Ok: LBWhaleAppResult<Unit>
}
}Kotlin extensions include onSuccess, onError, map, fold, getOrNull, and getOrElse.
class LBWhaleAppConfig(
val appName: LBWhaleAppConfigName,
val appKey: String,
val appSecret: String,
val appId: String,
val token: String,
val refreshToken: String,
var theme: LBWhaleAppConfigTheme = LBWhaleAppConfigTheme.AUTO,
var language: LBWhaleAppConfigLanguage = LBWhaleAppConfigLanguage.EN,
var priceColor: LBWhaleAppConfigPriceColor =
LBWhaleAppConfigPriceColor.FOLLOW_USER_SETTING,
val defaultAccountChannel: String = "",
val webDomainPrefix: String = "",
val extraCustomConfig: Map<String, Any>? = null,
val env: LBWhaleAppConfigEnv = LBWhaleAppConfigEnv.PROD
)See Application name, Theme mode, Display language, Runtime environment, and Price and color display below for the related types.
data class LBWhaleAppConfigName(
val en: String,
val zhCN: String? = null,
val zhHK: String? = null
)en is required; zhCN and zhHK are optional.
enum class LBWhaleAppConfigTheme {
AUTO,
LIGHT,
DARK
}enum class LBWhaleAppConfigLanguage {
EN,
ZH_CN,
ZH_HK
}enum class LBWhaleAppConfigEnv {
PROD,
SIT,
TEST
}Do not select a non-production environment unless the Whale project team supplies matching configuration.
enum class LBWhaleAppConfigPriceColor(val value: Int) {
FOLLOW_USER_SETTING(0),
RED_UP_GREEN_DOWN(1),
GREEN_UP_RED_DOWN(2)
}RED_UP_GREEN_DOWN and GREEN_UP_RED_DOWN hide the in-SDK price-color setting entry point. FOLLOW_USER_SETTING keeps that entry point visible and follows the user’s own in-SDK choice.
extraCustomConfig supports project-level font and push-tip color overrides, the debug panel flag, and the exceptional renewal callback timeout. Use the constants in ExtraCustomConfigKey; do not copy string literals into Broker App.
| Constant | Value | Purpose |
|---|---|---|
LB_APPSDK_CONFIG_DEBUG_EGG |
Boolean |
Enable the debug panel |
LB_APPSDK_CONFIG_KEY_FONT_CRYPTO |
Typeface |
Market-data digit font |
LB_APPSDK_CONFIG_KEY_FONT_MONOSPACED |
Typeface |
Regular monospaced font |
LB_APPSDK_CONFIG_KEY_FONT_MONOSPACED_BOLD |
Typeface |
Bold monospaced font |
LB_APPSDK_CONFIG_KEY_COLOR |
Map |
Root color map |
LB_APPSDK_CONFIG_KEY_PUSH_TIP_VIEW |
Map |
Push-tip color map |
LB_APPSDK_CONFIG_KEY_DEVICE_ID |
String |
Broker-provided device identifier |
LB_APPSDK_CONFIG_KEY_REFRESH_TOKEN_RESOLVER_TIMEOUT |
seconds | Exceptional renewal callback timeout; defaults to 30 seconds |
Push-tip color maps support order-completed, order-failed, order-limit, other-message, default background, title, content, and icon values. Light and dark values use LB_APPSDK_CONFIG_KEY_COLOR_LIGHT and LB_APPSDK_CONFIG_KEY_COLOR_DARK.
Built through LBWhalePushConfig.Builder().
class LBWhalePushConfig private constructor(
val appKey: String,
val appSecret: String,
val disableMultiDevice: Boolean,
val notificationConfig: NotificationConfig,
val pushCallback: LBWhalePushCallback?
) {
class Builder {
fun appKey(appKey: String): Builder
fun appSecret(appSecret: String): Builder
fun disableMultiDevice(disableMultiDevice: Boolean): Builder
fun notificationConfig(config: NotificationConfig): Builder
fun pushCallback(callback: LBWhalePushCallback): Builder
fun build(): LBWhalePushConfig
}
}Create directly through the constructor. All three fields are required, or an IllegalArgumentException is thrown.
data class NotificationConfig(
val notificationIcon: Int,
val notificationSmallIcon: Int,
val notificationChannelId: String = ""
)All four fields of this public data type are required, or an IllegalArgumentException is thrown. The current LBWhalePushConfig.Builder does not accept this object — configure FCM through Broker App and forward its token and payload through repotThirdToken and onThirdPushMsg; do not infer a builder API from older README examples.
data class FCMPushConfig(
val sendId: String,
val applicationId: String,
val projectId: String,
val apiKey: String
)interface LBWhaleAppCallback {
fun onLBWhaleAppStarted()
fun onLBWhaleAppStartFailed(error: Exception)
fun onLBWhaleAppRunInError(error: Exception)
fun onLBWhaleAppTokenChanged(token: String, refreshToken: String)
fun onWhaleAppSdkOpenUrlRequested(url: String)
fun onLBWhaleAppAnalyticsEvent(eventName: String, properties: JSONObject) {}
fun onLBWhaleAppRefreshTokenExpired(
resolver: LBWhaleAppRefreshTokenResolver
) {
resolver.resolve("", "")
}
}onLBWhaleAppAnalyticsEvent has a default empty implementation; override it only when the project needs to capture SDK analytics events:
override fun onLBWhaleAppAnalyticsEvent(
eventName: String,
properties: JSONObject
) {
analytics.track(eventName, properties)
}Forward only event names and properties agreed for the project. Never append credentials or Client-sensitive information.
interface LBWhalePushCallback {
fun onNotificationOpened(
title: String,
summary: String,
extMap: Map<String, String>
)
fun onPushRegisterFailed(errorCode: String?, errorMessage: String?) {}
}interface LBWhaleAppUICallback {
fun onLBWhaleAppEnterSdkPage() {}
fun onLBWhaleAppExitSdkPage() {}
}