biniyam112
biniyam112

Reputation: 1182

How to customize SearchDelegate(Create custom search Field)

I've been trying to customize Flutter SearchDelgate to the type of search field I want it to be. It got a method named appBarTheme with return type ThemeData. Usually using ThemeData you can change the appbar theme but it's not making any change in my case. I am able to customize the hint text style searchFieldStyle method but nothing more.

here is code:

class CustomSearchDelegate extends SearchDelegate<Country> {
  @override
  ThemeData appBarTheme(BuildContext context) {
    return ThemeData(
      appBarTheme: AppBarTheme(
        elevation: 0,
        color: themeColor,
        //app bar color I wanted
      ),
    );
  }

  @override
  TextStyle get searchFieldStyle => TextStyle(
        color: whiteTextColor,
        fontWeight: FontWeight.w600,
        fontFamily: GoogleFonts.poppins().fontFamily,
      );

  @override
  List<Widget> buildActions(BuildContext context) {
    return [
        IconButton(
          icon: Icon(
            Icons.close_rounded,
            color: Colors.white,
          ),
          onPressed: () => query = '',
        ),
    ];
  }

  @override
  Widget buildLeading(BuildContext context) {
    return IconButton(
      icon: Icon(
        Icons.arrow_back_ios,
        color: Colors.white,
      ),
      onPressed: () {
        close(context, null);
      },
    );
  }

  @override
  Widget buildResults(BuildContext context) {
    return Column(
      children: [],
    );
  }

  @override
  Widget buildSuggestions(BuildContext context) {
    return Column(
      children: [],
    );
  }
}

It would be super helpful if someone could help me out with this.

also, a similar question has been raised before but never got answered Flutter create custom search UI extends SearchDelegate

Upvotes: 4

Views: 7411

Answers (2)

Siddharth Gondaliya
Siddharth Gondaliya

Reputation: 98

I found one way to customize flutter search delegate the way you want. you just have to copy flutter's search delegates code and then customize the code you want.

Here is the Solution: 1: this is the code of showSearch.

 Container(
                  padding: EdgeInsets.only(left: 15.w, right: 15.w, top: 15.h, bottom: 15.h),
                  decoration: BoxDecoration(borderRadius: BorderRadius.circular(5.r)),
                  child: CustomSearchButton(
                    onTap: () async {
                      final String? result = await showSearchForCustomiseSearchDelegate(
                        context: context,
                        delegate: SearchScreen(
                          hintText: AppLocalizations.of(context)!.searchHere,
                        ),
                      );
                    },
                  ),
                ),   

2: this is the code of customised flutter searchDelegate.

