How to use BLOC to search through a list?

Reading time: 3 minutes

Here we will resolve the situation with search inside a long list of items. We will use the same structure as our favorite items. The repository will be a high-level component, while CountriesRest will be responsible for HTTP calls. We will have “cashing” of countries list and one more field to save the search phase.

class CountriesRepository {
  final rest = CountriesRest();
  String search = "";
  final countries = List<Country>();

  Future<List<Country>> getCountries() async {
    if (countries.isEmpty) countries.addAll(await rest.fetchCountries());
    return countries;
  }
}

We’ll add an event to perform a search with the String field for a phrase. Handle that event by saving phrase in the repository and filtering list according to it. Pay attention that here create a new list of countries before “streaming” updated state. It’s done this way because we modify the list of countries, and we don’t want to corrupt the “cached” list from the repository by doing that on the original list.

import 'dart:async';

import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'countries_repository.dart';
import 'country.dart';

class CountriesBloc extends Bloc<CountriesEvent, CountriesState> {
  CountriesBloc({this.repository}) : super(CountriesLoading());

  final CountriesRepository repository;

  @override
  Stream<CountriesState> mapEventToState(CountriesEvent event) async* {
    if (event is LoadCountriesEvent)
      yield* _loadCountries();
    else if (event is SearchCountriesEvent) yield* _updateSearchCountries(event);
  }

  Stream<CountriesState> _loadCountries() async* {
    yield CountriesLoading();
    try {
      var list = List<Country>()..addAll(await repository.getCountries());
      if (repository.search != null && repository.search.isNotEmpty)
        list.removeWhere((element) => !element.name.toLowerCase().contains(repository.search.toLowerCase()));
      yield CountriesLoaded(countries: list);
    } catch (e) {
      yield CountriesFailed();
    }
  }

  Stream<CountriesState> _updateSearchCountries(SearchCountriesEvent event) async* {
    repository.search = event.search;
    yield* _loadCountries();
  }
}

abstract class CountriesEvent extends Equatable {
  const CountriesEvent();
}

class SearchCountriesEvent extends CountriesEvent {
  final String search;

  SearchCountriesEvent({this.search});

  @override
  List<Object> get props => [search];
}

class LoadCountriesEvent extends CountriesEvent {
  @override
  List<Object> get props => [];
}

abstract class CountriesState extends Equatable {
  const CountriesState();
}

class CountriesLoading extends CountriesState {
  @override
  List<Object> get props => [];
}

class CountriesFailed extends CountriesState {
  @override
  List<Object> get props => [];
}

class CountriesLoaded extends CountriesState {
  final List<Country> countries;

  CountriesLoaded({this.countries});

  @override
  List<Object> get props => [countries];

We check if the search word exists and is not empty as it would mean that we don’t have to remove items from the list.

And now an example of how to use it:

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'countries_bloc.dart';
import 'countries_repository.dart';

class BlocSearchExample extends StatelessWidget {
  final TextEditingController _searchQuery = new TextEditingController();

  @override
  Widget build(BuildContext context) {
    return BlocProvider(
        create: (context) => CountriesBloc(repository: CountriesRepository())..add(LoadCountriesEvent()),
        child: BlocBuilder<CountriesBloc, CountriesState>(builder: (context, state) {
          return Scaffold(
              appBar: AppBar(centerTitle: true, title: _buildAppTitle(context), elevation: 3.0),
              body: _buildPage(context, state));
        }));
  }

  Widget _buildPage(BuildContext context, CountriesState state) {
    if (state is CountriesLoaded) {
      return ListView(
          children:
              state.countries.map((c) => ListTile(title: Text("${c.name}"), subtitle: Text("${c.capital}"))).toList());
    } else
      return Center(
        child: CircularProgressIndicator(),
      );
  }

  Widget _buildAppTitle(BuildContext context) {
    return TextField(
      controller: _searchQuery,
      cursorColor: Colors.white,
      onSubmitted: (String text) {
        BlocProvider.of<CountriesBloc>(context).add(SearchCountriesEvent(search: text));
      },
      style: TextStyle(
        color: Colors.white,
      ),
      decoration: InputDecoration(
          suffixIcon: (_searchQuery.text.isEmpty
              ? Text("")
              : IconButton(
                  onPressed: () {
                    _searchQuery.clear();
                    BlocProvider.of<CountriesBloc>(context).add(SearchCountriesEvent());
                  },
                  icon: Icon(Icons.clear, color: Colors.white),
                )),
          border: InputBorder.none,
          prefixIcon: Icon(Icons.search, color: Colors.white),
          hintText: "Search...",
          hintStyle: new TextStyle(color: Colors.white70)),
    );
  }
}

TextEditingController is used to hold Text field data. Pay attention that all events should be added inside BlocBuilder, that’s why this example is slightly different from favorite and REST example. We build a nice appBar with a search field, hint text, and a clear button, that will perform search event with no search phare, returning the whole list of items.

As usual lets have a look at this example in action:

Leave a Reply

Your email address will not be published. Required fields are marked *