Angular SDK migration guide

Upgrading to the new SDK libraries

The Angular SDK node packages have been updated to support Workspaces, introduced in FlowX v5.0. All container apps that want to use the new SDKs should update to the latest package versions and implement the required workspace configuration.
1

Remove old packages and install new FlowX v5.0 packages

  • Remove old FlowX SDK libraries (if upgrading from earlier versions):
npm uninstall @flowx/angular-sdk @flowx/core-sdk @flowx/core-theme @flowx/angular-theme @flowx/angular-ui-toolkit
  • Install new FlowX v5.0 packages:
npm install \
  @flowx/core-sdk@<version> \
  @flowx/core-theme@<version> \
  @flowx/angular-sdk@<version> \
  @flowx/angular-theme@<version> \
  @flowx/angular-ui-toolkit@<version> \
  @angular/cdk@19 \
  @types/event-source-polyfill
Replace <version> with the correct version corresponding to your FlowX v5.0 platform version. Check: Release Notes → v5.0 → Deployment guidelines → Component versions.
2

Verify Angular dependencies

  • Ensure your Angular version is compatible:
npm install -g @angular/cli@19
  • Verify system requirements:
    • Node.js: v20.9.0 or higher
    • npm: v10.1.0 or higher
    • Angular: ~19
3

Update flx-process-renderer configuration

SDK API changes

In the Angular SDK, the <flx-process-renderer> component has a new mandatory parameter: workspaceId.
NameDescriptionTypeRequirement
workspaceIdWorkspace identifier that contains the project and process to be startedstringMandatory
Add the definition for this property in your component:
export class AppComponent {
  workspaceId = 'your-workspace-id';
  // ... other existing properties
}
Use this parameter as input for the <flx-process-renderer> component:
<flx-process-renderer
  [workspaceId]="workspaceId"
  [projectInfo]="projectInfo"
  <!-- ... other existing inputs -->
>
</flx-process-renderer>

Task management component changes

The task management component now also requires workspace context:
<flx-task-management
  [apiUrl]="apiUrl"
  [authToken]="accessToken"
  [appId]="appId"
  [viewDefinitionId]="viewDefinitionId"
  [workspaceId]="workspaceId"
  <!-- ... other inputs -->
>
</flx-task-management>

React SDK migration guide

Upgrading to the new SDK libraries

The React SDK node packages have been updated to support Workspaces, introduced in FlowX v5.0. All container apps that want to use the new SDKs should update to the latest package versions and implement the required workspace configuration.
1

Remove old packages and install new FlowX v5.0 packages

  • Remove old FlowX SDK libraries:
npm uninstall @flowx/react-sdk @flowx/core-sdk @flowx/core-theme @flowx/react-theme @flowx/react-ui-toolkit
  • Install new FlowX v5.0 packages:
npm install \
  react@18 \
  react-dom@18 \
  @flowx/core-sdk@<version> \
  @flowx/core-theme@<version> \
  @flowx/react-sdk@<version> \
  @flowx/react-theme@<version> \
  @flowx/react-ui-toolkit@<version> \
  air-datepicker@3 \
  axios \
  ag-grid-react@32
Replace <version> with the correct version corresponding to your FlowX v5.0 platform version. Check: Release Notes → v5.0 → Deployment guidelines → Component versions.
2

Update React dependencies (if needed)

  • Ensure your React version is compatible:
npm install react@~18 react-dom@~18
  • Verify Node.js version compatibility:
    • Node.js: v18.16.9 or higher
    • npm: v10.8.0 or higher
3

Update FlxProcessRenderer configuration

New SDK API changes

In the React SDK, the <FlxProcessRenderer> component has a new mandatory parameter: workspaceId.
NameDescriptionTypeRequirement
workspaceIdWorkspace identifier that contains the project and process to be startedstringMandatory
Add the definition for this property in your component:
const workspaceId = 'your-workspace-id';
Use this parameter as input for the <FlxProcessRenderer> component:
<FlxProcessRenderer
    workspaceId={workspaceId}
    projectInfo={{ projectId: 'your-project-id' }}
    // ... other existing props
/>

Task management component changes

The task management component now also requires workspace context:
<FlxTaskManager
    apiUrl={baseUrl}
    authToken={authToken}
    appInfo={appInfo}
    viewId={viewId}
    workspaceId={workspaceId}
    // ... other existing props
/>

Android SDK migration guide

System requirements

System requirements:
  • minSdk = 26
  • compileSdk = 35
The SDK library was build using:

Library dependencies

Impactful dependencies: Other dependencies:
  • Koin dependency has been removed
  • Gson dependency has been removed
Some dependencies were removed, others got updated.
It is highly recommended that the container projects to be aligned with these versions in order to avoid any compatibility issues.

New SDK Migration Guide

To successfully build and run your project with the updated SDK, please follow these sequential steps within your container project:
1