Widget build(BuildContext context) {
    assert(debugCheckHasMaterialLocalizations(context));
    final ThemeData theme = widget.delegate.appBarTheme(context);
    final String searchFieldLabel = widget.delegate.searchFieldLabel ?? MaterialLocalizations.of(context).searchFieldLabel;
    Widget? body;
    switch (widget.delegate._currentBody) {
      case _SearchBody.suggestions:
        body = KeyedSubtree(
          key: const ValueKey<_SearchBody>(_SearchBody.suggestions),
          child: widget.delegate.buildSuggestions(context),
        );
        break;
      case _SearchBody.results:
        body = KeyedSubtree(
          key: const ValueKey<_SearchBody>(_SearchBody.results),
          child: widget.delegate.buildResults(context),
        );
        break;
      case null:
        break;
    }

    late final String routeName;
    switch (theme.platform) {
      case TargetPlatform.iOS:
      case TargetPlatform.macOS:
        routeName = '';
        break;
      case TargetPlatform.android:
      case TargetPlatform.fuchsia:
      case TargetPlatform.linux:
      case TargetPlatform.windows:
        routeName = searchFieldLabel;
    }

    return Semantics(
      explicitChildNodes: true,
      scopesRoute: true,
      namesRoute: true,
      label: routeName,
      child: Theme(
        data: theme,
        child: Scaffold(
          appBar: AppBar(
            elevation: 0,
            automaticallyImplyLeading: false,
            backgroundColor: Colors.transparent,
            leadingWidth: 0,
            titleSpacing: 0,
            //leading: widget.delegate.buildLeading(context),
            title: Row(
              mainAxisAlignment: MainAxisAlignment.start,
              children: [
                Expanded(flex: 1, child: widget.delegate.buildLeading(context)!),
                Expanded(
                  flex: 6,
                  child: Container(
                    margin: EdgeInsets.only(right: 15.w),
                    decoration: const BoxDecoration(
                      color: AppColors.white,
                    ),
                    child: TextField(
                      controller: widget.delegate._queryTextController,
                      //focusNode: focusNode,
                      onSubmitted: (String _) {
                        widget.delegate.showResults(context);
                      },
                      textInputAction: widget.delegate.textInputAction,
                      keyboardType: widget.delegate.keyboardType,
                      decoration: InputDecoration(
                          fillColor: AppColors.white,
                          filled: true,
                          isDense: true,
                          hintText: searchFieldLabel,
                          hintStyle: TextStyle(fontSize: 14.sp),
                          contentPadding: EdgeInsets.symmetric(horizontal: 10.w),
                          prefixIcon: widget.delegate._queryTextController.text.isNotEmpty
                              ? null
                              : Padding(
                                  padding: EdgeInsets.only(right: 5.w),
                                  child: Image.asset(
                                    AppImages.searchBoxIcon1,
                                    scale: 3.5.sp,
                                  ),
                                ),
                          suffixIcon: widget.delegate._queryTextController.text.isEmpty
                              ? Image.asset(
                                  AppImages.searchBoxIcon2,
                                  scale: 3.5.sp,
                                )
                              : InkWell(
                                  onTap: () {
                                    widget.delegate._queryTextController.clear();
                                  },
                                  child: Image.asset(
                                    AppImages.closeCircle,
                                    scale: 3.5.sp,
                                  ),
                                ),
                          focusedBorder: OutlineInputBorder(
                            borderRadius: BorderRadius.all(Radius.circular(8.r)),
                            borderSide: const BorderSide(width: 1, color: AppColors.primaryColor),
                          ),
                          enabledBorder: OutlineInputBorder(
                            borderRadius: BorderRadius.all(Radius.circular(8.r)),
                            borderSide: const BorderSide(width: 1, color: AppColors.white),
                          ),
                          border: OutlineInputBorder(
                            borderSide: const BorderSide(color: AppColors.primaryColor),
                            borderRadius: BorderRadius.all(Radius.circular(8.r)),
                          )),
                    ),

                    // TextField(
                    //   controller: widget.delegate._queryTextController,
                    //   focusNode: focusNode,
                    //   style: theme.textTheme.headline6,
                    //   textInputAction: widget.delegate.textInputAction,
                    //   keyboardType: widget.delegate.keyboardType,
                    //   onSubmitted: (String _) {
                    //     widget.delegate.showResults(context);
                    //   },
                    //   decoration: InputDecoration(hintText: searchFieldLabel),
                    // ),
                  ),
                ),
              ],
            ),
            actions: widget.delegate.buildActions(context),
            bottom: widget.delegate.buildBottom(context),
          ),
          body: AnimatedSwitcher(
            duration: const Duration(milliseconds: 300),
            child: body,
          ),
        ),
      ),
    );

3: Here is the full code.

import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:test_app/constants/app_images.dart';
import 'package:test_app/theme/colors.dart';
import 'package:test_app/widgets/custom_buttons.dart';

class SearchScreen extends SearchDelegate<String> {
  SearchScreen({
    String? hintText,
  }) : super(
          searchFieldLabel: hintText,
          keyboardType: TextInputType.text,
          textInputAction: TextInputAction.search,
        );

  @override
  List<Widget>? buildActions(BuildContext context) {
    return [
      Container(),
    ];
  }

  @override
  Widget? buildLeading(BuildContext context) {
    return CustomBackButton(onTap: () {
      close(context, '');
    });
  }

  @override
  Widget buildResults(BuildContext context) {
    return Container();
  }

  @override
  Widget buildSuggestions(BuildContext context) {
    return Container();
  }
}

Future<T?> showSearchForCustomiseSearchDelegate<T>({
  required BuildContext context,
  required SearchDelegate<T> delegate,
  String? query = '',
  bool useRootNavigator = false,
}) {
  assert(delegate != null);
  assert(context != null);
  assert(useRootNavigator != null);
  delegate.query = query ?? delegate.query;
  delegate._currentBody = _SearchBody.suggestions;
  return Navigator.of(context, rootNavigator: useRootNavigator).push(_SearchPageRoute<T>(
    delegate: delegate,
  ));
}

abstract class SearchDelegate<T> {
  SearchDelegate({
    this.searchFieldLabel,
    this.searchFieldStyle,
    this.searchFieldDecorationTheme,
    this.keyboardType,
    this.textInputAction = TextInputAction.search,
  }) : assert(searchFieldStyle == null || searchFieldDecorationTheme == null);

  Widget buildSuggestions(BuildContext context);

  Widget buildResults(BuildContext context);

  Widget? buildLeading(BuildContext context);

  List<Widget>? buildActions(BuildContext context);

  PreferredSizeWidget? buildBottom(BuildContext context) => null;

  ThemeData appBarTheme(BuildContext context) {
    assert(context != null);
    final ThemeData theme = Theme.of(context);
    final ColorScheme colorScheme = theme.colorScheme;
    assert(theme != null);
    return theme.copyWith(
      appBarTheme: AppBarTheme(
        brightness: colorScheme.brightness,
        backgroundColor: colorScheme.brightness == Brightness.dark ? Colors.grey[900] : Colors.white,
        iconTheme: theme.primaryIconTheme.copyWith(color: Colors.grey),
        textTheme: theme.textTheme,
      ),
      inputDecorationTheme: searchFieldDecorationTheme ??
          InputDecorationTheme(
            hintStyle: searchFieldStyle ?? theme.inputDecorationTheme.hintStyle,
            border: InputBorder.none,
          ),
    );
  }

  String get query => _queryTextController.text;

  set query(String value) {
    assert(query != null);
    _queryTextController.text = value;
    queryTextController.selection = TextSelection.fromPosition(TextPosition(offset: queryTextController.text.length));
  }

  void showResults(BuildContext context) {
    _focusNode?.unfocus();
    currentBody = SearchBody.results;
  }

  void showSuggestions(BuildContext context) {
    assert(_focusNode != null, '_focusNode must be set by route before showSuggestions is called.');
    _focusNode!.requestFocus();
    currentBody = SearchBody.suggestions;
  }

  void close(BuildContext context, T result) {
    _currentBody = null;
    _focusNode?.unfocus();
    Navigator.of(context)
      ..popUntil((Route<dynamic> route) => route == _route)
      ..pop(result);
  }

  final String? searchFieldLabel;

  final TextStyle? searchFieldStyle;

  final InputDecorationTheme? searchFieldDecorationTheme;

  final TextInputType? keyboardType;

  final TextInputAction textInputAction;

  Animation<double> get transitionAnimation => _proxyAnimation;

  // The focus node to use for manipulating focus on the search page. This is
  // managed, owned, and set by the _SearchPageRoute using this delegate.
  FocusNode? _focusNode;

  final TextEditingController _queryTextController = TextEditingController();

  final ProxyAnimation _proxyAnimation = ProxyAnimation(kAlwaysDismissedAnimation);

  final ValueNotifier<_SearchBody?> _currentBodyNotifier = ValueNotifier<_SearchBody?>(null);

  SearchBody? get currentBody => _currentBodyNotifier.value;
  set _currentBody(_SearchBody? value) {
    _currentBodyNotifier.value = value;
  }

  SearchPageRoute<T>? route;
}

enum _SearchBody {
  suggestions,

  results,
}

class _SearchPageRoute<T> extends PageRoute<T> {
  _SearchPageRoute({
    required this.delegate,
  }) : assert(delegate != null) {
    assert(
      delegate._route == null,
      'The ${delegate.runtimeType} instance is currently used by another active '
      'search. Please close that search by calling close() on the SearchDelegate '
      'before opening another search with the same delegate instance.',
    );
    delegate._route = this;
  }

  final SearchDelegate<T> delegate;

  @override
  Color? get barrierColor => null;

  @override
  String? get barrierLabel => null;

  @override
  Duration get transitionDuration => const Duration(milliseconds: 300);

  @override
  bool get maintainState => false;

  @override
  Widget buildTransitions(
    BuildContext context,
    Animation<double> animation,
    Animation<double> secondaryAnimation,
    Widget child,
  ) {
    return FadeTransition(
      opacity: animation,
      child: child,
    );
  }

  @override
  Animation<double> createAnimation() {
    final Animation<double> animation = super.createAnimation();
    delegate._proxyAnimation.parent = animation;
    return animation;
  }

  @override
  Widget buildPage(
    BuildContext context,
    Animation<double> animation,
    Animation<double> secondaryAnimation,
  ) {
    return _SearchPage<T>(
      delegate: delegate,
      animation: animation,
    );
  }

  @override
  void didComplete(T? result) {
    super.didComplete(result);
    assert(delegate._route == this);
    delegate._route = null;
    delegate._currentBody = null;
  }
}

class _SearchPage<T> extends StatefulWidget {
  const _SearchPage({
    required this.delegate,
    required this.animation,
  });

  final SearchDelegate<T> delegate;
  final Animation<double> animation;

  @override
  State<StatefulWidget> createState() => _SearchPageState<T>();
}

class _SearchPageState<T> extends State<_SearchPage<T>> {
  FocusNode focusNode = FocusNode();

  @override
  void initState() {
    super.initState();
    widget.delegate._queryTextController.addListener(_onQueryChanged);
    widget.animation.addStatusListener(_onAnimationStatusChanged);
    widget.delegate._currentBodyNotifier.addListener(_onSearchBodyChanged);
    focusNode.addListener(_onFocusChanged);
    widget.delegate._focusNode = focusNode;
  }

  @override
  void dispose() {
    super.dispose();
    widget.delegate._queryTextController.removeListener(_onQueryChanged);
    widget.animation.removeStatusListener(_onAnimationStatusChanged);
    widget.delegate._currentBodyNotifier.removeListener(_onSearchBodyChanged);
    widget.delegate._focusNode = null;
    focusNode.dispose();
  }

  void _onAnimationStatusChanged(AnimationStatus status) {
    if (status != AnimationStatus.completed) {
      return;
    }
    widget.animation.removeStatusListener(_onAnimationStatusChanged);
    if (widget.delegate._currentBody == _SearchBody.suggestions) {
      focusNode.requestFocus();
    }
  }

  @override
  void didUpdateWidget(_SearchPage<T> oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (widget.delegate != oldWidget.delegate) {
      oldWidget.delegate._queryTextController.removeListener(_onQueryChanged);
      widget.delegate._queryTextController.addListener(_onQueryChanged);
      oldWidget.delegate._currentBodyNotifier.removeListener(_onSearchBodyChanged);
      widget.delegate._currentBodyNotifier.addListener(_onSearchBodyChanged);
      oldWidget.delegate._focusNode = null;
      widget.delegate._focusNode = focusNode;
    }
  }

  void _onFocusChanged() {
    if (focusNode.hasFocus && widget.delegate._currentBody != _SearchBody.suggestions) {
      widget.delegate.showSuggestions(context);
    }
  }

  void _onQueryChanged() {
    setState(() {
      // rebuild ourselves because query changed.
    });
  }

  void _onSearchBodyChanged() {
    setState(() {
      // rebuild ourselves because search body changed.
    });
  }

  @override
  Widget build(BuildContext context) {
    assert(debugCheckHasMaterialLocalizations(context));
    final ThemeData theme = widget.delegate.appBarTheme(context);
    final String searchFieldLabel = widget.delegate.searchFieldLabel ?? MaterialLocalizations.of(context).searchFieldLabel;
    Widget? body;
    switch (widget.delegate._currentBody) {
      case _SearchBody.suggestions:
        body = KeyedSubtree(
          key: const ValueKey<_SearchBody>(_SearchBody.suggestions),
          child: widget.delegate.buildSuggestions(context),
        );
        break;
      case _SearchBody.results:
        body = KeyedSubtree(
          key: const ValueKey<_SearchBody>(_SearchBody.results),
          child: widget.delegate.buildResults(context),
        );
        break;
      case null:
        break;
    }

    late final String routeName;
    switch (theme.platform) {
      case TargetPlatform.iOS:
      case TargetPlatform.macOS:
        routeName = '';
        break;
      case TargetPlatform.android:
      case TargetPlatform.fuchsia:
      case TargetPlatform.linux:
      case TargetPlatform.windows:
        routeName = searchFieldLabel;
    }

    return Semantics(
      explicitChildNodes: true,
      scopesRoute: true,
      namesRoute: true,
      label: routeName,
      child: Theme(
        data: theme,
        child: Scaffold(
          appBar: AppBar(
            elevation: 0,
            automaticallyImplyLeading: false,
            backgroundColor: Colors.transparent,
            leadingWidth: 0,
            titleSpacing: 0,
            //leading: widget.delegate.buildLeading(context),
            title: Row(
              mainAxisAlignment: MainAxisAlignment.start,
              children: [
                Expanded(flex: 1, child: widget.delegate.buildLeading(context)!),
                Expanded(
                  flex: 6,
                  child: Container(
                    margin: EdgeInsets.only(right: 15.w),
                    decoration: const BoxDecoration(
                      color: AppColors.white,
                    ),
                    child: TextField(
                      controller: widget.delegate._queryTextController,
                      //focusNode: focusNode,
                      onSubmitted: (String _) {
                        widget.delegate.showResults(context);
                      },
                      textInputAction: widget.delegate.textInputAction,
                      keyboardType: widget.delegate.keyboardType,
                      decoration: InputDecoration(
                          fillColor: AppColors.white,
                          filled: true,
                          isDense: true,
                          hintText: searchFieldLabel,
                          hintStyle: TextStyle(fontSize: 14.sp),
                          contentPadding: EdgeInsets.symmetric(horizontal: 10.w),
                          prefixIcon: widget.delegate._queryTextController.text.isNotEmpty
                              ? null
                              : Padding(
                                  padding: EdgeInsets.only(right: 5.w),
                                  child: Image.asset(
                                    AppImages.searchBoxIcon1,
                                    scale: 3.5.sp,
                                  ),
                                ),
                          suffixIcon: widget.delegate._queryTextController.text.isEmpty
                              ? Image.asset(
                                  AppImages.searchBoxIcon2,
                                  scale: 3.5.sp,
                                )
                              : InkWell(
                                  onTap: () {
                                    widget.delegate._queryTextController.clear();
                                  },
                                  child: Image.asset(
                                    AppImages.closeCircle,
                                    scale: 3.5.sp,
                                  ),
                                ),
                          focusedBorder: OutlineInputBorder(
                            borderRadius: BorderRadius.all(Radius.circular(8.r)),
                            borderSide: const BorderSide(width: 1, color: AppColors.primaryColor),
                          ),
                          enabledBorder: OutlineInputBorder(
                            borderRadius: BorderRadius.all(Radius.circular(8.r)),
                            borderSide: const BorderSide(width: 1, color: AppColors.white),
                          ),
                          border: OutlineInputBorder(
                            borderSide: const BorderSide(color: AppColors.primaryColor),
                            borderRadius: BorderRadius.all(Radius.circular(8.r)),
                          )),
                    ),

                    // TextField(
                    //   controller: widget.delegate._queryTextController,
                    //   focusNode: focusNode,
                    //   style: theme.textTheme.headline6,
                    //   textInputAction: widget.delegate.textInputAction,
                    //   keyboardType: widget.delegate.keyboardType,
                    //   onSubmitted: (String _) {
                    //     widget.delegate.showResults(context);
                    //   },
                    //   decoration: InputDecoration(hintText: searchFieldLabel),
                    // ),
                  ),
                ),
              ],
            ),
            actions: widget.delegate.buildActions(context),
            bottom: widget.delegate.buildBottom(context),
          ),
          body: AnimatedSwitcher(
            duration: const Duration(milliseconds: 300),
            child: body,
          ),
        ),
      ),
    );
  }
}

4: OUTPUT Output video

Upvotes: 6

Calvin Gonsalves
Calvin Gonsalves

Reputation: 2020

Unfortunately, you do not have full control over the Theme of the AppBar in the SearchDelegate since some of the theme property values that you specify in the appBarTheme are not assigned to the app bar widget used in SearchDelegate. You can take a look at the source code. It only takes the values specified in the ThemeData specified in MaterialApp theme property. In my case, I needed to change the cursor color but changing the color in the MaterialApp would also modify the color in TextFields used elsewhere.

One solution is you can change the color before even opening the SearchDelegate i.e before showSearch and change it back again to the original color after navigating back from showSearch.

Upvotes: 1

Related Questions