MOBILE-492: Keep the embedded block alive in a lazy list - #218
Merged
Merged
Conversation
added 3 commits
September 15, 2026 21:26
A block scrolled past the cacheExtent of a ListView was disposed with its row: the widget sent `release`, the native container tore its page down, and every return cost a new web view and a full reload behind a shimmer. The native iOS and RN blocks never behave that way — a view in a scroll is paused off screen, not destroyed. The state now mixes in AutomaticKeepAliveClientMixin, so the list keeps the block — and the platform view with the SDK's page behind it — while the list lives. Off screen the native block pauses itself, on the way back it resumes the same page. `keepAlive` (default true) lets a host with many blocks opt out and pay a reload instead of memory; it is live, so a change takes effect on the block in place. Verified on iOS (ios-sdk 2.16.0-rc) and Android (mobile-sdk 2.16.0-rc): ten scroll-away-and-back cycles build the page once, send no `release`, and add no Inapp.Targeting or Inapp.Show.
… off screen A block kept alive by a lazy list is not painted off screen, but what the platform does with its view differs. iOS takes the UIView out of the window, so the native container pauses the page on its own. Android keeps the PlatformViewWrapper attached and visible and only stops updating its texture — the container has no way to tell such a block from one in view, so off screen it kept running its page, spending the waiting budget and accounting a show nobody saw. The widget now reads `keptAlive` from the parent data of the nearest sliver child after every frame while it is kept alive, and folds it into the hostVisible signal the wrapper already carries for TickerMode. A parked block is reported hidden — the same pause the block gets behind a pushed route — and shown again on the way back. On iOS the signal is redundant and idempotent. A post-frame callback runs only when frames are produced, so a list standing still costs nothing. Verified on Android (mobile-sdk 2.16.0-rc): ten scroll-away-and-back cycles pause and resume the kept block on every pass with the page built once; before the change the same run produced no pause at all. iOS keeps pausing through the window and builds the page once as before.
…tate The keep-alive request travels past the nearest lazy list to all the others, so a block in a carousel inside a feed is parked by the feed while the carousel's own parent data still says the block is in place. The walk used to stop at that first answer and never reported such a block hidden — on Android its page kept running off screen one nesting level up from the case the previous commit fixed. It now goes to the root and reports parked if any enclosing list says so. Also: a platform without a native block no longer asks to be kept alive; turning keepAlive off on a parked block no longer shows it for its last frame — the check already armed for that frame finds it gone, or in place if the row came back; docs name the real price of the default (the whole row in every enclosing list, not just the page); CHANGELOG mentions the default. Tests: a carousel inside a feed, opting out while parked, a disabled TickerMode through parking and return. Verified on an Android emulator with the demo's Scrolls scenario: a block in a tab's list is paused when TabBarView parks the tab and resumed when it comes back.
There was a problem hiding this comment.
🟡 Changes recommended
Unresolved moderate implementation and Android coverage issues remain.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
Adds lazy-list keep-alive support for MindboxEmbeddedBlock, preserving native pages while rows are parked off screen.
Changes:
- Adds live
keepAliveconfiguration, enabled by default. - Detects parked blocks and reports host visibility changes.
- Adds documentation, changelog updates, and lifecycle tests.
File summaries
| File | Reviewed changes and final comments |
|---|---|
mindbox/lib/src/embedded_block.dart |
Implements keep-alive and visibility tracking. Moderate findings remain for parked opt-out handling and hosts disabling automatic keep-alives. |
mindbox/test/embedded_block_test.dart |
Adds lazy-list lifecycle tests. Moderate finding: Android coverage is needed; nit: one comment is ungrammatical. |
mindbox/README.md |
Documents the new option and tradeoffs. |
mindbox/CHANGELOG.md |
Records the new behavior. |
Review details
Suppressed comments (2)
mindbox/lib/src/embedded_block.dart:487
- This callback is re-registered for every kept block and runs on every frame produced by any animation or ticker, not only while this list scrolls. A long feed with many kept blocks therefore performs a render-tree walk per block per frame even when the feed is stationary; please avoid a permanent callback per block or gate checks to relevant scroll/layout frames.
if (_isKeptAliveCheckArmed || !widget.keepAlive || !_isSupported) {
return;
}
_isKeptAliveCheckArmed = true;
mindbox/lib/src/embedded_block.dart:100
AutomaticKeepAliveClientMixinonly works when the enclosing sliver delegate insertsAutomaticKeepAlive(for example,addAutomaticKeepAlivesremains true). WithListView.builder(addAutomaticKeepAlives: false)or a custom delegate that disables it, the documented default silently falls back to disposal and reload, despite this text promising the behavior for any lazy sliver. Either use an explicitKeepAlivewrapper or document this host requirement.
/// A `ListView`, a `GridView` or any other lazy sliver builds only what is near the viewport and
/// throws the rest away — a block scrolled far enough would be disposed with its row, and on the
/// way back a *new* block would load its content from scratch: a full cycle with the shimmer on
/// every pass across the screen. The native iOS and Android blocks do not behave that way: a view
/// in a scroll is paused off screen, not destroyed, and its page is shown again as it was.
- Files reviewed: 4/4 changed files
- Comments generated: 3
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
…s mounted The check used to run only while the block's own keepAlive was on. But the block's request is not the only thing that can park its row: any keep-alive client in the row does — a host's stateful row widget, another block. A block that opted out, or turned the flag off while parked, in such a row stayed mounted and parked with the check gone, and once the row came back nothing lifted the hidden flag — the native block stayed paused for good. Now the check runs whatever the flag says; the flag only decides whether the block itself asks the list to keep it. Tests run on both platforms now — the keep-alive and hidden/shown paths are the same Dart on both, only the teardown differs, so `release` is asserted on iOS alone — and two new cases cover a row kept by someone else.
sergeysozinov
approved these changes
Sep 21, 2026
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.
Problem
A
MindboxEmbeddedBlockin aListViewwas disposed with its row past thecacheExtent. The widget sentrelease, the native container tore its page down, and every return rebuilt the web view behind a shimmer. Native iOS and RN blocks never behave that way: a view in a scroll is paused off screen, not destroyed.Reproduced on both platforms with the Pushok long-list scenario, ten scroll-away-and-back cycles of one block:
Creating WebView36 across three blocks)releaseper passReleased by the host wrapper36)Change
_EmbeddedBlockStatemixes inAutomaticKeepAliveClientMixin(e81daec). The list keeps the block, and the platform view with the SDK page behind it, for as long as the list lives. New parameterkeepAlive(defaulttrue, live) lets a host with many blocks opt out and pay a reload instead of memory. Hosts change nothing.A parked block is reported hidden (aca500c, ef1ff79). On iOS Flutter takes the
UIViewout of the window and the container pauses on its own. On Android Flutter keeps thePlatformViewWrapperattached and visible (texture layer composition) and only stops updating its texture, so the container had no way to tell a parked block from one in view: its page kept running, the waiting budget kept counting, a show could be accounted off screen. The widget now readskeptAlivefrom the parent data of every enclosing lazy list after each frame (a post-frame callback that re-arms itself and never schedules a frame) and folds it into thesetHostVisiblesignal the wrapper already carries forTickerMode. Every enclosing list is asked, not the nearest: the keep-alive request travels past the first list, so a carousel inside a feed, or a list inside aTabBarViewtab, is parked one level up.Verification
Same scenario,
keepAlive: true, ten cycles:Creating WebView3)releaseleft the window/back on screen, resuming20 / 19was hidden by the host wrapper→Off screen, pausing content40,shown→On screen, starting content38Inapp.Targeting/Inapp.ShowBefore the hidden signal the same Android run produced no pause at all (
Off screen, pausing content0). Nested case checked live with the Scrolls scenario: a block in a tab'sListViewis paused whenTabBarViewparks the tab and resumed on return.keepAlive: falseon both platforms goes back to the old dispose-and-rebuild path.Widget tests: 31 (10 new: lazy list survive / opt-out / live flag both ways, hidden and shown through parking, in-view and out-of-list guards, carousel inside a feed, opt-out while parked, disabled
TickerMode). Flutter ≥3.0 compatibility of the new APIs checked (SchedulerBinding.instance,RenderObject.parenttyping,KeepAliveParentDataMixin.keptAlive).Out of scope
A block built inside the
cacheExtentbut not yet in view is live, not parked, so it has nokeptAlive; on Android it counts as visible and loads (pre-existing, unrelated to keep-alive). Needs a viewport-intersection check; belongs with MOBILE-493.Demo: mindbox-cloud flutter-app
feature/MOBILE-492(GitLab) reworks the long-list scenario so the switch is the widget's ownkeepAlive.🤖 Generated with Claude Code