Implementing Dependent Dropdowns with BLoC in Flutter

Introduction

Handling dependent dropdowns in Flutter using the BLoC pattern is a common scenario when implementing forms that require cascading selections. A typical example is selecting a country, then a state, and finally a city. In this guide, we will cover how to properly manage this scenario using BLoC events, states, and efficient state management.

Requirements

Step 1: Define BLoC Events

To handle user interactions and API calls, we define different events for loading data and selecting values.

abstract class DropdownEvent {}class LoadCountries extends DropdownEvent {}
class CountrySelected extends DropdownEvent {
  final String selectedCountry;
  CountrySelected(this.selectedCountry);
}class StateSelected extends DropdownEvent {
  final String selectedState;
  StateSelected(this.selectedState);
}class CitySelected extends DropdownEvent {
  final String selectedCity;
  CitySelected(this.selectedCity);
}class EditSelection extends DropdownEvent {
  final String selectedCountry;
  final String selectedState;
  final String selectedCity;
EditSelection(this.selectedCountry, this.selectedState, this.selectedCity);
}

Step 2: Define BLoC States

We define states to hold the list of available options and selected values.

abstract class DropdownState {}
class DropdownInitial extends DropdownState {}
class DropdownLoaded extends DropdownState {
  final List<String> countries;
  final List<String> states;
  final List<String> cities;
  final String? selectedCountry;
  final String? selectedState;
  final String? selectedCity;
  DropdownLoaded({
    required this.countries,
    required this.states,
    required this.cities,
    this.selectedCountry,
    this.selectedState,
    this.selectedCity,
  });
}

Step 3: Implement the BLoC

The BLoC will handle fetching data, user selections, and emitting new states.

class DropdownBloc extends Bloc<DropdownEvent, DropdownState> {
  DropdownBloc() : super(DropdownInitial()) {
    on<LoadCountries>(_loadCountries);
    on<CountrySelected>(_onCountrySelected);
    on<StateSelected>(_onStateSelected);
    on<CitySelected>(_onCitySelected);
    on<EditSelection>(_onEditSelection);
  }
Future<void> _loadCountries(LoadCountries event, Emitter<DropdownState> emit) async {
    final countries = await fetchCountries();
    emit(DropdownLoaded(countries: countries, states: [], cities: []));
  }
  Future<void> _onCountrySelected(CountrySelected event, Emitter<DropdownState> emit) async {
    final states = await fetchStates(event.selectedCountry);
    emit(DropdownLoaded(
      countries: (state as DropdownLoaded).countries,
      states: states,
      cities: [],
      selectedCountry: event.selectedCountry,
    ));
  }
  Future<void> _onStateSelected(StateSelected event, Emitter<DropdownState> emit) async {
    final cities = await fetchCities(event.selectedState);
    emit(DropdownLoaded(
      countries: (state as DropdownLoaded).countries,
      states: (state as DropdownLoaded).states,
      cities: cities,
      selectedCountry: (state as DropdownLoaded).selectedCountry,
      selectedState: event.selectedState,
    ));
  }
  void _onCitySelected(CitySelected event, Emitter<DropdownState> emit) {
    emit(DropdownLoaded(
      countries: (state as DropdownLoaded).countries,
      states: (state as DropdownLoaded).states,
      cities: (state as DropdownLoaded).cities,
      selectedCountry: (state as DropdownLoaded).selectedCountry,
      selectedState: (state as DropdownLoaded).selectedState,
      selectedCity: event.selectedCity,
    ));
  }
  Future<void> _onEditSelection(EditSelection event, Emitter<DropdownState> emit) async {
    final countries = await fetchCountries();
    final states = await fetchStates(event.selectedCountry);
    final cities = await fetchCities(event.selectedState);
    emit(DropdownLoaded(
      countries: countries,
      states: states,
      cities: cities,
      selectedCountry: event.selectedCountry,
      selectedState: event.selectedState,
      selectedCity: event.selectedCity,
    ));
  }
}

Step 4: Implement the UI

Now, we create the UI that interacts with the BLoC.

class DropdownPage extends StatelessWidget {
  final String? editCountry;
  final String? editState;
  final String? editCity;

DropdownPage({this.editCountry, this.editState, this.editCity});
  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (_) {
        final bloc = DropdownBloc();
        if (editCountry != null) {
          bloc.add(EditSelection(editCountry!, editState!, editCity!));
        } else {
          bloc.add(LoadCountries());
        }
        return bloc;
      },
      child: Column(
        children: [
          BlocBuilder<DropdownBloc, DropdownState>(
            builder: (context, state) {
              if (state is DropdownLoaded) {
                return DropdownButton<String>(
                  value: state.selectedCountry,
                  hint: Text("Select Country"),
                  items: state.countries.map((country) => DropdownMenuItem(value: country, child: Text(country))).toList(),
                  onChanged: (value) => context.read<DropdownBloc>().add(CountrySelected(value!)),
                );
              }
              return CircularProgressIndicator();
            },
          ),
          BlocBuilder<DropdownBloc, DropdownState>(
            builder: (context, state) {
              if (state is DropdownLoaded && state.selectedCountry != null) {
                return DropdownButton<String>(
                  value: state.selectedState,
                  hint: Text("Select State"),
                  items: state.states.map((state) => DropdownMenuItem(value: state, child: Text(state))).toList(),
                  onChanged: (value) => context.read<DropdownBloc>().add(StateSelected(value!)),
                );
              }
              return SizedBox.shrink();
            },
          ),
        ],
      ),
    );
  }
}

Conclusion

By implementing dependent dropdowns using BLoC, we ensure a clean architecture, efficient state management, and easy handling of prepopulated selections. This approach is scalable for larger applications and keeps the UI responsive.

Hope you enjoyed this article!

0
Subscribe to my newsletter

Read articles from NonStop io Technologies directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

NonStop io Technologies
NonStop io Technologies

Product Development as an Expertise Since 2015 Founded in August 2015, we are a USA-based Bespoke Engineering Studio providing Product Development as an Expertise. With 80+ satisfied clients worldwide, we serve startups and enterprises across San Francisco, Seattle, New York, London, Pune, Bangalore, Tokyo and other prominent technology hubs.