Best Practices for Managing Multi-Screen Customer Onboarding with Bloc and DTO in Flutter

  • I am designing a customer onboarding flow in Flutter with about 8 screens, each collecting a part of the customer’s data. All the data together forms a central DTO with sub-DTOs like PersonalInfo, AddressInfo, OccupationInfo, ContactInfo, etc.
    • Is it better to use one Bloc that holds the full DTO for all screens, or multiple Blocs, one per screen?
    • What are the pros and cons of each approach regarding performance, data persistence, and maintainability?
  • The requirement is that data should be preserved even if the user goes back to a screen without submitting the form.
    • How can this be achieved if using multiple Blocs?
    • Should I use BlocProvider.value when navigating between screens, or should each Bloc be created in its screen with an initial value from the central DTO?
  • Each screen has a form, TextFields, controllers, and a FormKey.
    • What is the best way to organize the code so that the Bloc remains the single source of truth, but each screen manages its own fields safely?
  • In the case of using a single Bloc:
    • How should I structure the DTO and copyWith methods to safely update each part of the data?
    • Is this approach good for performance if the DTO is large and 8 screens are updating different parts of it?
  • If using multiple Blocs:
    • What is the best way to share or pass data between Blocs without losing it?
    • Is there an enterprise-level design pattern recommended for this scenario?
  • In general, what is the optimal design for Bloc + DTO + multiple onboarding screens so that:
    • Each screen handles its own UI and form logic
    • The state/data is consistent across all screens
    • Navigation back and forth does not lose user input

Most of your concerns about data persistence can be resolved by placing the BlocProvider at a location in the widget tree where it’s above all the widgets that need access to it. Bloc is built on Provider, and Provider uses InheritedWidgets, so you could wrap the entire app in a BlocProvider and it won’t really impact performance. (It would also guarantee that the user data isn’t disposed of before you’re done using it.)

I believe Bloc allows you to either use one Bloc or multiple Blocs for the customer data. In my opinion, the most straightforward implementation would be one Bloc that holds the full DTO.

@immutable
class CustomerInfo {
  final PersonalInfo personalInfo;
  final AddressInfo addressInfo;
  ...
}

You can implement copyWith, ==, and hashCode by hand or with the freezed package.

Then when you want to access the data, you can configure BlocBuilder to only rebuild when the data relevant to that widget changes.

The individual components can be stateful widgets that use buttons and TextEditingControllers to keep the DTO up-to-date.

If you have further questions let me know!

1 Like

Thank you for the detailed response! It’s really validating to see this because I actually arrived at this exact solution just before I saw your message.

After researching the best way to handle this, I implemented the single BLoC + DTO architecture you described, but I added a custom Mixin to make it even cleaner. Here is how I set it up:

  • Single Source of Truth: One OpenAccountBloc with a single immutable DTO.
  • The Mixin Strategy: I created a FormFieldManagerMixin for my StatefulWidgets. This mixin handles all the boilerplate for
TextEditingControllers

and

FocusNodes

.

  • Performance Optimization: The mixin automatically registers “blur” listeners. So, instead of rebuilding on every keystroke, it only syncs with the BLoC when the user leaves a field or selects a value.

It’s working flawlessly and separates the UI logic from the business logic perfectly. Glad to know we are on the same page regarding the best practices!

1 Like