maheshj01 / searchfield

A highly customizable simple and easy to use flutter Widget to add a searchfield to your Flutter Application.This Widget allows you to search and select from list of suggestions.
MIT License
84 stars 63 forks source link

Exception: RangeError (index): Index out of range: index must not be negative: -1 #174

Closed twfungi closed 1 month ago

twfungi commented 1 month ago

Describe the bug Exception: RangeError (index): Index out of range: index must not be negative: -1 This issue exists for a while.

To Reproduce Steps to reproduce the behavior:

  1. Add a suggestion
  2. Click on this newly added suggestion
  3. OnSuggestionTap: context.push to another widget
  4. Back to the searchfield widget
  5. Often this exception occurs in step 3.

[ X] By clicking this checkbox, I confirm I am using the latest version of the package found on pub.dev/searchfield 1.1.1

Expected behavior No assertion failure

Actual behavior Assertion failure

Screenshots ======== Exception caught by widgets library ======================================================= The following IndexError was thrown building RawGestureDetector(state: RawGestureDetectorState#b6d79(gestures: [tap])): RangeError (index): Index out of range: index must not be negative: -1

The relevant error-causing widget was: ... When the exception was thrown, this was the stack: dart-sdk/lib/_internal/js_dev_runtime/private/ddcruntime/errors.dart 296:3 throw dart-sdk/lib/_internal/js_dev_runtime/private/js_array.dart 592:7 _get] packages/searchfield/src/searchfield.dart 668:46 dart-sdk/lib/_internal/js_dev_runtime/private/js_array.dart 624:15 indexWhere] packages/searchfield/src/searchfield.dart 667:39 didUpdateWidget packages/flutter/src/widgets/framework.dart 5789:55 update packages/flutter/src/widgets/framework.dart 3941:14 updateChild packages/flutter/src/widgets/framework.dart 6907:14 update packages/flutter/src/widgets/framework.dart 3941:14 updateChild packages/flutter/src/widgets/framework.dart 4090:32 updateChildren

Without knowing the root case, my temporary workaround in searchfield.dart :666 Adding a few checks can avoid the exception, and I haven't observed any side effect.

// if a item was already selected
      if (selected != null && selected! >= 0 && selected! < oldWidget.suggestions.length) {
        selected = widget.suggestions.indexWhere(
            (element) => element == oldWidget.suggestions[selected!]);
      }
maheshj01 commented 1 month ago

Hi @twfungi, Thanks for filing the issue. Can you please share a minimal reproducible code sample for me to investigate.

Thanks

twfungi commented 1 month ago

STR

  1. Enter "Q" and select the suggestion "Queen". It will go to Page2.
  2. Press Back button to go from Page2 to HomePage.
  3. Click on "Queen" in the TextEdit, and the exception will occur without any edit.

Console messages

Performing hot restart... Waiting for connection from debug service on Chrome... Restarted application in 184ms. [log] [onSuggestionTap] selected Queen

