Question #99MediumFlutter Basics

Flutter flow builder plugin

#flutter

Answer

Overview

Flow Builder is a Flutter package for building multi-step flows (wizards, onboarding, checkout) with navigation managed by state changes. It's similar to state machine-based navigation.


Installation

Add to

text
pubspec.yaml
:

yaml
dependencies:
  flow_builder: ^0.1.0

Basic Concept

Flow Builder displays different screens based on a state object. When state changes, the flow automatically navigates to the correct screen.

dart
// State represents current step
enum OnboardingStep { welcome, profile, permissions, complete }

// Flow Builder navigates based on state
FlowBuilder<OnboardingStep>(
  state: currentStep,
  onGeneratePages: (step, pages) {
    return [
      if (step == OnboardingStep.welcome) MaterialPage(child: WelcomePage()),
      if (step == OnboardingStep.profile) MaterialPage(child: ProfilePage()),
      if (step == OnboardingStep.permissions) MaterialPage(child: PermissionsPage()),
      if (step == OnboardingStep.complete) MaterialPage(child: CompletePage()),
    ];
  },
)

Simple Example: Onboarding Flow

dart
import 'package:flow_builder/flow_builder.dart';

enum OnboardingState { welcome, signup, profile, done }

class OnboardingFlow extends StatefulWidget {
  
  _OnboardingFlowState createState() => _OnboardingFlowState();
}

class _OnboardingFlowState extends State<OnboardingFlow> {
  OnboardingState _state = OnboardingState.welcome;

  void _updateState(OnboardingState newState) {
    setState(() => _state = newState);
  }

  
  Widget build(BuildContext context) {
    return FlowBuilder<OnboardingState>(
      state: _state,
      onGeneratePages: (state, pages) {
        return [
          MaterialPage(
            child: WelcomePage(
              onNext: () => _updateState(OnboardingState.signup),
            ),
          ),
          if (state.index >= OnboardingState.signup.index)
            MaterialPage(
              child: SignupPage(
                onNext: () => _updateState(OnboardingState.profile),
              ),
            ),
          if (state.index >= OnboardingState.profile.index)
            MaterialPage(
              child: ProfilePage(
                onNext: () => _updateState(OnboardingState.done),
              ),
            ),
          if (state == OnboardingState.done)
            MaterialPage(child: DonePage()),
        ];
      },
    );
  }
}

Complex State Example: Checkout Flow

dart
class CheckoutState {
  final bool hasCart;
  final bool hasShipping;
  final bool hasPayment;

  CheckoutState({
    this.hasCart = false,
    this.hasShipping = false,
    this.hasPayment = false,
  });

  CheckoutState copyWith({
    bool? hasCart,
    bool? hasShipping,
    bool? hasPayment,
  }) {
    return CheckoutState(
      hasCart: hasCart ?? this.hasCart,
      hasShipping: hasShipping ?? this.hasShipping,
      hasPayment: hasPayment ?? this.hasPayment,
    );
  }
}

class CheckoutFlow extends StatefulWidget {
  
  _CheckoutFlowState createState() => _CheckoutFlowState();
}

class _CheckoutFlowState extends State<CheckoutFlow> {
  CheckoutState _state = CheckoutState();

  void _updateState(CheckoutState newState) {
    setState(() => _state = newState);
  }

  
  Widget build(BuildContext context) {
    return FlowBuilder<CheckoutState>(
      state: _state,
      onGeneratePages: (state, pages) {
        return [
          // Cart screen (always first)
          MaterialPage(
            child: CartPage(
              onNext: () => _updateState(state.copyWith(hasCart: true)),
            ),
          ),

          // Shipping screen (after cart)
          if (state.hasCart)
            MaterialPage(
              child: ShippingPage(
                onNext: () => _updateState(state.copyWith(hasShipping: true)),
              ),
            ),

          // Payment screen (after shipping)
          if (state.hasShipping)
            MaterialPage(
              child: PaymentPage(
                onNext: () => _updateState(state.copyWith(hasPayment: true)),
              ),
            ),

          // Confirmation screen (after payment)
          if (state.hasPayment)
            MaterialPage(child: ConfirmationPage()),
        ];
      },
    );
  }
}

With BLoC

Flow Builder works well with BLoC for state management.

