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.yamlyamldependencies: 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
dartimport '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
dartclass 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.
dartclass 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
onCompletedartFlowBuilder<OnboardingState>( state: state, onGeneratePages: (state, pages) => [...], onComplete: (finalState) { // Flow completed — navigate elsewhere Navigator.of(context).pushReplacement( MaterialPageRoute(builder: (_) => HomePage()), ); }, )
Complete Example: Multi-Step Form
dartimport '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
| Feature | Navigator | Flow Builder |
|---|---|---|
| Navigation style | Imperative | Declarative ✅ |
| Multi-step flows | Manual stack | State-driven ✅ |
| Back button | Manual | Automatic ✅ |
| State persistence | Manual | Built-in ✅ |
| Conditional routes | Manual | Simple ✅ |
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