Flutter中如何实现GridView网格项的局部刷新,避免全量重建以优化性能?
我明白你的痛点——16×16的网格每次操作就全量重建,不仅掉帧还拉低体验,尤其是还有填充、画线这种批量修改单元格的逻辑,确实头疼。咱们一步步解决这个问题,核心思路就是让每个网格项只监听自己的状态变化,实现类似React.memo的精准刷新效果。
先分析你之前的问题根源:你用了RxList<RxList<String>>这种嵌套的可观察列表,虽然每个子列表是可监听的,但外层大列表也是可观察的——只要任何一个单元格变化,整个外层列表的监听就会触发,导致GridView的所有item跟着重建。GridView.builder本身只是复用item的布局容器,但状态变化触发的全量重建它拦不住。
下面给你两种适配你场景的可行方案,结合你的现有代码调整:
方案一:ValueNotifier+ValueListenableBuilder(最直观,适配网格场景)
这个方案让每个单元格绑定独立的状态监听,只有自身状态变化时才会重建,完全不影响其他单元格。
步骤1:修改ArtController的状态结构
把嵌套的RxList换成二维ValueNotifier数组,每个单元格对应一个独立的ValueNotifier:
class ArtController extends GetxController { static ArtController get to => Get.find(); static const String eraseColor = "0xFFFFFFFF"; // 替换成你实际的擦除色格式 // 每个单元格用独立的ValueNotifier存储颜色 late List<List<ValueNotifier<String>>> drawList; @override void onInit() { super.onInit(); // 初始化16×16的ValueNotifier网格 drawList = List.generate( 16, (row) => List.generate( 16, (col) => ValueNotifier<String>(eraseColor), ), ); } // 修改单个单元格颜色的方法 void setDrawList(int col, int row, String color) { drawList[row][col].value = color; } // 保留你原有的getActivePaintColor、calculateFill等方法 }
步骤2:重构GridView的网格项
每个网格项用ValueListenableBuilder包裹,只监听对应位置的ValueNotifier变化:
GridView.builder( gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 16, crossAxisSpacing: 0.5, mainAxisSpacing: 0.5, ), itemCount: 16 * 16, itemBuilder: (context, index) { final row = index ~/ 16; final col = index % 16; final cellNotifier = ArtController.to.drawList[row][col]; return ValueListenableBuilder<String>( valueListenable: cellNotifier, // 静态部分(比如边框)用child参数传递,避免重复重建 child: Container( decoration: BoxDecoration( border: Border.all(color: Colors.grey.shade200, width: 0.3), ), ), builder: (context, currentColor, staticChild) { return GestureDetector( // 绑定你原有的点击、拖拽逻辑 onTapDown: (_) { ArtController.to.calculateFill(CalculateFillParams( rowIndex: row, columnIndex: col, pointerColor: cellNotifier.value, )); }, // 复用静态child,只更新颜色部分 child: Container( color: Color(int.parse(currentColor)), child: staticChild, ), ); }, ); }, )
步骤3:调整递归填充逻辑
你的calculateFill递归修改多个单元格时,直接调用setDrawList即可——每个修改只会触发对应单元格的刷新,不会批量重建:
void calculateFill(CalculateFillParams params) { int rowIndex = params.rowIndex; int columnIndex = params.columnIndex; String pointerColor = params.pointerColor; final targetColor = ArtController.to.getActivePaintColor.color; // 边界判断与填充条件 if (rowIndex >= 0 && rowIndex < 16 && columnIndex >= 0 && columnIndex < 16 && ArtController.to.drawList[rowIndex][columnIndex].value == pointerColor) { // 修改当前单元格 ArtController.to.setDrawList(columnIndex, rowIndex, targetColor); // 递归填充上下左右单元格 calculateFill(CalculateFillParams( rowIndex: rowIndex, columnIndex: columnIndex - 1, pointerColor: pointerColor)); calculateFill(CalculateFillParams( rowIndex: rowIndex + 1, columnIndex: columnIndex, pointerColor: pointerColor)); calculateFill(CalculateFillParams( rowIndex: rowIndex, columnIndex: columnIndex + 1, pointerColor: pointerColor)); calculateFill(CalculateFillParams( rowIndex: rowIndex - 1, columnIndex: columnIndex, pointerColor: pointerColor)); } }
方案二:GetX GetBuilder+Tag(精准批量刷新)
如果你习惯用GetX的状态管理体系,可以用GetBuilder的Tag机制,给每个单元格打唯一标签,修改时只刷新对应标签的item。
步骤1:修改ArtController的状态与更新逻辑
换成普通二维列表存储颜色,同时记录需要刷新的单元格标签,实现批量更新:
class ArtController extends GetxController { static ArtController get to => Get.find(); static const String eraseColor = "0xFFFFFFFF"; // 普通二维列表存储颜色 late List<List<String>> drawList; // 记录需要刷新的单元格标签 final List<String> _dirtyCells = []; @override void onInit() { super.onInit(); drawList = List.generate( 16, (row) => List.generate(16, (col) => eraseColor), ); } void setDrawList(int col, int row, String color, {bool shouldRefresh = true}) { drawList[row][col] = color; final cellTag = "cell_$row_$col"; if (!_dirtyCells.contains(cellTag)) { _dirtyCells.add(cellTag); } // 批量刷新:仅最后一次调用时统一更新 if (shouldRefresh) { update(_dirtyCells); _dirtyCells.clear(); } } // 保留你原有的其他方法 }
步骤2:重构GridView的网格项
每个item用GetBuilder,指定唯一的cell标签:
GridView.builder( gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount:16, crossAxisSpacing:0.5, mainAxisSpacing:0.5 ), itemCount:16*16, itemBuilder:(context, index) { final row = index ~/16; final col = index %16; final cellTag = "cell_$row_$col"; return GetBuilder<ArtController>( tag: cellTag, builder: (controller) { final currentColor = controller.drawList[row][col]; return GestureDetector( onTapDown: (_) { // 递归填充时先批量修改,不触发刷新 controller.calculateFill(CalculateFillParams( rowIndex: row, columnIndex: col, pointerColor: currentColor)); // 填充完成后统一刷新所有修改的单元格 controller.update(controller._dirtyCells); controller._dirtyCells.clear(); }, child: Container( color: Color(int.parse(currentColor)), decoration: BoxDecoration( border: Border.all(color: Colors.grey.shade200, width:0.3) ), ), ); }, ); }, )
步骤3:调整递归填充逻辑
递归修改时关闭自动刷新,最后统一批量更新:
void calculateFill(CalculateFillParams params) { int rowIndex = params.rowIndex; int columnIndex = params.columnIndex; String pointerColor = params.pointerColor; final targetColor = ArtController.to.getActivePaintColor.color; if (rowIndex >=0 && rowIndex <16 && columnIndex >=0 && columnIndex <16 && ArtController.to.drawList[rowIndex][columnIndex] == pointerColor) { // 递归时不自动刷新,标记为待更新单元格 ArtController.to.setDrawList(columnIndex, rowIndex, targetColor, shouldRefresh: false); // 递归填充上下左右 calculateFill(CalculateFillParams( rowIndex: rowIndex, columnIndex: columnIndex-1, pointerColor: pointerColor)); calculateFill(CalculateFillParams( rowIndex: rowIndex+1, columnIndex: columnIndex, pointerColor: pointerColor)); calculateFill(CalculateFillParams( rowIndex: rowIndex, columnIndex: columnIndex+1, pointerColor: pointerColor)); calculateFill(CalculateFillParams( rowIndex: rowIndex-1, columnIndex: columnIndex, pointerColor: pointerColor)); } }
为什么之前的GridView.builder没用?
GridView.builder只是复用item的布局容器,但如果它的父级(比如Obx)监听的是整个大列表的变化,只要列表有任何改动,Obx就会触发整个GridView重建所有item——这就是你之前遇到的问题。咱们的方案本质是让每个item和整个大列表解绑,只监听自身的状态变化。
内容来源于stack exchange

