Searchable SliverGrid Rendering Wrong Items

Issue

I have a SliverGrid. I have a search field. In my search field onChange event I have a function that searches my local sqlite db based on the keyword entered by the user returns the results and reassigns to a variable and calls notifyListeners(). Now my problem is for some weird reason whenever I search for an item the wrong item is rendered.

I checked the results from my functions by iterating over the list and logging the title and the overall count as well and the results were correct however my view always rendered the wrong items. Not sure how this is possible.

I also noticed something strange, whenever it rendered the wrong item and I went back to my code and hit save, triggering live reload, when I switched back to my emulator it now displayed the right item.

I have tried the release build on an actual phone and it’s the same behaviour. Another weird thing is sometimes certain items will duplicate and show twice in my list while the user is typing.

This is my function that searches my sqlite db:

Future<List<Book>> searchBookshelf(String keyword) async {
  try {
    Database db = await _storageService.database;
    final List<Map<String, dynamic>> rows = await db
        .rawQuery("SELECT * FROM bookshelf WHERE title LIKE '%$keyword%'; ");

    return rows.map((i) => Book.fromJson(i)).toList();
  } catch (e) {
    print(e);
    return null;
  }
}

This is my function that calls the above function from my viewmodel:

Future<void> getBooksByKeyword(String keyword) async {
  books = await _bookService.searchBookshelf(keyword);
  notifyListeners();
}

This is my actual view where i have the SliverGrid:

class BooksView extends ViewModelBuilderWidget<BooksViewModel> {
  @override
  bool get reactive => true;

  @override
  bool get createNewModelOnInsert => true;

  @override
  bool get disposeViewModel => true;

  @override
  void onViewModelReady(BooksViewModel vm) {
    vm.initialise();
    super.onViewModelReady(vm);
  }

  @override
  Widget builder(BuildContext context, vm, Widget child) {
    var size = MediaQuery.of(context).size;
    final double itemHeight = (size.height) / 4.3;
    final double itemWidth = size.width / 3;

    var heading = Container(
      margin: EdgeInsets.only(top: 35),
      padding: const EdgeInsets.symmetric(horizontal: 20),
      child: Align(
        alignment: Alignment.centerLeft,
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              'Books',
              textAlign: TextAlign.left,
              style: TextStyle(fontSize: 24, fontWeight: FontWeight.w900),
            ),
            Text(
              'Lorem ipsum dolor sit amet.',
              textAlign: TextAlign.left,
              style: TextStyle(fontSize: 14),
            ),
          ],
        ),
      ),
    );

    var searchField = Container(
      margin: EdgeInsets.only(top: 5, left: 15, bottom: 15, right: 15),
      decoration: BoxDecoration(
        color: Colors.white,
        borderRadius: BorderRadius.all(Radius.circular(15)),
        boxShadow: [
          BoxShadow(
            color: Colors.black12,
            blurRadius: 1.0,
            spreadRadius: 0.0,
            offset: Offset(2.0, 1.0), // shadow direction: bottom right
          ),
        ],
      ),
      child: TextFormField(
        decoration: InputDecoration(
          border: InputBorder.none,
          prefixIcon: Icon(
            FlutterIcons.search_faw,
            size: 18,
          ),
          suffixIcon: Icon(
            FlutterIcons.filter_fou,
            size: 18,
          ),
          hintText: 'Search...',
        ),
        onChanged: (keyword) async {
          await vm.getBooksByKeyword(keyword);
        },
        onFieldSubmitted: (keyword) async {},
      ),
    );

    return Scaffold(
        body: SafeArea(
            child: Container(
                padding: EdgeInsets.only(left: 1, right: 1),
                child: LiquidPullToRefresh(
                  color: Colors.amber,
                  key: vm.refreshIndicatorKey, // key if you want to add
                  onRefresh: vm.refresh,
                  showChildOpacityTransition: true,
                  child: CustomScrollView(
                    slivers: [
                      SliverToBoxAdapter(
                        child: Column(
                          children: [
                            heading,
                            searchField,
                          ],
                        ),
                      ),
                      SliverToBoxAdapter(
                        child: SpaceY(15),
                      ),
                      SliverToBoxAdapter(
                        child: vm.books.length == 0
                            ? Column(
                                children: [
                                  Image.asset(
                                    Images.manReading,
                                    width: 250,
                                    height: 250,
                                    fit: BoxFit.contain,
                                  ),
                                  Text('No books in your bookshelf,'),
                                  Text('Grab a book from our bookstore.')
                                ],
                              )
                            : SizedBox(),
                      ),
                      SliverPadding(
                        padding: EdgeInsets.only(bottom: 35),
                        sliver: SliverGrid.count(
                          childAspectRatio: (itemWidth / itemHeight),
                          mainAxisSpacing: 20.0,
                          crossAxisCount: 3,
                          children: vm.books
                              .map((book) => BookTile(book: book))
                              .toList(),
                        ),
                      )
                    ],
                  ),
                ))));
  }

  @override
  BooksViewModel viewModelBuilder(BuildContext context) =>
      BooksViewModel();
}

Now the reason I am even using SliverGrid in the first place is because I have a search field and a title above the grid and I want all items to scroll along with the page, I didn’t want just the list to be scrollable.

Solution

So I found the problem and the solution:

The widget tree is remembering the list items place and providing the
same viewmodel as it had originally. Not only that it also takes every
item that goes into index 0 and provides it with the same data that
was enclosed on the Construction of the object.

Taken from here.

So basically the solution was to add and set a key property for each list item generated:

SliverPadding(
  padding: EdgeInsets.only(bottom: 35),
  sliver: SliverGrid(
    gridDelegate:
        SliverGridDelegateWithFixedCrossAxisCount(
      crossAxisCount: 3,
      childAspectRatio: (itemWidth / itemHeight),
      mainAxisSpacing: 20.0,
    ),
    delegate: SliverChildListDelegate(vm.books
        .map((book) => BookTile(
            key: Key(book.id.toString()), book: book))
        .toList()),
  ),
)

And also here:

const BookTile({Key key, this.book}) : super(key: key, reactive: false);

My search works perfectly now. 🙂

Answered By – user3718908x100

Answer Checked By – Mary Flores (FlutterFixes Volunteer)

Leave a Reply

Your email address will not be published.