Update dependency coordinates

[rootProject]/app/build.gradle.kts
implementation("ai.flowx.android:android-sdk:4.0.25") 
implementation("ai.flowx.android:sdk:9.0.0") 
2

Increase the `compileSdk` version to `35`

[rootProject]/app/build.gradle.kts
android {
    compileSdk = 34
    compileSdk = 35
}
3

Recommendation: Upgrade JVM compatibility to Java 17

[rootProject]/app/build.gradle.kts
compileOptions {
    sourceCompatibility = JavaVersion.VERSION_1_8 
    sourceCompatibility = JavaVersion.VERSION_17 
    targetCompatibility = JavaVersion.VERSION_1_8 
    targetCompatibility = JavaVersion.VERSION_17 
}

kotlinOptions { 
    jvmTarget = "1.8"
} 
kotlin { 
    compilerOptions { 
        jvmTarget.set(JvmTarget.JVM_17) 
    } 
} 
4

Update the `Android Gradle plugin` version to at least `8.10.0`

[rootProject]/build.gradle.kts
plugins {
    id("com.android.application") version "M.m.p" apply false // where M.m.p < 8.10.0
    id("com.android.application") version "8.10.0" apply false
}
5

Update `Gradle` version to at least `8.11.1`

[root-project]/gradle/wrapper/gradle-wrapper.properties
distributionUrl=https\://services.gradle.org/distributions/gradle-M.m.p-bin.zip # where M.m.p < 8.11.1
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip 
6

Update `Kotlin` version to at least `2.2.0`

[rootProject]/build.gradle.kts
plugins {
    id("org.jetbrains.kotlin.android") version "1.m.p" apply false
    id("org.jetbrains.kotlin.android") version "2.2.0" apply false
}
7

Apply the `Compose Compiler Gradle plugin`

[rootProject]/build.gradle.kts
plugins {
    id("org.jetbrains.kotlin.plugin.compose") version "2.2.0" apply false
}
[rootProject]/app/build.gradle.kts
plugins {
    id("org.jetbrains.kotlin.plugin.compose") 
}
[rootProject]/app/build.gradle.kts
android {
    composeOptions { 
        kotlinCompilerExtensionVersion = "1.m.p"
    } 
}
It is recommended to use the same version as the one configured for the Kotlin plugin
8

Enable core library desugaring

[rootProject]/app/build.gradle.kts
android {
    compileOptions { 
        isCoreLibraryDesugaringEnabled = true
    } 
}
dependencies {
    coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.5") 
}
9

Ensure your `android.app.Application` extending class implements the `FlowxOwner` interface

[rootProject]/app/src/main/[java|kotlin]/com/example/MyApp.kt
import ai.flowx.android.sdk.main.FlowxOwner

class MyApp : Application(), FlowxOwner { 
    override val flowx: Lazy<Flowx> = lazy { Flowx.getInstance() } 
}
10

Rename `FlowxSdkApi` class to `Flowx` and update imports accordingly

As a result of this change, all calls to FlowxSdkApi.getInstance() must be replaced with Flowx.getInstance().
import ai.flowx.android.sdk.FlowxSdkApi 
import ai.flowx.android.sdk.main.Flowx 

FlowxSdkApi.getInstance() 
Flowx.getInstance() 
11

Update the `Flowx.getInstance().init()` method parameters

Detailed step-by-step changes

Summary of changes:
diff v4.0.25..v5.0.0
import ai.flowx.android.sdk.FlowxSdkApi 
import ai.flowx.android.sdk.main.FlowxSdk 

import ai.flowx.android.sdk.process.model.SdkConfig 
import ai.flowx.android.sdk.api.Config 

// if used explicitly
import ai.flowx.android.sdk.ui.components.custom.CustomComponentsProvider 
import ai.flowx.android.sdk.api.custom.components.CustomComponentsProvider 

// if used explicitly
import ai.flowx.android.sdk.ui.components.custom.CustomStepperHeaderProvider 
import ai.flowx.android.sdk.api.custom.stepper.CustomStepperHeaderProvider 

// if used explicitly
import ai.flowx.android.sdk.analytics.AnalyticsCollector 
import ai.flowx.android.sdk.api.analytics.AnalyticsCollector 

import ai.flowx.android.sdk.analytics.Event 
import ai.flowx.android.sdk.api.analytics.Event 

// if used explicitly
import ai.flowx.android.sdk.NewProcessStartedHandler 
import ai.flowx.android.sdk.api.NewProcessStartedHandler 

import ai.flowx.android.core.logger.api.FxLogger 

