Introducing N26 Backend Driven Navigation: FlowKit

Alex Martinez
InsideN26
Published in
8 min readNov 23, 2022

--

At N26 we don’t have Quality Assurance, we’ve got Quality Engineers. The responsibility of manual testing is on the whole team and the goal is to completely automate the regression testing. Last year a big chunk of the remaining features to be automated were about banking products.

Banking products (credit, overdraft, savings, etc.) have a bunch of regulatory requirements which means they often need “complex flows” with many steps and variations for each country we support. As an example to apply for a credit you might have to go through a 20+ screen flows 😵

We started about the way we could handle all of this more efficiently, while improving testability… and that’s how FlowKit started.

What’s FlowKit?

To put it simply, it’s a dynamic flow framework that tells the client which step they’d have to showcase and in which order. Based on this definition, we could describe a flow as a collection of independent steps where ‘Step’ is the fundamental building block. By isolating each step (screen) from the flow logic we could then model the complex flow logic in an easier way, and even move it on the backend side in a dedicated service to be shared between all the platforms (Android, iOS, web).

An application that uses FlowKit is able to receive the Flow remotely in JSON format from Back-end, which will contain all the necessary information to generate all possible specific flows and send it to the application. Receiving the flow remotely is only an option: it’s possible to load a Flow from a local device JSON file, or create it programmatically in the code.

What is a step?

First let’s start explaining what is a Step:

  • Step is the fundamental building block: It’s an independent building block that could be a screen, a notification or a request.
  • Strongly typed: The compiler will help you to not mix up the things.
  • Access to previous step output: You have access to all the previous step output and you don’t need to know the step or steps source.
  • Dynamic content: Each step has its own typed content decoded at runtime that could be used to display different info, to render the UI, or whatever you’d like to use.

What is a flow?

A flow is a collection of independent steps that are executed in a concrete order and produce an output at the end. FlowKit allows two type of flows:

  • Linear: A sequence of consecutive steps.
Linear flow
  • Non linear: A complex flow with different branches based on users input.
Non linear flow

When the flow ends you’ll get a flow output through a completion that you could use to perform an action.

How to use it?

FlowKit is currently available only on iOS so the example will be focused on this platform. Along with FlowKit we added FlowKitAdditions, a set of basic implementations that will allow you to use it with minimum effort. The example below uses FlowKitAdditions.

We’re going to use the sign up flow example from FlowKitAdditions:

Sign up flow example

This flow is composed of 6 steps: an intro screen, 4 questions, 1 summary screen. We’ll see how we are going to implement this flow by only using 3 different steps and how FlowKit will manage the navigation between them allowing us to reuse the screens inside the flows.

First, let see how this flow it’s created:

let flow = FlowKit<SignUpFlowDefinition>.init(
flowData: flowData,
featureStepFactory: SignUpStepFactory()
)

As you can see we need 3 components to create a flow:

  • SignUpFlowDefinition: It defines the flow output type and step type to be used by our flow.
  • flowData: Struct containing all the needed information to build and present the flow.
  • SignUpStepFactory: Factory in charge of creating steps based on the type

Let’s go deeper on each one.

SignUpFlowDefinition

Struct that has to conform to the FlowDefinition protocol:

public protocol FlowDefinition {
associatedtype OUTPUT: FlowOutputDefinition
associatedtype STEP: StepProtocol & Decodable
}

FlowDefinition defines two associated types that will force us to specify the type:

  • OUTPUT: Define a flow output that FlowKit will use to generate a typed output with autocompletion support while you are developing.
  • STEP: The step concrete implementation.

In our signup flow example we’ve defined SignUpFlowDefinition:

struct SignUpFlowDefinition: FlowDefinition {
typealias OUTPUT = FlowOutputEmptyDefinition
typealias STEP = Step
}

As OUTPUT we have used the default FlowOutputEmptyDefinition as we don’t want a typed output and STEP is the implementation provided in FlowKitAdditions that should work for you in almost all cases. When you need to extend the StepProtocol with more attributes, create your own implementation.

Now that we’ve defined the STEP, FlowKit will force you to use it in the FlowData and in the step factory. Take a look at the next diagram to see how the generics have to match between the different components.

The compiler will help you to not mix types.

FlowData

The FlowKit object used to describe a flow is the FlowData. This struct contains all the information required to build a flow and all the possible branches.

  • Id: Flow ID
  • initialStepId: Initial step id of the flow
  • steps: Array of steps

You could instantiate it programmatically or using a JSON that could be retrieved from Backend or just stored locally.

This is the content of the local JSON file of the sign up flow example:

