> ## Documentation Index
> Fetch the complete documentation index at: https://docs.flowx.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Android SDK

## Android project requirements

System requirements:

* **minSdk = 26** (Android 8.0)
* **compileSdk = 34**

The SDK library was build using:

* **[Android Gradle Plugin](https://developer.android.com/build/releases/gradle-plugin) 8.1.4**
* **[Kotlin](https://kotlinlang.org/) 1.9.24**

## Installing the library

1. Add the maven repository in your project's `settings.gradle.kts` file:

```kotlin theme={"system"}
dependencyResolutionManagement {
    ...
    repositories {
        ...
        maven {
            url = uri("https://nexus-jx.dev.rd.flowx.ai/repository/flowx-maven-releases/")
            credentials {
                username = "your_username"
                password = "your_password"
            }
        }
    }
}
```

2. Add the library as a dependency in your `app/build.gradle.kts` file:

```kotlin theme={"system"}
dependencies {
    ...
    implementation("ai.flowx.android:android-sdk:4.0.24")
    ...
}
```

### Library dependencies

Impactful dependencies:

* **[Koin](https://insert-koin.io/) 3.2.2**, including the implementation for **[Koin Context Isolation](https://insert-koin.io/docs/reference/koin-core/context-isolation/)**
* **[Compose BOM](https://developer.android.com/jetpack/compose/bom/bom-mapping) 2024.06.00** + **[Compose Compiler](https://developer.android.com/jetpack/androidx/releases/compose-compiler) 1.5.14**
* **[Accompanist](https://google.github.io/accompanist/) 0.32.0**
* **[Kotlin Coroutines](https://kotlinlang.org/docs/coroutines-overview.html) 1.8.0**
* **[OkHttp BOM](https://square.github.io/okhttp/) 4.11.0**
* **[Retrofit](https://square.github.io/retrofit/) 2.9.0**
* **[Coil Image Library](https://coil-kt.github.io/coil/) 2.5.0**
* **[Gson](https://github.com/google/gson) 2.11.0**

### Public API

The SDK library is managed through the `FlowxSdkApi` singleton instance, which exposes the following methods:

| Name                     | Description                                                                                                        | Definition                                                                                                                                                                                                                                            |
| ------------------------ | ------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `init`                   | Initializes the FlowX SDK. Must be called in your application's `onCreate()`                                       | `fun init(context: Context, config: SdkConfig, accessTokenProvider: FlowxSdkApi.Companion.AccessTokenProvider? = null, customComponentsProvider: CustomComponentsProvider? = null, customStepperHeaderProvider: CustomStepperHeaderProvider? = null)` |
| `setAccessTokenProvider` | Updates the access token provider (i.e. a functional interface) inside the renderer                                | `fun setAccessTokenProvider(accessTokenProvider: FlowxSdkApi.Companion.AccessTokenProvider)`                                                                                                                                                          |
| `setupTheme`             | Sets up the theme to be used when rendering a process                                                              | `fun setupTheme(themeUuid: String, fallbackThemeJsonFileAssetsPath: String? = null, @MainThread onCompletion: () -> Unit)`                                                                                                                            |
| `changeLocaleSettings`   | Changes the current locale settings (i.e. locale and language)                                                     | `fun changeLocaleSettings(locale: Locale, language: String)`                                                                                                                                                                                          |
| `startProcess`           | Starts a FlowX process instance, by returning a `@Composable` function where the process is rendered.              | `fun startProcess(projectId: String, processName: String, params: JSONObject = JSONObject(), isModal: Boolean = false, closeModalFunc: ((processName: String) -> Unit)? = null): @Composable () -> Unit`                                              |
| `continueProcess`        | Continues an existing FlowX process instance, by returning a `@Composable` function where the process is rendered. | `fun continueProcess(processUuid: String, isModal: Boolean = false, closeModalFunc: ((processName: String) -> Unit)? = null): @Composable () -> Unit`                                                                                                 |
| `executeAction`          | Runs an action from a custom component                                                                             | `fun executeAction(action: CustomComponentAction, params: JSONObject? = null)`                                                                                                                                                                        |
| `getMediaResourceUrl`    | Extracts a media item URL needed to populate the UI of a custom component                                          | `fun getMediaResourceUrl(key: String): String?`                                                                                                                                                                                                       |
| `replaceSubstitutionTag` | Extracts a substitution tag value needed to populate the UI of a custom component                                  | `fun replaceSubstitutionTag(key: String): String`                                                                                                                                                                                                     |

## Configuring the library

To configure the SDK, call the `init` method in your project's application class `onCreate()` method:

```kotlin theme={"system"}
fun init(
    context: Context,
    config: SdkConfig,
    accessTokenProvider: AccessTokenProvider? = null,
    customComponentsProvider: CustomComponentsProvider? = null,
    customStepperHeaderProvider: CustomStepperHeaderProvider? = null,
    analyticsCollector = null,
    onNewProcessStarted = null,
)
```

#### Parameters

| Name                          | Description                                                                                         | Type                                                                 | Requirement                   |
| ----------------------------- | --------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------- | ----------------------------- |
| `context`                     | Android application `Context`                                                                       | `Context`                                                            | Mandatory                     |
| `config`                      | SDK configuration parameters                                                                        | `ai.flowx.android.sdk.process.model.SdkConfig`                       | Mandatory                     |
| `accessTokenProvider`         | Functional interface provider for passing the access token                                          | `ai.flowx.android.sdk.FlowxSdkApi.Companion.AccessTokenProvder?`     | Optional. Defaults to `null`. |
| `customComponentsProvider`    | Provider for the `@Composable`/`View` custom components                                             | `ai.flowx.android.sdk.component.custom.CustomComponentsProvider?`    | Optional. Defaults to `null`. |
| `customStepperHeaderProvider` | Provider for the `@Composable` custom stepper header view                                           | `ai.flowx.android.sdk.component.custom.CustomStepperHeaderProvider?` | Optional. Defaults to `null`. |
| `analyticsCollector`          | Collector interface for SDK analytics events                                                        | `ai.flowx.android.sdk.analytics.AnalyticsCollector`                  | Optional. Defaults to `null`. |
| `onNewProcessStarted`         | Callback for when a new process was started as a consequence for executing a `START_PROJECT` action | `ai.flowx.android.sdk.NewProcessStartedHandler`                      | Optional. Defaults to `null`. |

• Providing the `access token` is explained in the [authentication](#authentication) section.<br />
• The `custom components` implementation is explained in [its own section](#custom-components).<br />
• The implementation for providing a `custom view for the header` of the [Stepper](../docs/building-blocks/process/navigation-areas#stepper) component is detailed in [its own section](#custom-header-view-for-the-stepper-component).<br />
• Collecting analytics events from the SDK is explained in [its own section](#collecting-analytics-events).<br />
• Handling the start of a new process while in a running process is explained in [its own section](#handling-start-of-a-new-process).<br />

#### Sample

```kotlin theme={"system"}
class MyApplication : Application() {
    override fun onCreate() {
        super.onCreate()
        initFlowXSdk()
    }

    private fun initFlowXSdk() {
        FlowxSdkApi.getInstance().init(
            context = applicationContext,
            config = SdkConfig(
                baseUrl = "URL to FlowX backend",
                imageBaseUrl = "URL to FlowX CMS Media Library",
                enginePath = "some_path",
                language = "en",
                locale = Locale.getDefault(),
                validators = mapOf("exact_25_in_length" to { it.length == 25 }),
                enableLog = false,
            ),
            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 = { processInstanceUuid ->
                // Send a broadcast message to notify the Activity currently displaying the running process.
                // The Activity should handle the broadcast to reload and display the newly started process identified by `processInstanceUuid`.
            }
        )
    }
}
```

The configuration properties that should be passed as `SdkConfig` data for the `config` parameter above are:

| Name           | Description                                                         | Type                                | Requirement                                 |
| -------------- | ------------------------------------------------------------------- | ----------------------------------- | ------------------------------------------- |
| `baseUrl`      | URL to connect to the FlowX back-end environment                    | `String`                            | Mandatory                                   |
| `imageBaseUrl` | URL to connect to the FlowX Media Library module of the CMS         | `String`                            | Mandatory                                   |
| `enginePath`   | URL path segment used to identify the process engine service        | `String`                            | Mandatory                                   |
| `language`     | The language used for retrieving enumerations and substitution tags | `String`                            | Optional. Defaults to `en`.                 |
| `locale`       | The locale used for date, number and currency formatting            | `java.util.Locale`                  | Optional. Defaults to `Locale.getDefault()` |
| `validators`   | Custom validators for form elements                                 | `Map<String, (String) -> Boolean>?` | Optional.                                   |
| `enableLog`    | Flag indicating if logs should be printed                           | `Boolean`                           | Optional. Defaults to `false`               |

#### Custom validators

The `custom validators` map is a collection of lambda functions, referenced by *name* (i.e. the value of the `key` in this map), each returning a `Boolean` based on the `String` which needs to be validated.
For a custom validator to be evaluated for a form field, its *name* must be specified in the form field process definition.

<Tip>
  By looking at the example from above:

  ```kotlin theme={"system"}
  mapOf("exact_25_in_length" to { it.length == 25 })
  ```

  if a form element should be validated using this lambda function, a custom validator named `"exact_25_in_length"` should be specified in the process definition.
</Tip>

## Using the library

### Authentication

To be able to use the SDK, **authentication is required**. Therefore, before calling any other method on the singleton instance, make sure that the access token provider is set by calling:

```kotlin theme={"system"}
FlowxSdkApi.getInstance().setAccessTokenProvider(accessTokenProvider = { "your access token" })
```

The lambda passed in as parameter has the `ai.flowx.android.sdk.FlowxSdkApi.Companion.AccessTokenProvider` type, which is actually a functional interface defined like this:

```kotlin theme={"system"}
fun interface AccessTokenProvider {
    fun get(): String
}
```

<Tip>Whenever the access token changes based on your own authentication logic, it must be updated in the renderer by calling the `setAccessTokenProvider` method again.</Tip>

### Theming

<Warning>Prior setting up the theme, make sure the `access token provider` was set.<br />Check the [authentication](#authentication) section for details.</Warning>

To be able to use styled components while rendering a process, the theming mechanism must be invoked by calling the `suspend`-ing `setupTheme(...)` method over the singleton instance of the SDK:

```kotlin theme={"system"}
suspend fun setupTheme(
    themeUuid: String,
    fallbackThemeJsonFileAssetsPath: String? = null,
    @MainThread onCompletion: () -> Unit
)
```

#### Parameters

| Name                              | Description                                                                                                                                                  | Type         | Requirement                  |
| --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------ | ---------------------------- |
| `themeUuid`                       | UUID string of the theme configured in FlowX Designer                                                                                                        | `String`     | Mandatory. Can be empty      |
| `fallbackThemeJsonFileAssetsPath` | Android asset relative path to the corresponding JSON file to be used as fallback, in case fetching the theme fails and there is no cached version available | `String?`    | Optional. Defaults to `null` |
| `onCompletion`                    | `@MainThread` invoked closure, called when setting up the theme completes                                                                                    | `() -> Unit` | Mandatory                    |

<Note>
  If the `themeUuid` parameter value is empty (`""`), no theme will be fetched, and the mechanism will rely only on the fallback file, if set.<br /><br />
  If the `fallbackThemeJsonFileAssetsPath` parameter value is `null`, there will be no fallback mechanism set in place, meaning if fetching the theme fails, the redered process will have no style applied over it's displayed components.
</Note>

<Note>
  The SDK caches the fetched themes, so if a theme fetch fails, a cached version will be used, if available. Otherwise, it will use the file given as fallback.
</Note>

#### Sample

```kotlin theme={"system"}
viewModelScope.launch {
    FlowxSdkApi.getInstance().setupTheme(
        themeUuid = "some uuid string",
        fallbackThemeJsonFileAssetsPath = "theme/a_fallback_theme.json",
    ) {
        // theme setup complete
        // do specific logic
    }
}
```

<Tip>
  The `fallbackThemeJsonFileAssetsPath` always search for files under your project's `assets/` directory, meaning the example parameter value is translated to `file://android_asset/theme/a_fallback_theme.json` before being evaluated.
</Tip>

<Warning>Do not [start](#start-a-flowx-process) or [resume](#resume-a-flowx-process) a process before the completion of the theme setup mechanism.</Warning>

### Changing current locale settings

The current `locale` and `language` can be also changed after the [initial setup](#configuring-the-library), by calling the `changeLocaleSettings` function:

```kotlin theme={"system"}
fun changeLocaleSettings(locale: Locale, language: String)
```

#### Parameters

| Name       | Description                   | Type               | Requirement |
| ---------- | ----------------------------- | ------------------ | ----------- |
| `locale`   | The new locale                | `java.util.Locale` | Mandatory   |
| `language` | The code for the new language | `String`           | Mandatory   |

<Warning>
  **Do not change the locale or the language while a process is rendered.**<br />
  The change is successful only if made before [starting](#start-a-flowx-process) or [resuming](#resume-a-flowx-process) a process.
</Warning>

#### Sample

```kotlin theme={"system"}
FlowxSdkApi.getInstance().changeLocaleSettings(locale = Locale("en", "US"), language = "en")
```

<Info>
  For locale, use the format BCP 47. BCP 47 is the standard for representing language and region codes, here is a [link](https://www.w3.org/International/articles/language-tags/) to the BCP 47 specification.
  An example of BCP 47 is `en-us` (language code - `en` and region code - `us`).
</Info>

### Start a FlowX process

<Warning>Prior starting a process, make sure the [authentication](#authentication) and [theming](#theming) were correctly set up</Warning>

After performing all the above steps and all the prerequisites are fulfilled, a new instance of a FlowX process can be started, by using the `startProcess` function:

```kotlin theme={"system"}
fun startProcess(
    projectId: String,
    processName: String,
    params: JSONObject = JSONObject(),
    isModal: Boolean = false,
    onProcessEnded: (() -> Unit)? = null,
    closeModalFunc: ((processName: String) -> Unit)? = null,
): @Composable () -> Unit
```

#### Parameters

| Name             | Description                                                                                        | Type                               | Requirement                                         |
| ---------------- | -------------------------------------------------------------------------------------------------- | ---------------------------------- | --------------------------------------------------- |
| `projectId`      | The id of the project containing the process to be started                                         | `String`                           | Mandatory                                           |
| `processName`    | The name of the process                                                                            | `String`                           | Mandatory                                           |
| `params`         | The starting params for the process, if any                                                        | `JSONObject`                       | Optional. If omitted, if defaults to `JSONObject()` |
| `isModal`        | Flag indicating whether the process can be closed at anytime by tapping the top-right close button | `Boolean`                          | Optional. It defaults to `false`.                   |
| `onProcessEnded` | Lambda function where you can do additional processing when the started process ends               | `(() -> Unit)?`                    | Optional. It defaults to `null`.                    |
| `closeModalFunc` | Lambda function where you should handle closing the process when `isModal` flag is `true`          | `((processName: String) -> Unit)?` | Optional. It defaults to `null`.                    |

<Tip>
  The returned **[@Composable](https://developer.android.com/reference/kotlin/androidx/compose/runtime/Composable)** function must be included in its own **[Activity](https://developer.android.com/reference/android/app/Activity)**, which is part of (controlled and maintained by) the container application.<br /><br />
  This wrapper activity must display only the `@Composable` returned from the SDK (i.e. it occupies the whole activity screen space).
</Tip>

#### Sample

```kotlin theme={"system"}
class ProcessActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        ...
        setContent {
            FlowxSdkApi.getInstance().startProcess(
                projectId = "your project id",
                processName = "your process name",
                params: JSONObject = JSONObject(),
                isModal = true,
                onProcessEnded = {
                    // NOTE: possible processing could involve doing something in the container app (i.e. navigating to a different screen)
                },
                closeModalFunc = { processName ->
                    // NOTE: possible handling could involve doing something differently based on the `processName` value
                },
            ).invoke()
        }
    }
    ...
}
```

### Resume a FlowX process

<Warning>Prior resuming process, make sure the [authentication](#authentication) and [theming](#theming) were correctly set up</Warning>

To resume an existing instance of a FlowX process, after fulfilling all the prerequisites, use the `continueProcess` function:

```kotlin theme={"system"}
fun continueProcess(
    processUuid: String,
    isModal: Boolean = false,
    onProcessEnded: (() -> Unit)? = null,
    closeModalFunc: ((processName: String) -> Unit)? = null,
): @Composable () -> Unit
```

#### Parameters

| Name             | Description                                                                                        | Type                               | Requirement                       |
| ---------------- | -------------------------------------------------------------------------------------------------- | ---------------------------------- | --------------------------------- |
| `processUuid`    | The UUID string of the process                                                                     | `String`                           | Mandatory                         |
| `isModal`        | Flag indicating whether the process can be closed at anytime by tapping the top-right close button | `Boolean`                          | Optional. It defaults to `false`. |
| `onProcessEnded` | Lambda function where you can do additional processing when the continued process ends             | `(() -> Unit)?`                    | Optional. It defaults to `null`.  |
| `closeModalFunc` | Lambda function where you should handle closing the process when `isModal` flag is `true`          | `((processName: String) -> Unit)?` | Optional. It defaults to `null`.  |

<Tip>
  The returned **[@Composable](https://developer.android.com/reference/kotlin/androidx/compose/runtime/Composable)** function must be included in its own **[Activity](https://developer.android.com/reference/android/app/Activity)**, which is part of (controlled and maintained by) the container application.<br /><br />
  This wrapper activity must display only the `@Composable` returned from the SDK (i.e. it occupies the whole activity screen space).
</Tip>

#### Sample

```kotlin theme={"system"}
class ProcessActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        ...
        setContent {
            FlowxSdkApi.getInstance().continueProcess(
                processUuid = "some process UUID string",
                isModal = true,
                onProcessEnded = {
                    // NOTE: possible processing could involve doing something in the container app (i.e. navigating to a different screen)
                },
                closeModalFunc = { processName ->
                    // NOTE: possible handling could involve doing something differently based on the `processName` value
                },
            ).invoke()
        }
    }
    ...
}
```

## Custom components

The container application should decide which custom component view to provide using the `componentIdentifier` configured in the UI designer.

A custom component receives `data` to populate the view and `actions` available to execute, as described below.<br />
It can also be validated and provide data back into the process when executing an action.

To handle custom components, an *implementation* of the `CustomComponentsProvider` interface should be passed as a parameter when initializing the SDK:

```kotlin theme={"system"}
interface CustomComponentsProvider {
    fun provideCustomComposableComponent(): CustomComposableComponent?
}
```

#### Sample

```kotlin theme={"system"}
class CustomComponentsProviderImpl : CustomComponentsProvider {
    override fun provideCustomComposableComponent(): CustomComposableComponent? {
        return object : CustomComposableComponent {...}
    }
}
```

### CustomComposableComponent

The implementation for providing a custom component is based on creating and binding a user defined [@Composable](https://developer.android.com/reference/kotlin/androidx/compose/runtime/Composable) function, through the `CustomComposable` interface:

```kotlin theme={"system"}
interface CustomComposableComponent {
    fun provideCustomComposable(componentIdentifier: String): CustomComposable
}
```

The returned `CustomComposable` object is an interface defined like this:

```kotlin theme={"system"}
interface CustomComposable {
    @Deprecated(message = "Will be removed in future releases")
    val isDefined: Boolean // always MUST return `true`

    // `@Composable` definitions for the custom components that can be handled
    val composable: @Composable () -> Unit

    /**
     * Called when the data is available for the custom component
     * (i.e. when the User Task that contains the custom component is displayed)
     *
     * @param data used to populate the custom component
     */
    fun populateUi(data: Any?)

    /**
     * Called when the actions are available for the custom component
     * (i.e. when the User Task that contains the custom component is displayed)
     *
     * @param actions that need to be attached to the custom component (e.g. onClick events)
     */
    fun populateUi(actions: Map<String, CustomComponentAction>)

    /**
     * This will be called when executing an action from a FlowX.AI UI Component, when the platform needs to know if the specified/marked components are valid.
     * Defaults to `true`.
     */
    fun validate(): Boolean = true

    /**
     * This will be called when executing an action from a FlowX.AI UI Component, on computing the data to be sent as body on the network request.
     * Returning `null` (i.e. default) means it does not contribute with any data to be sent.
     */
    fun saveData(): JSONObject? = null
}
```

<Tip>
  The value for the `data` parameter received in the `populateUi(data: Any?)` could be:<br />

  * `Boolean`
  * `String`
  * `java.lang.Number`
  * `org.json.JSONObject`
  * `org.json.JSONArray`

  The appropriate way to check and cast the data accordingly to the needs must belong to the implementation details of the custom component.
</Tip>

<Tip>
  Both validation and providing data back into process are optional, and, based on the needs, it may be included in the implementation or not.
</Tip>

#### Sample

Among multiple existing custom components, there may be one that:

* allows the input of a value representing an `age`
* the value should be validated (e.g. to be at least 35 years old)
* the value will be passed back into the process

```kotlin theme={"system"}
class CustomComponentsProviderImpl : CustomComponentsProvider {
    override fun provideCustomComposableComponent(): CustomComposableComponent? {
        return object : CustomComposableComponent {
            override fun provideCustomComposable(componentIdentifier: String): CustomComposable? =
                when (componentIdentifier) {
                    "other-custom-component-identifier" -> OtherCustomComposable().invoke()
                    "age" -> AgeCustomComposable().invoke()
                    else -> null
                }
        }
    }
}

private class AgeCustomComposable() {
    operator fun invoke(): CustomComposable = object : CustomComposable {
        val data: MutableStateFlow<Any?> = MutableStateFlow(null)
        var actions: Map<String, CustomComponentAction> = emptyMap()

        private val viewModel: AgeViewModel by lazy {
            AgeViewModel(data = data,actions = actions)
        }

        override val isDefined: Boolean = true // always MUST return `true`

        override val composable: @Composable () -> Unit =
            @Composable { Age(viewModel = viewModel) }

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

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

        override fun validate(): Boolean = viewModel.isValid()

        override fun saveData(): JSONObject? = viewModel.buildDataToSave()
    }
}

@Composable
private fun Age(
    viewModel: AgeViewModel = viewModel<AgeViewModel>()
) {
    val age by viewModel.ageFlow.collectAsState()
    val error by viewModel.errorFlow.collectAsState()
    val isError by remember(error) { mutableStateOf(error.isNotBlank()) }

    Column {
        OutlinedTextField(
            value = age,
            onValueChange = { viewModel.updateAge(it) },
            modifier = Modifier.fillMaxWidth(),
            label = { Text("Age") },
        )

        if (isError) {
            Text(
                modifier = Modifier.fillMaxWidth(),
                text = error,
                style = TextStyle(fontSize = 12.sp),
                color = Color.Red,
                textAlign = TextAlign.Start,
            )
        }
    }
}

class AgeViewModel(
    private val data: MutableStateFlow<Any?> = MutableStateFlow(null),
    private val actions: Map<String, CustomComponentAction> = emptyMap(),
) : ViewModel() {
    private val _ageFlow = MutableStateFlow("")
    val ageFlow: StateFlow<String> = _ageFlow.asStateFlow()

    private val _error = MutableStateFlow("")
    val errorFlow: StateFlow<String> = _error.asStateFlow()

    fun updateAge(text: String) {
        _ageFlow.value = text
    }

    fun isValid(): Boolean = ageFlow.value.toIntOrNull().let {
        when {
            it == null -> false.also { _error.update { "Unrecognized format" } }
            it < 35 -> false.also { _error.update { "You have to be at least 35 years old" } }
            else -> true.also { _error.update { "" } }
        }
    }

    fun buildDataToSave(): JSONObject? = ageFlow.value.takeUnless { it.isBlank() }
        ?.let {
            JSONObject(
            """
            {
                "app": {
                    "age": "$it"
                }
            }
            """.trimIndent()
        )
    }
}

class OtherCustomComposable() {
    operator fun invoke(): CustomComposable = object : CustomComposable {
        // deprecated property: will be removed in future releases.
        override val isDefined: Boolean = true // always MUST return `true`

        override val composable: @Composable () -> Unit = @Composable {
            /* add some @Composable implementation */
        }

        override fun populateUi(data: Any?) {
            // extract the necessary data to be used for displaying the custom components
        }

        override fun populateUi(actions: Map<String, CustomComponentAction>) {
            // extract the available actions that may be executed from the custom components
        }

        // Optional override, defaults to `true`.
        // Here one can pass validation logic from viewModel (e.g. by calling `viewModel.isValid()`)
        override fun validate(): Boolean = true

        // Optional override, defaults to `null`.
        // Here one can pass data to save from viewModel (e.g. by calling `viewModel.getDataToSave()`)
        override fun saveData(): JSONObject? = null
    }
}
```

### Execute action

The custom components which the container app provides may contain FlowX actions available for execution.<br /><br />
These actions are received through the `actions` parameter of the `populateUi(actions: Map<String, CustomComponentAction>)` method.<br /><br />
In order to run an action (i.e. on a click of a button in the custom component) you need to call the `executeAction` method:

```kotlin theme={"system"}
fun executeAction(action: CustomComponentAction, params: JSONObject? = null)
```

#### Parameters

| Name     | Description                                                                 | Type                                                          | Requirement                     |
| -------- | --------------------------------------------------------------------------- | ------------------------------------------------------------- | ------------------------------- |
| `action` | Action object extracted from the `actions` received in the custom component | `ai.flowx.android.sdk.component.custom.CustomComponentAction` | Mandatory                       |
| `params` | Parameters needed to execute the `action`                                   | `JSONObject?`                                                 | Optional. It defaults to `null` |

### Get a substitution tag value by key

```kotlin theme={"system"}
fun replaceSubstitutionTag(key: String): String
```

All substitution tags will be retrieved by the SDK before starting the process and will be stored in memory.

Whenever the container app needs a substitution tag value for populating the UI of the custom components, it can request the substitution tag using the method above, by providing the `key`.

It returns:

* the key's counterpart, if the `key` is valid and found
* the empty string, if the `key` is valid, but not found
* the unaltered string, if the key has the wrong format (i.e. not starting with `@@`)

### Get a media item url by key

```kotlin theme={"system"}
fun getMediaResourceUrl(key: String): String?
```

All media items will be retrieved by the SDK before starting the process and will be stored in memory.

Whenever the container app needs a media item url for populating the UI of the custom components, it can request the url using the method above, by providing the `key`.

It returns the `URL` string of the media resource, or `null`, if not found.

## Custom header view for the [STEPPER](../docs/building-blocks/process/navigation-areas#stepper) component

The container application can opt for providing a custom view in order to be used, for all the [Stepper](../docs/building-blocks/process/navigation-areas#stepper) components, as a replacement for the built-in header.<br />
The custom view receives `data` to populate its UI, as described below.

To provide a custom header for the [Stepper](../docs/building-blocks/process/navigation-areas#stepper), an *implementation* of the `CustomStepperHeaderProvider` interface should be passed as a parameter when initializing the SDK:

```kotlin theme={"system"}
interface CustomStepperHeaderProvider {
    fun provideCustomComposableStepperHeader(): CustomComposableStepperHeader?
}
```

#### Sample

```kotlin theme={"system"}
class CustomStepperHeaderProviderImpl : CustomStepperHeaderProvider {
    override fun provideCustomComposableStepperHeader(): CustomComposableStepperHeader? {
        return object : CustomComposableStepperHeader {...}
    }
}
```

### CustomComposableStepperHeader

To provide the custom header view as a [@Composable](https://developer.android.com/reference/kotlin/androidx/compose/runtime/Composable) function, you have to implement the `CustomComposableStepperHeader` interface:

```kotlin theme={"system"}
interface CustomComposableStepperHeader {
    fun provideComposableStepperHeader(): ComposableStepperHeader
}
```

The returned `ComposableStepperHeader` object is an interface defined like this:

```kotlin theme={"system"}
interface ComposableStepperHeader {
    /**
     * `@Composable` definition for the custom header view
     * The received argument contains the stepper header necessary data to render the view.
     */
    val composable: @Composable (data: CustomStepperHeaderData) -> Unit
}
```

The value for the `data` parameter received as function argument is an interface defined like this:<br />

```kotlin theme={"system"}
interface CustomStepperHeaderData {
    // title for the current step; can be empty or null
    val stepTitle: String?
    // title for the current selected substep; optional;
    // can be empty ("") if not defined or `null` if currently there is no selected substep
    val substepTitle: String?
    // 1-based index of the current step
    val step: Int
    // total number of steps
    val totalSteps: Int
    // 1-based index of the current substep; can be `null` when there are no defined substeps
    val substep: Int?
    // total number of substeps in the current step; can be `null` or `0`
    val totalSubsteps: Int?
}
```

#### Sample

```kotlin theme={"system"}
override fun provideComposableStepperHeader(): ComposableStepperHeader? {
    return object : ComposableStepperHeader {
        override val composable: @Composable (data: CustomStepperHeaderData) -> Unit
            get() = @Composable { data ->
                /* add some @Composable implementation which displays `data` */
            }
    }
}
```

## Collecting analytics events

To be able to collect analytics events from the SDK, an implementation for the `AnalyticsCollector` functional interface may be provided when initializing the SDK:

```kotlin theme={"system"}
fun interface AnalyticsCollector {
    fun onEvent(event: Event)
}
```

where the `Event` looks like this:

```kotlin theme={"system"}
sealed interface Event {
    interface Screen : Event {
        val data: Screen.Data

        interface Data {
            val value: String
        }
    }
    interface Action : Event {
        val data: Action.Data

        interface Data {
            val value: String
            val screen: String?
            val component: String?
            val label: String?
        }
    }
}
```

#### Sample

The implementation can be passed as a lambda, like:

```kotlin theme={"system"}
analyticsCollector = { event ->
    // do whatever is needed (e.g. log the event)
    when (it) {
        is Event.Screen -> Log.i("Analytics", "Event.Screen(value = ${it.data.value})")
        is Event.Action -> Log.i("Analytics", "Event.Action(value = ${it.data.value}, screen = ${it.data.screen}, component = ${it.data.component}, label = ${it.data.label})")
    }
}
```

<Info>
  The value property represents the identifier set in the process definition.

  For action type events there are some additional properties provided:

  * component - The type of component triggering the action
  * label - The label of the component, if available. (E.g. title of a button or label of a form element)
  * screen - The identifier of the screen containing the component, if set
</Info>

## Handling "Start of a new process"

When an action of type `START_PROJECT` is executed, the `onNewProcessStarted` lambda provided in the `FlowxSdkApi.getInstance().init(...)` function is invoked.

This callback provides the UUID of the newly started process, which can be used to resume the process by calling the `FlowxSdkApi.getInstance().continueProcess(...)` method.

It is the responsibility of the container application's developer to implement the necessary logic for displaying the appropriate UI for the newly started process.

#### Sample

One way to handle this is to send a broadcast message to notify the Activity currently displaying the running process.
The Activity should handle the broadcast to reload and display the newly started process identified by `processInstanceUuid` (received in the broadcast intent).

```kotlin theme={"system"}
FlowxSdkApi.getInstance().init(
    ...
    onNewProcessStarted = { processInstanceUuid ->
        applicationContext.sendBroadcast(
            Intent("some.intent.filter.indentifier").apply {
                putExtra("processInstanceUuid", processInstanceUuid)
                setPackage("your.application.package")
            }
        )
    }
    ...
)

class ProcessActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        ...

        setContent {
            val yourBroadcastReceiver = remember {
                YourBroadcastReceiver(handler = { processInstanceUuid -> /* do your own logic to refresh `ProcessContent()` */ })
            }

            val context = LocalContext.current
            LifecycleStartEffect(true) {
                ContextCompat.registerReceiver(context.applicationContext, yourBroadcastReceiver, IntentFilter("some.intent.filter.indentifier"), ContextCompat.RECEIVER_NOT_EXPORTED)
                onStopOrDispose {
                    runCatching {
                        context.applicationContext.unregisterReceiver(yourBroadcastReceiver)
                    }
                }
            }

            ProcessContent()
        }
    }

    @Composable
    private fun ProcessContent(...) { ... }
}

class YourBroadcastReceiver(private val handler: (String) -> Unit) : BroadcastReceiver() {
    override fun onReceive(context: Context?, intent: Intent?) {
        intent?.extras?.getString("processInstanceUuid")?.let { processUuid -> handler.invoke(processUuid) }
    }
}
```

## Known issues

* shadows are rendered only on **Android >= 28** having [hardware acceleration](https://developer.android.com/topic/performance/hardware-accel) **enabled**
* there is no support yet for [subprocesses](../docs/building-blocks/process/subprocess) started using the [Call Activity node](../docs/building-blocks/node/call-subprocess-tasks/call-activity-node) when configuring a [TabBar](../docs/building-blocks/process/navigation-areas#tab-bar) [Navigation Area](../docs/building-blocks/process/navigation-areas)

{/*- there is no support for **[Dark Mode](https://developer.android.com/develop/ui/views/theming/darktheme)** yet*/}

{/*- can not run multiple processes in parallel (e.g. in a Bottom Tab Navigation)*/}


## Related topics

- [Renderer SDKs](/release-notes/v5.x/v5.0.0-july-2025/migrating-from-v4.7.x-to-5.0/renderers.md)
- [SDKs overview](/4.7.x/sdks/sdks-overview.md)
- [FlowX.AI architecture](/4.7.x/docs/platform-overview/flowx-architecture.md)
- [FlowX.AI 5.9.0 Release Notes](/release-notes/v5.x/v5.9.0-june-2026/v5.9.0-june-2026.md)