FlowxSdkApi.getInstance().init( 
Flowx.getInstance().init( 
    context = applicationContext,
    config = SdkConfig( 
        baseUrl = "flowx base url", 
        enginePath = "flowx engine path", 
        imageBaseUrl = "flowx image base url", 
        language = "en", 
        locale = Locale.getDefault(), 
        validators = mapOf("cnp" to { it.length == 13 }), // a simplified example for custom validator, named "cnp", which checks only the length of the given data
        customHeaders = mapOf("Custom-Header" to "custom header value"), 
        updateStateEnabled = true, 
        cacheDocuments = true, 
        enableLog = true, 
    ), 
    config = object : Config { 
        override val baseUrl: String = "flowx base url"
        override val enginePath: String = "flowx engine path"
        override val imageBaseUrl: String = "flowx image base url"
        override val language: String = "en" // defaults to "en"
        override val locale: Locale = Locale.getDefault() // defaults to Locale.getDefault()
        override val validators: Map<String, (String) -> Boolean>? = mapOf("cnp" to { it.length == 13 }) // a simplified example for custom validator, named "cnp", which checks only the length of the given data // defaults to null
        override val customHeaders: Map<String, String>? = mapOf("Custom-Header" to "custom header value") // defaults to null
        override val updateStateEnabled: Boolean = true // defaults to true
        override val cacheDocuments: Boolean = true // defaults to true
    }, 
    accessTokenProvider = null, // null by default; can be set later, depending on the existing authentication logic
    customComponentsProvider = object : CustomComponentsProvider {...},
    customStepperHeaderProvider = object : CustomStepperHeaderProvider { ... },
    analyticsCollector = { event ->
        when (event) {
            is Event.Screen -> Log.i("Analytics", "Event.Screen(value = ${event.data.value})")
            is Event.Action -> Log.i("Analytics", "Event.Action(value = ${event.data.value}, screen = ${event.data.screen}, component = ${event.data.component}, label = ${event.data.label})")
        }
    },
    onNewProcessStarted = object : NewProcessStartedHandler.Delegate { ... },
    logger = object : FxLogger { ... }, // defaults to null
)
12

Update the method for providing access tokens

The setAccessTokenProvider method on the SDK instance has been replaced with setAccessToken.
import ai.flowx.android.sdk.FlowxSdkApi.Companion.AccessTokenProvider 

fun setAccessTokenProvider(accessTokenProvider: AccessTokenProvider) 
fun setAccessToken(accessToken: String?) // null or empty argument clears the token
Consequently, all calls to setAccessTokenProvider must be replaced with calls to setAccessToken:
FlowxSdkApi.getInstance().setAccessTokenProvider(accessTokenProvider = { "accessTokenValue" }) 
Flowx.getInstance().setAccessToken("accessTokenValue") 
13

`closeModalFunc` is now scoped to the `CloseModalProcessScope`

To be able to query for substitution tags or media library items in order to display them in the CloseModalProcessConfirmAlert, the closeModalFunc lambda passed as a parameter when starting (i.e. Flowx.getInstance.startProcess(...)) or continuing (i.e. Flowx.getInstance.continueProcess(...)) is now scoped to the CloseModalProcessScope interface, which allows that.
@Composable
    private fun ProcessContent(
        uiState: ProcessViewModel.UiState,
        onProcessEnded: (() -> Unit)? = null,
        onCloseProcessModalFunc: ((processName: String) -> Unit)? = null, 
        onCloseProcessModalFunc: (CloseModalProcessScope.(processName: String) -> Unit)? = null, 
    ) {
        when {
            !uiState.projectId.isNullOrBlank() && !uiState.processName.isNullOrBlank() -> {
                Flowx.getInstance().startProcess(
                    projectId = uiState.projectId,
                    processName = uiState.processName,
                    isModal = true,
                    onProcessEnded = { onProcessEnded?.invoke() },
                    closeModalFunc = { processName -> onCloseProcessModalFunc?.invoke(processName) }, 
                    closeModalFunc = { processName -> onCloseProcessModalFunc?.invoke(this, processName) }, 
                ).invoke()
            }
            !uiState.processUuid.isNullOrBlank() -> {
                Flowx.getInstance().continueProcess(
                    processUuid = uiState.processUuid,
                    isModal = true,
                    onProcessEnded = { onProcessEnded?.invoke() },
                    closeModalFunc = { processName -> onCloseProcessModalFunc?.invoke(processName) }, 
                    closeModalFunc = { processName -> onCloseProcessModalFunc?.invoke(this, processName) }, 
                ).invoke()
            }
        }
    }
When using it, an approach could be this:
ProcessActivity.kt
setContent {
    var closeModalProcessScope by remember { mutableStateOf<CloseModalProcessScope?>(null) }
    val showCloseModalProcessAlert = remember { mutableStateOf(false) }

    ProcessContent(
        ...
        onCloseProcessModalFunc = { processName ->
            closeModalProcessScope = this
            showCloseModalProcessAlert.value = true
        },
    )
    closeModalProcessScope?.CloseModalProcessConfirmAlert(show = showCloseModalProcessAlert)
}

@Composable
private fun CloseModalProcessScope.CloseModalProcessConfirmAlert(show: MutableState<Boolean>) {
    if (show.value) {
        AlertDialog(
            ...
            text = { Text(replaceSubstitutionTag("@@close_message")) } // `replaceSubstitutionTag` is now accessible through the `CloseModalProcessScope` scope
        )
    }
}
14

Update custom components implementation

Update the related imports:
import ai.flowx.android.sdk.ui.components.custom.CustomComponentsProvider 
import ai.flowx.android.sdk.api.custom.components.CustomComponentsProvider 

import ai.flowx.android.sdk.ui.components.custom.CustomComposable 
import ai.flowx.android.sdk.api.custom.components.CustomComponent 

import ai.flowx.android.sdk.ui.components.custom.CustomComposableComponent 
import ai.flowx.android.sdk.ui.components.custom.CustomView 
import ai.flowx.android.sdk.ui.components.custom.CustomViewComponent 

import ai.flowx.android.sdk.ui.components.custom.CustomComponentAction 
import ai.flowx.android.sdk.api.custom.components.CustomComponentAction 

import ai.flowx.android.sdk.ui.components.custom.FxEnumerationItem 
import ai.flowx.android.sdk.api.custom.components.FxEnumeration 

import ai.flowx.android.sdk.api.custom.components.CustomComponentScope 
The new class hierarchy structure is as follows:
v4.0.25
interface CustomComponentsProvider {
    fun provideCustomComposableComponent(): CustomComposableComponent?
    @Deprecated(...) fun provideCustomViewComponent(): CustomViewComponent? = null
}
interface CustomComposableComponent {
    fun provideCustomComposable(componentIdentifier: String): CustomComposable?
}
interface CustomComposable {
    @Deprecated(...) val isDefined: Boolean
    val composable: @Composable () -> Unit
    fun populateUi(data: Any?)
    fun populateUi(actions: Map<String, CustomComponentAction>)
    fun validate(): Boolean = true
    fun saveData(): JSONObject? = null
}
@Deprecated(...)
interface CustomViewComponent {
    @Deprecated(...) fun provideCustomView(componentIdentifier: String): CustomView
}
@Deprecated(...)
interface CustomView {...}
v5.0.0
interface CustomComponentsProvider {
    fun provideCustomComponent(componentIdentifier: String): CustomComponent?
}




interface CustomComponent {

    val composable: @Composable CustomComponentScope.() -> Unit
    fun populateUi(data: Any?)
    fun populateUi(actions: Map<String, CustomComponentAction>)
    fun validate(): Boolean = true
    fun saveData(): JSONObject? = null
}







The structural changes include:
  • The CustomComposable class has been renamed to CustomComponent.
  • All deprecated properties, methods, and interfaces have been removed. Support for the Android classical View system has been completely discontinued.
  • The provideCustomComposableComponent() method within CustomComponentsProvider has been replaced with fun provideCustomComponent(): CustomComponent?.
  • The CustomComposableComponent class has been removed.
  • The provided @Composable function passed to the composable property of the CustomComponent interface can now only exist and be called within the context of a CustomComponentScope receiver.
interface CustomComponent {
    val composable: @Composable () -> Unit 
    val composable: @Composable CustomComponentScope.() -> Unit 
}
interface CustomComponentScope {
    fun executeAction(action: CustomComponentAction, params: JSONObject? = null)
    fun replaceSubstitutionTag(string: String): String
    fun getMediaResourceUrl(key: String): String?
    suspend fun getEnumeration(name: String, parentName: String? = null): FxEnumeration?
}
This restricts the usage of previously unrestricted exposed functions to only within the context of the CustomComponentScope receiver (i.e., they can now be called only from within a custom component implementation).
Methods within the CustomComponentScope are now called through the actual scope itself, rather than relying on Flowx.getInstance(), since they are no longer visible on the SDK instance.
Flowx.getInstance().executeAction(...) 
flowxScope.executeAction(...) 

Flowx.getInstance().replaceSubstitutionTag(...) 
flowxScope.replaceSubstitutionTag(...) 

Flowx.getInstance().getMediaResourceUrl(...) 
flowxScope.getMediaResourceUrl(...) 

Flowx.getInstance().getEnumeration(...) 
flowxScope.getEnumeration(...) 
Consequently, the following changes should be made to access the scope within the custom component ViewModel:
MyCustomComponentViewModel.kt
class MyCustomComponentViewModel() : ViewModel() {
    private lateinit var flowxScope: CustomComponentScope

    fun setFlowxScope(scope: CustomComponentScope) {
        flowxScope = scope
    }

    fun executeSomeRealAction() {
        actions["someRealAction"]?.let {
            if (this@MyCustomComponentViewModel::flowxScope.isInitialized) {
                flowxScope.executeAction(
                    action = it,
                    params = JSONObject() // e.g. JSONObject("{\"someParameter\": \"someValue\"}")
                )
            }
        }
    }
}
MyCustomComponent.kt
@Composable
private fun CustomComponentScope.MyCustomComponent(
    viewModel: MyCustomComponentViewModel
) {
    viewModel.setFlowxScope(this@MyCustomComponent)

    // here goes the rest of the UI implementation
}
  • The CustomComponentAction is no longer a data class as before. It is now an interface.
    The exposed data has been reduced to the maximum required for executing the action:
v4.0.25
data class CustomComponentAction(
    internal val actionName: String?,
    internal val type: ActionType?,
    internal var tokenUuid: String?,
    internal var uiActionFlowxUuid: String,
    internal val context: String,
    internal val params: Params?,
    internal val keys: List<String>?,
    internal val customBody: JsonElement?,
    internal val collectionItemData: JsonObject?,
    internal val componentTemplateConfigId: Int,
)
v5.0.0
interface CustomComponentAction {
    val name: String
    val uiTemplateId: Int
    val uiTemplateContext: String
}







  • When querying for an enumeration by calling the CustomComponentScope.getEnumeration(...) method, the returned type has changed from List<FxEnumerationItem> to FxEnumeration
v4.0.25
data class FxEnumerationItem(
    val type: String? = null,
    val order: Int? = null,
    val childContentDescription: ChildContentDescription? = null,
    val code: String? = null,
    val content: String? = null
) {
    data class ChildContentDescription(
        val name: String? = null
    )
}

v5.0.0
interface FxEnumeration {
    val name: String
    val items: List<Item>
    val parentName: String?

    interface Item {
        val code: String
        val content: String?
        val childNomenclatorName: String?
        val order: Int
    }
}
These changes are reflected in an actual implementation as shown below:
v4.0.25
import ai.flowx.android.sdk.FlowxSdkApi
import ai.flowx.android.sdk.ui.components.custom.CustomComponentAction
import ai.flowx.android.sdk.ui.components.custom.CustomComponentsProvider
import ai.flowx.android.sdk.ui.components.custom.CustomComposable
import ai.flowx.android.sdk.ui.components.custom.CustomComposableComponent
import ai.flowx.android.sdk.ui.components.custom.CustomView
import ai.flowx.android.sdk.ui.components.custom.CustomViewComponent
import ai.flowx.external.android.template.app.R
import android.content.Context
import android.util.AttributeSet
import android.view.View
import android.widget.Button
import android.widget.LinearLayout
import android.widget.TextView
import android.widget.Toast
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LifecycleRegistry
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.ViewModelStore
import androidx.lifecycle.ViewModelStoreOwner
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.compose.viewModel
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.launch
import org.json.JSONObject

class CustomComponentsProviderImpl : CustomComponentsProvider {
    override fun provideCustomComposableComponent(): CustomComposableComponent? {
        return object : CustomComposableComponent {
            override fun provideCustomComposable(componentIdentifier: String): CustomComposable =
                object : CustomComposable {

                    val data: MutableStateFlow<Any?> = MutableStateFlow(null)
                    var actions: Map<String, CustomComponentAction> = emptyMap()

                    override val isDefined: Boolean
                        get() = when (componentIdentifier) {
                            "myCustomComponent" -> true // NOTE: set this to false to use the legacy view system instead of compose, which is mainstream now
                            else -> false
                        }

                    override val composable: @Composable () -> Unit = when (componentIdentifier) {
                        "myCustomComponent" -> { {
                            val viewModel = remember { MyCustomComponentViewModel(data, actions) }
                            MyCustomComponent(viewModel = viewModel)
                        } }

                        else -> { {} }
                    }

                    override fun populateUi(data: Any?) {
                        this.data.value = data
                    }

                    override fun populateUi(actions: Map<String, CustomComponentAction>) {
                        this.actions = actions
                    }

                    // Optional override, defaults to `true`.
                    override fun validate(): Boolean = true

                    // Optional override, defaults to `null`.
                    override fun saveData(): JSONObject? = null
                }
        }
    }

    override fun provideCustomViewComponent(): CustomViewComponent? {
        return object : CustomViewComponent {
            override fun provideCustomView(componentIdentifier: String) = object : CustomView {

                val data: MutableStateFlow<Any?> = MutableStateFlow(null)
                var actions: Map<String, CustomComponentAction> = emptyMap()

                override val isDefined: Boolean
                    get() = when (componentIdentifier) {
                        "myCustomComponent" -> true // NOTE: set the compose equivalent component to false to use the legacy view system instead of compose (which is mainstream now)
                        else -> false
                    }

                override fun getView(context: Context): View = when (componentIdentifier) {
                    "myCustomComponent" -> myCustomComponent(context, data, actions)
                    else -> View(context)
                }

                override fun populateUi(data: Any?) {
                    this.data.value = data
                }

                override fun populateUi(actions: Map<String, CustomComponentAction>) {
                    this.actions = actions
                }
            }
        }
    }
}

@Composable
private fun MyCustomComponent(
    viewModel: MyCustomComponentViewModel = viewModel<MyCustomComponentViewModel>()
) {
    val firstName by viewModel.firstName.collectAsState()
    val lastName by viewModel.lastName.collectAsState()
    val dateOfBirth by viewModel.dateOfBirth.collectAsState()

    Column(
        modifier = Modifier.fillMaxWidth(),
        horizontalAlignment = Alignment.CenterHorizontally,
    ) {
        Text(
            text = "Compose Custom Component",
            style = MaterialTheme.typography.titleLarge,
        )
        Spacer(modifier = Modifier.height(16.dp))
        Column(
            modifier = Modifier
                .background(color = Color(0x80FFFF00))
                .padding(16.dp)
                .fillMaxWidth(),
            horizontalAlignment = Alignment.CenterHorizontally,
            verticalArrangement = Arrangement.spacedBy(4.dp)
        ) {
            Text(
                text = "Client: $firstName $lastName",
                style = MaterialTheme.typography.titleMedium,
            )
            Text(
                text = "Date of Birth: $dateOfBirth",
                style = MaterialTheme.typography.titleMedium,
            )
        }
        val context = LocalContext.current
        TextButton(
            onClick = {
                // enable and adjust values to test the action (which was prior defined in the process)
//                viewModel.executeSomeRealAction()
                Toast.makeText(context, "Define action in the process and enable its execution in the code", Toast.LENGTH_LONG).show()
            }
        ) {
            Text(text = "Confirm")
        }
    }
}

private fun myCustomComponent(
    context: Context,
    data: MutableStateFlow<Any?> = MutableStateFlow(null),
    actions: Map<String, CustomComponentAction> = emptyMap(),
): View {
    return CustomComponentView(context = context, data = data, actions = actions)
}

class CustomComponentView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0,
    data: MutableStateFlow<Any?> = MutableStateFlow(null),
    actions: Map<String, CustomComponentAction> = emptyMap(),
) : LinearLayout(context, attrs, defStyleAttr), ViewModelStoreOwner, LifecycleOwner {

    private val registry = LifecycleRegistry(this)

    private var job: Job? = null
    private lateinit var client: TextView
    private lateinit var dateOfBirth: TextView

    private val viewModel: MyCustomComponentViewModel by lazy {
        ViewModelProvider(
            store = viewModelStore,
            factory = object : ViewModelProvider.Factory {
                override fun <T : ViewModel> create(modelClass: Class<T>): T {
                    @Suppress("UNCHECKED_CAST")
                    return MyCustomComponentViewModel(data, actions) as T
                }
            }
        )[MyCustomComponentViewModel::class.java]
    }

    init {
        initView()
    }

    private fun initView() {
        View.inflate(context, R.layout.my_custom_component, this)
        client = findViewById<TextView>(R.id.tvClient)
        dateOfBirth = findViewById<TextView>(R.id.tvDateOfBirth)

        findViewById<Button>(R.id.btnConfirm).also {
            it.setOnClickListener {
                // enable and adjust values to test the action (which was prior defined in the process)
//                viewModel.executeSomeRealAction()
                Toast.makeText(context, "Define action in the process and enable its execution in the code", Toast.LENGTH_LONG).show()
            }
        }
    }

    override fun onAttachedToWindow() {
        super.onAttachedToWindow()

        if (job?.isActive == true) {
            job?.cancel()
        }

        job = lifecycleScope.launch {
            combine(
                viewModel.firstName,
                viewModel.lastName,
                viewModel.dateOfBirth
            ) { firstName, lastName, dateOfBirth ->
                Triple(firstName, lastName, dateOfBirth)
            }.collect { (fn, ln, dob) ->
                client.text = String.format("Client: %s %s", fn, ln)
                dateOfBirth.text = String.format("Date of Birth: %s", dob)
            }
        }
    }

    override fun onDetachedFromWindow() {
        job?.cancel()
        job = null
        super.onDetachedFromWindow()
    }

    override val viewModelStore: ViewModelStore = ViewModelStore()

    override val lifecycle: Lifecycle = registry
}

