Android project requirements

System requirements:

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

The SDK library was build using:

Installing the library

  1. Add the maven repository in your project’s settings.gradle.kts file:
dependencyResolutionManagement {
    ...
    repositories {
        ...
        maven {
            url = uri("https://nexus-jx.dev.rd.flowx.ai/repository/flowx-maven-releases/")
            credentials {
                username = "your_username"
                password = "your_password"
            }
        }
    }
}
  1. Add the library as a dependency in your app/build.gradle.kts file:
dependencies {
    ...
    implementation("ai.flowx.android:android-sdk:3.0.4")
    ...
}

Library dependencies

Impactful dependencies:

Public API

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

NameDescriptionDefinition
initInitializes 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)
checkRendererCompatibilityChecks the renderer version compatibility with the deployed servicessuspend fun checkRendererCompatibility(action: ((Boolean) -> Unit)?)
setAccessTokenProviderUpdates the access token provider (i.e. a functional interface) inside the rendererfun setAccessTokenProvider(accessTokenProvider: FlowxSdkApi.Companion.AccessTokenProvider)
setupThemeSets up the theme to be used when rendering a processfun setupTheme(themeUuid: String, fallbackThemeJsonFileAssetsPath: String? = null, @MainThread onCompletion: () -> Unit)
startProcessStarts a FlowX process instance, by returning a @Composable function where the process is rendered.fun startProcess(processName: String, params: JSONObject = JSONObject(), isModal: Boolean = false, closeModalFunc: ((processName: String) -> Unit)? = null): @Composable () -> Unit
continueProcessContinues 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
executeActionRuns an action from a custom componentfun executeAction(action: CustomComponentAction, params: JSONObject? = null)
getMediaResourceUrlExtracts a media item URL needed to populate the UI of a custom componentfun getMediaResourceUrl(key: String): String?
replaceSubstitutionTagExtracts a substitution tag value needed to populate the UI of a custom componentfun replaceSubstitutionTag(key: String): String

Configuring the library

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

fun init(
    context: Context,
    config: SdkConfig,
    accessTokenProvider: AccessTokenProvider? = null,
    customComponentsProvider: CustomComponentsProvider? = null,
    customStepperHeaderProvider: CustomStepperHeaderProvider? = null,
)

Parameters

NameDescriptionTypeRequirement
contextAndroid application ContextContextMandatory
configSDK configuration parametersai.flowx.android.sdk.process.model.SdkConfigMandatory
accessTokenProviderFunctional interface provider for passing the access tokenai.flowx.android.sdk.FlowxSdkApi.Companion.AccessTokenProvder?Optional. Defaults to null.
customComponentsProviderProvider for the @Composable/View custom componentsai.flowx.android.sdk.component.custom.CustomComponentsProvider?Optional. Defaults to null.
customStepperHeaderProviderProvider for the @Composable custom stepper header viewai.flowx.android.sdk.component.custom.CustomStepperHeaderProvider?Optional. Defaults to null.

• Providing the access token is explained in the authentication section.
• The custom components implementation is explained in its own section.
• The implementation for providing a custom view for the header of the Stepper component is detailed in its own section.

Sample

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",
                validators = mapOf("exact_25_in_length" to { it.length == 25 }),
            ),
            accessTokenProvider = null, // null by default; can be set later, depending on the existing authentication logic
            customComponentsProvider = object : CustomComponentsProvider {...},
            customStepperHeaderProvider = object : CustomStepperHeaderProvider {...},
        )
    }
}

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

NameDescriptionTypeRequirement
baseUrlURL to connect to the FlowX back-end environmentStringMandatory
imageBaseUrlURL to connect to the FlowX Media Library module of the CMSStringMandatory
enginePathURL path segment used to identify the process engine serviceStringMandatory
languageThe language used for retrieving enumerations and substitution tagsStringOptional. Defaults to en.
validatorsCustom validators for form elementsMap<String, (String) -> Boolean>?Optional.

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.

