Skip to content

Instantly share code, notes, and snippets.

@felangel
Last active March 26, 2024 04:42
Show Gist options
  • Star 32 You must be signed in to star a gist
  • Fork 9 You must be signed in to fork a gist
  • Save felangel/11769ab10fbc4076076299106f48fc95 to your computer and use it in GitHub Desktop.
Save felangel/11769ab10fbc4076076299106f48fc95 to your computer and use it in GitHub Desktop.
Bloc with SearchDelegate
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:bloc/bloc.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
home: BlocProvider(
create: (_) => CityBloc(),
child: MyHomePage(),
));
}
}
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Search Delegate'),
),
body: Container(
child: Center(
child: RaisedButton(
child: Text('Show search'),
onPressed: () async {
City selected = await showSearch<City>(
context: context,
delegate: CitySearch(BlocProvider.of<CityBloc>(context)),
);
print(selected);
},
),
),
),
);
}
}
class City {
final String name;
const City(this.name);
@override
String toString() => 'City { name: $name }';
}
class CitySearch extends SearchDelegate<City> {
final Bloc<CitySearchEvent, CitySearchState> cityBloc;
CitySearch(this.cityBloc);
@override
List<Widget> buildActions(BuildContext context) => null;
@override
Widget buildLeading(BuildContext context) {
return IconButton(
icon: BackButtonIcon(),
onPressed: () {
close(context, null);
},
);
}
@override
Widget buildResults(BuildContext context) {
cityBloc.add(CitySearchEvent(query));
return BlocBuilder(
bloc: cityBloc,
builder: (BuildContext context, CitySearchState state) {
if (state.isLoading) {
return Center(
child: CircularProgressIndicator(),
);
}
if (state.hasError) {
return Container(
child: Text('Error'),
);
}
return ListView.builder(
itemBuilder: (context, index) {
return ListTile(
leading: Icon(Icons.location_city),
title: Text(state.cities[index].name),
onTap: () => close(context, state.cities[index]),
);
},
itemCount: state.cities.length,
);
},
);
}
@override
Widget buildSuggestions(BuildContext context) => Container();
}
class CitySearchEvent {
final String query;
const CitySearchEvent(this.query);
@override
String toString() => 'CitySearchEvent { query: $query }';
}
class CitySearchState {
final bool isLoading;
final List<City> cities;
final bool hasError;
const CitySearchState({this.isLoading, this.cities, this.hasError});
factory CitySearchState.initial() {
return CitySearchState(
cities: [],
isLoading: false,
hasError: false,
);
}
factory CitySearchState.loading() {
return CitySearchState(
cities: [],
isLoading: true,
hasError: false,
);
}
factory CitySearchState.success(List<City> cities) {
return CitySearchState(
cities: cities,
isLoading: false,
hasError: false,
);
}
factory CitySearchState.error() {
return CitySearchState(
cities: [],
isLoading: false,
hasError: true,
);
}
@override
String toString() =>
'CitySearchState {cities: ${cities.toString()}, isLoading: $isLoading, hasError: $hasError }';
}
class CityBloc extends Bloc<CitySearchEvent, CitySearchState> {
@override
CitySearchState get initialState => CitySearchState.initial();
@override
void onTransition(Transition<CitySearchEvent, CitySearchState> transition) {
print(transition.toString());
}
@override
Stream<CitySearchState> mapEventToState(CitySearchEvent event) async* {
yield CitySearchState.loading();
try {
List<City> cities = await _getSearchResults(event.query);
yield CitySearchState.success(cities);
} catch (_) {
yield CitySearchState.error();
}
}
Future<List<City>> _getSearchResults(String query) async {
// Simulating network latency
await Future.delayed(Duration(seconds: 1));
return [City('Chicago'), City('Los Angeles')];
}
}
@kururu-abdo
Copy link

if have toJson or fromJson in my model class
how to implement?

@felangel
Copy link
Author

