Search code examples
flutterflutter-providerflutter-sliver

Searchable SliverGrid Rendering Wrong Items


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. :)