class MyCustomComponentViewModel(
    private val data: MutableStateFlow<Any?> = MutableStateFlow(null),
    val actions: Map<String, CustomComponentAction> = emptyMap(),
) : ViewModel() {

    private val _firstName = MutableStateFlow("")
    val firstName = _firstName.asStateFlow()

    private val _lastName = MutableStateFlow("")
    val lastName = _lastName.asStateFlow()

    private val _dateOfBirth = MutableStateFlow("")
    val dateOfBirth = _dateOfBirth.asStateFlow()

    init {
        viewModelScope.launch {
            data.collect {
                _firstName.value = (it as? JSONObject)?.optString("firstname") ?: ""
                _lastName.value = (it as? JSONObject)?.optString("lastname") ?: ""
                _dateOfBirth.value = (it as? JSONObject)?.optString("dob") ?: ""
            }
        }
    }

    fun executeSomeRealAction() {
        actions["someRealAction"]?.let {
            FlowxSdkApi.getInstance().executeAction(
                action = it,
                params = JSONObject() // e.g. JSONObject("{\"someParameter\": \"someValue\"}")
            )
        } ?: println("MyCustomComponent: `someRealAction` action was not found")
    }
}
v5.0.0
import ai.flowx.android.sdk.api.custom.components.CustomComponent
import ai.flowx.android.sdk.api.custom.components.CustomComponentAction
import ai.flowx.android.sdk.api.custom.components.CustomComponentScope
import ai.flowx.android.sdk.api.custom.components.CustomComponentsProvider
import android.widget.Toast
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.lifecycle.ViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import org.json.JSONObject

