> ## 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

> Install and configure the FlowX.AI Android SDK to render process-driven UI in native Android apps. Requires Android 8.0+ (API 26).

## Android project requirements

System requirements:

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

The SDK library was built using:

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

Your project must build with at least:

* **Android Gradle Plugin 9.1.0**, with [built-in Kotlin](https://developer.android.com/build/migrate-to-built-in-kotlin) (no `org.jetbrains.kotlin.android` plugin)
* **Gradle 9.3.1** (the minimum depends on your Android Gradle Plugin version)
* **Kotlin 2.4**
* **JDK 17**

## Installing the library

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

```kotlin theme={"dark"}
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={"dark"}
dependencies {
    ...
    implementation("ai.flowx.android:sdk:<version>")
    ...
}
```

> [!NOTE]
>
> Replace `<version>` with the correct version corresponding to your platform version.\\
>
> To find the right version, navigate to:
> `Release Notes → Choose your platform version → Deployment guidelines → Component versions`

3. Enable core library desugaring in your `app/build.gradle.kts` file, as the SDK requires it:

```kotlin theme={"dark"}
android {
    compileOptions {
        isCoreLibraryDesugaringEnabled = true
    }
}
dependencies {
    coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.5")
}
```

### Library dependencies

Impactful dependencies:

* **[Android Core KTX](https://developer.android.com/kotlin/ktx#core) 1.19.1**
* **[Android Activity Compose](https://developer.android.com/jetpack/androidx/releases/activity) 1.13.0**
* **[Compose BOM](https://developer.android.com/jetpack/compose/bom/bom-mapping) 2026.09.00**
* **[Accompanist Permissions](https://google.github.io/accompanist/permissions/) 0.37.3**
* **[Kotlin Coroutines](https://kotlinlang.org/docs/coroutines-overview.html) 1.11.0**
* **[Compose Navigation](https://developer.android.com/develop/ui/compose/navigation) 2.10.2**
* **[Android Lifecycle](https://developer.android.com/jetpack/androidx/releases/lifecycle) 2.11.0**
* **[Android Core Library Desugaring](https://developer.android.com/studio/write/java8-support#library-desugaring) 2.1.5**
* **[OkHttp BOM](https://square.github.io/okhttp/) 5.5.1**
* **[Retrofit](https://square.github.io/retrofit/) 3.0.0**
* **[Moshi](https://github.com/square/moshi) 1.15.2**
* **[Coil BOM](https://coil-kt.github.io/coil/) 3.6.3**

> [!WARNING]
>
> The SDK depends on **OkHttp 5** and **Retrofit 3**. Gradle resolves your app to these versions even if you declare older ones, so any code in your app that uses OkHttp or Retrofit must be compatible with them.

### Public API

The SDK library is managed through the `Flowx` 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: Config, customComponentsProvider: CustomComponentsProvider? = null, customStepperHeaderProvider: CustomStepperHeaderProvider? = null, customLoaderProvider: CustomLoaderProvider? = null, analyticsCollector: AnalyticsCollector? = null, logSink: LogSink.Collector? = null, onNewProcessStarted: NewProcessStartedHandler.Delegate? = null)` |
| `setAccessToken` | Sets the access token | `fun setAccessToken(accessToken: String)` |
| `setupTheme` | Sets up the theme to be used when rendering a process, a UI Flow or the chat | `fun setupTheme(workspaceUuid: String, themeUuid: String, fallbackThemeJsonFileAssetsPath: String? = null, appearance: ThemeAppearance = ThemeAppearance.LIGHT, @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(workspaceId: String, projectId: String, processName: String, params: JSONObject? = null, isModal: Boolean = false, onProcessStarted: ((String) -> Unit)? = null, onProcessEnded: (() -> Unit)? = null, onDismissRequested: (ProjectScope.(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, onProcessEnded: (() -> Unit)? = null, onDismissRequested: (ProjectScope.(processName: String) -> Unit)? = null): @Composable () -> Unit` |
| `startUiFlow` | Starts a FlowX UI Flow, by returning a `@Composable` function where the UI Flow is rendered. | `fun startUiFlow(workspaceId: String, projectId: String, uiFlowName: String, params: JSONObject? = null, isModal: Boolean = false, onDismissRequested: (ProjectScope.(uiFlowName: String) -> Unit)? = null): @Composable () -> Unit` |
| `chatView` | Displays the standalone chat, by returning a `@Composable` function where the chat is rendered. | `fun chatView(config: ChatConfig): @Composable () -> Unit` |

## Configuring the library

To configure the SDK, there are two things needed in the project's application class:

1. first, make it implement the `FlowxOwner` interface:

```kotlin theme={"dark"}
class MyApplication : Application(), FlowxOwner {
    override val flowx: Lazy<Flowx> = lazy { Flowx.getInstance() }
    // ...
}
```

2. then, call the `init` method inside the `onCreate()` method:

```kotlin theme={"dark"}
fun init(
    context: Context,
    config: Config,
    customComponentsProvider: CustomComponentsProvider? = null,
    customStepperHeaderProvider: CustomStepperHeaderProvider? = null,
    customLoaderProvider: CustomLoaderProvider? = null,
    analyticsCollector: AnalyticsCollector? = null,
    logSink: LogSink.Collector? = null,
    onNewProcessStarted: NewProcessStartedHandler.Delegate? = null,
)
```

#### Parameters

| Name | Description | Type | Requirement |
| - | - | - | - |
| `context` | Android application `Context` | `Context` | Mandatory |
| `config` | SDK configuration parameters | `ai.flowx.android.sdk.api.Config` | Mandatory |
| `customComponentsProvider` | Provider for the `@Composable` custom components | `ai.flowx.android.sdk.api.custom.components.CustomComponentsProvider?` | Optional. Defaults to `null`. |
| `customStepperHeaderProvider` | Provider for the `@Composable` custom stepper header view | `ai.flowx.android.sdk.api.custom.stepper.CustomStepperHeaderProvider?` | Optional. Defaults to `null`. |
| `customLoaderProvider` | Provider for the `@Composable` custom loader view | `ai.flowx.android.sdk.api.custom.loader.CustomLoaderProvider?` | Optional. Defaults to `null`. |
| `analyticsCollector` | Collector interface for SDK analytics events | `ai.flowx.android.sdk.api.analytics.AnalyticsCollector` | Optional. Defaults to `null`. |
| `logSink` | Collector interface for exposed logging events | `ai.flowx.android.sdk.api.logger.LogSink.Collector` | 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.api.NewProcessStartedHandler.Delegate` | Optional. Defaults to `null`. |

• The `custom components` implementation is explained in [its own section](#custom-components).\

• The implementation for providing a `custom view for the header` of the [Stepper](https://docs.flowx.ai/6.x/docs/building-blocks/process/navigation-areas.md#stepper) component is detailed in [its own section](#custom-header-view-for-the-stepper-component).\

• The implementation for providing a `custom loader` is explained in [its own section](#custom-loaders).\

• Collecting analytics events from the SDK is explained in [its own section](#collecting-analytics-events).\

• Collecting log events from the SDK is explained in [its own section](#collecting-log-events).\

• Handling the start of a new process while in a running process is explained in [its own section](#handling-start-of-a-new-process).\


#### Sample

```kotlin theme={"dark"}
class MyApplication : Application(), FlowxOwner {
    override val flowx: Lazy<Flowx> = lazy { Flowx.getInstance() }

    override fun onCreate() {
        super.onCreate()
        initFlowXSdk()
    }

    private fun initFlowXSdk() {
        Flowx.getInstance().init(
            context = applicationContext,
            config = object : Config {
                override val organizationId: String = "organization id"
                override val baseUrl = "URL to FlowX backend"
                override val assetsBaseUrl = "URL to FlowX CMS Media Library"
                override val enginePath = "URL path segment used to identify the process engine service"
                override val language = "en"
                override val locale = Locale.getDefault()
                override val validators: Map<String, (String) -> Boolean>? = mapOf("exact_25_in_length" to { it.length == 25 })
                override val logEnabled: Boolean get() = 0 != applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE
            },
            customComponentsProvider = object : CustomComponentsProvider {...},
            customStepperHeaderProvider = object : CustomStepperHeaderProvider {...},
            customLoaderProvider = object : CustomLoaderProvider {...},
            analyticsCollector = { event ->
                // Process / Send the event to some specialized Analytics platform
            },
            logSink = { event ->
                // Process / Send the log event to some specialized logging platform
            },
            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 `Config` data for the `config` parameter above are:

| Name | Description | Type | Requirement |
| - | - | - | - |
| `organizationId` | Id of the organization used by FLOWX to identify your installation | `String` | Mandatory |
| `baseUrl` | URL to connect to the FlowX back-end environment | `String` | Mandatory |
| `assetsBaseUrl` | 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.forLanguageTag("en-US")` |
| `validators` | Custom validators for form elements | `Map<String, (String) -> Boolean>?` | Optional. |
| `logEnabled` | Flag indicating if logs should be printed | `Boolean` | Optional. Defaults to `false` |

#### Where to find these values

The mandatory configuration values are properties of your FlowX deployment, not of the SDK. Your DevOps team holds them, in the same configuration the FlowX Designer web app connects through:

| Value | Where it comes from |
| - | - |
| `organizationId` | The UUID assigned at FlowX organization registration. It is the same value set as `ORGANIZATION_ID` on your backend services and it is also the name of your Keycloak user realm. All three must match. |
| `baseUrl` | The entry point of your FlowX backend, from your deployment's ingress configuration. |
| `assetsBaseUrl` | The URL of the CMS Media Library module on your deployment, from the ingress configuration. If it is wrong, images and icons render blank. |
| `enginePath` | The URL path segment under which the process engine service is exposed, defined in your deployment's [ingress configuration](https://docs.flowx.ai/6.x/setup-guides/ingress-configuration.md). On standard FlowX deployments this is `onboarding`. It is the mobile equivalent of the `PROCESS_API_PATH` variable used by the web container. If it is wrong, process start and advance calls return a 404 error. |

#### 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={"dark"}
> 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.

## Using the library

### Authentication

To render processes, UI Flows and the chat for an authenticated user, set the user's access token before calling any other method on the singleton instance:

```kotlin theme={"dark"}
Flowx.getInstance().setAccessToken(accessToken = "your access token")
```

> [!TIP]
>
> Whenever the access token changes based on your own authentication logic (e.g. it is refreshed), update it in the renderer by calling the `setAccessToken` method again. Refreshing the token of the same user while a process, a UI Flow or the chat is running is supported.

> [!WARNING]
>
> * The token can't be cleared once set. When another user logs in, call `setAccessToken` with that user's token, between runs (not while a process, a UI Flow or the chat is running).
> * Do not pass an empty string (`""`). The SDK sends it as is and the backend rejects the requests.

> [!WARNING]
>
> The SDK performs no authentication of its own: an authentication base URL or a Keycloak client ID are **not** SDK configuration parameters. Your container application runs its own OIDC flow against Keycloak and passes the resulting token through `setAccessToken`. If you are starting from the FlowX TemplateApp, the authentication values it sets belong to the application, not to the SDK.

#### Anonymous mode

If the access token is never set, the SDK runs in **anonymous mode**: it sends no `Authorization` header and uses an anonymous session provided by the backend.\

Use it to render processes and UI Flows that are exposed to unauthenticated users. For the platform setup, see [Anonymous runtime access](https://docs.flowx.ai/6.x/docs/platform-deep-dive/user-roles-management/anonymous-runtime-access.md).

> [!NOTE]
>
> Setting an access token switches the SDK to authenticated mode and discards the anonymous session. Switching back to anonymous mode is not available.

### Theming

> [!WARNING]
>
> Prior setting up the theme, make sure the `access token` was set.\
> Check the [authentication](#authentication) section for details.

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

```kotlin theme={"dark"}
fun setupTheme(
    workspaceUuid: String,
    themeUuid: String,
    fallbackThemeJsonFileAssetsPath: String? = null,
    appearance: ThemeAppearance = ThemeAppearance.LIGHT,
    @MainThread onCompletion: () -> Unit
)
```

#### Parameters

| Name | Description | Type | Requirement |
| - | - | - | - |
| `workspaceUuid` | UUID string identifier of the workspace that contains the theme to be loaded | `String` | Mandatory. Should not be empty |
| `themeUuid` | UUID string of the theme configured in FlowX Designer | `String` | Mandatory. Must not 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` |
| `appearance` | Indicator for the appearance of the theme (LIGHT, DARK) | `Flowx.ThemeAppearance` | Optional. Defaults to `Flowx.ThemeAppearance.LIGHT` |
| `onCompletion` | `@MainThread` invoked closure, called when setting up the theme completes | `() -> Unit` | Mandatory |

> [!NOTE]
>
> If the `fallbackThemeJsonFileAssetsPath` parameter value is `null`, there will be no fallback mechanism set in place, meaning if fetching the theme fails, the rendered process will have no style applied over its displayed components.

> [!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.

#### Sample

```kotlin theme={"dark"}
Flowx.getInstance().setupTheme(
    workspaceUuid = "some workspace uuid string",
    themeUuid = "some uuid string",
    fallbackThemeJsonFileAssetsPath = "theme/a_fallback_theme.json",
    appearance = Flowx.ThemeAppearance.LIGHT,
) {
    // 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.

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

### 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={"dark"}
fun changeLocaleSettings(locale: Locale?, language: String?)
```

#### Parameters

| Name | Description | Type | Requirement |
| - | - | - | - |
| `locale` | The new locale | `java.util.Locale?` | Mandatory. Pass `null` to keep the current one |
| `language` | The code for the new language | `String?` | Mandatory. Pass `null` to keep the current one |

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

#### Sample

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

> [!NOTE]
>
> The `Locale` satisfies the IETF BCP 47 standard for representing language and country/region codes.\
> \\
>
> More information regarding the standard can be found by reading [RFC 4647 "Matching of Language Tags"](https://datatracker.ietf.org/doc/html/rfc4647) and [RFC 5646 "Tags for Identifying Languages"](https://datatracker.ietf.org/doc/html/rfc5646).\
> \\
>
> An example of BCP 47 is `en-US` (language code `en` and country `US`).

### Start a FlowX process

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

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={"dark"}
fun startProcess(
    workspaceId: String,
    projectId: String,
    processName: String,
    params: JSONObject? = null,
    isModal: Boolean = false,
    onProcessStarted: ((String) -> Unit)? = null,
    onProcessEnded: (() -> Unit)? = null,
    onDismissRequested: (ProjectScope.(processName: String) -> Unit)? = null,
): @Composable () -> Unit
```

#### Parameters

| Name | Description | Type | Requirement |
| - | - | - | - |
| `workspaceId` | The id of the workspace that contains the project and process to be started | `String` | Mandatory |
| `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, it 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`. |
| `onProcessStarted` | Lambda function where you can do additional processing when the process starts | `((String) -> Unit)?` | Optional. It defaults to `null`. |
| `onProcessEnded` | Lambda function where you can do additional processing when the started process ends | `(() -> Unit)?` | Optional. It defaults to `null`. |
| `onDismissRequested` | Lambda function where you should handle closing the process when `isModal` flag is `true` | `(ProjectScope.(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.\
> \\
>
> This wrapper activity must display only the `@Composable` returned from the SDK (i.e. it occupies the whole activity screen space).

#### Sample

```kotlin theme={"dark"}
class ProcessActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        ...
        setContent {
            Flowx.getInstance().startProcess(
                workspaceId = "your workspace id",
                projectId = "your project id",
                processName = "your process name",
                params = JSONObject(),
                isModal = true,
                onProcessStarted = { processInstanceUuid ->
                    // NOTE: possible processing could involve doing something based on the `processInstanceUuid` value received as argument
                },
                onProcessEnded = {
                    // NOTE: possible processing could involve doing something in the container app (i.e. navigating to a different screen)
                },
                onDismissRequested = { 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

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

```kotlin theme={"dark"}
fun continueProcess(
    processUuid: String,
    isModal: Boolean = false,
    onProcessEnded: (() -> Unit)? = null,
    onDismissRequested: (ProjectScope.(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`. |
| `onDismissRequested` | Lambda function where you should handle closing the process when `isModal` flag is `true` | `(ProjectScope.(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.\
> \\
>
> This wrapper activity must display only the `@Composable` returned from the SDK (i.e. it occupies the whole activity screen space).

#### Sample

```kotlin theme={"dark"}
class ProcessActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        ...
        setContent {
            Flowx.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)
                },
                onDismissRequested = { processName ->
                    // NOTE: possible handling could involve doing something differently based on the `processName` value
                },
            ).invoke()
        }
    }
    ...
}
```

### `onDismissRequested` parameter

The `onDismissRequested` parameter is a function defined within the `ProjectScope` context.

This gives the ability to query for substitution tags, media library items or enumerations to use them when handling this callback (i.e. showing a snackbar or an alert).

```kotlin theme={"dark"}
interface ProjectScope {
    fun getSubstitutionTagContent(key: String): String
    fun getMediaItemUrl(key: String): String?
    suspend fun getEnumerationItems(name: String, parentName: String? = null): FxEnumeration?
}
```

> [!NOTE]
>
> `ProjectScope` is also the base of the [`CustomComponentScope`](#customcomponentscope) and [`CustomLoaderScope`](#customloaderscope) contexts, so the same methods are available in custom components and custom loaders.

#### Get a substitution tag value by key

```kotlin theme={"dark"}
fun getSubstitutionTagContent(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, it can request it through the `ProjectScope` context, 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={"dark"}
fun getMediaItemUrl(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, it can request it through the `ProjectScope` context, using the method above, by providing the `key`.

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

#### Obtain enumeration data

```kotlin theme={"dark"}
suspend fun getEnumerationItems(name: String, parentName: String? = null): FxEnumeration?
```

Whenever the container app needs enumeration data, it can request it through the `ProjectScope` context, using the method above, by providing the `name` (and the `parentName`, if the requested enumeration is a child of another one).

It returns the enumeration data, as an `FxEnumeration` object, or `null`, if not found.

#### Sample

```kotlin theme={"dark"}
setContent {
    var projectScope by remember { mutableStateOf<ProjectScope?>(null) }
    val showCloseModalProcessAlert = remember { mutableStateOf(false) }

    Flowx.getInstance().startProcess(
        workspaceId = "your workspace id",
        projectId = "your project id",
        processName = "your process name",
        isModal = true,
        onProcessStarted = { ... },
        onProcessEnded = { ... },
        onDismissRequested = { processName ->
            projectScope = this
            showCloseModalProcessAlert.value = true
        },
    ).invoke()

    projectScope?.CloseModalProcessConfirmAlert(show = showCloseModalProcessAlert)
}

@Composable
fun ProjectScope.CloseModalProcessConfirmAlert(show: MutableState<Boolean>) {
    if (show.value) {
        AlertDialog(
            onDismissRequest = {},
            title = null,
            text = {
                Text(this@CloseModalProcessConfirmAlert.getSubstitutionTagContent("@@close_message")) // IMPORTANT: call `getSubstitutionTagContent` using the `ProjectScope` context
            },
            confirmButton = { ... },
            dismissButton = {
                Button(onClick = { show.value = false }) {
                    Text("Cancel")
                }
            }
        )
    }
}
```

### Start an UI Flow

> [!WARNING]
>
> Prior starting an UI Flow, make sure the [authentication](#authentication) and [theming](#theming) were correctly set up

After performing all the above steps and all the prerequisites are fulfilled, an UI Flow can be started, by using the `startUiFlow` function:

```kotlin theme={"dark"}
fun startUiFlow(
    workspaceId: String,
    projectId: String,
    uiFlowName: String,
    params: JSONObject? = null,
    isModal: Boolean = false,
    onDismissRequested: (ProjectScope.(uiFlowName: String) -> Unit)? = null,
): @Composable () -> Unit
```

#### Parameters

| Name | Description | Type | Requirement |
| - | - | - | - |
| `workspaceId` | The id of the workspace that contains the project and UI Flow to be started | `String` | Mandatory |
| `projectId` | The id of the project containing the UI Flow to be started | `String` | Mandatory |
| `uiFlowName` | The name of the UI Flow | `String` | Mandatory |
| `params` | The starting params for the UI Flow, if any | `JSONObject` | Optional. If omitted, it defaults to `JSONObject()` |
| `isModal` | Flag indicating whether the UI Flow can be closed at anytime by tapping the top-right close button | `Boolean` | Optional. It defaults to `false`. |
| `onDismissRequested` | Lambda function where you should handle closing the UI Flow when `isModal` flag is `true` | `(ProjectScope.(uiFlowName: String) -> Unit)?` | Optional. It defaults to `null`. |

> [!TIP]
>
> The `onDismissRequested` lambda runs in the `ProjectScope` context, the same as for processes. See the [`onDismissRequested` parameter](#ondismissrequested-parameter) section.

> [!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.\
> \\
>
> This wrapper activity must display only the `@Composable` returned from the SDK (i.e. it occupies the whole activity screen space).

#### Sample

```kotlin theme={"dark"}
class UiFlowActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        ...
        setContent {
            Flowx.getInstance().startUiFlow(
                workspaceId = "your workspace id",
                projectId = "your project id",
                uiFlowName = "your ui flow name",
                params = JSONObject(),
                isModal = true,
                onDismissRequested = { uiFlowName ->
                    // NOTE: possible handling could involve asking for confirmation, then closing the activity
                },
            ).invoke()
        }
    }
    ...
}
```

### Display the standalone chat

> [!WARNING]
>
> Prior displaying the chat, make sure the [authentication](#authentication) and [theming](#theming) were correctly set up

The chat component can be used on its own, outside of any process or UI Flow. It is connected to an AI conversational workflow and can be displayed by using the `chatView` function:

```kotlin theme={"dark"}
fun chatView(config: ChatConfig): @Composable () -> Unit
```

#### Parameters

| Name | Description | Type | Requirement |
| - | - | - | - |
| `config` | The connection and UI configuration of the chat | `ai.flowx.android.sdk.api.chat.ChatConfig` | Mandatory |

The `ChatConfig` properties are:

| Name | Description | Type | Requirement |
| - | - | - | - |
| `workspaceId` | The id of the workspace that contains the project | `String` | Mandatory. Must not be empty |
| `projectId` | The id of the project the conversational workflow belongs to | `String` | Mandatory. Must not be empty |
| `workflowResourceDefinitionId` | The resource definition id of the AI conversational workflow | `String` | Mandatory |
| `chatSessionId` | The id of an existing conversation to open. `null` or blank starts a new conversation | `String?` | Mandatory |
| `hideHeader` | Hides the chat header | `Boolean` | Optional. Defaults to `false` |
| `welcomeMessage` | Message displayed when a new conversation starts | `String` | Optional. Defaults to `"You're chatting with an AI assistant. Its responses may be inaccurate, so verify important information."` |
| `thinkingMessage` | Message displayed while the agent prepares its response | `String` | Optional. Defaults to `"Thinking..."` |
| `title` | Title of the chat header | `String` | Optional. Defaults to `"My chat"` |
| `subtitle` | Subtitle of the chat header | `String` | Optional. Defaults to `"You're chatting with an AI assistant"` |
| `hideHistory` | Hides the conversation history option from the chat header | `Boolean` | Optional. Defaults to `false` |
| `hideNewConversation` | Hides the new conversation option from the chat header | `Boolean` | Optional. Defaults to `false` |
| `showChatIcon` | Shows the chat icon in the chat header | `Boolean` | Optional. Defaults to `true` |
| `showSeparator` | Shows the chat header separator | `Boolean` | Optional. Defaults to `true` |
| `showAgentMessageAvatar` | Shows the avatar next to the agent's messages | `Boolean` | Optional. Defaults to `true` |
| `genericErrorMessage` | Message displayed when generating a response fails | `String` | Optional. Defaults to `"There was an error generating this message."` |
| `showErrorIcon` | Shows the icon next to the error message | `Boolean` | Optional. Defaults to `true` |
| `suggestedPromptsTitle` | Title displayed above the suggested prompts | `String` | Optional. Defaults to `"Suggested Prompts"` |
| `suggestedPrompts` | Prompts the user can send with a tap | `List<String>?` | Optional. Defaults to `null` |
| `showMessageCopyAction` | Shows the copy action on messages | `Boolean` | Optional. Defaults to `true` |
| `showMessageTimestamp` | Shows the timestamp of messages | `Boolean` | Optional. Defaults to `true` |
| `showResponseFeedbackActions` | Shows the positive and negative feedback actions on the agent's responses | `Boolean` | Optional. Defaults to `true` |
| `feedbackReasons` | Reasons the user can pick from when giving negative feedback | `List<String>?` | Optional. Defaults to `null` |
| `enableVoiceInput` | Enables voice input | `Boolean` | Optional. Defaults to `false` |
| `enableFileAttachments` | Enables file attachments | `Boolean` | Optional. Defaults to `false` |
| `inputPlaceholder` | Placeholder of the text input | `String` | Optional. Defaults to `"Ask me anything..."` |
| `maxInputRows` | Maximum number of rows the text input grows to | `Int` | Optional. Defaults to `10` |
| `acceptedFileTypes` | Accepted file types for attachments, as MIME types or file extensions (e.g. `application/pdf`, `image/*`, `.doc`) | `List<String>?` | Optional. Defaults to `null` |
| `invalidFileTypeErrorMessage` | Error message displayed when an attached file type is not accepted | `String?` | Optional. Defaults to `null` |
| `maxFiles` | Maximum number of attached files | `Int?` | Optional. Defaults to `null` |
| `filesExceededErrorMessage` | Error message displayed when too many files are attached | `String?` | Optional. Defaults to `null` |
| `maxFileSizeInBytes` | Maximum size of an attached file, in bytes | `Long?` | Optional. Defaults to `null` |
| `fileSizeExceededErrorMessage` | Error message displayed when an attached file is too large | `String?` | Optional. Defaults to `null` |
| `chatHistoryTitle` | Title of the conversation history screen | `String` | Optional. Defaults to `"Conversation History"` |

> [!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.\
> \\
>
> This wrapper activity must display only the `@Composable` returned from the SDK (i.e. it occupies the whole activity screen space).

#### Sample

```kotlin theme={"dark"}
class ChatActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        ...
        setContent {
            Flowx.getInstance().chatView(
                config = object : ChatConfig {
                    override val workspaceId = "your workspace id"
                    override val projectId = "your project id"
                    override val workflowResourceDefinitionId = "your conversational workflow resource definition id"
                    override val chatSessionId: String? = null // starts a new conversation
                    override val title = "Assistant"
                    override val enableVoiceInput = true
                    override val enableFileAttachments = true
                    override val acceptedFileTypes = listOf("application/pdf", "image/*")
                },
            ).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.\

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={"dark"}
interface CustomComponentsProvider {
    fun provideCustomComponent(componentIdentifier: String): CustomComponent?
}
```

#### Sample

```kotlin theme={"dark"}
class FxCustomComponentsProvider : CustomComponentsProvider {
    override fun provideCustomComponent(componentIdentifier: String): CustomComponent? =
        when (componentIdentifier) {
            "myCustomComponent" -> object : CustomComponent {...}
            else -> null
        }
}
```

### CustomComponent

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 `CustomComponent` interface:

```kotlin theme={"dark"}
interface CustomComponent {
    /**
     * Returns the [Composable]s for every custom component identifier defined in the FlowX Designer
     */
    val composable: @Composable CustomComponentScope.() -> Unit

    /**
     * This will be called when 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?)

    /**
     * This will be called when 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, 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, 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:\\
>
> * `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]
>
> Both validation and providing data back into process are optional, and, based on the needs, it may be included in the implementation or not.

### CustomComponentScope

The `composable` property of the [CustomComponent](#customcomponent) is a [@Composable](https://developer.android.com/reference/kotlin/androidx/compose/runtime/Composable) function which may be defined and run only within the context of a `CustomComponentScope` receiver.

```kotlin theme={"dark"}
val composable: @Composable CustomComponentScope.() -> Unit
```

```kotlin theme={"dark"}
interface CustomComponentScope : ProjectScope {
    fun runAction(action: CustomComponentAction, params: JSONObject? = null)
    fun runUploadAction(action: CustomComponentAction, fileUri: Uri, mimeType: String? = null, params: JSONObject? = null)
}
```

Besides the methods for running actions, it inherits the [`ProjectScope`](#ondismissrequested-parameter) methods (`getSubstitutionTagContent`, `getMediaItemUrl` and `getEnumerationItems`).

This allows calling predefined SDK methods for running actions, querying for substitution tags, media item URLs or obtaining enumerations data, directly from the custom component through the scope itself.

#### Run action

The custom components which the container app provides may contain FlowX actions available for execution.\
\

These actions are received through the `actions` parameter of the `populateUi(actions: Map<String, CustomComponentAction>)` method.\
\

To run an action (e.g., on a click of a button in the custom component) you need to call the `runAction` method, through the `CustomComponentScope` context:

```kotlin theme={"dark"}
fun runAction(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.api.custom.components.CustomComponentAction` | Mandatory |
| `params` | Parameters needed to execute the `action` | `JSONObject?` | Optional. It defaults to `null` |

#### Run upload action

A specific use case for running actions from custom components involves uploading files into the FlowX platform.\
\

To run an upload action (e.g., on a click of a button in the custom component) you need to call the `runUploadAction` method, through the `CustomComponentScope` context:

```kotlin theme={"dark"}
fun runUploadAction(action: CustomComponentAction, fileUri: Uri, mimeType: String? = null, params: JSONObject? = null)
```

##### Parameters

| Name | Description | Type | Requirement |
| - | - | - | - |
| `action` | Action object extracted from the `actions` received in the custom component | `ai.flowx.android.sdk.api.custom.components.CustomComponentAction` | Mandatory |
| `fileUri` | The local URI for the file to be uploaded | `android.net.Uri` | Mandatory |
| `mimeType` | The mimeType of the file to be uploaded | `String?` | Optional. It defaults to `null` |
| `params` | Parameters needed to execute the `action` | `JSONObject?` | Optional. It defaults to `null` |

> [!TIP]
>
> It is recommended that a value for the `mimeType` parameter is always provided.\\
>
> When missing, the SDK tries computing it internally.

##### Sample

```kotlin expandable theme={"dark"}
class FileUploadCustomComponent : CustomComponent {
    var actions: MutableMap<String, CustomComponentAction> = mutableMapOf()

    private val viewModel: FileUploadViewModel by lazy {
        FileUploadViewModel(actions = actions)
    }

    override val composable: @Composable CustomComponentScope.() -> Unit =
        @Composable {
            val rememberedViewModel = remember { viewModel }
            FileUpload(viewModel = rememberedViewModel)
        }

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

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

@Composable
private fun CustomComponentScope.FileUpload(viewModel: FileUploadViewModel) {
    viewModel.setFlowxScope(this@FileUpload)
    val context = LocalContext.current
    val openDocumentLauncher =
        rememberLauncherForActivityResult(ActivityResultContracts.OpenDocument()) { uri ->
            uri?.let { viewModel.runUploadAction(fileUri = it, mimeType = it.mimeType(context)) }
            Flowx.getInstance().setUpdateStateEnabled(enabled = true) // IMPORTANT: restore the SDK internal update mechanism after selecting a file to be uploaded
        }

    Column(
        modifier = Modifier.fillMaxWidth(),
        horizontalAlignment = Alignment.CenterHorizontally,
    ) {
        Button(
            onClick = {
                Flowx.getInstance().setUpdateStateEnabled(enabled = false) // IMPORTANT: disable SDK internal update mechanism while selecting a file to be uploaded
                openDocumentLauncher.launch(listOf("application/pdf").toTypedArray()) // select PDF files only
            }
        ) {
            Text(text = "Upload File")
        }
    }
}

private class FileUploadViewModel(
    private var actions: Map<String, CustomComponentAction> = emptyMap(),
) : ViewModel() {
    private lateinit var flowxScope: CustomComponentScope

    fun runUploadAction(fileUri: Uri, mimeType: String?) {
        actions["uploadFile"]?.let { action ->
            if (this@FileUploadViewModel::flowxScope.isInitialized) {
                flowxScope.runUploadAction(
                    action = action,
                    params = null,
                    fileUri = fileUri,
                    mimeType = mimeType,
                )
            }
        }
    }

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

private fun Uri.mimeType(context: Context): String? = context.contentResolver.getType(this)
```

> [!NOTE]
>
> In order for the upload behave correctly, it's highly IMPORTANT to disable the SDK internal update mechanism while selecting a file.

#### Get a substitution tag value by key

```kotlin theme={"dark"}
fun getSubstitutionTagContent(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, through the `CustomComponentScope` context, 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={"dark"}
fun getMediaItemUrl(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, through the `CustomComponentScope` context, using the method above, by providing the `key`.

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

#### Obtain enumeration data

```kotlin theme={"dark"}
suspend fun getEnumerationItems(name: String, parentName: String? = null): FxEnumeration?
```

Whenever the container app needs enumeration data for populating the UI of the custom components, it can request it through the `CustomComponentScope` context, using the method above, by providing the `name` (and the `parentName`, if the requested enumeration is a child of another one).

It returns the enumeration data, as an `FxEnumeration` object, or `null`, if not found.

```kotlin theme={"dark"}
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
    }
}
```

#### 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
* runs an action, through the `CustomComponentScope` context, to skip setting the age

```kotlin expandable theme={"dark"}
class FxCustomComponentsProvider : CustomComponentsProvider {
    override fun provideCustomComponent(componentIdentifier: String): CustomComponent? =
        when (componentIdentifier) {
            "other-custom-component-identifier" -> OtherCustomComponent()
            "age" -> AgeCustomComponent()
            else -> null
        }
}

private class AgeCustomComponent() : CustomComponent {
    private val data: MutableStateFlow<Any?> = MutableStateFlow(null)
    private var actions: MutableMap<String, CustomComponentAction> = mutableMapOf()

    private val viewModel = AgeViewModel(data = data, actions = actions)

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

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

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

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

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

@Composable
private fun CustomComponentScope.Age(viewModel: AgeViewModel) {
    viewModel.setFlowxScope(this@Age) // IMPORTANT: keep a reference to the custom context/scope in order to access the available predefined SDK methods

    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,
            )
        }

        TextButton(
            onClick = { viewModel.executeSkipAction() }
        ) {
            Text(text = "Skip")
        }
    }
}

private class AgeViewModel(
    private val data: MutableStateFlow<Any?> = MutableStateFlow(null),
    private val actions: Map<String, CustomComponentAction> = emptyMap(),
) : ViewModel() {
    private lateinit var flowxScope: CustomComponentScope // IMPORTANT: keeps the custom context/scope which allows to call the available predefined methods exposed to custom component

    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 setFlowxScope(scope: CustomComponentScope) {
        flowxScope = scope
    }

    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()
            )
        }

    fun executeSkipAction() {
        actions["skipAction"]?.let {
            if (this@AgeViewModel::flowxScope.isInitialized) { // IMPORTANT: if the custom context/scope was initialized, the action can be safely run
                flowxScope.runAction(
                    action = it,
                    params = JSONObject() // e.g. JSONObject("{\"someParameter\": \"someValue\"}")
                )
            }
        } ?: println("AgeCustomComponent: `skipAction` action was not found")
    }
}

private class OtherCustomComponent() : CustomComponent {
    override val composable: @Composable (CustomComponentScope.() -> Unit)
        get() = @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
}
```

## Custom header view for the [STEPPER](https://docs.flowx.ai/6.x/docs/building-blocks/process/navigation-areas.md#stepper) component

The container app can opt for providing a custom view to use for all the [Stepper](https://docs.flowx.ai/6.x/docs/building-blocks/process/navigation-areas.md#stepper) components, as a replacement for the built-in header.\

The custom view receives `data` to populate its UI, as described below.

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

```kotlin theme={"dark"}
interface CustomStepperHeaderProvider {
    fun provideCustomStepperHeader(): CustomStepperHeader?
}
```

#### Sample

```kotlin theme={"dark"}
class FxCustomStepperHeaderProvider : CustomStepperHeaderProvider {
    override fun provideCustomStepperHeader(): CustomStepperHeader? {
        return object : CustomStepperHeader {...}
    }
}
```

### CustomStepperHeader

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 `CustomStepperHeader` interface:

```kotlin theme={"dark"}
interface CustomStepperHeader {
    /**
     * Returns the [Composable]s used to render the stepper header.
     * The received argument contains the stepper header necessary data to render the view.
     */
    val composable: @Composable (data: Data) -> Unit

    interface Data {
        // 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={"dark"}
override fun provideCustomStepperHeader(): CustomStepperHeader? {
    return object : CustomStepperHeader {
        override val composable: @Composable ((CustomStepperHeader.Data) -> Unit)
            get() = @Composable { data ->
                /* add some @Composable implementation which displays the `data` */
            }
    }
}
```

## Custom loaders

The container application can decide to provide custom loaders to be displayed at certain moments based on a given predefined `actionName`.\

To provide custom loaders, an *implementation* of the `CustomLoaderProvider` interface should be passed as a parameter when initializing the SDK:

```kotlin theme={"dark"}
interface CustomLoaderProvider {
    fun provideCustomLoader(loaderType: CustomLoaderProvider.LoaderType?): CustomLoader?
}
```

where the `CustomLoaderProvider.LoaderType` is defined like this:

```kotlin theme={"dark"}
interface LoaderType {
    interface StartProcess : LoaderType
    interface ReloadProcess : LoaderType
    interface Action : LoaderType { val name: String }
    interface Upload : LoaderType { val name: String }
}
```

> [!NOTE]
>
> The available types for the `loaderType` parameter are:
>
> * `LoaderType.StartProcess` - received for overriding the loader displayed when starting a new process
> * `LoaderType.ReloadProcess` - received for overriding the loader displayed when resuming an existing process
> * `LoaderType.Action` - received for overriding the loader displayed while the action having the `name` name is executed
> * `LoaderType.Upload` - received for overriding the loader displayed while the **upload** action having the `name` name is executed
>
> The `name` property received for the `LoaderType.Action` and `LoaderType.Upload` types represents the name of an action, as defined at process definition time, received for being able to override the loader displayed while that action is executed.

> [!NOTE]
>
> Returning an implementation of a `CustomLoader` replaces the built-in platform loader with the provided one for the specified use cases.\
> \\
>
> Returning `null` keeps the built-in platform loader for the specified use cases.

### CustomLoader

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

```kotlin theme={"dark"}
interface CustomLoader {
    val composable: @Composable CustomLoaderScope.() -> Unit
}
```

### CustomLoaderScope

The `composable` property of the [CustomLoader](#customloader) is a [@Composable](https://developer.android.com/reference/kotlin/androidx/compose/runtime/Composable) function which may be defined and run only within the context of a `CustomLoaderScope` receiver.

```kotlin theme={"dark"}
val composable: @Composable CustomLoaderScope.() -> Unit
```

```kotlin theme={"dark"}
interface CustomLoaderScope : ProjectScope
```

It provides the [`ProjectScope`](#ondismissrequested-parameter) methods (`getSubstitutionTagContent`, `getMediaItemUrl` and `getEnumerationItems`).

This allows calling predefined SDK methods for querying for substitution tags, media item URLs or obtaining enumerations data directly from the custom loader through the scope itself.

#### Sample

```kotlin expandable theme={"dark"}
class FxCustomLoaderProvider : CustomLoaderProvider {
    override fun provideCustomLoader(loaderType: CustomLoaderProvider.LoaderType?): CustomLoader? =
        when (loaderType) {
            is CustomLoaderProvider.LoaderType.StartProcess -> MyCustomLoader(backgroundColor = Color.Black.copy(alpha = 0.38f), indicatorColor = Color.Red)
            is CustomLoaderProvider.LoaderType.ReloadProcess -> MyCustomLoader(backgroundColor = Color.Yellow.copy(alpha = 0.38f), indicatorColor = Color.Green)
            is CustomLoaderProvider.LoaderType.Action -> {
                when (loaderType.name) {
                    "action1" -> ActionCustomLoader()
                    "action2" -> ComplexCustomLoader()
                    else -> null
                }
            }
            is CustomLoaderProvider.LoaderType.Upload -> {
                when (loaderType.name) {
                    "upload1" -> ActionCustomLoader()
                    "upload2" -> ComplexCustomLoader()
                    else -> null
                }
            }
            else -> null
        }
}

class MyCustomLoader(val backgroundColor: Color, val indicatorColor: Color) : CustomLoader {
    override val composable: @Composable (CustomLoaderScope.() -> Unit)
        get() = @Composable {
            Box(
                modifier = Modifier
                    .fillMaxSize()
                    .background(color = backgroundColor),
                contentAlignment = Alignment.Center,
            ) {
                CircularProgressIndicator(color = indicatorColor)
                BackHandler(enabled = true) {} // block back navigation
            }
        }
}

class ActionCustomLoader() : CustomLoader {...}

class ComplexCustomLoader() : CustomLoader {
    private val animatedViewModel = AnimatedLoaderViewModel(...)
    override val composable: @Composable (CustomLoaderScope.() -> Unit)
        get() = @Composable {
            AnimatedLoader(viewModel = animatedViewModel)
        }
}

@Composable
private fun CustomLoaderScope.AnimatedLoader(
    viewModel: AnimatedLoaderViewModel
) {
    viewModel.setFlowxScope(this@AnimatedLoader) // IMPORTANT: keep a reference to the custom context/scope in order to access the available predefined SDK methods

    val state by viewModel.loaderState.collectAsStateWithLifecycle()

    Box {
        // use state to build what is to be displayed
    }
}

private class AnimatedLoaderViewModel(...) : ViewModel() {
    private lateinit var flowxScope: CustomLoaderScope // IMPORTANT: keeps the custom context/scope which allows to call the available predefined methods exposed to custom component

    val loaderState: StateFlow<AnimatedLoaderState?> =
        someData
            .process {
                if (this@AnimatedLoaderViewModel::flowxScope.isInitialized) { // IMPORTANT: if the custom context/scope was initialized, the substitution tag can be safely queried
                    flowxScope.getSubstitutionTagContent(it)
                } else {
                    it
                }
            }
            .stateIn(...)

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

```

## Collecting log events

To be able to collect log events from the SDK, an implementation for the `LogSink.Collector` functional interface may be provided when initializing the SDK:

```kotlin theme={"dark"}
interface LogSink {
    fun interface Collector {
        fun onEvent(event: LogSink.Event)
    }
}
```

The `Event` is structured like this:

```kotlin theme={"dark"}
interface LogSink {
    interface Event {
        /** Capture time, ISO 8601 @ UTC: `yyyy-MM-dd'T'HH:mm:ss.SSS'Z'` */
        val timestamp: String

        /** One of "rest", "sse", "expression", "cms" */
        val category: String

        /**
        * The event type within [category]:
        * - rest → one of "request", "response", "error"
        * - sse → the raw server-sent event name
        * - expression → one of "hide", "disabled", "computed", "conditional", "validator", "localScript", "triggerCondition", "prototypeSelection"
        * - cms → "fontRegistered"
        */
        val type: String

        val payload: JSONObject
    }
}
```

#### Sample

The implementation can be passed as a lambda, like:

```kotlin theme={"dark"}
logSink = { event ->
    // do whatever is needed (e.g. log the event, send it you external telemetry systems, etc.)
    android.util.Log.i("FxLogSink", event.toString())
}
```

> [!NOTE]
>
> The `payload` inside the `LogSink.Event` contains raw data specific to the event type.

## 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={"dark"}
fun interface AnalyticsCollector {
    fun onEvent(event: Event)
}
```

There are two types of events, `Screen` and `Action`, both of them containing some `Data` and an optional `CustomPayload`, as defined at process definition time.

The `Event` is structured like this:

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

        interface Data {
            val value: String
            val customPayload: CustomPayload?
        }
    }

    interface Action : Event {
        val data: Action.Data

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

    sealed interface CustomPayload {
        interface Object : CustomPayload {
            val data: JSONObject
        }

        interface Array : CustomPayload {
            val data: JSONArray
        }
    }
}
```

#### Sample

The implementation can be passed as a lambda, like:

```kotlin theme={"dark"}
analyticsCollector = { event ->
    // do whatever is needed (e.g. log the event)
    when (it) {
        is Event.Screen -> {
            val customPayload: String? =
                when (val payload = it.data.customPayload) {
                    is Event.CustomPayload.Object -> payload.data.toString()
                    is Event.CustomPayload.Array -> payload.data.toString()
                    else -> null
                }
            Log.i("Analytics", "Event.Screen(value = ${it.data.value}, customPayload = $customPayload)")
        }

        is Event.Action -> {
            val customPayload: String? =
                when (val payload = it.data.customPayload) {
                    is Event.CustomPayload.Object -> payload.data.toString()
                    is Event.CustomPayload.Array -> payload.data.toString()
                    else -> null
                }
            Log.i("Analytics", "Event.Action(value = ${it.data.value}, screen = ${it.data.screen}, component = ${it.data.component}, label = ${it.data.label}, customPayload = $customPayload)")
        }
    }
}
```

> [!NOTE]
>
> 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
>
> The `customPayload` is defined at process definition time, and then processed inside the platform before sending it to being collected.

## Handling "Start of a new process"

When an action of type `START_PROJECT` is executed, the `onNewProcessStarted` lambda provided in the `Flowx.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 `Flowx.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={"dark"}
Flowx.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) }
    }
}
```

## ProGuard / R8

The SDK ships with its own `consumer-rules.pro` file containing the required keep rules for correct functionality.\

These rules are automatically merged into the container app’s ProGuard/R8 configuration and applied during code shrinking and obfuscation.\

The container app may add additional rules if its specific logic requires them.

## Known issues

* shadows are rendered only on **Android >= 28** having [hardware acceleration](https://developer.android.com/topic/performance/hardware-accel) **enabled**


## Related topics

- [Android SDK](https://docs.flowx.ai/release-notes/v6.x/v6.0.0-october-2026/migrating-from-v5.9-to-v6.0/android.md)
- [Renderer SDKs](https://docs.flowx.ai/release-notes/v5.x/v5.0.0-july-2025/migrating-from-v4.7.x-to-5.0/renderers.md)
- [React Native SDK](https://docs.flowx.ai/6.x/sdks/react-native-renderer.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.