可搜索的 SliverGrid 呈现错误的项目

Searchable SliverGrid Rendering Wrong Items

我有一个 SliverGrid。我有一个搜索字段。在我的搜索字段 onChange 事件中,我有一个函数可以根据用户输入的关键字搜索我的本地 sqlite 数据库 returns 结果并重新分配给一个变量并调用 notifyListeners()。现在我的问题是出于某种奇怪的原因,每当我搜索一个项目时,就会呈现错误的项目。

我通过遍历列表并记录标题和总计数来检查函数的结果,结果是正确的,但是我的视图总是呈现错误的项目。不知道这怎么可能。

我还注意到一些奇怪的事情,每当它呈现错误的项目时,我回到我的代码并点击保存,触发实时重新加载,当我切换回我的模拟器时,它现在显示正确的项目。

我已经在实际 phone 上尝试过发布版本,结果是一样的。另一件奇怪的事情是,有时某些项目会在用户输入时重复并在我的列表中显示两次。

这是我搜索 sqlite 数据库的函数:

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;
  }
}

这是我的函数,它从我的视图模型调用上述函数:

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

这是我拥有 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();
}

现在我什至首先使用 SliverGrid 的原因是因为我在网格上方有一个搜索字段和一个标题,我希望所有项目都与页面一起滚动,我不想要列表可滚动。

我相信这种奇怪的行为可以归因于您在 onChanged 中调用 vm.getBooksByKeyword()。由于这是一个 async 方法,因此不能保证最后返回的结果将是 TextFormField 中最终文本的结果。您在实时重新加载后看到正确结果的原因是因为正在使用当前在 TextFormField.

中的全文再次调用该方法

验证这一点的最快方法是将函数调用移动到 onFieldSubmittedonEditingComplete 并查看其行为是否正确。

如果您需要在每次更改文本时调用该函数,您将需要向 controller 添加一个侦听器,并确保仅在输入停止指定时间后才进行调用,使用 Timer,像这样:

final _controller = TextEditingController();
Timer _timer;

...

_controller.addListener(() {
  _timer?.cancel();
  if(_controller.text.isNotEmpty) {
    // only call the search method if keyword text does not change for 300 ms
    _timer = Timer(Duration(milliseconds: 300),
              () => vm.getBooksByKeyword(_controller.text));
  }
});

...

@override
void dispose() {
  // DON'T FORGET TO DISPOSE OF THE TextEditingController
  _controller.dispose();
  super.dispose();
}

...

TextFormField(
  controller: controller,
  ...
);

所以我找到了问题和解决方案:

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.

摘自 here.

所以基本上解决方案是为生成的每个列表项添加并设置一个键 属性:

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()),
  ),
)

还有这里:

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

我的搜索现在完美无缺。 :)