class CustomComponentsProviderImpl : CustomComponentsProvider {
    override fun provideCustomComponent(componentIdentifier: String): CustomComponent? =
        when (componentIdentifier) {
            "myCustomComponent" -> MyCustomComponent()
            else -> null
        }
}

[private|internal] class MyCustomComponent : CustomComponent {
    private val data: MutableStateFlow<Any?> = MutableStateFlow(null)
    private var actions: MutableMap<String, CustomComponentAction> = mutableMapOf()

    private val viewModel = MyCustomComponentViewModel(data, actions)

    override val composable: @Composable (CustomComponentScope.() -> Unit)
        get() = @Composable {
            MyCustomComponent(viewModel = viewModel)
        }

    override fun populateUi(data: Any?) {
        this@MyCustomComponent.data.update { _ -> data }
    }

    override fun populateUi(actions: Map<String, CustomComponentAction>) {
        this@MyCustomComponent.actions.apply {
            clear()
            putAll(actions)
        }
    }

    // Optional override, defaults to `true`.
    override fun validate(): Boolean = true

    // Optional override, defaults to `null`.
    override fun saveData(): JSONObject? = null
}

@Composable
private fun CustomComponentScope.MyCustomComponent(
    viewModel: MyCustomComponentViewModel
) {
    viewModel.setFlowxScope(this@MyCustomComponent)

    val firstName by viewModel.firstName.collectAsStateWithLifecycle()
    val lastName by viewModel.lastName.collectAsStateWithLifecycle()
    val dateOfBirth by viewModel.dateOfBirth.collectAsStateWithLifecycle()

    Column(
        modifier = Modifier.fillMaxWidth(),
        horizontalAlignment = Alignment.CenterHorizontally,
    ) {
        Text(
            text = "Compose Custom Component",
            style = MaterialTheme.typography.titleLarge,
        )
        Spacer(modifier = Modifier.height(16.dp))
        Column(
            modifier = Modifier
                .background(color = Color(0x80FFFF00))
                .padding(16.dp)
                .fillMaxWidth(),
            horizontalAlignment = Alignment.CenterHorizontally,
            verticalArrangement = Arrangement.spacedBy(4.dp)
        ) {
            Text(
                text = "Client: $firstName $lastName",
                style = MaterialTheme.typography.titleMedium,
            )
            Text(
                text = "Date of Birth: $dateOfBirth",
                style = MaterialTheme.typography.titleMedium,
            )
        }
        val context = LocalContext.current
        TextButton(
            onClick = {
                // enable and adjust values to test the action (which was prior defined in the process)
//                viewModel.executeSomeRealAction()
                Toast.makeText(context, "Define action in the process and enable its execution in the code", Toast.LENGTH_LONG).show()
            }
        ) {
            Text(text = "Confirm")
        }
    }
}

