Start Flow¶
Starting a Flow¶
There are two APIs in FlowUtil you can use to start a flow.
fun startFlow(
context: Context,
flowContext: FlowContext,
updateIntent: ((Intent) -> Unit)? = null,
callback: FlowActivityResultCallback = { _, _ -> }
) { ... }
fun startRestoreIfLocked(
context: Context,
flowContext: FlowContext,
updateIntent: ((Intent) -> Unit)? = null,
callback: FlowActivityResultCallback = { _, _ -> }
) { ... }
The signature of these two APIs is the same. The first one is the most common one to use for most cases, for example, starting the change passcode flow after the onboarding or restore flow:
FlowUtil.startFlow(
context,
flowContext = DemoApplication.getOnboardingFlowContext(context)
) { resultCode, _ ->
if (resultCode == Activity.RESULT_OK) {
val intent = Intent(this, MainActivity::class.java).apply {
addFlags(
Intent.FLAG_ACTIVITY_NEW_TASK or
Intent.FLAG_ACTIVITY_CLEAR_TASK or
Intent.FLAG_ACTIVITY_CLEAR_TOP
)
}
startActivity(intent)
} else finish()
}
The second API is used to start the restore flow only if the app is locked at the time of its being called. For example, when you're on the widget configuration activity to create a widget, you may want to call the API to check the app's lock status. If the app is not locked, this API will return immediately. If the app is locked, then it will bring the restore flow back, then the widget configuration activity. For example:
private fun startOnboarding(callback: FlowActivityResultCallback) {
FlowUtil.startRestoreIfLocked(
this@ODataWidgetConfigurationActivity,
DemoApplication.getOnboardingFlowContext(
this@ODataWidgetConfigurationActivity
),
callback = {
if (resultCode == Activity.RESULT_CANCELED) {
setResult(resultCode)
finish()
}
}
)
}
Arguments¶
This section describes the arguments that are needed to start a flow. context could be your activity to start a flow. The following sections explain the other arguments.
FlowContext¶
data class FlowContext(
val flowType: FlowType = FlowType.Onboarding,
val flow: BaseFlow? = null,
val appConfig: AppConfig = <Default AppConfig>,
val flowOptions: FlowOptions = FlowOptions(),
val flowStateListener: FlowStateListener = FlowStateListener(),
val flowActionHandler: FlowActionHandler = FlowActionHandler()
) {
fun getCurrentUser(): DeviceUser? = currentUser
fun getPreviousUser(): DeviceUser? = previousUser
}
FlowContext is almost the same as that in the view-based flows, except that the screen settings property is moved into FlowOptions.
Use flowType for predefined flows and flow for customized flows. When flow is provided, flowType will be ignored by the flows framework.
Use FlowOptions to customize the flow. FlowActionHandler allows the client code to inject logic for the onboarding process, for example, to add custom steps. FlowStateListener allows the client code to observe the states during the onboarding or restore flow.
AppConfig now has a default value. If the client needs to activate the app with either the Discovery service, QR code, or the managed configuration, you don't need to provide AppConfig as is required in the view-based flow.
An additional difference is that there is no 'builder' in FlowContext, you can simply initialize this 'data' class, or use the 'copy' function of the Kotlin 'data' class.
FlowActivityResultCallback¶
The definition of FlowActivityResultCallback is:
typealias FlowActivityResultCallback = (resultCode: Int, data: Intent?) -> Unit
The difference between this and the view-based flow is that there are only two arguments (requestCode was removed).
It has also been marked as deprecated in the view-based flow because it was designed before FlowActivityResultCallback was introduced in the SDK and onActivityResult was the only way to observe the flow finish status.
The other difference is that FlowActivityResultCallback is not nullable and now has a default value.
updateIntent¶
This argument is not available in the view-based flows component. It is used mainly for the following cases:
Add Intent Flags¶
In some cases, additional intent flags might be needed to start a flow. For example, when testing your own flow, or starting the predefined flow from a widget, INTENT.FLAG_ACTIVITY_NEW_TASK may be required. See the test code below:
@Test
fun test_restore_flow_no_passcode() = runBlocking {
rule.setContent {
prepareScreen {
Button(onClick = {
FlowUtil.startFlow(applicationContext, flowContext = flowContext,
updateIntent = {
it.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}) { _, _ -> }
}) {
Text("StartRestore")
}
}
}
rule.onNodeWithText("StartRestore").performClick()
...
}
Configurations¶
It can also be used to configure how the flow should start. For example, you can disable the confirmation dialog for the reset flow or force the user to log out when starting the restore flow. To support these configurations, FlowUtil provides several extension functions:
| Extension Function | Description |
|---|---|
| fun Bundle.requirePostNotificationPermission(require: Boolean = true) | Indicates whether the app must request post notification permission during onboarding. The default value is true |
| fun Bundle.skipConfirmForResetFlow(skip: Boolean = false) | Indicates whether to skip the confirmation dialog for the reset flow. The default value is false. |
| fun Bundle.skipConfirmForDeleteRegistrationFlow(skip: Boolean = false) | Indicates whether to skip the confirmation dialog for the registration deletion flow. The default value is false. |
| fun Bundle.skipConfirmForLogoutFlow(skip: Boolean = false) | Indicates whether to skip the confirmation dialog for the logout flow. The default value is false. |
| fun Bundle.skipNotificationWhenRestore(skip: Boolean = false) | Whether to call APIs after unlocking with the passcode during the restore flow, the default value is false. |
| fun Bundle.forceLogoutWhenRestore(force: Boolean = true) | Indicates whether to log out the current user after unlocking the app during the restore flow. The default value is true. |
For example, if your app does not want the SDK to request the post notification permission, you can start the onboarding flow like this:
FlowUtil.startFlow(
context = context,
flowContext = DemoApplication.getOnboardingFlowContext(context),
updateIntent = { intent ->
intent.populateCustomBundle {
requirePostNotificationPermission(require = false)
}
}
) { resultCode, data -> ... }
Check Flow Result¶
There are 2 extension functions in FlowsUtil on Intent to check the flow's finished status.
fun Intent.getFinishedFlowName(): String? = ...
fun Intent.getDeleteUserId(): String? = ...
The first extension function can be used to check the name of the finished flow. This can be used when LockAndWipeService is enabled and the restore flow is automatically canceled due to the current user being locked or wiped. In such cases, you can check the flow's finish result as follows:
FlowUtil.startFlow(
context = context,
flowContext = DemoApplication.getOnboardingFlowContext(context),
updateIntent = { intent ->
intent.populateCustomBundle {
requirePostNotificationPermission(require = true)
}
}
) { resultCode, data ->
if (resultCode == Activity.RESULT_OK) {
val intent = Intent().apply {
setClass(context, MainActivity::class.java)
addFlags(
Intent.FLAG_ACTIVITY_NEW_TASK or
Intent.FLAG_ACTIVITY_CLEAR_TASK or
Intent.FLAG_ACTIVITY_CLEAR_TOP
)
}
startActivity(intent)
finish()
} else {
data?.getFinishedFlowName()?.also { flowName ->
when (flowName) {
FlowType.Logout.name -> {
//lock
}
FlowType.DeleteRegistration.name -> {
//wipe
}
}
}
startOnboarding(context)
}
}
In the above sample code, when the restore flow is canceled, your app should check the name of the finished flow to determine the reason. For example, if the finished flow name is FlowType.Logout.name, it indicates that the current user is locked. If the finished flow name is FlowType.DeleteRegistration.name, it means the user data has been wiped, and the user must onboard again.
The second function is used in the registration deletion flow to check which user has been deleted.
Initial Data¶
Besides handling the onboarding processes, the flows component (both the view-based and the compose versions) can also be used as a framework to host custom flows. That is, you can develop your own flow and start it with the flows framework. In some cases, the custom flow may need to pass in some initial data to be shared by all the steps in this flow.
To do this in the view-based flow, you can override the getInitialData function in your flow. In the compose version, you can do this using the prepareCustomBundle function in FlowUtil. For example:
FlowUtil.startFlow(context, flowContext = flowContext, updateIntent = { intent ->
FlowUtil.prepareCustomBundle(intent) {
putString("my_custom_prop_1", "Custom Property 1")
}
}) { code, _ ->
...
}
You can retrieve the my_custom_property_key property value in your flow using the following code:
...
addSingleStep("step_2") {
OnboardingScreenContainer {
OnboardingScreenTitleSection()
Column(
modifier = Modifier
.fillMaxSize()
.padding(24.dp),
verticalArrangement = Arrangement.Center
) {
Text(
text = this@DemoCustomFlow.getCustomBundle()?.getString("my_custom_prop_1")
?: "No Such Property"
)
...
}
}
}