felangel commented Feb 7, 2020

You could just do something like:

Future<List<City>> _getSearchResults(String query) async {
  final response = await httpClient.get(...);
  if (response.statusCode != 200) throw Exception('Request Failed; ${response.statusCode}');
  try {
    final body = json.decode(response.body);
    return CitiesResponse.fromJson(body).cities;
  } catch (_) {
    throw Exception('Error Parsing Response Body');
  }
}

Hope that helps! 👍

@longa78
Copy link

longa78 commented Feb 29, 2020

Hi Felix, using your same code but inverting the buildResults code with the buildSuggestions cause a double buildSuggestion call. Is this an issue of the package or is it a flutter logic?

@felangel
Copy link
Author

felangel commented Mar 2, 2020

@longa78 what do you mean inverting the buildResults code? Can you share a link to a gist which illustrates the problem you're having?

@longa78
Copy link

longa78 commented Mar 2, 2020

@felangel, sure: https://gist.github.com/longa78/4ce68484848d0c53e581e5c4cd3b38f7

I only move the BlocBuilder from buildResults to buildSuggestions.
In this way the buildSuggestions is invoked 2 times.

@EliuTimana
Copy link

I am newbie in this, I have this code, I need show the search widget when an action button in the app bar is clicked, but I receive this error BlocProvider.of() called with a context that does not contain a Bloc, It only works if I wrap the entire scaffold widget in the BlocProvider widget and create a stateless widget for each action button, how to solve this?, is it really necessary, what am I doing wrong?, Thanks in advance 😃 .

  Widget build(BuildContext context) {
    // TODO: implement build
    return BlocProvider(create: (context) => new SearchBloc(medicineRepository: medicineRepository),
        child: Scaffold(
          appBar: AppBar(
            title: Text('test'),
            actions: [ Btn1()],
          ),
          body: Test(),
        )
    );
  }
}

class Btn1 extends StatelessWidget { // <- action button
  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return IconButton(
      icon: Icon(Icons.search),
      onPressed: () {
        showSearch(
          context: context,
          delegate: new SearchPageDelegate(bloc: BlocProvider.of<SearchBloc>(context)),
        );
      },
    );
  }
}

@felangel
Copy link
Author

@EliuTimana you can use Builder widget instead of creating a separate widget for Btn1

 Widget build(BuildContext context) {
    // TODO: implement build
    return BlocProvider(create: (context) => new SearchBloc(medicineRepository: medicineRepository),
        child: Scaffold(
          appBar: AppBar(
            title: Text('test'),
            actions: [
              Builder(builder: (context) =>
                IconButton(
                  icon: Icon(Icons.search),
                  onPressed: () {
                    showSearch(
                      context: context,
                      delegate: new SearchPageDelegate(bloc: BlocProvider.of<SearchBloc>(context)),
                    );
                  },
                ),
              ),
            ],
          ),
          body: Test(),
        )
    );
  }
}

@EliuTimana
Copy link

Thank you @felangel, I got another question, when I click on a search result and navigate to another screen and then press the back button, the search is performed again, is that the right behavior? It looks like the bloc sequence is restarted, in the search action, I make an HTTP call to my server.

//buildResults method of SearchDelegate class
@override
  Widget buildResults(BuildContext context) {
    if (query.isNotEmpty) {
      bloc.add(new SearchButtonPressed(term: query));
    }

    return new BlocBuilder(
        bloc: bloc,
        builder: (context, state) {
          if (state is SearchLoading) {
            return Center(child: CircularProgressIndicator());
          }

          if (state is SearchError) {
            return Center(child: Text(state.error));
          }

          if (state is SearchLoaded) {
            return ListView.builder(
                itemBuilder: (context, index) {
                  return _buildSearchItem(context, state.autocompleteResults[index]);
                },
                itemCount: state.autocompleteResults.length);
          }

          if (state is SearchInitial) {
            return new Test();
          }

          return new Container();
        });
  }