class MyCustomComponentViewModel(
    val data: MutableStateFlow<Any?> = MutableStateFlow(null),
    private var actions: Map<String, CustomComponentAction> = emptyMap(),
) : ViewModel() {
    private lateinit var flowxScope: CustomComponentScope

    private val _firstName = MutableStateFlow("")
    val firstName = _firstName.asStateFlow()

    private val _lastName = MutableStateFlow("")
    val lastName = _lastName.asStateFlow()

    private val _dateOfBirth = MutableStateFlow("")
    val dateOfBirth = _dateOfBirth.asStateFlow()

    fun setFlowxScope(scope: CustomComponentScope) {
        flowxScope = scope
    }

    init {
        viewModelScope.launch {
            data.collect {
                _firstName.value = (it as? JSONObject)?.optString("firstname") ?: ""
                _lastName.value = (it as? JSONObject)?.optString("lastname") ?: ""
                _dateOfBirth.value = (it as? JSONObject)?.optString("dob") ?: ""
            }
        }
    }

    fun executeSomeRealAction() {
        actions["someRealAction"]?.let {
            if (this@MyCustomComponentViewModel::flowxScope.isInitialized) {
                flowxScope.executeAction(
                    action = it,
                    params = JSONObject() // e.g. JSONObject("{\"someParameter\": \"someValue\"}")
                )
            }
        } ?: println("MyCustomComponent: `someRealAction` action was not found")
    }
}
15

