felangel / bloc

A predictable state management library that helps implement the BLoC design pattern
https://bloclibrary.dev
MIT License
11.76k stars 3.39k forks source link

fix: Bloc with searchAnchor Widget #4164

Closed abdulbosit209 closed 4 months ago

abdulbosit209 commented 5 months ago

Description How to use bloc with searchAnchor widget

final state = context.watch<SearchAnchorBloc>().state;

suggestionsBuilder: (
                BuildContext context,
                SearchController controller,
              ) {
                if (controller.text.isEmpty) {
                  return [
                    const Center(
                      child: Text('No Data'),
                    )
                  ];
                }
                if (state.status == SearchAnchorStatus.loading) {
                  return [
                    const Center(
                      child: CircularProgressIndicator.adaptive(),
                    ),
                  ];
                }
                return List.generate(
                  state.results.length,
                  (index) => ListTile(
                      title: Text(state.results[index]),
                      onTap: () {
                        setState(() {
                          controller.closeView(state.results[index]);
                        });
                      }),
                );
              },

suggestionsBuilder isnot update

here is the code

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

void main() {
  Bloc.observer = SimpleBlocObserver();
  runApp(const SearchAnchorApp());
}

// fake api
class _FakeAPI {
  static const List<String> _kOptions = <String>[
    'aardvark',
    'bobcat',
    'chameleon',
    'virginia',
    'landry',
    'kelly',
    'eugene',
    'lina',
    'thaddeus',
    'peyton',
    'ezekiel',
    'drew',
    'daxton',
    'nova',
    'castiel',
    'aliya',
    'leighton',
    'layne',
    'kayson',
  ];

  static Future<Iterable<String>> search(String query) async {
    await Future<void>.delayed(
      const Duration(seconds: 1),
    ); // Fake 1 second delay.
    if (query == '') {
      return const Iterable<String>.empty();
    }
    return _kOptions.where((String option) {
      return option.contains(query.toLowerCase());
    });
  }
}

class SimpleBlocObserver extends BlocObserver {
  @override
  void onEvent(Bloc bloc, Object? event) {
    super.onEvent(bloc, event);
    print('${bloc.runtimeType} $event');
  }

  @override
  void onChange(BlocBase bloc, Change change) {
    super.onChange(bloc, change);
    print('${bloc.runtimeType} $change');
  }

  @override
  void onTransition(Bloc bloc, Transition transition) {
    super.onTransition(bloc, transition);
    print('${bloc.runtimeType} $transition');
  }
}

/// bloc
const _duration = Duration(seconds: 1);

EventTransformer<Event> debounce<Event>(Duration duration) {
  return (events, mapper) => events.debounce(duration).switchMap(mapper);
}

class SearchAnchorBloc extends Bloc<SearchAnchorEvent, SearchAnchorState> {
  SearchAnchorBloc() : super(const SearchAnchorState()) {
    on<SearchTermChanged>(
      _searchTermChanged,
      transformer: debounce(_duration),
    );
  }

  Future<void> _searchTermChanged(
    SearchTermChanged event,
    Emitter<SearchAnchorState> emit,
  ) async {
    emit(state.copyWith(status: SearchAnchorStatus.loading));
    try {
      final results = (await _FakeAPI.search(event.searchTerm)).toList();
      emit(
        state.copyWith(
          status: SearchAnchorStatus.success,
          results: results,
        ),
      );
    } catch (error, stackTrace) {
      emit(
        state.copyWith(
          status: SearchAnchorStatus.failure,
          error: error.toString(),
        ),
      );
      addError(error, stackTrace);
    }
  }
}

/// event
sealed class SearchAnchorEvent extends Equatable {
  const SearchAnchorEvent();

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

final class SearchTermChanged extends SearchAnchorEvent {
  const SearchTermChanged({required this.searchTerm});

  final String searchTerm;

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

/// state
enum SearchAnchorStatus { initial, loading, success, failure }

final class SearchAnchorState extends Equatable {
  const SearchAnchorState({
    this.error = '',
    this.results = const [],
    this.status = SearchAnchorStatus.initial,
  });

  final SearchAnchorStatus status;
  final String error;
  final List<String> results;

