Skip to content

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 and addNestedFlow. 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 of Flow, so it has the same interface as discussed here.
  • onStart. This is the optimal place to call addSingleStep and addNestedFlow to add steps into the flow. The businessDataData 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 or SAML.
  • onFinish. This is called when the flow finishes. For example, to call an API to fetch the passcode policy for AuthenticationFlow.
  • 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 in getNextStep of Flow, so customized flows need to make sure that onStart is invoked when overwriting getNextStep and FLOW_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 and verify. The create step can use args to pass the passcode to verify 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.


Last update: November 18, 2021