Last active
March 26, 2024 04:42
-
-
Save felangel/11769ab10fbc4076076299106f48fc95 to your computer and use it in GitHub Desktop.
Bloc with SearchDelegate
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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')]; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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 blocThis 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 likecontext.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.