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')];
}
}
@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