perf(home): widget rebuild optimization and PasswordBloc memory leak fix#46
Merged
Merged
Conversation
… to eliminate animation-driven subtree rebuilds Previously, AppAnimatedListItem used FadeTransition and SlideTransition directly, which are AnimatedWidget subclasses that call setState on every animation frame. This caused the entire child subtree (GroupedPasswordListTile, AppCard, etc.) to rebuild at ~90 times/sec during list entrance animations. Replace both with a single AnimatedBuilder that receives the child as a pre-built constant parameter. The builder closure only re-runs Opacity + FractionalTranslation wrappers, leaving the child widget untouched during animation ticks. Impact: Widget rebuild count for list items dropped from 450 rebuilds/5s → 12/5s (97% reduction), verified via flutter_agent_lens MCP widget rebuild tracking.
…on background reload Without buildWhen, the BlocBuilder<PasswordBloc> on HomeScreen rebuilds the entire SliverList every time PasswordBloc emits PasswordLoading — which happens on every background data-change (triggered by repository stream events after add/update/delete in other screens). This destroyed all GroupedPasswordListTile widgets and triggered a new round of AppAnimatedListItem entrance animations on every cross-screen navigation. Add buildWhen guard: skip rebuild when transitioning from PasswordLoaded to PasswordLoading (background refresh), so the list remains stable while data reloads silently in the background. The list only re-renders when new data (PasswordLoaded) arrives with updated content.
…ordBloc Two issues fixed in PasswordBloc.close() and the stream subscription callback: 1. Stale event after dispose: When PasswordBloc is closed (e.g. hot restart in debug mode), the _BroadcastSubscription from _repository.dataChanges could still fire and call add(LoadPasswords()) on the closed bloc, causing an UnhandledStreamError or state emission on a disposed stream. Fix: Add isClosed guard inside the listener. 2. Memory leak via closure retention: _dataChangeSubscription captures 'this' (PasswordBloc) in its closure. Calling .cancel() removes the subscription but the closure object itself still holds a reference. Setting the field to null after cancel() allows the closure to be garbage collected, preventing the old PasswordBloc instance from being retained by the broadcast stream's subscriber list. Verified via flutter_agent_lens MCP memory audit: leak path via _BroadcastSubscription → _Closure → PasswordBloc (mounted=false).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Three performance issues were found via flutter_agent_lens MCP widget rebuild tracking and memory auditing:
FadeTransitionandSlideTransitioninAppAnimatedListItemtriggered full subtree rebuilds on every animation frame, reaching roughly 90 rebuilds per second during list entrance animations.BlocBuilder<PasswordBloc>had nobuildWhenguard, so every background data refresh emitted aPasswordLoadingstate that destroyed and recreated the entireSliverList.PasswordBlocinstance after hot restart, preventing garbage collection.Type of change
Changes
app_animated_list_item.dart
Replaced
FadeTransitionandSlideTransitionwithAnimatedBuilderusing a pre-builtchildparameter. The builder closure now only rebuilds the thinOpacityandFractionalTranslationwrappers on each animation frame rather than the full child subtree. Rebuild count dropped from 450 to 12 per 5-second window (97% reduction).home_screen.dart
Added
buildWhentoBlocBuilder<PasswordBloc>. Rebuilds are skipped when transitioning fromPasswordLoadedtoPasswordLoading, keeping the list stable during background refreshes instead of destroying and rebuilding the entireSliverList.password_bloc.dart
Added an
isClosedguard in the_dataChangeSubscriptionlistener to prevent stale events on a disposed bloc. Set_dataChangeSubscription = nullaftercancel()inclose()to release the closure reference and allow GC to collect the old instance.Testing instructions
flutter run --debugget_widget_rebuild_countsfor 5 seconds.AnimatedScalefromAppCardtap animations at around 12 rebuilds. No animation cascade should appear.Rebuild metrics (flutter_agent_lens)
Checklist
Code coverage
Generated by PassVault CI