Closed wujek-srujek closed 3 years ago
Hi @wujek-srujek π Thanks for opening an issue!
I would probably recommend splitting the loading of the form content into a separate bloc so you would have something like:
class FormContentBloc extends Bloc<FormContentEvent, FormContentState> {
FormContentBloc() : super(FormContentLoadInProgress());
@override
Stream<FormContentState> mapEventToState(FormContentEvent event) async* {
if (event is FormContentStarted) {
try {
final content = await Future.wait([_getCountries, _getStates(), ...]);
yield FormContentLoadSuccess(
countries: content[0],
states: content[1],
...,
);
} on Exception catch(_) {
yield FormContentLoadFailure();
}
}
}
Then you can have a FormSelectionBloc
like:
class FormSelectionBloc extends Bloc<FormSelectionEvent, FormSelectionState> {
FormSelectionBloc(this._formContent) : super(FormSelectionState());
final FormContent _formContent;
Stream<FormContentState> mapEventToState(FormContentEvent event) async* {
if (event is CountrySelected) {
yield state.copyWith(selectedCountry: event.country);
} else if (event is StateSelected) {
yield state.copyWith(selectedState: event.state);
} else if (event is FormSubmitted) {
yield state.copyWith(status: FormStatus.submissionInProgress);
try {
await _repo.submitForm(state.selectedCountry, state.selectedState, ...);
yield state.copyWith(status: FormStatus.submissionSuccess);
} on Exception catch (_) {
yield state.copyWith(status: FormStatus.submissionFailure);
}
}
}
}
I would highly recommend using the formz package to help you manage the state of you user inputs (especially when you want to perform local validation).
You can see check out the form validation example for more detail.
Hope that helps π
Hi Felix, thanks for your answer and recommendation.
final content = await Future.wait([_getCountries, _getStates(), ...]);
I don't think we can use this as we can get state list only after a country is selected, otherwise we would have to get states for all the possible countries in the world (or at least those that our application knows), which would be even more dramatic for cities. This is a step by step process, hence my name 'wizard', which was a name for installation wizards etc. on Windows machines a few decades back ;d
We decided to go for the separate BLoCs solution, and each next one will listen to the previous one and decide when it needs to reset. We aren't ready yet but this solution seems very promising.
@wujek-srujek have you taken a look at the flutter_dynamic_form example? It sounds like it might be what you're looking for π
Hi @felangel , I looked at it, and while it works, I'm sorry to say I'm not a big fan. It just creates a single state type with all possible fields inside, so my widgets will always get the same state (different instances) and instead of checking for state type and acting/building accordingly, they will perform null checks. Don't get my wrong, it will work fine for a simpler app, but I don't think it will scale that well to our use case: more steps/data, with more repositories do fetch it from (i.e. more dependencies in the bloc, not just a single repo), more states (e.g. we need to react to failures by showing dialogs with a retry option, and whatever else the UI/UX people come up with) and so on. I'm afraid a single bloc would get unwieldy sooner than later. Not to mention how much we would need to mock and stub to be able to test a scenario from the beginning to the end.
I'm not sure, what do you think? Would you recommend this single-state approach instead of splitting the bloc into smaller ones, pretty much each one handling it's own repository, data fetching, errors etc. in separation? Similarly for testing: each step/bloc is tested separately, and the next one gets an event to start its own process. This approach was kinda growing on me ;d
@wujek-srujek yeah I agree itβs just a simple example which is why one bloc and one state works. If it were a larger more complex feature then splitting into multiple blocs would be more manageable. I can update this example to be more complex later today and you can have a look π
Hi @felangel , thanks for being so helpful, but there is no need as far as I'm concerned, I think I'll manage ;d Thank you again for investing so much of your time and energy to help!
@wujek-srujek no problem but I definitely donβt want others to see the dynamic form example and try to extend it for complex cases with a single bloc. I want to ensure the examples will help developers build apps of all sizes/complexity so I will update the example to adopt a more scalable approach to avoid confusion π
Thanks for bringing this up and donβt hesitate to reach out with any other questions. If you havenβt already, you can join the bloc discord server and chat with many community members about architecture/design as well as many other topics π
Reopening this issue to track the work to update the dynamic form example.
@wujek-srujek prior to bloc we've had the same feeling using every MV* pattern - state transitions were becoming too complex for a more-complex scenarios. E.g. ticket buying process - splitting into multiple blocs made it even more difficult to understand and extend. That's definitely not a part of a bloc, but you might be interested in extending your logic to state machine graphs.
I've made a bloc extension which enumerates possible states and transitions between them: https://pub.dev/packages/state_graph_bloc
Also, there're many alternatives: https://pub.dev/packages?q=state+machine
Hey @wujek-srujek π Sorry for the delayed response but I finally managed to put together an example of how you can approach this using the flow_builder package at https://github.com/felangel/flow_builder/tree/master/example/lib/location_flow.
Let me know what you think π
Hi @felangel I had a quick look and it seems like you also have 3 separate cubits for each part of the flow, i.e. separate cubits for coutry, state and city selection. If that's the case, and there is nothing that I missed, this is very similar to what we did as well, which means we were not wrong ;d
Hei Felix. To start with, sorry for the longish question, but I wanted to make sure I describe the use case correctly, I hope you don't fall asleep reading it!
I have a question about BLoC usage. Our use case is the following (simplified, our actual use case is more complex and the 'wizard' has more steps): we first need to choose a country (country list loaded from backend), then the next step is to choose a state (e.g. California in US, Bavaria in DE) (also loaded from backend), and then a city within that state (also from backend). All of these pieces of information are visible in one screen (imagine the ASCII-art below is a
ListView
with three items that show what is currently selected and clicking it navigates to the country/state/city chooser screen, symbolized by the arrow):Only by selecting preceding data is the user allowed to pick the next one; until then, its
ListItem
is disabled. E.g. you cannot chose a state or city if the country is not selected, and until then, the state and city items are disabled. (Once all is selected, you can change the country and the rest will have to be reset, but let's leave it for now.)Once everything is selected, the button (otherwise disabled) can be pressed, and it invokes some logic which uses all data, so we need to preserve the country, state, and city until the very end.
As the UI always needs to present all data, we need to 'collect' it in the states as we go 'deeper'. So, we have these states:
Initial
CountrySelected
(has country) ...StateSelected
(has country and state) ...CitySelected
(has country, state and city)(There are actually more states symbolized by '...' above, e.g. between
CountrySelected
andStateSelected
there are states waiting for the backend to get the state list, which also needs to keep the country so that the UI can show it, and the next state can be initialized with it within the BLoC.)This makes is a bit cumbersome to use the
BlocBuilder
. For example, take theCountryListItem
: we can fill it with data if the state isCountrySelected
,StateSelected
andCitySelected
(and everything in between). Similarly,StateListItem
can be filled for statesStateSelected
andCitySelected
(and all states in between). Additionally, enabled statuses of the items also depend on various states.This is a bit cumbersome, we always need an
if
with a couple ofis <type>
joined with an or, e.g. forStateListItem
, within theBlocBuilder.builder
callback:I hope the use case is clear ;d
We are not convinced this way is the right approach, it is getting very confusing in code already, with many, many
if
s, and we are looking into different solutions:Solution 1: we can add abstract state subtypes, like (pseudocode):
class WithCountry extends OurBaseState
class WithCountryState extends WithCountry
class WithCountryStateCity extends WithCountryState
and then have our states use them:
class CountrySelected extends WithCountry
class StateSelected extends WithCountryState
class CitySelected extends WithCountryStateCity
Pros: it does allow for simpler code and checks in
BlocBuilder.builder
s, e.g. forStateListItem
:Cons: it causes a proliferation of abstract state types, introduces a pretty complex inheritance hierarchy. Remember a class can extend only one superclass, maybe mixins could help but they are for code, not for introducing fields...
Solution 2: we define a custom field in our BLoC, let's call it
CountryStateCity
, with fields for all of country, state and city (all nullable), and it is a custom field and it accessible from the outside. When an event likeCountrySelected(country)
is added, the country is put into theCountryStateCity
field by the BLoC (we can prevent clients from setting the field, and the type immutable so that only the BLoC can change it, of course).Client code could look like this (again, example for
StateListItem
and itsBlocBuilder.builder
):Pros: it seems way simpler to implement.
Cons: it adds a custom field to the BLoC (i.e. introduces some kind of a 'parallel state' concept), and it introduces tighter coupling to the BLoC - even code within
BlocBuilder.builder
needs to look the BLoC up again to be able to use the custom field. Also, it makes the BLoC interface more complex, it is not a simple 'event stream input, state output'; it now has an additional field that all clients must use. I.e. some of us find it 'dirty' or 'impure' ;d.Solution 3: use separate BLoCs, one for each piece of data, and for example once the country BLoC reaches a certain state, it notifies state BLoC that it can start loading from backend, and once state is chosen by the user, this BLoC notifies the city BLoC to start loading etc. Once city is selected, another BLoC, which manages the whole page, is notified that data is complete and enables the button (
If a user has chosen country, state and city, and chooses a new different country, the page BLoC tells the state and city BLoCs to 'reset'. If a user chooses a new city, the page BLoC tells the city BLoC to 'reset'.
Something like this, not completely thought through, came up with it while I was writing the other options ;d
What would be your recommendation for our 'wizard' use case?