// search action button
Widget build(BuildContext context) {
    return IconButton(
      icon: Icon(Icons.search),
      onPressed: () {
        showSearch(
          context: context,
          delegate: SearchPageDelegate(bloc: BlocProvider.of<SearchBloc>(context)),
        );
      },
    );
  }

// _buildSearchItem method
  ListTile _buildSearchItem(BuildContext context, AutocompleteResult data) {
    return new ListTile(
      title: Text(data.value, maxLines: 1, overflow: TextOverflow.ellipsis),
      subtitle: Text(
        data.activeIngredient ?? '',
        style: TextStyle(fontSize: 10),
      ),
      dense: true,
      onTap: () {
        Navigator.push(context, new MaterialPageRoute(builder: (context) => new ListPage(id: data.id,)));
      },
    );
  }

@richardhdezf
Copy link

this gist really help, thanks!!!

@basitis
Copy link

basitis commented Apr 2, 2020

@felengel how to add debounce time during search? Currently, my bloc continuously called if the user types rapdily.

@EliuTimana
Copy link

@felengel how to add debounce time during search? Currently, my bloc continuously called if the user types rapdily.

you can override the transformEvents method in your bloc, here you can see how to add a debounce time, you will need rxdart.

@override
Stream<PostState> transformEvents(
  Stream<PostEvent> events,
  Stream<PostState> Function(PostEvent event) next,
) {
  return super.transformEvents(
    events.debounceTime(
      Duration(milliseconds: 500),
    ),
    next,
  );
}

@georgiossalon
Copy link

Is there a possibility to use a BlocListener within the SearchDelegate and if so where would it get implemented? I need one in order to use a Navigator.pop(context); within

@samramez
Copy link

samramez commented Aug 15, 2020

@felengel this is really helpful gist but it seems that it's outdated.

BlockBuilder no longer accepts bloc and only takes cubit which is not how I built my application. Any alternatives to call BlocBuilder inside SearchDelegate?

EDIT (UPDATE):
I realized I can simply assign my Bloc object to the cubit field and it just works as expected.