======== Exception caught by widgets library ======================================================= The following IndexError was thrown building RawGestureDetector(state: RawGestureDetectorState#4f3ec(gestures: [tap])): RangeError (index): Index out of range: index must not be negative: -1

The relevant error-causing widget was: GestureDetector GestureDetector:file:///Users/.../AndroidStudioProjects/searchfield_issue1/lib/main.dart:98:15 When the exception was thrown, this was the stack: dart-sdk/lib/_internal/js_dev_runtime/private/ddcruntime/errors.dart 296:3 throw dart-sdk/lib/_internal/js_dev_runtime/private/js_array.dart 592:7 _get] packages/searchfield/src/searchfield.dart 669:46 dart-sdk/lib/_internal/js_dev_runtime/private/js_array.dart 624:15 indexWhere] packages/searchfield/src/searchfield.dart 668:39 didUpdateWidget packages/flutter/src/widgets/framework.dart 5789:55 update packages/flutter/src/widgets/framework.dart 3941:14 updateChild packages/flutter/src/widgets/framework.dart 6907:14 update packages/flutter/src/widgets/framework.dart 3941:14 updateChild

The complete source code

import 'package:flutter/material.dart';
import 'package:searchfield/searchfield.dart';
import 'dart:developer' as d;

void main() {
  runApp(const MyApp());
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});
  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> with AutomaticKeepAliveClientMixin {
  @override
  bool wantKeepAlive = true;
  final focus = FocusNode();
  final _controller = TextEditingController();
  bool submitted = false;
  var suggestions = <String>["Alpha", "Beta", "Charlie", "Delta", "Echo", "Fox", "Gamma", "Hello", "Indigo"];

  var defaultSuggestions = <String>[
    "Alpha",
    "Beta",
    "Charlie",
    "Delta",
    "Echo",
    "Fox",
    "Gamma",
    "Hello",
    "Indigo",
    "Jimmy",
    "Kitty",
    "Lemon",
    "Monkey",
    "Nimo",
    "Oxford",
    "Peter",
    "Queen",
    "Rabbit",
    "Snake",
    "Timothy",
    "Uncle",
    "Victor",
    "Whiskey",
    "Xray",
    "Yellow",
    "Zullu"
  ];

  @override
  void initState() {
    super.initState();
    suggestions = getSuggestions();
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    super.build(context);
    Widget searchChild(x) => Padding(
          padding: const EdgeInsets.symmetric(vertical: 4.0, horizontal: 12),
          child: Text(x, style: Theme.of(context).textTheme.bodyMedium!),
        );

    SuggestionDecoration suggestionDecoration = SuggestionDecoration(
      elevation: 16.0,
      color: Colors.white54,
      borderRadius: BorderRadius.circular(24),
    );

    return Scaffold(
        appBar: AppBar(centerTitle: true, automaticallyImplyLeading: true, title: const Text('test')),
        body: GestureDetector(
            onTap: () {
              FocusScope.of(context).unfocus();
            },
            child: SizedBox(
                height: double.infinity,
                width: double.infinity,
                child: Column(
                  children: [
                    SizedBox(
                        width: 400,
                        child: SearchField(
                          controller: _controller,
                          onSearchTextChanged: (query) {
                            List<String> filter;
                            query = query.trim();
                            if (query.isEmpty) {
                              filter = suggestions = getSuggestions();
                            } else {
                              filter = defaultSuggestions
                                  .where((element) => element.toLowerCase().contains(query.toLowerCase()))
                                  .toList();
                            }
                            return filter.map((e) => SearchFieldListItem<String>(e, child: searchChild(e))).toList();
                          },
                          onSubmit: (str) async {
                            submitted = true;
                            d.log('[onSubmit] $str');
                            saveSuggestion(str.trim());
                            if (mounted) {
                              await Navigator.push(
                                  context, MaterialPageRoute(builder: (context) => const Page2(title: 'onSubmit')));
                            }
                          },
                          initialValue: null,
                          onTap: () async {
                            suggestions = getSuggestions();
                            setState(() {});
                          },
                          showEmpty: false,
                          emptyWidget: Container(
                              decoration: suggestionDecoration, height: 200, child: const Center(child: Text('Empty'))),
                          key: const Key('searchfield'),
                          dynamicHeight: true,
                          maxSuggestionBoxHeight: 300,
                          scrollbarDecoration: ScrollbarDecoration(minThumbLength: 30, thickness: 10),
                          onTapOutside: null,
                          suggestionDirection: SuggestionDirection.down,
                          suggestionStyle: Theme.of(context).textTheme.bodyMedium!,
                          searchInputDecoration: SearchInputDecoration(
                            searchStyle: Theme.of(context).textTheme.bodyMedium!,
                            prefixIcon: const Icon(Icons.search),
                            fillColor: Colors.white38,
                            suffixIcon: IconButton(
                              onPressed: _controller.clear,
                              icon: const Icon(Icons.clear),
                            ),
                            focusedBorder: OutlineInputBorder(
                              borderRadius: BorderRadius.circular(24),
                              borderSide: const BorderSide(
                                width: 1,
                                color: Colors.grey,
                                style: BorderStyle.solid,
                              ),
                            ),
                            border: OutlineInputBorder(
                              borderRadius: BorderRadius.circular(24),
                              borderSide: const BorderSide(
                                width: 1,
                                color: Colors.black,
                                style: BorderStyle.solid,
                              ),
                            ),
                            filled: true,
                            contentPadding: const EdgeInsets.symmetric(
                              horizontal: 20,
                            ),
                          ),
                          suggestionsDecoration: suggestionDecoration,
                          suggestions:
                              suggestions.map((e) => SearchFieldListItem<String>(e, child: searchChild(e))).toList(),
                          focusNode: focus,
                          suggestionState: Suggestion.expand,
                          onSuggestionTap: (SearchFieldListItem<String> x) {
                            submitted = false;
                            focus.unfocus();
                            d.log('[onSuggestionTap] selected ${x.searchKey}');
                            saveSuggestion(x.searchKey);
                            Future.delayed(const Duration(milliseconds: 200), () async {
                              if (!submitted) {
                                if (mounted) {
                                  await Navigator.push(context,
                                      MaterialPageRoute(builder: (context) => const Page2(title: 'onSuggestionTap')));
                                }
                              }
                            });
                          },
                        )),
                  ],
                ))));
  }

  List<String> getSuggestions() {
    return suggestions;
  }

  void saveSuggestion(String item) {
    if (item.isNotEmpty) {
      suggestions.removeWhere((e) => e == item);
      suggestions.insert(0, item);
      if (suggestions.length > 10) {
        suggestions.removeLast();
      }
    }
  }
}

class Page2 extends StatefulWidget {
  const Page2({super.key, required this.title});
  final String title;

  @override
  State<Page2> createState() => _Page2State();
}

class _Page2State extends State<Page2> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(centerTitle: true, automaticallyImplyLeading: true, title: Text(widget.title)),
        body: Center(child: Text('This is the Page2 from [${widget.title}]')));
  }
}
hob-nguyen commented 1 month ago

I'm getting same issue here. Please check it

maheshj01 commented 1 month ago

Fixed and released in searchfield: ^1.1.2