精通
英语
和
开源
,
擅长
开发
与
培训
,
胸怀四海
第一信赖

锐英源精品原创文章,禁止转载和任何形式的非法内容使用,违者必究
前几天,我需要开发手机App,AI推荐了Flutter,并快速生成了代码,连接FastAPI接口,获取数据,绘制结果,花了200左右的积分,迅速完成。AI还贴心地生成了手机拍照功能,拍照照片上传到FastAPI服务器上。现在普遍对AI生成代码不太放心,这里强调两点FastAPI的安装部署和运行,Flutter的内存泄漏问题,不是说Flutter会轻易内存泄漏,Flutter是成熟的平台,我只是说从某个点来讲内存泄漏,让普通程序员和新手怎么查泄漏,有谨慎心态能落地实践。
Dart是Google开发的⼀种面向对象的计算机编程语言,和Java类似。Flutter是Google开源的UI⼯具包,帮助开发者通过⼀套代码库高效构建多平台精美应用,支持移动、Web、桌面和嵌入式平台。 Dart是flutter的程序开发语言。Flutter能被AI推荐,有大量的QQ群,群里有很多爱好者,都证明Flutter的成功。Flutter多端就是Flutter多平台的意思,移动、Web、桌面和嵌入式平台都可以用Flutter开发,所以叫Flutter多端。这里面最重要的是苹果的iOS开发,xcode需要运行在iOS下,一般是用虚拟机来做,环境不太有好。Flutter的一次开发,多端(多平台)兼容,会节省很多时间。如果不用Flutter,需要掌握多门编程语言和多个开发环境,工作量大。
# 牛身检测 Flutter App
一个简单的Flutter应用,用于上传牛只图片并显示检测到的牛身轮廓。
## 项目结构
```
cowapp/
├── lib/
│ ├── main.dart # 应用入口
│ ├── home_page.dart # 主页面(按钮+图片控件)
│ ├── services/
│ │ └── api_service.dart # API服务(与后端通信)
│ └── widgets/
│ └── contour_painter.dart # 轮廓绘制组件
├── pubspec.yaml # 项目配置
└── README.md # 本文件
```
## 功能
1. 点击按钮从图库选择牛只图片
2. 自动上传到后端服务器进行检测
3. 显示检测到的牛身轮廓(绿色线条)
4. 在原图上叠加显示轮廓结果
## 技术栈
- **Flutter**: 前端框架
- **image_picker**: 图片选择
- **http**: 网络请求
- **CustomPainter**: 轮廓绘制
## 运行步骤
### 1. 启动后端服务
```bash
cd d:\cow
python server.py
```
服务将在 `http://localhost:8000` 启动。
### 2. 安装Flutter依赖
```bash
cd d:\cowapp
flutter pub get
```
### 3. 运行Flutter应用
```bash
flutter run
```
## API接口
### POST /view_body_new_service
**请求**:
- Content-Type: multipart/form-data
- Body: 图片文件(字段名: file)
**响应**:
```json
{
"success": true,
"message": "检测成功",
"contour_points": [[x1,y1], [x2,y2], ...],
"image_width": 4096,
"image_height": 3072
}
```
## 注意事项
1. **后端服务地址**: 默认连接到 `http://localhost:8000`,可在 `api_service.dart` 中修改
2. **图片大小**: 支持最大4096x3072分辨率,会自动缩放显示
3. **轮廓绘制**: 使用绿色线条绘制,红色圆点标记起点
4. **缩放处理**: CustomPainter 会根据原图尺寸和显示尺寸自动计算缩放比例,确保轮廓点正确对齐
## 待优化功能
- [ ] 添加进度显示
- [ ] 支持相机拍照
- [ ] 显示更多检测信息(如牛身特征)
- [ ] 支持多张图片批量处理
- [ ] 离线模式缓存
Flutter多端开发能力在这个牛身App里有体现,在Windows下可以运行,编译成apk就可以在安卓手机上运行,在Windows下功能测试完成,则安卓手机上的测试就相对简单多了。
import 'package:flutter/material.dart';//导入库
import 'package:flutter/foundation.dart';
class ContourPainter extends CustomPainter {
final List<List<int>> contourPoints;//坐标数组
final int imageWidth;
final int imageHeight;
final Color contourColor;
final double strokeWidth;
ContourPainter({
required this.contourPoints,
required this.imageWidth,
required this.imageHeight,
this.contourColor = Colors.green,
this.strokeWidth = 3.0,
});
@override
void paint(Canvas canvas, Size size) {//重写的绘制函数
if (kDebugMode) {
print('[ContourPainter] paint 被调用');
print('[ContourPainter] 画布尺寸: ${size.width}x${size.height}');
print('[ContourPainter] 原图尺寸: ${imageWidth}x$imageHeight');
print('[ContourPainter] 轮廓点数: ${contourPoints.length}');
if (contourPoints.isNotEmpty) {
print('[ContourPainter] 第一个点: ${contourPoints.first}');
print('[ContourPainter] 最后一个点: ${contourPoints.last}');
}
}
if (contourPoints.isEmpty || imageWidth == 0 || imageHeight == 0) {
if (kDebugMode) {
print('[ContourPainter] 提前返回(数据为空)');
}
return;
}
// 现在外层 FittedBox 统一缩放,画布 size 应等于原图像素尺寸
// 这里仍然计算一个 scale 以防御不一致情况
final scaleX = size.width / imageWidth;
final scaleY = size.height / imageHeight;
final scale = (scaleX > 0 && scaleY > 0)
? scaleX // 正常情况下 scaleX == scaleY == 1.0(因为 FittedBox)
: 1.0;
if (kDebugMode) {
print('[ContourPainter] scaleX=$scaleX, scaleY=$scaleY, scale=$scale');
}
// 创建画笔
final paint = Paint()
..color = contourColor
..strokeWidth = strokeWidth // 直接使用像素线宽,FittedBox会整体缩放
..style = PaintingStyle.stroke
..strokeCap = StrokeCap.round
..strokeJoin = StrokeJoin.round;
// 绘制轮廓路径
if (contourPoints.length > 1) {
final path = Path();
// 移动到第一个点
final startX = contourPoints[0][0] * scale;
final startY = contourPoints[0][1] * scale;
path.moveTo(startX, startY);
if (kDebugMode) {
print('[ContourPainter] 路径起点: ($startX, $startY)');
}
// 连接所有轮廓点
for (int i = 1; i < contourPoints.length; i++) {
final x = contourPoints[i][0] * scale;
final y = contourPoints[i][1] * scale;
path.lineTo(x, y);
}
// 闭合路径
path.close();
// 绘制路径
canvas.drawPath(path, paint);
if (kDebugMode) {
print('[ContourPainter] 路径已绘制');
}
}
// 绘制起点标记(红色圆点)
if (contourPoints.isNotEmpty) {
final startX = contourPoints[0][0] * scale;
final startY = contourPoints[0][1] * scale;
final startPaint = Paint()
..color = Colors.red
..style = PaintingStyle.fill;
canvas.drawCircle(
Offset(startX, startY),
6.0, // 固定像素大小,外层会缩放
startPaint,
);
}
// 绘制所有轮廓点(小圆点),间隔绘制避免太密集
final pointPaint = Paint()
..color = contourColor.withValues(alpha: 0.6)
..style = PaintingStyle.fill;
for (int i = 0; i < contourPoints.length; i++) {
if (i % 5 == 0) {
final x = contourPoints[i][0] * scale;
final y = contourPoints[i][1] * scale;
canvas.drawCircle(
Offset(x, y),
2.0,
pointPaint,
);
}
}
}
@override
bool shouldRepaint(covariant ContourPainter oldDelegate) {//重写的触发绘制函数
// 当轮廓点、颜色、线宽、图片尺寸变化时重绘
return oldDelegate.contourPoints != contourPoints ||
oldDelegate.contourColor != contourColor ||
oldDelegate.strokeWidth != strokeWidth ||
oldDelegate.imageWidth != imageWidth ||
oldDelegate.imageHeight != imageHeight;
}
}
dart语言类似Java,当然也像C#,这里面没有指针,使用还是很方便。这里面Path()是常用的资源对象,资源对象用不好会导致内存泄漏。Flutter在频繁分配Path()时会出问题,但是这些代码明显是不频繁的,是可以接受的。为什么不频繁?paint绘制的前提是数据对象改变让shouldRepaint返回真,才绘制,不频繁绘制就不会分配Path()对象,所以代码是正确的。其它的对象不涉及内存泄漏。