{
"id": "SIGN_UP_FLOW",
"initialStepId": "infoStepId",
"steps": [
{
"id": "infoStepId",
"nextStep": "firstNameId",
"type": "INFO",
"content": {
"title": "Welcome to the signup flow example",
"description": "The purpose of this flow is showcasing how to create a Flow with different kind of steps.",
"primaryButtonText": "Let's start"
}
},
{
"id": "firstNameId",
"nextStep": "lastNameId",
"type": "TEXT_INPUT",
"content": {
"title": "We would like to start with your first name",
"placeholder": "Write your first name",
"primaryButtonText": "Continue"
}
},
{
"id": "lastNameId",
"nextStep": "emailId",
"type": "TEXT_INPUT",
"content": {
"title": "And your last name",
"placeholder": "Write your last name",
"primaryButtonText": "Continue",
"skipButtonText": "Omit step"
}
},
{
"id": "emailId",
"nextStep": "passwordId",
"type": "TEXT_INPUT",
"content": {
"title": "Which email do you want to register with",
"subtitle": "We will only send significant emails",
"placeholder": "Write your email",
"primaryButtonText": "Continue"
}
},
{
"id": "passwordId",
"type": "TEXT_INPUT",
"nextStep": "summaryStepId",
"content": {
"title": "Choose your secure password",
"placeholder": "Write your password",
"primaryButtonText": "Register in FlowKit"
}
},
{
"id": "summaryStepId",
"type": "SUMMARY",
"content": { }
}
]
}

As you can see this flow contains six steps of three different types: INFO, TEXT_INPUT and SUMMARY.

SignUpStepFactory

Every time FlowKit needs to present a new Step, it uses the defined SignUpStepFactory to request a new StepHandler for the step type. Our concrete SignUpStepFactory must conform to the StepFactory protocol:

public protocol StepFactory {
associatedtype OUTPUT: FlowOutputDefinition
associatedtype STEP: StepProtocol
func makeHandler(for stepRawType: String) -> AnyStepHandler<STEP, OUTPUT>?
}

Let’s take a look at our SignUpStepFactory:

typealias OUTPUT = FlowOutputEmptyDefinition
typealias STEP = Step

As you can see, we’re using the same types as in the SignUpFlowDefinition so that the compiler will help to avoid mixing types. This way, our factory can only create step handlers for that concrete definition.

The next important code is the implementation of the makeHandler method:

func makeHandler(for stepRawType: String) -> AnyStepHandler<STEP, OUTPUT>? {
switch stepRawType {
case "TEXT_INPUT": return AnyStepHandler(textInputStepHandler())
case "INFO": return AnyStepHandler(infoStepHandler())
case "SUMMARY": return AnyStepHandler(flowSummaryStepHandler())
default: return nil
}
}

This method creates a stepHandler based on the step type, in our case INFO, TEXT_INPUT and SUMMARY, if you would like to add a new step type you have to add it to your factory. If your factory doesn’t recognise a step type, FlowKit wouldn’t start the flow and will raise an error.

StepHandler

Now that we have understood that every time FlowKit is going to present a new step it calls the step factory to get a StepHandler based on the type, let’s talk about the StepHandler.

StepHandler defines the step content type and output type by providing a completion closure. When we call the completion closure in our step with the desired output, Flowkit stores the output in the flow output and present next step.

First, to be able to create a step handler you have to create a definition using the StepHandlerDefinition. Let’s take a look at the TEXT_INPUT step definition:

struct TextInputStepHandlerDefinition: StepHandlerDefinition {
typealias CONTENT = TextInputContent
typealias STEP_OUTPUT = String
typealias FLOW_OUTPUT = FlowOutputEmptyDefinition
typealias STEP = Step
static let registerOutputKeyPath: KeyPath<FLOW_OUTPUT, STEP_OUTPUT>? = nil
}

As we remember from FlowDefinition we defined two generics: STEP & OUTPUT; they’re also present in the StepHandlerDefinition. We introduce two new generics:

  • CONTENT: The dynamic content of the step, where you should place all the specific data that your step needs to be performed. For example, the image URL or text content we want to display in our step. In our case it is TextInputContent.
  • STEP_OUTPUT: The output type generated. In our case it is a String.

Now that we have defined our step handler we could create it:

private func textInputStepHandler() -> StepHandler<TextInputStepHandlerDefinition> {
.create { _, stepContent, navigation, _, completion in
TextInputWireframe.push(on: navigation, content: stepContent, completion: completion)
}
}

You should use the navigation to present your view and the completion to inform FlowKit that the step has been completed. You could take a look at the full implementation if you’d like to know how we’ve used the step content and the completion (FlowKit also supports SwiftUI). In future posts we’ll dig deeper into how to handle navigation with SwiftUI.

Launching the sign up flow

If we come back to where we started, we were creating a flow instance:

let flow = FlowKit<SignUpFlowDefinition>.init(
flowData: flowData,
featureStepFactory: SignUpStepFactory()
)

Begin your flow by using the start method:

flow.start(on: navigation) { step in
print("Will present step \(step.id)")
} onErrorHandler: { error, navigation in
FlowErrorWireframe.push(on: navigation, error: error) { }
} onFinish: { output in
print("Flow output: \(output.rawData.description)")
navigation.dismiss(animated: true)
}

Your flow will be executed and Flowkit will inform you when:

  • A new step is going to be performed
  • An error has occurred
  • The flow finish with and output

Now, it may be that you’re building flows on all your projects or features, and you’re not able to reuse them. It can happen that you’re spending a lot of time coding when product or design requirements change depending on user segment, feature flags, country, etc. I hope this article can help you solve all the previously mentioned issues, or simply as an example to separate flow logic from UI, in a different way than the classical wireframes/coordinators (which usually aren’t able to achieve!).

You could find available FlowKit for:

Follow us to get notified about how to build complex flows, how to use it with SwiftUI, when the Android version is released

--

--