  @override
  List<Object> get props => [status, error, results];

  SearchAnchorState copyWith({
    SearchAnchorStatus? status,
    String? error,
    List<String>? results,
  }) =>
      SearchAnchorState(
        error: error ?? this.error,
        status: status ?? this.status,
        results: results ?? this.results,
      );
}

class SearchAnchorApp extends StatelessWidget {
  const SearchAnchorApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: BlocProvider(
        create: (_) => SearchAnchorBloc(),
        child: const SearchAnchorExampleWidget(),
      ),
    );
  }
}

class SearchAnchorExampleWidget extends StatefulWidget {
  const SearchAnchorExampleWidget({super.key});

  @override
  State<SearchAnchorExampleWidget> createState() =>
      _SearchAnchorExampleWidgetState();
}

class _SearchAnchorExampleWidgetState extends State<SearchAnchorExampleWidget> {
  final SearchController controller = SearchController();

  @override
  Widget build(BuildContext context) {
    final state = context.watch<SearchAnchorBloc>().state;
    return Scaffold(
      appBar: AppBar(
        title: const Text('SearchAnchor - example'),
      ),
      body: Align(
        alignment: Alignment.topCenter,
        child: Column(
          children: [
            SearchAnchor.bar(
              searchController: controller,
              onChanged: (searchTerm) => context.read<SearchAnchorBloc>().add(
                    SearchTermChanged(searchTerm: searchTerm),
                  ),
              barHintText: 'Search',
              suggestionsBuilder: (
                BuildContext context,
                SearchController controller,
              ) {
                if (controller.text.isEmpty) {
                  return [
                    const Center(
                      child: Text('No Data'),
                    )
                  ];
                }
                if (state.status == SearchAnchorStatus.loading) {
                  return [
                    const Center(
                      child: CircularProgressIndicator.adaptive(),
                    ),
                  ];
                }
                return List.generate(
                  state.results.length,
                  (index) => ListTile(
                      title: Text(state.results[index]),
                      onTap: () {
                        setState(() {
                          controller.closeView(state.results[index]);
                        });
                      }),
                );
              },
            ),
            const SizedBox(height: 50),
            Center(
              child: controller.text.isEmpty
                  ? const Text('No item selected')
                  : Text('Selected item: ${controller.value.text}'),
            )
          ],
        ),
      ),
    );
  }

  @override
  void dispose() {
    controller.dispose();
    super.dispose();
  }
}
felangel commented 4 months ago

Hi @abdulbosit209 👋 Thanks for opening an issue! Are you able to share a link to a minimal reproduction sample that illustrates the issue you're facing? It would be much easier to help if I'm able to clone, run, and debug a reproduction sample locally, thanks!

alexboulay commented 4 months ago

@abdulbosit209 The suggestionsBuilder will only be called once when the text field updates, by the time your bloc emits a new state, it is too late!

I think you might want to use viewBuilder instead of suggestionsBuilder! Inside viewBuilder you can have a BlocBuilder and when a new state gets emitted, the SearchAnchor results will get updated. Hope this helps!

Here is how I did it

SearchAnchor(
      viewLeading: IconButton(
        icon: Icon(Icons.arrow_back),
        onPressed: () {
          context.read<AutocompleteBloc>().add(AutocompleteViewClosed());
          Navigator.of(context).pop();
        },
      ),
      viewOnChanged: (text) {
        context.read<AutocompleteBloc>().add(AutocompleteTextChanged(text));
      },
      viewBuilder: (suggestions) {
        return BlocBuilder<AutocompleteBloc, AutocompleteState>(
          builder: (context, state) {
            return MediaQuery.removePadding(
              removeTop: true,
              context: context,
              child: ListView.builder(
                itemCount: state.results.length,
                itemBuilder: (context, index) {
                  return Text(state.results[index]);
                },
              ),
            );
          },
        );
      },
      suggestionsBuilder: (context, controller) => [],
      builder: (context, controller) => IconButton(
        icon: Icon(Icons.search),
        onPressed: () async {
          controller.openView();
        },
      ),
    );
abdulbosit209 commented 4 months ago

Thank you both, @felangel and @alexboulay, for your responses Special thanks to @alexboulay for providing the solution that resolved the issue