By looking at the example from above:

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

FlowxSdkApi.getInstance().setAccessTokenProvider(accessTokenProvider = { "your access token" })

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

fun interface AccessTokenProvider {
    fun get(): String
}
Whenever the access token changes based on your own authentication logic, it must be updated in the renderer by calling the setAccessTokenProvider method again.

Check renderer compatibility

Before actually using the SDK, it is recommended to check the compatibility between the renderer and the currently deployed FlowX services.

This can be achieved by calling the suspend-ing checkRendererCompatibility method:

suspend fun checkRendererCompatibility(action: ((Boolean) -> Unit)?)

where the action lambda parameter is where one should add their own logic to be executed when compatible or not.

Sample

CoroutineScope(Dispatchers.IO).launch {
    FlowxSdkApi.getInstance().checkRendererCompatibility {
        when (it) {
            true -> { /* compatible */ }
            false -> { /* NOT compatible */ }
        }
    }
}

Theming

Prior setting up the theme, make sure the access token provider was set.
Check the authentication section for details.

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:

suspend fun setupTheme(
    themeUuid: String,
    fallbackThemeJsonFileAssetsPath: String? = null,
    @MainThread onCompletion: () -> Unit
)

Parameters

NameDescriptionTypeRequirement
themeUuidUUID string of the theme configured in FlowX DesignerStringMandatory. Can be empty
fallbackThemeJsonFileAssetsPathAndroid 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 availableString?Optional. Defaults to null
onCompletion@MainThread invoked closure, called when setting up the theme completes() -> UnitMandatory

If the themeUuid parameter value is empty (""), no theme will be fetched, and the mechanism will rely only on the fallback file, if set.

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.

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

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

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.

Do not start or resume a process before the completion of the theme setup mechanism.

Start a FlowX process

Prior starting a process, make sure the authentication and 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:

fun startProcess(
    processName: String,
    params: JSONObject = JSONObject(),
    isModal: Boolean = false,
    closeModalFunc: ((processName: String) -> Unit)? = null,
): @Composable () -> Unit

Parameters

NameDescriptionTypeRequirement
processNameThe name of the processStringMandatory
paramsThe starting params for the process, if anyJSONObjectOptional. If omitted, if defaults to JSONObject()
isModalFlag indicating whether the process can be closed at anytime by tapping the top-right close buttonBooleanOptional. It defaults to false.
closeModalFuncLambda function where you should handle closing the process when isModal flag is true((processName: String) -> Unit)?Optional. It defaults to null.

The returned @Composable function must be included in its own 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

class ProcessActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        ...
        setContent {
            FlowxSdkApi.getInstance().startProcess(
                processName = "your process name",
                params: JSONObject = JSONObject(),
                isModal = true,
                closeModalFunc = { processName ->
                    // NOTE: possible handling could involve doing something differently based on the `processName` value
                },
            ).invoke()
        }
    }
    ...
}

Resume a FlowX process

Prior resuming process, make sure the authentication and theming were correctly set up

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

fun continueProcess(
    processUuid: String,
    isModal: Boolean = false,
    closeModalFunc: ((processName: String) -> Unit)? = null,
): @Composable () -> Unit

Parameters

NameDescriptionTypeRequirement
processUuidThe UUID string of the processStringMandatory
isModalFlag indicating whether the process can be closed at anytime by tapping the top-right close buttonBooleanOptional. It defaults to false.
closeModalFuncLambda function where you should handle closing the process when isModal flag is true((processName: String) -> Unit)?Optional. It defaults to null.

The returned @Composable function must be included in its own 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

class ProcessActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        ...
        setContent {
            FlowxSdkApi.getInstance().continueProcess(
                processUuid = "some process UUID string",
                isModal = true,
                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.

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

interface CustomComponentsProvider {
    fun provideCustomComposableComponent(): CustomComposableComponent?
    fun provideCustomViewComponent(): CustomViewComponent?
}

There are two methods to provide a custom component:

  1. by implementing the CustomComposableComponent interface
  2. by implementing the CustomViewComponent interface

Sample

class CustomComponentsProviderImpl : CustomComponentsProvider {
    override fun provideCustomComposableComponent(): CustomComposableComponent? {
        return object : CustomComposableComponent {...}
    }
    override fun provideCustomViewComponent(): CustomViewComponent? {
        return object : CustomViewComponent {...}
    }
}

CustomComposableComponent

To provide the custom component as a @Composable function, you have to implement the CustomComposableComponent interface:

interface CustomComposableComponent {
    fun provideCustomComposable(componentIdentifier: String): CustomComposable
}

The returned CustomComposable object is an interface defined like this:

interface CustomComposable {
    // `true` for the custom components that are implemented and can be handled
    // `false` otherwise
    val isDefined: Boolean

    // `@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>)
}

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.

Sample

override fun provideCustomComposableComponent(): CustomComposableComponent? {
    return object : CustomComposableComponent {
        override fun provideCustomComposable(componentIdentifier: String) = object : CustomComposable {
            override val isDefined: Boolean = when (componentIdentifier) {
                "some custom component identifier" -> true
                "other custom component identifier" -> true
                else -> false
            }

            override val composable: @Composable () -> Unit = {
                when (componentIdentifier) {
                    "some custom component identifier" -> { /* add some @Composable implementation */ }
                    "other custom component identifier" -> { /* add other @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
            }
        }
    }
}

CustomViewComponent

To provide the custom component as a classical Android View function, you have to implement the CustomViewComponent interface:

interface CustomViewComponent {
    fun provideCustomView(componentIdentifier: String): CustomView
}

The returned CustomView object is an interface defined like this:

interface CustomView {

    // `true` for the custom components that are implemented and can be handled
    // `false` otherwise
    val isDefined: Boolean

    /**
     * returns the `View`s for the custom components that can be handled
     */
    fun getView(context: Context): View

    /**
     * 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>)
}

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.

Sample

override fun provideCustomViewComponent(): CustomViewComponent? {
    return object : CustomViewComponent {
        override fun provideCustomView(componentIdentifier: String) = object : CustomView {
            override val isDefined: Boolean = when (componentIdentifier) {
                "some custom component identifier" -> true
                "other custom component identifier" -> true
                else -> false
            }

            override fun getView(context: Context): View {
                return when (componentIdentifier) {
                    "some custom component identifier" -> { /* return some View */ }
                    "other custom component identifier" -> { /* return other View */ }
                }
            }

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

Execute 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.

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:

fun executeAction(action: CustomComponentAction, params: JSONObject? = null)

Parameters

NameDescriptionTypeRequirement
actionAction object extracted from the actions received in the custom componentai.flowx.android.sdk.component.custom.CustomComponentActionMandatory
paramsParameters needed to execute the actionJSONObject?Optional. It defaults to null

Get a substitution tag value by key

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

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 component

The container application can opt for providing a custom view in order to be used, for all the 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, an implementation of the CustomStepperHeaderProvider interface should be passed as a parameter when initializing the SDK:

interface CustomStepperHeaderProvider {
    fun provideCustomComposableStepperHeader(): CustomComposableStepperHeader?
}

As opposed to the Custom components, the only supported way is by providing the view as a @Composable function.

Sample

class CustomStepperHeaderProviderImpl : CustomStepperHeaderProvider {
    override fun provideCustomComposableStepperHeader(): CustomComposableStepperHeader? {
        return object : CustomComposableStepperHeader {...}
    }
}

CustomComposableStepperHeader

To provide the custom header view as a @Composable function, you have to implement the CustomComposableStepperHeader interface:

interface CustomComposableStepperHeader {
    fun provideComposableStepperHeader(): ComposableStepperHeader
}

The returned ComposableStepperHeader object is an interface defined like this:

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:

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

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

Known issues