Repository navigation
MOBILE-556: Report three outcomes with a reason, and let the block start hidden, with a placeholder, or by the place's memory - #219
Conversation
… onFail(reason) The native blocks report three outcomes since MOBILE-504 (iOS) and MOBILE-505 (Android): the content is shown, the place has nothing to show, or the block could not be shown, with a reason. The widget now reports the same three. `onEmpty` is new and carries no reason. `onFail` takes a `MindboxEmbeddedBlockFailReason` — a string-backed class with `networkError` and `internalError`, the same raw values as on the native side, open to words a later SDK adds. The report on the per-view channel carries the reason under `reason`, and both native halves implement the new listener and delegate methods: the Android one no longer compiles against the old `onFail(view)`, the iOS one would otherwise keep compiling against a protocol whose default implementation silently swallows the failure. Outcomes are still deduplicated by kind: a failure that repeats with another reason is the same outcome. A platform without a native block reports `internalError`.
… place's memory, and reveal with the SDK's animation The native blocks take a loading strategy since MOBILE-516 (iOS) and MOBILE-517 (Android): `automatic` — the default — keeps the block hidden until the place has shown content once on this device and puts a placeholder there from then on, `placeholder` takes the space up front, `hidden` never takes it before the content. Content is revealed with the SDK's own animation unless `animatesReveal` is off. The widget takes both, fixed at creation like `timeout`, and hands them to the native block. Three things the Dart side has to do itself: The first look. Both natives decide it synchronously, before the container exists, through a static `initialAppearance`; Dart cannot reach anything synchronously. `placeholder` and `hidden` are decided from the strategy alone. `automatic` asks the plugin on a channel shared by every block (`.../embedded_block/plugin`, method `initialAppearance`) and takes no space until it answers: a place that never showed content must not flash reserved space, and one that has gets its placeholder a frame late rather than a frame early. The native block's own report, once the platform view exists, outranks a later answer. The slot. A block that waits hidden used to be impossible on Android: the engine never creates a platform view sized to nothing, so the block would never load and never grow. The layout now gives the slot — 0 for a collapsed block — and the block inside keeps its full height under a ClipRect, so it runs its whole cycle unseen, as the native blocks do. The growth. The container fades the content in on its own; the growth of a block that waited hidden is the wrapper's, as in SwiftUI and Compose. The native block says whether the change is the animated reveal — it owns the gates: animatesReveal, Reduce Motion, "only the arrival of content" — and how long it takes (`animated` and `revealDurationMs` in the report; Android's isRevealAnimated and REVEAL_ANIMATION_DURATION_MS, the same two added to iOS as @_spi in ios-sdk feature/MOBILE-556). The slot then grows from 0 to the height over that duration; everything else lands at once.
The widget used to say in the log that a padded name "is used as it is, so
it will not match the place", because the native blocks once matched the
name byte for byte. They no longer do: iOS strips the whitespace around
the name since MOBILE-419 ("in sync with Android"), and Android's PlaceKey
trims it too. A name pasted from the admin panel with a stray space finds
its place on both, so the warning was wrong and the doc with it.
The widget still passes the name down as given — normalizing is the SDK's
business, not the wrapper's — and the test now pins that, with the log
staying quiet.
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
The documented native SDK pin incompatibilities block Android compilation and iOS failure callback delivery.
Review effort: Balanced
Findings: 2
Open (2)
What changed in this PR
Aligns Flutter embedded blocks with native SDK outcomes, loading strategies, and reveal behavior.
Changes:
- Adds
onEmptyand reason-bearingonFail. - Adds loading strategies, initial-appearance lookup, and animated reveal.
- Updates public documentation and Dart tests.
| File | Description |
|---|---|
| mindbox/test/embedded_block_test.dart | Tests outcomes, loading strategies, and reveal. |
| mindbox/test/embedded_block_fail_reason_test.dart | Tests failure-reason values and equality. |
| mindbox/README.md | Documents callbacks and loading options. |
| mindbox/lib/src/embedded_block.dart | Implements callbacks, initial appearance, and reveal. |
| mindbox/lib/src/embedded_block_loading_strategy.dart | Defines loading strategies. |
| mindbox/lib/src/embedded_block_fail_reason.dart | Defines string-backed failure reasons. |
| mindbox/lib/mindbox.dart | Exports new public types. |
| mindbox/CHANGELOG.md | Records embedded-block enhancements. |
| mindbox_platform_interface/test/src/embedded_block_test.dart | Tests channel constants and report parsing. |
| mindbox_platform_interface/lib/src/embedded_block.dart | Extends the channel protocol. |
| mindbox_ios/ios/mindbox_ios/Sources/mindbox_ios/MindboxIosPlugin.swift | Registers initial-appearance channel. |
| mindbox_ios/ios/mindbox_ios/Sources/mindbox_ios/EmbeddedBlockPlatformView.swift | Bridges new iOS block APIs. |
| mindbox_android/android/src/main/kotlin/cloud/mindbox/mindbox_android/MindboxAndroidPlugin.kt | Manages initial-appearance channel lifecycle. |
| mindbox_android/android/src/main/kotlin/cloud/mindbox/mindbox_android/EmbeddedBlockPlatformView.kt | Bridges new Android block APIs. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
justSmK
left a comment
There was a problem hiding this comment.
Первый круг. Прогнал тесты: mindbox_platform_interface 55, mindbox 78, анализатор чистый. Ключевые тесты краснеют на своих поломках, проверил мутациями: 7 из 9. Одна зелёная эквивалентна коду, про вторую — в комментарии к тесту. Нативные половины не собирал, сигнатуры сверил с ios-sdk #783 и android-sdk mission/stories — сходится, включая делегат с reason:.
Нужны две правки. Свой placeholder хоста пропадает рывком, когда натив проявляет контент. Доки errorBuilder и onFail обещают экран ошибки, которого при стратегии по умолчанию не будет. Остальное в комментариях — мелочи.
Вне кода:
- В доке класса осталось «Both outcomes can be customized», а README уже говорит «Both looks».
- CHANGELOG
mindbox_platform_interface,mindbox_androidиmindbox_iosмолчат про каналplugin, исходempty,reasonи поля раскрытия. - Пины и ios-sdk #783 — как в описании PR, не повторяю.
…fades in A host's placeholder or error screen is drawn by the widget, and the native block's own layer under it is a transparent stand-in. When the content arrived with the SDK's reveal the widget dropped its layer on the first frame while the native block was still fading the content in from transparent: for most of the 250 ms there was nothing to see but the screen's background. SwiftUI writes the look inside withAnimation and Compose keeps the host's placeholder inside the block, so neither shows the gap. The widget now keeps the layer the content replaces for as long as the native block says the reveal runs, fading it out under IgnorePointer, and a look that arrives mid-fade takes it down at once. The slot and the fade run on the curve the native reveal uses: the standard Material curve on Android, ease-in-out on iOS. Along the way, from the review: a height that reserves no space builds no native block on iOS either, as the log already claimed; a creation value changed after creation is said out loud once per value given, as Compose says it; and the docs say that a block waiting hidden builds neither placeholder nor errorBuilder before its first content, that onLoad on such a block finds it still growing, and that errorBuilder does not apply to the default's first wait on a fresh install. Tests: the reveal is reported with a duration that is not the SDK's default, so a hardcoded 250 would show; an error screen or a placeholder arriving mid-growth is pinned to take the full height at once; the fade of a host placeholder and of an error screen, their removal on a look arriving mid-fade, and the instant drop without the native block's word are covered.
The nominal 250 ms went down the channel, but the native fade is a ValueAnimator and the system multiplies its duration by the animator scale from the developer settings; a Flutter animation knows nothing of it, so at 0.5x or 2x the slot and the content came apart. The platform view now sends the nominal duration times Settings.Global.ANIMATOR_DURATION_SCALE, which is what the content actually takes.
… changelogs The README and the strategy's doc promised that the layout does not jump, while an automatic block at a place with memory stands at zero for a frame and takes its height when the plugin answers; both now say so, as the doc of `automatic` already did. The README example names `loadingStrategy: placeholder` and says that a block waiting hidden builds neither placeholder nor errorBuilder, so a host copying it gets the error screen it reads about. The reason doc marks the targeting fetch as iOS-only: Android answers that failure with onEmpty. The changelogs of the platform interface and the two native halves record the plugin channel, the `empty` outcome, the reason and the reveal fields.
|
Первый круг отработан тремя коммитами: fcf391e (затухание слоя хоста под проявлением, остановка роста, нулевая высота на iOS, предупреждение раз на значение, доки виджета), 36db10a (эффективная длительность с Android), 34eaf27 (README, шапка enum, причина сбоя, CHANGELOG |
justSmK
left a comment
There was a problem hiding this comment.
Остальное поправлено, спасибо. Осталась одна правка, она в треде про высоту.
fcf391e built no native block for a height that reserves no space, but checked the height on every build: a host taking a live block through 104 → 0 → 104 lost the platform view on the way down and got a new one on the way up, page reloaded and all, against the promise of a live `height` with no reload. The check now holds only until the block has had a height once. Two tests: a block created with no height builds nothing; a live block passing through zero keeps the one native block it has.
…pin two fade cases The warning for a height that reserves no space said "nothing loads", which stopped being true once a live block kept its native view through zero: a block created at 0 and given a height later does load. The log now says so. Two cases of the host-layer fade were open to a mutation: a reveal that names no duration must drop the host's placeholder at once rather than fade it, and a tap during the fade must go past the fading placeholder to the content under it. Both are pinned; the second through a hit test, with the test placeholder painted so it takes touches the way a real one does.

Problem
MindboxEmbeddedBlockreported two outcomes,onLoadandonFail, and always took its space up front behind a shimmer. The native blocks have moved on: iOS (MOBILE-504/516) and Android (MOBILE-505/517) report three outcomes —onLoad,onEmpty,onFail(reason)— and take aloadingStrategywith a per-place memory and ananimatesRevealflag. A Flutter host could not tell an empty place from a breakage, and every place with no campaign behind it flashed reserved space on every launch.Change
Three outcomes (af19de7).
onEmptyis new;onFailnow takes aMindboxEmbeddedBlockFailReason— a string-backed value (networkError,internalError) rather than an enum, so a later SDK can add a reason without breaking an exhaustiveswitch. The wire carries the native word unchanged. Deduplication stays by outcome, as in the native blocks: a retry that fails for another reason is not delivered again. Breaking for a host onmission/storiesthat wroteonFail: () {}; nothing has shipped with the old signature.Loading strategy and reveal (67ce7ce).
loadingStrategy(automaticby default,placeholder,hidden) andanimatesRevealgo down as creation params, fixed for the block's life liketimeout. The first look is decided in Dart forplaceholderandhidden; forautomaticit depends on the SDK's memory of the place, so the widget asks the plugin channel…/embedded_block/plugininitialAppearanceand starts collapsed until it answers — a place that never showed content must not flash reserved space. The native block's own report outranks a late answer. The widget lays the block out as a zero-height clipped slot over a block of full height (ClipRect+OverflowBox): Android never creates a platform view sized to nothing, so a block that waited hidden would otherwise never load. The growth 0→height is the wrapper's animation on every platform; the native block reportsanimatedandrevealDurationMswith the look (isRevealAnimated,REVEAL_ANIMATION_DURATION_MSon Android;@_spi(Internal)isRevealAnimated,revealAnimationDurationon iOS from mindbox-cloud/ios-sdk#783), so the gates —animatesReveal, Reduce Motion, "only the arrival of content is a reveal" — live in one place, the view, and the Dart side holds no constants.Padded names (c770dd7). The warning about whitespace around
placeSystemNamegoes: both native SDKs trim the name since MOBILE-419 / MOBILE-454, so the Dart doc now says so instead.Verification
Dart: platform interface 55 tests,
mindbox78 tests (new groups: strategy and flag, first look, reveal, outcome), analyzer clean throughto-local-dependensies.sh. Both native halves compiled and checked by hand against local SDK builds (ios-sdkfeature/MOBILE-556, android-sdkmission/stories) with the Pushok demo: all five strategy cases on the outcome screen, the loading strategy scene —automaticwith and without the place's memory,hiddengrowing from zero with the SDK's animation and landing at once withanimatesReveal: false,onFail(networkError)with and withouterrorBuilder,onEmptyon an unknown place. Logs confirm the first look on the native side (starts with hidden, taking no space (strategy automatic)) and the memory being written on content.Blocked on
The plugin is pinned to the native
2.16.0-rctags (build.gradle,Package.swift, podspec), and those tags predate the API: the Kotlin half overridesonFail(view)withoutreasonand does not compile, the Swift half implements the delegate withoutreasonand silently stops receivingonFail. Dart CI is green regardless; the native halves need the next native release and a pin bump in a follow-up. Do not merge before that.Demo: mindbox-cloud flutter-app
feature/MOBILE-556(GitLab) — the outcome screen, the loading strategy scene and a mock server for both platforms.🤖 Generated with Claude Code