Flutter中FutureBuilder处理耗时Voronoi图生成时UI卡顿、加载指示器不及时显示的问题
嗨,我看了你的代码,问题的核心是你把耗时的Voronoi图生成逻辑放在了主线程里,导致UI完全被阻塞,FutureBuilder根本没机会显示加载指示器。咱们一步步来解决这个问题:
问题根源分析
你写的createDiagram()是一个同步的耗时函数,虽然你把它包在了drawDiagram()的Future.value()里,但Future.value()的特性是先同步执行传入的代码,再返回Future。也就是说,点击按钮后,主线程会立刻开始执行createDiagram()的所有计算,直到完成才会继续处理UI更新——这就导致你的APP卡顿,而FutureBuilder的ConnectionState.waiting状态根本没机会触发,因为等计算完,Future已经直接进入完成状态了。
解决方案:把耗时计算移到后台Isolate
Flutter提供了compute函数,可以轻松把耗时任务放到后台隔离线程(Isolate)执行,这样主线程就能保持响应,加载指示器也能正常显示。
步骤1:重构createDiagram为静态/顶层函数,并打包参数
因为Isolate不能访问类的实例成员,所以我们需要把createDiagram改成静态函数,同时把需要的参数(width、height、station数量)打包成一个简单的数据类:
// 定义参数类,传递计算所需的配置 class DiagramParams { final int width; final int height; final int stationCount; DiagramParams(this.width, this.height, this.stationCount); } // 把createDiagram改成静态函数,接收参数类 static Uint8List createDiagram(DiagramParams params) { int width = params.width; int height = params.height; int basedStation = params.stationCount; // 以下是你原来的生成逻辑,完全不变 img.Image diagram = img.Image(width: width, height: height); print('image created'); List<DrawPixels> baseStationList = generatePixels(basedStation, width, height); for (var pixel in baseStationList) { pixel.color = generateRandomColor(); } for (int i = 0; i < width; i++) { for (int j = 0; j < height; j++) { double minDistance = double.infinity; int minIndex = -1; for (int k = 0; k < basedStation; k++) { double distance = calculateDistance(i, j, baseStationList[k].x, baseStationList[k].y); if (distance < minDistance) { minDistance = distance; minIndex = k; } } img.drawPixel(diagram, i, j, baseStationList[minIndex].color); } } for (var pixel in baseStationList) { img.fillCircle(diagram, x: pixel.x, y: pixel.y, radius: 3, color: img.ColorFloat16.rgb(255, 255, 255)); } return img.encodePng(diagram); }
步骤2:修改drawDiagram方法,用compute执行后台计算
把原来的Future.value()替换成compute,让任务在后台线程执行:
Future<Uint8List> drawDiagram() async { try { int width = int.tryParse(textEditingControllerWidth.text) ?? 0; int height = int.tryParse(textEditingControllerHeight.text) ?? 0; int basedStation = int.tryParse(textEditingControllerStations.text) ?? 0; // 使用compute将耗时任务丢到后台线程 final data = await compute(createDiagram, DiagramParams(width, height, basedStation)); return data; } catch (e) { throw Exception('生成失败:$e'); } }
注意:这里把参数解析移到了
drawDiagram里,原来在createDiagram中调用setState是错误的——后台线程不能更新UI,而且参数解析不需要触发UI更新,直接在这里处理就好。
步骤3:调整FutureBuilder的逻辑(更严谨)
原来的判断顺序有点问题,应该先判断连接状态,再处理数据和错误:
FutureBuilder( future: diagramImage, builder: (context, AsyncSnapshot<Uint8List> snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { print('正在生成...'); return const CircularProgressIndicator(); } if (snapshot.hasError) { return Text('生成出错:${snapshot.error}'); } if (snapshot.hasData) { return Image.memory(snapshot.data!); } // 初始未点击按钮的状态 return const Icon(Icons.image); }, )
步骤4:修正按钮点击逻辑
点击按钮时要调用setState,让FutureBuilder感知到diagramImage的变化:
ElevatedButton( onPressed: () { setState(() { diagramImage = drawDiagram(); }); }, child: const Text('Create diagram'), )
原来的代码没有调用
setState,这会导致FutureBuilder不会重新构建,也是一个隐藏的小问题!
效果说明
修改后,点击按钮时主线程会立刻触发UI更新,FutureBuilder显示加载指示器;耗时的Voronoi计算在后台线程执行,完全不会阻塞UI;计算完成后,FutureBuilder会自动切换到显示生成的图片。
备注:内容来源于stack exchange,提问作者Vladislav Lebedev

