War*_*sen 4 memory-leaks dart flutter bloc
我正在使用 Flutter 构建一个清单应用程序,我似乎正在跨检查点和检查建立内存。我已经来来回回 2 周了,现在试图重组页面无济于事。
我正在使用 Flutter Bloc https://felangel.github.io/bloc在检查点屏幕上进行状态管理。我怀疑 Bloc 导致了我的内存泄漏。
检查点屏幕非常复杂:
当用户提交检查点时,将存储答案并为用户显示下一个检查点,直到他们到达检查结束并关闭它。
这是屏幕的屏幕截图,不幸的是这里无法看到 TextFormField,但它就在“发现”这个词的下方。
我注意到的事情:
当检查点屏幕第一次加载并且我在 DevTools 中拍摄快照时,我可以看到每个小部件的 1 个实例(AnswerOptions、CheckHeader、Comments、ImageGrid)。但是,一旦我开始切换选项,即。在 OK、DEFECTIVE、N/A 之间跳跃,实例(AnswerOptions、CheckHeader、Comments、ImageGrid)开始堆积。当用户提交检查点甚至完全退出检查时,这些类会留在内存堆中并且永远不会被释放。
我还注意到重复的实例仅从 CheckpointForm 向下通过小部件树开始。AssetInspection 和 InspectionView 不会在堆中复制实例。
页面首次加载时的示例:
然后我切换 OK、DEFECTIVE 和 N/A 并拍摄另一个快照:
附上代码:
资产检查
class AssetInspection extends StatefulWidget
{
final Checklist checklist;
final Asset asset;
final Job job;
final AssetPoolDatabase database;
AssetInspection({
Key key,
@required this.checklist,
@required this.asset,
@required this.job,
@required this.database,
}) : super(key: key);
@override
AssetInspectionState createState() => new AssetInspectionState();
}
class AssetInspectionState extends State<AssetInspection>
{
InspectionBloc _inspectionBloc;
CheckpointBloc _checkpointBloc;
@override
void initState() {
_checkpointBloc = CheckpointBloc(
database: widget.database,
answerRepo: AnswerRepo(database: widget.database),
);
_inspectionBloc = InspectionBloc(
checklist: widget.checklist,
job: widget.job,
asset: widget.asset,
inspectionRepo: InspectionRepo(database: widget.database),
checkpointBloc: _checkpointBloc
);
super.initState();
}
@override
void dispose() {
_inspectionBloc.dispose();
_checkpointBloc.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
BlocProvider<InspectionBloc>(
builder: (BuildContext context) => _inspectionBloc..dispatch(LoadInspection()),
),
BlocProvider<CheckpointBloc>(
builder: (BuildContext context) => _checkpointBloc,
)
],
child: InspectionView(),
);
}
}
Run Code Online (Sandbox Code Playgroud)
检查视图
class InspectionView extends StatelessWidget
{
@override
Widget build(BuildContext context) {
final InspectionBloc _inspectionBloc = BlocProvider.of<InspectionBloc>(context);
return BlocListener(
bloc: _inspectionBloc,
listener: (context, InspectionState state) {
if(state is AnswerStored) {
_inspectionBloc..dispatch(LoadInspection());
}
if(state is InspectionClosed) {
Navigator.pushReplacement(
context,
CupertinoPageRoute(
builder: (context) => JobManager(
jobId: state.inspection.jobId,
),
),
);
}
},
child: BlocBuilder<InspectionBloc, InspectionState>(
builder: (BuildContext context, InspectionState state) {
if (state is InspectionInProgress) {
return CheckpointView(
currentCheck: state.currentCheck,
totalChecks: state.totalChecks,
);
}
if(state is InspectionNeedsSubmission) {
return SubmitInspection(
inspection: state.inspection,
checklist: state.checklist,
);
}
if(state is InspectionLoading) {
return LoadingIndicator();
}
return LoadingIndicator();
},
),
);
}
}
Run Code Online (Sandbox Code Playgroud)
检查点视图
class CheckpointView extends StatelessWidget {
final int totalChecks;
final int currentCheck;
CheckpointView({
Key key,
@required this.totalChecks,
@required this.currentCheck,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return BlocBuilder<CheckpointBloc, CheckpointState>(
builder: (context, CheckpointState state) {
if(state is CheckpointLoaded) {
return CheckpointForm(
totalChecks: totalChecks,
currentCheck: currentCheck,
);
}
if(state is ManagingImage) {
return ImageOptions();
}
return Container(color: Colors.white,);
},
);
}
}
Run Code Online (Sandbox Code Playgroud)
检查点表格
class CheckpointForm extends StatelessWidget
{
final int totalChecks;
final int currentCheck;
CheckpointForm({
this.totalChecks,
this.currentCheck,
Key key
}) : super(key: key);
@override
Widget build(BuildContext context) {
final InspectionBloc _inspectionBloc = BlocProvider.of<InspectionBloc>(context);
final CheckpointBloc _checkpointBloc = BlocProvider.of<CheckpointBloc>(context);
final CheckpointLoaded currentState = _checkpointBloc.currentState as CheckpointLoaded;
return Scaffold(
appBar: AppBar(
title: Text(_inspectionBloc.checklist.name),
leading: IconButton(
icon: const Icon(Icons.close),
onPressed: () {
Navigator.pushReplacement(
context,
CupertinoPageRoute(
builder: (context) => JobManager(
jobId: _inspectionBloc.job.id,
),
),
);
},
),
),
body: GestureDetector(
onTap: () {
FocusScope.of(context).requestFocus(new FocusNode());
},
child: SingleChildScrollView(
padding: const EdgeInsets.only(left: 15, right: 15, top: 20, bottom: 20),
child: Column(
children: <Widget>[
CheckHeader(
totalChecks: totalChecks,
currentCheck: currentCheck,
),
AnswerOptions(),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
const Text('Evidence',
style: const TextStyle(
fontSize: 20, fontWeight: FontWeight.w600)
),
Padding(
padding: const EdgeInsets.only(left: 10),
child: Text(
_getImageValidationText(currentState),
style: const TextStyle(
color: Colors.deepOrange,
fontWeight: FontWeight.w500
),
),
)
],
),
const Divider(),
ImageGrid(),
CheckpointComments(),
],
),
),
),
);
}
String _getImageValidationText(CheckpointLoaded state) {
if ((state.checkpoint.imageRule == 'when-defective' &&
state.answer.answer == '0' &&
state.answer.images.length == 0) ||
(state.checkpoint.imageRule == 'always-required' &&
state.answer.images.length == 0)) {
return 'Please take up to 2 images';
}
return '';
}
}
Run Code Online (Sandbox Code Playgroud)
检查头
class CheckHeader extends StatelessWidget
{
final int totalChecks;
final int currentCheck;
CheckHeader({
Key key,
@required this.totalChecks,
@required this.currentCheck,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final CheckpointBloc _checkpointBloc = BlocProvider.of<CheckpointBloc>(context);
return BlocBuilder(
bloc: _checkpointBloc,
builder: (context, CheckpointState state) {
if(state is CheckpointLoaded) {
return Container(
padding: const EdgeInsets.only(top: 20, bottom: 20),
margin: const EdgeInsets.only(bottom: 30),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text('Check: $currentCheck/$totalChecks'),
Text(
state.checkpoint.name,
style: const TextStyle(
fontSize: 25,
fontWeight: FontWeight.w900
),
),
const Divider(),
Text(
state.checkpoint.task,
style: const TextStyle(
fontSize: 18
),
)
],
),
);
}
return Container(color: Colors.white,);
},
);
}
}
Run Code Online (Sandbox Code Playgroud)
答案选项
class AnswerOptions extends StatelessWidget
{
AnswerOptions({
Key key
}) : super(key: key);
@override
Widget build(BuildContext context) {
final CheckpointBloc _checkpointBloc = BlocProvider.of<CheckpointBloc>(context);
CheckpointLoaded state = _checkpointBloc.currentState as CheckpointLoaded;
return Column(
children: <Widget>[
_option(
label: 'Pass Check',
value: '1',
activeValue: state.answer.answer,
activeColor: AssetPoolTheme.green,
activeTextColor: Colors.white,
passiveTextColor: Colors.blueGrey,
passiveColor: AssetPoolTheme.grey,
icon: Icons.check_circle_outline,
state: state,
checkpointBloc: _checkpointBloc
),
_option(
icon: Icons.highlight_off,
label: 'Fail Check',
value: '0',
activeValue: state.answer.answer,
activeColor: AssetPoolTheme.red,
activeTextColor: Colors.white,
passiveTextColor: Colors.blueGrey,
passiveColor: AssetPoolTheme.grey,
state: state,
checkpointBloc: _checkpointBloc
),
_option(
icon: Icons.not_interested,
label: 'Not Applicable',
value: '-1',
activeValue: state.answer.answer,
activeTextColor: Colors.white,
passiveTextColor: Colors.blueGrey,
passiveColor: AssetPoolTheme.grey,
activeColor: AssetPoolTheme.orange,
state: state,
checkpointBloc: _checkpointBloc
),
],
);
}
_option({
icon,
label,
value,
activeValue,
activeTextColor,
passiveTextColor,
passiveColor,
activeColor,
state,
checkpointBloc
}) {
return Container(
margin: const EdgeInsets.only(bottom: 10),
child: FlatButton(
color: activeValue == value ? activeColor : passiveColor,
textColor: Colors.white,
disabledColor: Colors.grey,
disabledTextColor: Colors.black,
padding: const EdgeInsets.all(20),
splashColor: activeColor,
onPressed: () {
checkpointBloc.dispatch(
UpdateAnswer(answer: state.answer.copyWith(answer: value))
);
},
child: Row(
children: <Widget>[
Padding(
child: Icon(
icon,
color: activeValue == value ? activeTextColor : passiveTextColor,
),
padding: const EdgeInsets.only(right: 15),
),
Text(
label,
style: TextStyle(color: activeValue == value ? activeTextColor : passiveTextColor, fontSize: 20),
)
],
),
),
);
}
}
Run Code Online (Sandbox Code Playgroud)
图像网格
class ImageGrid extends StatelessWidget
{
ImageGrid({
Key key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return BlocBuilder<CheckpointBloc, CheckpointState>(
builder: (BuildContext context, CheckpointState state) {
if(state is CheckpointLoaded) {
return GridView.count(
addAutomaticKeepAlives: false,
shrinkWrap: true,
physics: const ScrollPhysics(),
crossAxisCount: 2,
childAspectRatio: 1.0,
mainAxisSpacing: 4.0,
crossAxisSpacing: 4.0,
children: _imagesRow(state.answer.images),
);
}
return Container();
},
);
}
List<Widget> _imagesRow(stateImages) {
final List<Widget> previewImages = [];
stateImages.forEach((imagePath) {
final preview = new ImagePreview(
key: Key(imagePath),
imagePath: '$imagePath',
imageName: imagePath
);
previewImages.add(preview,);
});
final takePicture = TakePicture();
if (stateImages.length < 2) previewImages.add(takePicture,);
return previewImages;
}
}
Run Code Online (Sandbox Code Playgroud)
检查组
class InspectionBloc extends Bloc<InspectionEvents, InspectionState>
{
final Checklist checklist;
final Job job;
final Asset asset;
final InspectionRepo inspectionRepo;
final CheckpointBloc checkpointBloc;
InspectionBloc({
@required this.checklist,
@required this.job,
@required this.asset,
@required this.inspectionRepo,
@required this.checkpointBloc,
});
@override
InspectionState get initialState => InspectionUnintialized();
@override
void dispose() {
checkpointBloc.dispose();
super.dispose();
}
@override
Stream<InspectionState> mapEventToState(InspectionEvents event) async* {
if(event is LoadInspection) {
yield InspectionLoading();
await Future.delayed(Duration(seconds: 1));
final Inspection inspection = await initializeInspection();
if(inspection == null) {
yield InspectionNotLoaded();
} else if(inspection.syncedAt != null) {
yield InspectionSynced(inspection: inspection);
} else if(inspection.completedAt != null) {
yield InspectionSynced(inspection: inspection);
} else if(inspection.completedAt == null && inspection.syncedAt == null) {
yield* _mapCurrentCheckpoint(inspection);
}
} else if(event is CheckpointWasSubmitted) {
final bool isValid = _validateCheckpoint(event.answer, event.checkpoint);
if(isValid == false) {
Toaster().error('Invalid, please complete the checkpoint before submitting');
} else {
Inspection inspection = await inspectionRepo.getInspection(job.id, asset.localId, checklist.id);
await _storeAnswer(event.answer, event.checkpoint, inspection);
await inspectionRepo.jobIsInProgress(job.id);
yield AnswerStored(
checklist: checklist,
asset: asset,
job: job
);
}
} else if(event is CloseInspection) {
inspectionRepo.closeInspection(event.closingComments, event.location, event.inspection.sourceUuid);
yield InspectionClosed(inspection: event.inspection);
}
}
Stream<InspectionState> _mapCurrentCheckpoint(Inspection inspection) async* {
final List<Check> checks = await inspectionRepo.getChecksForChecklist(checklist.id);
if(await inspectionRepo.hasAnswers(inspection.sourceUuid) == false) {
final Check checkpoint = await inspectionRepo.firstCheckOnChecklist(inspection.checklistId);
yield InspectionInProgress(
totalChecks: checks.length,
currentCheck: 1,
inspection: inspection,
checkpoint: checkpoint
);
checkpointBloc.dispatch(LoadForInspection(checkpoint: checkpoint));
} else {
final Answer lastAnswer = await inspectionRepo.getLatestAnswer(inspection.sourceUuid);
final int latestAnswerIndex = checks.indexWhere((check) => check.id == lastAnswer.checkId);
final int updatedIndex = latestAnswerIndex + 1;
if(updatedIndex < checks.length) {
final Check checkpoint = checks.elementAt(updatedIndex);
yield InspectionInProgress(
totalChecks: checks.length,
currentCheck: updatedIndex + 1,
checkpoint: checkpoint,
inspection: inspection,
);
checkpointBloc.dispatch(LoadForInspection(checkpoint: checkpoint));
}
if(updatedIndex == checks.length) {
yield InspectionNeedsSubmission(
inspection: inspection,
checklist: checklist
);
}
}
}
Future<Inspection> initializeInspection() async {
return await inspectionRepo.getInspection(job.id, asset.localId, checklist.id)
?? await inspectionRepo.createInspection(job.id, asset.localId, checklist.id);
}
bool _validateCheckpoint(AnswerModel answer, Check checkpoint) {
if(answer.answer == null) return false;
if(checkpoint.imageRule == 'always-required' && answer.images.length == 0) return false;
if(checkpoint.commentRule == 'always-required' && answer.comments.length == 0) return false;
if(checkpoint.imageRule == 'when-defective' && answer.answer == '0' && answer.images.length == 0) {
return false;
}
if(checkpoint.commentRule == 'when-defective' && answer.answer == '0' && answer.comments.length == 0) return false;
return true;
}
Future _storeAnswer(AnswerModel answerModel, Check checkpoint, Inspection inspection) async {
inspectionRepo.storeAnswer(
answerModel,
checkpoint,
inspection
);
}
}
Run Code Online (Sandbox Code Playgroud)
检查站集团
class CheckpointBloc extends Bloc<CheckpointEvent, CheckpointState>
{
final AssetPoolDatabase database;
final AnswerRepo answerRepo;
CheckpointBloc({
@required this.database,
@required this.answerRepo,
});
@override
CheckpointState get initialState => CheckpointNotLoaded();
@override
Stream<CheckpointState> mapEventToState(event) async* {
if(event is LoadForInspection) {
yield CheckpointLoaded(
checkpoint: event.checkpoint,
answer: new AnswerModel(
checkId: event.checkpoint.id,
images: [],
)
);
} else if(event is UpdateAnswer) {
final state = currentState as CheckpointLoaded;
yield CheckpointLoaded(
checkpoint: state.checkpoint,
answer: event.answer
);
} else if(event is AddImage) {
final state = currentState as CheckpointLoaded;
List<String> images = state.answer.images;
images.add(event.imagePath);
yield CheckpointLoaded(
checkpoint: state.checkpoint,
answer: state.answer.copyWith(images: images)
);
} else if(event is RemoveImage) {
print('HERE');
print(event.imageName);
List<String> images = event.answer.images.where((imageName) => imageName != event.imageName).toList();
yield CheckpointLoaded(
checkpoint: event.checkpoint,
answer: event.answer.copyWith(images: images)
);
} else if(event is ManageImage) {
yield ManagingImage(
image: event.image,
checkpoint: event.checkpoint,
answer: event.answer,
imageName: event.imageName
);
} else if(event is CloseImageManager) {
yield CheckpointLoaded(
checkpoint: event.checkpoint,
answer: event.answer
);
}
}
}
Run Code Online (Sandbox Code Playgroud)
我设法找到了内存泄漏。原因实际上是集团。我通过推动导航器以模态打开相机。问题是我不是从 Bloc 侦听器推送到此模式,而是从小部件内部推送。
使用 Flutter Bloc 时,建议从 Bloc 侦听器中执行导航。
我最终完全删除了导航,并简单地显示了相机小部件以响应状态的变化。
内存使用方面的变化是巨大的,垃圾收集器开始以更可预测的方式运行。