return BlocBuilder(
      cubit: searchBloc,
      builder: (BuildContext context, SearchState state)

@basitis
Copy link

basitis commented Sep 24, 2020

Thanks @EliuTimana

@ashutoshjak
Copy link

ashutoshjak commented Oct 22, 2020

how can we delay search request also build only suggestion that we searched like using in autocomplete text in search delegate

@Holofox
Copy link

Holofox commented Mar 15, 2023

Alternative solution:

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

class BlocSearchDelegateBuilder<B extends StateStreamable<S>, S>
    extends SearchDelegate<S?> {
  BlocSearchDelegateBuilder({
    required this.builder,
    required this.bloc,
    this.buildWhen,
    this.onQuery,
    super.searchFieldLabel,
    super.searchFieldStyle,
    super.searchFieldDecorationTheme,
    super.keyboardType,
    super.textInputAction = TextInputAction.search,
  });

  final BlocWidgetBuilder<S> builder;
  final B bloc;
  final BlocBuilderCondition<S>? buildWhen;
  final ValueChanged<String>? onQuery;

  @override
  List<Widget>? buildActions(BuildContext context) {
    return [
      IconButton(
        onPressed: () {
          if (query.isEmpty) return close(context, null);
          query = '';
        },
        icon: const Icon(Icons.close),
      ),
    ];
  }

  @override
  Widget buildSuggestions(BuildContext context) {
    onQuery?.call(query);
    return BlocBuilder<B, S>(
      builder: builder,
      bloc: bloc,
      buildWhen: buildWhen,
    );
  }

  @override
  Widget buildResults(BuildContext context) {
    onQuery?.call(query);
    return BlocBuilder<B, S>(
      builder: builder,
      bloc: bloc,
      buildWhen: buildWhen,
    );
  }

  @override
  Widget? buildLeading(BuildContext context) => null;
}

Usage:

...

  @override
  Widget build(BuildContext context) {
    return BlocProvider<CityBloc>(
      create: (_) => CityBloc(),
      child: Scaffold(
        appBar: AppBar(
          title: Text('Search Delegate'),
          actions: [
            Builder(
              builder: (context) => IconButton(
                onPressed: () async {
                  await showSearch(
                    context: context,
                    delegate: BlocSearchDelegateBuilder(
                      builder: _builder,
                      bloc: BlocProvider.of<CityBloc>(context),
                      onQuery: (query) => BlocProvider.of<CityBloc>(context).add(CitySearchEvent(query)),
                    ),
                  );
                },
                icon: const Icon(Icons.search),
              ),
            ),
          ],
        ),
        body: SafeArea(
          child: BlocBuilder<CityBloc, CityState>(
            builder: _builder,
          ),
        ),
      ),
    );
  }

  void _builder(BuildContext context, CityState state) {
    // ListView.builder
  }

...

@nerder
Copy link

nerder commented Mar 7, 2024

Hey @Holofox this is awsome! I've used it in my codebase but I've found some limitations and i've expanded it. Hopefully this is useful for somebody else too.

My additions are the following:

When the search delegate is dismissed I also close the bloc

This might not be necessary depending on how you instantiate the bloc on the parent, but since it might be possible to also create the bloc without using a BlocProvider (as in my case) I thought it would be safer to close anyway.

onQuery provides also te bloc

This is handy and maybe also a bit better in performance, since is quite likely that you add an action provide the bloc back might be useful. I've taken inspiration from how bloc_test does this.

Added a BlocProvider.value to provide the bloc to the builder

One issue I had with your code was that then I attempt to retrieve the bloc from the context inside the builder function it wasn't in context, this is due to the fact that when BlocBuilder is instanciated with bloc, it doesn't add it into the context as BlocProvider do. So if inside the code of the builder you tried to do something like context.read<MyBloc>() orBlocProvider.of<MyBloc>(context) that will trigger an error as the lookup would fail. Since the bloc is already instantiated, we can pass by value. I handle the close in the dismiss also to be extra sure, since BlocProvider.value doesn't do that for you.

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

class BlocSearchDelegateBuilder<B extends StateStreamableSource<S>, S> extends SearchDelegate<S?> {
  BlocSearchDelegateBuilder({
    required this.builder,
    required this.bloc,
    this.buildWhen,
    this.onQuery,
    super.searchFieldLabel,
    super.searchFieldStyle,
    super.searchFieldDecorationTheme,
    super.keyboardType,
    super.textInputAction = TextInputAction.search,
  });

  final BlocWidgetBuilder<S> builder;
  final B bloc;
  final BlocBuilderCondition<S>? buildWhen;
  final Function(B, String)? onQuery;

  @override
  List<Widget>? buildActions(BuildContext context) {
    return [
      IconButton(
        onPressed: () {
          if (query.isEmpty) {
            (bloc as Bloc).close();
            return close(context, null);
          }
          query = '';
        },
        icon: const Icon(Icons.close),
      ),
    ];
  }

  @override
  Widget buildSuggestions(BuildContext context) {
    onQuery?.call(bloc, query);
    return BlocProvider.value(
      value: bloc,
      child: BlocBuilder<B, S>(
        builder: builder,
        buildWhen: buildWhen,
      ),
    );
  }

  @override
  Widget buildResults(BuildContext context) {
    onQuery?.call(bloc, query);
    return BlocProvider.value(
      value: bloc,
      child: BlocBuilder<B, S>(
        builder: builder,
        buildWhen: buildWhen,
      ),
    );
  }

  @override
  Widget? buildLeading(BuildContext context) =>
      IconButton(
        onPressed: () {
          (bloc as Bloc).close();
          close(context, null);
        },
        icon: const Icon(Icons.arrow_back),
      );
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment