Write Your Own Flow¶
Create Your Own Flow Class¶
Your own flow class must extend from Flow
, and override some of the methods defined in it.
protected fun <T : FlowStepFragment> addSingleStep(stepId: Int, fragmentClass: KClass<T>) {...}
protected fun addNestedFlow(nestedFlow: Flow) {...}
protected open suspend fun onStart(businessData: BusinessDataMap) = Unit
open suspend fun onFinish(businessData: BusinessDataMap) = Unit
open suspend fun onTerminate(businessData: BusinessDataMap) = Unit
open suspend fun getNextStep(
currentStepId: Int,
businessData: BusinessDataMap
): Pair<Int, KClass<*>?>? { ... }
/**
* The initial data map of this flow, which will be shared by all the steps inside the flow.
*/
open fun getInitialData(): Map<String, Any>? = null
addSingleStep
andaddNestedFlow
. Use these two methods to add steps into the flow. The steps can be either a single simple step, or steps nested in another flow. Using a nested flow allows you to categorize steps to make it easier to extend your flow, or add common functions shared by all steps inside a nested flow. For example,AuthenticationFlow
in onboarding is a nested flow. Based on the application configuration, it can have different steps for different authentication methods, and after the steps are completed, it will call an API to fetch passcode policies from the server side, therefore the steps don't have to. A nested flow is also a child class ofFlow
, so it has the same interface as discussed here.onStart
. This is the optimal place to calladdSingleStep
andaddNestedFlow
to add steps into the flow. ThebusinessDataData
argument contains data shared by all steps within the flow. Based on the data in the parameter, you can choose how to add steps into this flow. For example,AuthenticationFlow
will check the authentication method in the application configuration and then choose what step should be added:basic
,OAuth
orSAML
.onFinish
. This is called when the flow finishes. For example, to call an API to fetch the passcode policy forAuthenticationFlow
.onTerminate
. A flow might encounter a critical problem that prevents the flow from being processed, causing it to be terminated. You can use this method to clean up, as necessary.getNextStep
. This method controls the step navigation of a flow. The basic logic is defined in the parent class. We recommend that you do not overwrite this unless absolutely necessary.getInitialData
. If a flow has some initial data to be shared among all steps, this function can be overridden for this purpose. When the flow is started, the framework will get initial data here and save it into the view model, which is shared with every step.
Note
If a flow contains nested flows, only the function of the outermost flow will be called by the framework. The function of the nested flows will be ignored even if it is overridden.
Steps are saved in the flow in a sequence list: when one step tells the framework that it's done, the framework will visit the list to find the next one.
Within the framework implementation, a single step is also wrapped as a nested flow, so that the framework has the same interface to handle both single steps and nested flows. When the framework tries to find the next step, the following logic will apply for each nested flow:
- The step that notifies
done
is one of mine, and it has a step to follow inside me. Tell the outer flow the ID of the next step; - The step that notifies
done
is one of mine, but it's the last step of mine, so navigate to the first step of the nested flow after me. - The step that notifies
done
is not mine. Find it in the nested flows after me.
For the above cases, the SingleStepFlow
looks like this:
private class SingleStepFlow<T : FlowStepFragment>(
internal val stepId: Int,
internal val fragmentClass: KClass<T>,
application: Application
) : Flow(application) {
override suspend fun getNextStep(
currentStepId: Int,
businessData: BusinessDataMap
): Pair<Int, KClass<*>?>? {
return when (currentStepId) {
FlowConstants.FLOW_STATUS_START -> Pair(stepId, fragmentClass)
stepId -> Pair(FlowConstants.FLOW_STATUS_END, null)
else -> null
}
}
}
From the preceding code, we can see that to find the first step of a nested flow, FlowConstants.FLOW_STATUS_START
is needed. When the step that notifies done
is the last one, it will return FlowConstants.FLOW_STATUs_END
, and if the step that notifies done
is not one of mine, return null
.
In addition to the preceding rules, when overwriting getNextStep
the following should also be considered:
onStart
is called ingetNextStep
ofFlow
, so customized flows need to make sure thatonStart
is invoked when overwritinggetNextStep
andFLOW_STATUS_START
needs to be handled.super.getNextStep
must be called for all cases that your own flow does not need to handle.
So, please be careful when overwriting this method. The safest practice is to use the information saved in businessDataMap
, prepare the steps in onStart
, and then let the framework handle the navigation logic.
The return type of getNextStep
is the same as the argument of addSingleStep
. The integer is the step id and the other is the step fragment class that extends FlowStepFragment
. The framework will add the step fragment lazily when it's actually being used.
Flow Step Implementation¶
All steps will be running in a single activity and they must extend from FlowStepFragment
. The parent class only contains one important helper function to notify the framework that it's done.
protected fun stepDone(
currentStepId: Int,
popCurrent: Boolean = false,
vararg args: Pair<String, String>
) {...}
currentStepId
is the current step id that notifies the done event;popCurrent
lets the framework know whether to keep the current step in the back stack or not after navigating to the next;args
is used to pass information to the next step, which does not need, or it's not good to save into,businessDataMap
and share to all steps. For example, Change passcode flow has two steps:create
andverify
. Thecreate
step can useargs
to pass the passcode toverify
step to confirm.
Start Your Flow¶
To start your own flow, use the APIs described in Start Flow. You can create an instance of your flow, save it into flowContext
, and then start it.
val myflow = MyFlow(application)
val flowContext = FlowContext(flow = myflow)
Flow.start(activity, flowContext)
MyFlow myflow = new MyFlow(application);
FlowContext flowContext = new FlowContextBuilder()
.setFlow(myflow)
.build();
Flow.start(activity, flowContext);
Terminate Your Flow¶
There will be cases when your flow cannot proceed because of errors and must be terminated in mid-stream. For example, if authentication fails during onboarding because of a network failure, the flow has to be terminated.
In cases such as these, some data needs to be cleared. To do this gracefully, use the following API:
FlowInterruptEvent.Error(
message = application.getString(R.string.forgot_passcode_user_not_match)
).apply {
FlowInterruptEvent.notifyInterruptEvent(businessData, this)
}
FlowInterruptEvent.Error
has an optional 'message'. When it's 'null', the code will terminate the current running flow without any prompt. Otherwise, a dialog is displayed to the user. Upon clicking the OK button, the flow will be terminated.
Confirmation or Information Messages¶
In addition to FlowInterruptEvent.Error
, there are two other events, Info
and Warning
.
data class Info(
override val message: String,
val infoMessageOption: InfoMessageOption? = null,
val title : String? = null,
val positiveButtonText: String? = null,
val action: (() -> Unit)? = null
) : FlowInterruptEvent(message)
data class Warning(
/** The message */
override val message: String,
/** The positive button text of the warning dialog */
val positiveButtonText: String? = null,
/** The negative button text of the warning dialog */
val negativeButtonText: String? = null,
/** Optional positive action */
val positiveAction: (suspend () -> Unit)? = null,
/** Optional negative action. The default action is to return to the previous step */
val negativeAction: (suspend () -> Unit)? = null
) : FlowInterruptEvent(message)
Info
displays either a dialog or a toast message. If infoMessageOption
is not null, it will be used to display the message, otherwise, the settings in FlowOptions
will be used.
Warning
displays a confirmation dialog allowing the user to choose what to do. For example, when a user clicks the Disagree button in the end user license agreement screen, we ask for confirmation using this API.