dart
class OnboardingBloc extends Bloc<OnboardingEvent, OnboardingState> {
  OnboardingBloc() : super(OnboardingInitial()) {
    on<NextStepRequested>((event, emit) {
      if (state is WelcomeStep) emit(SignupStep());
      else if (state is SignupStep) emit(ProfileStep());
      else if (state is ProfileStep) emit(CompleteStep());
    });
  }
}

class OnboardingFlow extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return BlocBuilder<OnboardingBloc, OnboardingState>(
      builder: (context, state) {
        return FlowBuilder<OnboardingState>(
          state: state,
          onGeneratePages: (state, pages) {
            return [
              if (state is WelcomeStep)
                MaterialPage(child: WelcomePage()),
              if (state is SignupStep)
                MaterialPage(child: SignupPage()),
              if (state is ProfileStep)
                MaterialPage(child: ProfilePage()),
              if (state is CompleteStep)
                MaterialPage(child: CompletePage()),
            ];
          },
        );
      },
    );
  }
}

Back Button Handling

Handle back button with

text
onComplete
callback.

dart
FlowBuilder<OnboardingState>(
  state: state,
  onGeneratePages: (state, pages) => [...],
  onComplete: (finalState) {
    // Flow completed — navigate elsewhere
    Navigator.of(context).pushReplacement(
      MaterialPageRoute(builder: (_) => HomePage()),
    );
  },
)

Complete Example: Multi-Step Form

dart
import 'package:flow_builder/flow_builder.dart';

enum FormStep { name, email, password, confirm }

class User {
  String? name;
  String? email;
  String? password;
}

class SignupFlow extends StatefulWidget {
  
  _SignupFlowState createState() => _SignupFlowState();
}

class _SignupFlowState extends State<SignupFlow> {
  FormStep _step = FormStep.name;
  final User _user = User();

  void _nextStep() {
    setState(() {
      if (_step == FormStep.name) _step = FormStep.email;
      else if (_step == FormStep.email) _step = FormStep.password;
      else if (_step == FormStep.password) _step = FormStep.confirm;
    });
  }

  void _prevStep() {
    setState(() {
      if (_step == FormStep.email) _step = FormStep.name;
      else if (_step == FormStep.password) _step = FormStep.email;
      else if (_step == FormStep.confirm) _step = FormStep.password;
    });
  }

  
  Widget build(BuildContext context) {
    return FlowBuilder<FormStep>(
      state: _step,
      onGeneratePages: (step, pages) {
        return [
          MaterialPage(
            child: NamePage(
              user: _user,
              onNext: _nextStep,
            ),
          ),
          if (step.index >= FormStep.email.index)
            MaterialPage(
              child: EmailPage(
                user: _user,
                onNext: _nextStep,
                onBack: _prevStep,
              ),
            ),
          if (step.index >= FormStep.password.index)
            MaterialPage(
              child: PasswordPage(
                user: _user,
                onNext: _nextStep,
                onBack: _prevStep,
              ),
            ),
          if (step == FormStep.confirm)
            MaterialPage(
              child: ConfirmPage(
                user: _user,
                onBack: _prevStep,
              ),
            ),
        ];
      },
    );
  }
}

Comparison: Navigator vs Flow Builder

FeatureNavigatorFlow Builder
Navigation styleImperativeDeclarative ✅
Multi-step flowsManual stackState-driven ✅
Back buttonManualAutomatic ✅
State persistenceManualBuilt-in ✅
Conditional routesManualSimple ✅

Use Cases

  • Onboarding flows (welcome → profile → permissions)
  • Checkout flows (cart → shipping → payment → confirm)
  • Multi-step forms (registration, surveys)
  • Wizards (setup, configuration)
  • Authentication flows (login → 2FA → success)
  • Simple navigation (use GoRouter or AutoRoute)

Best Practices

dart
// ✅ Use state classes for complex flows
class CheckoutState {
  final bool hasCart;
  final bool hasShipping;
  final bool hasPayment;
  CheckoutState({...});
}

// ✅ Use BLoC for state management in complex flows
FlowBuilder<OnboardingState>(
  state: context.watch<OnboardingBloc>().state,
  ...
)

// ✅ Handle onComplete for final navigation
FlowBuilder(
  onComplete: (state) => Navigator.pushReplacement(...),
  ...
)

// ❌ Don't use for simple navigation (overkill)
// Use GoRouter or Navigator.push for simple screens

Learn more: Flow Builder Package