可搜索的 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
.
中的全文再次调用该方法
验证这一点的最快方法是将函数调用移动到 onFieldSubmitted
或 onEditingComplete
并查看其行为是否正确。
如果您需要在每次更改文本时调用该函数,您将需要向 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);
我的搜索现在完美无缺。 :)
我有一个 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
.
验证这一点的最快方法是将函数调用移动到 onFieldSubmitted
或 onEditingComplete
并查看其行为是否正确。
如果您需要在每次更改文本时调用该函数,您将需要向 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);
我的搜索现在完美无缺。 :)