Reputation: 131
I am building a checklist application using Flutter and i seem to be getting a build up in memory across checkpoints and inspections. I've been going back and forth for 2 weeks now trying to restructure the page to no avail.
I am using Flutter Bloc https://felangel.github.io/bloc for state management on the checkpoint screen. I suspect that the Bloc is causing my memory leak.
The checkpoint screen is quite complex:
When the user submits the checkpoint, the answer is stored and the next checkpoint is displayed for the user until they reach the end of the inspection and close it out.
Here is a screenshot of the screen, unfortunately the TextFormField cannot be seen here but it is just below the word "findings".
Screenshot of Checkpoint Screen
Things i have noticed:
When the checkpoint screen first loads and i take a snapshot in DevTools i can see 1 instance of each widget (AnswerOptions, CheckHeader, Comments, ImageGrid). However as soon as i start toggling the options ie. hopping between OK, DEFECTIVE, N/A the instances (AnswerOptions, CheckHeader, Comments, ImageGrid) start stacking up. When the user submits the checkpoint or even exits the inspection altogether those classes stay in the memory heap and are never released.
I have also noticed that the duplicated instances only start from the CheckpointForm downward through the widget tree. AssetInspection and InspectionView do not duplicate instances in the heap.
Example when the page first loads:
I then toggle the OK, DEFECTIVE and N/A and take another snapshot:
Instances have accumulated after toggling options
Herewith the code:
AssetInspection
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(),
);
}
}
InspectionView
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();
},
),
);
}
}
CheckpointView
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,);
},
);
}
}
CheckpointForm
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 '';
}
}
CheckHeader
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,);
},
);
}
}
AnswerOptions
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),
)
],
),
),
);
}
}
ImageGrid
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;
}
}
Inspection Bloc
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
);
}
}
Checkpoint Bloc
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
);
}
}
}
Upvotes: 4
Views: 4922
Reputation: 131
I managed to locate the memory leak. The cause was in fact Bloc. I was opening the camera in a modal by pushing with the navigator. The problem was that i was not pushing to this modal from a Bloc listener but rather from within the widget.
With Flutter Bloc it is recommended to perform Navigation from within a Bloc Listener.
I ended up removing the Navigation altogether and simply showed the camera widget in response to a change in state.
The change was dramatic in terms of memory usage, and the garbage collector has started behaving in a much more predictable way.
Upvotes: 9
Reputation: 2448
Yeah, what i was suspecting is true. You have to override the dispose method on your blocs.
Here you call checkpointBloc.dispose but you never implemented the dispos method on checkpointBloc
@override
void dispose() {
checkpointBloc.dispose();
super.dispose();
}
You have to override dispose method checkpointBloc doing all type of cleaning up there
Upvotes: 0