1

I have a huge json data which I'm loading from assets. I need to search from the recipe's using recipeName and I'm using flutter typeahead to create the search bar and functionalities.

I'm loading all the recipe objects when the app starts. The below code basically returns all the recipes.

  List<RecipeModel> getAllRecipes() {
    for (int i = 0; i < _appData.recipeCategories!.length; i++) {
      for (int j = 0; j < _appData.recipeCategories![i].recipes!.length; j++) {
        _recipeList.add(_appData.recipeCategories![i].recipes![j]);
      }
    }
    return _recipeList;
  }

This code is responsible for the search(Search.dart):

class Search {
  List<RecipeModel> _recipeList = Store.instance.getAllRecipes(); // Store loads data during splash screen
  late RecipeModel recipe;
  RecipeModel returnRecipe(String? suggestion) {
    for (int i = 0; i < _recipeList.length; i++) {
      if (suggestion == _recipeList[i].recipeName) {
        return _recipeList[i];
      }
    }

    return recipe;
  }
}

And now comes the typeahead part. Flutter typeahead takes 4 parameter textFieldConfiguration, suggestionsCallback, itemBuilder, onSuggestionSelected

for suggestionsCallback I'm loading all the recipenames into a list when the screen loads and searching from that list using:

  Future<List<String>> getSuggestions(String str) async {
    return List.of(recipeNamesList).where(
      (recipe) {
        final recipeLower = recipe.toLowerCase();
        final queryLower = str.toLowerCase();

        return recipeLower.contains(queryLower);
      },
    ).toList();
  }

and here's the entire typeahead widget:

  Widget buildSearchTextInput() {
    return Container(
      padding: EdgeInsets.all(10),
      child: TypeAheadFormField<String?>(
        textFieldConfiguration: TextFieldConfiguration(
          controller: _textEditingController,
          focusNode: _focusNode,
          autofocus: true,
          decoration: InputDecoration(
            hintText: "Enter recipe name",
          ),
        ),
        suggestionsCallback: (query) async {
          print("query: $query");
          if (query.length == 0) return [];
          return await getSuggestions(query);
        },
        hideOnEmpty: true,
        itemBuilder: (context, String? suggestion) =>
            SearchResultCard(suggestion: suggestion),
        onSuggestionSelected: (String? suggestion) {
          openRecipeDetailsPage(
            context,
            Search().returnRecipe(suggestion),
          );
        },
      ),
    );
  }

As you can see I'm using the afore mentioned class for a ad-hoc search. Now for openSuggestionsSelected I have to send the user to a recipeDetailsScreen which takes a object of RecipeModel.

  1. I was thinking all though I'm going ad-hoc search for this can I reduce the clutter to some extent? I mean apart from the function to load all the recipes in store I have two list to work one. One carries names of the recipes and one carries the all the recipes. Is there a way I can do it using one list?

  2. Can I improve the efficiency for the search? I don't think working with two list is a good idea.

N.B: The solution I've written works. I just need something that'll remove the clutter and hopefully reduce the time complexity of the search or at the very least improve functional efficiency.

  • Related: [Trie complexity and searching](https://stackoverflow.com/q/13032116/9997212) – enzo Jun 01 '21 at 22:28
  • Hi was actually looking for something very simple in order to implement the search function responsible for returning the object. I guess implementing the trie would take a lot of time? –  Jun 02 '21 at 20:31
  • No need to implement it, there are already [a package that does that for you](https://pub.dev/packages/trie). It's even optimized for autocompleting tasks. – enzo Jun 02 '21 at 20:34
  • are you familiar with typeahead? I need to return a object. After looking at the package I don't think it can do so? –  Jun 02 '21 at 20:38
  • Have you read the `trie` documentation? Just use the [`getAllWordsWithPrefix`](https://pub.dev/documentation/trie/latest/trie/Trie/getAllWordsWithPrefix.html) method passing the `query` to it and return it in the `suggestionsCallback`, e.g. `trie.getAllWordsWithPrefix(query)`. – enzo Jun 02 '21 at 20:41
  • yes I have read it. My confusion is `getSuggestionCallback` returns a list of string right? in this case the names of recipe. But I need to pass an object for the `onSuggestionSelected` part and if you see my `Search()` you will see that I'm ad-hoc searching again for the `recipe` obejct. I wanna know how a trie would solve this issue? –  Jun 02 '21 at 20:44
  • The trie would reduce the complexity of the typeahead part, since currently is O(n²) and with trie this would be reduced - that's why I wrote "Related" on the first comment. For the search part, you could use a Map where each key is a recipe name mapping to its recipe. Currently, the search is O(n), but with map it will be O(1). – enzo Jun 02 '21 at 20:50
  • Okay then that takes care of the recipeName search and keeping you're asking me to use the recipeNames to map to it's recipe. and return it for the onSuggestionSelected part right? –  Jun 02 '21 at 20:53
  • I've added an answer, see if this helps. – enzo Jun 02 '21 at 21:07
  • @enzo hi sorry to bother you again. But do you have any suggestion for the null safety issue? I wanted to use autotrie but that's not possible so I've to work with trie but it doesn't have the null safety. so can i take the source code and add null safety, hopefully if that works maybe even contribute to the project? –  Jun 03 '21 at 10:04

1 Answers1

0

You can reduce complexity in two ways in your code:

  1. Using a Map<String, RecipeModel> for your search part:
class Search {
  // This will still be O(n)
  Map<String, RecipeModel> _map = Map.fromIterable(
    Store.instance.getAllRecipes(),
    key: (recipe) => recipe.name);

  late RecipeModel recipe;

  RecipeModel returnRecipe(String? suggestion) {
    if (suggestion == null) return recipe;
    // This will be O(1) instead of O(n) [better]
    final RecipeModel? found = _map[suggestion];
    return found ?? recipe;
  }
}
  1. Using a Trie in your typeahead part (here I'm using trie, but you can use autotrie instead):
 class Search {
  final Map<String, RecipeModel> _map = Map.fromIterable(
    Store.instance.getAllRecipes(),
    key: (recipe) => recipe.name);

  final late Trie trie;

  Search() {
    // This will be O(n)
    trie = Trie.list(map.keys().toList());
  }

  // ...

  List<String> returnSuggestions(String prefix) {
    // This will be O(W*L) instead of O(n^2) [better]
    return trie.getAllWordsWithPrefix(prefix);
  }
}

Changing in your code:

final Search search = Search();

// ...

Widget buildSearchTextInput() {
  return Container(
    padding: EdgeInsets.all(10),
    child: TypeAheadFormField<String?>(
      // ...
      suggestionsCallback: (query) async {
        if (query.isEmpty) return [];
        return search.returnSuggestions(query);
      },
      // ...
      onSuggestionSelected: (String? suggestion) {
        openRecipeDetailsPage(context, search.returnRecipe(suggestion));
      },
    ),
  );
}

You will still have two "lists", but your complexity got better in general.

enzo
  • 9,861
  • 3
  • 15
  • 38
  • alright I get the idea. I was way too wrapped up in the idea of two list(one for recipeNames and one for recipe objects) that I completely oversaw the simple thing. –  Jun 02 '21 at 21:09
  • the solution worked except the issue with null safety. I think I would migrate to autotrie since it has null safety –  Jun 02 '21 at 21:23