Update custom stepper header implementation

Update the related imports:
import ai.flowx.android.sdk.ui.components.custom.CustomComposableStepperHeader 
import ai.flowx.android.sdk.ui.components.custom.CustomStepperHeaderData 

import ai.flowx.android.sdk.ui.components.custom.ComposableStepperHeader 
import ai.flowx.android.sdk.api.custom.stepper.CustomStepperHeader 

import ai.flowx.android.sdk.ui.components.custom.CustomStepperHeaderProvider 
import ai.flowx.android.sdk.api.custom.stepper.CustomStepperHeaderProvider 
The new class hierarchy structure is as follows:
v4.0.25
interface CustomStepperHeaderProvider {
    fun provideCustomComposableStepperHeader(): CustomComposableStepperHeader?
}
interface CustomComposableStepperHeader {
    fun provideComposableStepperHeader(): ComposableStepperHeader
}
interface ComposableStepperHeader {
    val composable: @Composable (data: CustomStepperHeaderData) -> Unit
}
interface CustomStepperHeaderData { ... }
v5.0.0
interface CustomStepperHeaderProvider {
    fun provideCustomStepperHeader(): CustomStepperHeader?
}



interface CustomStepperHeader {
    val composable: @Composable (data: Data) -> Unit
    interface Data { ... }
}
The structural changes include:
  • The ComposableStepperHeader class has been renamed to CustomStepperHeader.
  • The CustomStepperHeaderData class has been moved under the CustomStepperHeader and renamed to Data.
  • The provideCustomComposableStepperHeader() method within CustomStepperProvider has been replaced with fun provideCustomStepperHeader(): CustomStepperHeader?.
  • The CustomComposableStepperHeader class has been removed.
