-
-
Save felangel/11769ab10fbc4076076299106f48fc95 to your computer and use it in GitHub Desktop.
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')]; | |
} | |
} |
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! 👍
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?
@longa78 what do you mean inverting the buildResults
code? Can you share a link to a gist which illustrates the problem you're having?
@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.
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)),
);
},
);
}
}
@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(),
)
);
}
}
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,)));
},
);
}
this gist really help, thanks!!!
@felengel how to add debounce time during search? Currently, my bloc continuously called if the user types rapdily.
@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,
);
}
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
@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)
Thanks @EliuTimana
how can we delay search request also build only suggestion that we searched like using in autocomplete text in search delegate
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
}
...
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),
);
}
if have toJson or fromJson in my model class
how to implement?