PlutoGrid不能替换行并重新构建网格
给定如下小部件:
import 'package:cerebellum/const.dart';
import 'package:cerebellum/models/api/patientfinder.dart';
import 'package:cerebellum/models/api/patientfindrec.dart';
import 'package:cerebellum/utils/dateutil.dart';
import 'package:flutter/material.dart';
import 'package:flutter_it/flutter_it.dart';
import 'package:pluto_grid/pluto_grid.dart';
class PatientFinderGridScreen extends StatefulWidget {
final List<PatientFindRec> patients;
const PatientFinderGridScreen({super.key, required this.patients});
@override
State<PatientFinderGridScreen> createState() =>
_PatientFinderGridScreenState();
}
class _PatientFinderGridScreenState extends State<PatientFinderGridScreen> {
final List<PlutoColumn> columns = [];
final List<PlutoRow> rows = [];
PlutoGridStateManager? stateManager;
@override
void initState() {
super.initState();
_buildGridColumns();
_handleIncomingDataUpdate();
}
@override
void didUpdateWidget(covariant PatientFinderGridScreen oldWidget) {
super.didUpdateWidget(oldWidget);
// TODO: what if the signal's value was not replaced but altered???
if (widget.patients != oldWidget.patients) {
_handleIncomingDataUpdate();
}
}
void _handleIncomingDataUpdate() {
_buildGridRows(widget.patients);
if (stateManager != null) {
stateManager!.refRows.clear();
stateManager!.refRows.addAll(rows);
stateManager!.notifyListeners();
}
// Tell the data model that the current patient has changed.
// Not important for this question
di<PatientFinderModel>().patient.value = widget.patients.isEmpty
? null
: widget.patients[0];
}
void _buildGridRows(List<PatientFindRec> patientList) {
rows.clear();
rows.addAll(
patientList.map((patient) {
return PlutoRow(
cells: {
'id': PlutoCell(value: patient.id),
// more cell values are here...
},
);
}).toList(),
);
}
void _gridStateListener() {
if (stateManager == null) return;
final cr = stateManager!.currentRowIdx;
// Tell the model that the current patient has changed
// Not important for this question
if (cr == null) {
di<PatientFinderModel>().patient.value = null;
} else {
if (cr >= 0 && cr < widget.patients.length) {
di<PatientFinderModel>().patient.value = widget.patients[cr];
}
}
}
@override
void dispose() {
stateManager?.removeListener(_gridStateListener);
super.dispose();
}
void _onGridLoaded(PlutoGridOnLoadedEvent event) {
stateManager = event.stateManager;
stateManager!.setShowColumnFilter(false); // turn off in-memory filter
stateManager!.addListener(_gridStateListener);
// Tell the data model that the current patient has changed.
di<PatientFinderModel>().patient.value = widget.patients.isEmpty
? null
: widget.patients[0];
}
void _buildGridColumns() {
columns.addAll([
PlutoColumn(
title: 'Identifier',
field: 'id',
type: PlutoColumnType.text(),
enableFilterMenuItem: false,
width: 140,
),
// more columns are here...
]);
}
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: PlutoGrid(
columns: columns,
rows: rows,
onLoaded: _onGridLoaded,
// this is a constant value taken from the const module, not important for this question
configuration: PlutoGridConfiguration(
localeText: plutoLocaleText,
style: plutoGridStyle(context),
shortcut: PlutoGridShortcut(),
),
),
);
}
}
注意:PatientFindRec 只是一个freezed类,PatientFinderModel 是一个全局实例,用于在 Signal 个实例中保存应用状态。
问题在于:初始时,patients 是一个空列表。当我把 patients 改成非空列表时,_PatientFinderGridScreenState._handleIncomingDataUpdate 会被调用,stateManager 是非空的,但网格在界面上并未更新。即使我向 stateManager.refRows 增加了100行,并且也调用了 stateManager.notifyListeners(),也什么都没有发生。网格只显示列头。
也尝试了下面这个,但不起作用:
void _handleIncomingDataUpdate() {
_buildGridRows(widget.patients);
if (stateManager != null) {
stateManager!.removeAllRows();
stateManager!.appendRows(rows);
stateManager!.notifyListeners();
}
如果我把这个widget从 widget树中移除,然后在初始时使用一个非空的 patients 值重新挂载,它就能正确显示行。但我不能这么做,因为PlutoGrid在挂载时会抢走焦点。我不能改变焦点(因为网格显示的是实时搜索的结果,更新网格时焦点必须保持在搜索文本框内)。
那么,为什么网格在屏幕上没有更新呢?缺少了什么?
是否有关于PlutoGrid,特别是PlutoGridStateManager的权威文档?我所能找到的只有一些演示视频和所谓的API参考(https://pub.dev/documentation/pluto_grid/latest/pluto_grid/PlutoGridStateManager-class.html)。没有任何一个讲清楚状态管理器到底应该怎样使用。
解决方案
经过几个小时的折腾,我终于找到了答案。但首先,PlutoGrid的确有关于状态管理器的文档,只是因为某种原因我没能找到它:https://pluto.weblaze.dev/add-and-remove-columns-and-rows
我代码的问题在于PlutoGrid是用 rows: rows 初始化的。这意味着 _PatientFinderGridScreenState.rows 就是同一个对象。看起来状态管理器并不会创建它内部的行列表,它直接使用传给网格的那个列表。因此当我调用 stateManager!.removeAllRows() 时,它实际上是在清空同一个列表。它变成了空的。之后,当我调用 stateManager!.appendRows(rows) 时,它又从一个空列表中追加项目。结果,网格就变成空的。
结论:如果把一个列表传给 PlutoGrid,那么这个列表不应该再用于其他用途。你必须使用另一份列表来计算新行;如果你想调用 stateManager.appendRows() 或 prependNewRows 等,也必须使用另一份列表。
在我的示例中,我把 _handleIncomingDataUpdate和 _buildGridRows合并成这个,现在它起作用了!
void _buildGridRows(List<PatientFindRec> patientList) {
final newRows = patientList.map((patient) {
return PlutoRow(
cells: {
'id': PlutoCell(value: patient.id),
// more cell values are here...
},
); }).toList();
// https://pluto.weblaze.dev/add-and-remove-columns-and-rows
if (stateManager != null) {
stateManager!.removeAllRows();
stateManager!.appendRows(newRows);
stateManager!.notifyListeners();
}
// Possibly select the patient that was previously selected?
di<PatientFinderModel>().patient.value = widget.patients.isEmpty
? null
: widget.patients[0];
}