Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Bloc with searchAnchor Widget #4164

Closed
abdulbosit209 opened this issue May 5, 2024 · 3 comments
Closed

fix: Bloc with searchAnchor Widget #4164

abdulbosit209 opened this issue May 5, 2024 · 3 comments
Assignees
Labels
question Further information is requested

Comments

@abdulbosit209
Copy link

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();
  }
}
@abdulbosit209 abdulbosit209 added the bug Something isn't working label May 5, 2024
@felangel
Copy link
Owner

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!

@felangel felangel added question Further information is requested waiting for response Waiting for follow up and removed bug Something isn't working labels May 13, 2024
@alexboulay
Copy link

@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
Copy link
Author

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

@felangel felangel removed the waiting for response Waiting for follow up label May 14, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
question Further information is requested
Projects
None yet
Development

No branches or pull requests

3 participants