These changes are reflected in an actual implementation as shown below:
diff v4.0.25..v5.0.0
import ai.flowx.android.sdk.ui.components.custom.CustomStepperHeaderProvider 
import ai.flowx.android.sdk.api.custom.stepper.CustomStepperHeaderProvider 

import ai.flowx.android.sdk.ui.components.custom.ComposableStepperHeader 
import ai.flowx.android.sdk.api.custom.stepper.CustomStepperHeader 

import ai.flowx.android.sdk.ui.components.custom.CustomComposableStepperHeader 
import ai.flowx.android.sdk.ui.components.custom.CustomStepperHeaderData

class CustomStepperHeaderProviderImpl : CustomStepperHeaderProvider {
    override fun provideCustomComposableStepperHeader(): CustomComposableStepperHeader? { 
        return object : CustomComposableStepperHeader { 
            override fun provideComposableStepperHeader(): ComposableStepperHeader { 
            override fun provideCustomStepperHeader(): CustomStepperHeader? { 
                return object : ComposableStepperHeader { 
                return object : CustomStepperHeader { 
                    override val composable: @Composable (data: CustomStepperHeaderData) -> Unit 
                    override val composable: @Composable ((CustomStepperHeader.Data) -> Unit) 
                        get() = @Composable { data ->
                            // custom header implementation
                        }
                }
            }
        } 
    } 
}
16

Remove usage of the undocumented `updateConfig(config: SdkConfig)` function

For runtime environment changes, use the changeEnvironment method available on the SDK instance.
fun updateConfig(config: SdkConfig) 
fun changeEnvironment(baseUrl: String, imageBaseUrl: String, enginePath: String) 
Do not change the environment while displaying a running process
When changing the environment, ensure the access token is updated properly

iOS SDK migration guide

SDK API changes

Start process

The start process API require a workspaceId.
public func startProcess(navigationController: UINavigationController,
                         workspaceId: String, 
                         projectId: String,
                         name: String,
                         params: [String: Any]?,
                         isModal: Bool = false,
                         showLoader: Bool = false,
                         onProcessEnded: (() -> Void)? = nil)
public func startProcess(tabBarController: UITabBarController,
                         workspaceId: String,
                         projectId: String,
                         name: String,
                         params: [String: Any]?,
                         isModal: Bool = false,
                         showLoader: Bool = false,
                         onProcessEnded: (() -> Void)? = nil)

Setup theme

The setup theme API requires a workspaceId.
public func setupTheme(withUuid uuid: String,
                       workspaceId: String,
                       localFileUrl: URL? = nil,
                       appearance: SwiftUI.ColorScheme? = .light,
                       completion: (() -> Void)?)