Skip to content

[Android] Add preload state observability#451

Open
markmur wants to merge 4 commits into
mainfrom
android-preload-observability
Open

[Android] Add preload state observability#451
markmur wants to merge 4 commits into
mainfrom
android-preload-observability

Conversation

@markmur

@markmur markmur commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Adds observable lifecycle state to preloaded checkouts on Android, mirroring the iOS CheckoutPreload API. preload(checkoutUrl, context, onStateChange) now returns a retained CheckoutPreload handle exposing a state property and an onStateChange callback that emit Loading → Ready → Expired, plus Failed(reason) for HTTP and navigation failures, and Idle on manual invalidation. State is driven from a single transition choke point in the preload cache and broadcast to a weak observer, so consumers can drive spinners or fall back to a fresh preload when a warm entry lapses.

Android has no keep-alive timer, so there is no keepAliveLost reason and expiry is evaluated lazily when a cached entry is taken.

// Retain the handle for as long as you want to observe.
val preload = ShopifyCheckoutKit.preload(checkoutUrl, activity) { state ->
    when (state) {
        PreloadState.Loading -> showSpinner()
        PreloadState.Ready -> hideSpinner()
        PreloadState.Expired -> preloadCheckoutIfNeeded() // warm entry is gone — kick off a fresh preload
        is PreloadState.Failed -> log("preload failed: ${state.reason}")
        PreloadState.Idle -> Unit
    }
}

// Pull the current state at any time.
if (preload.state == PreloadState.Ready) {
    present(checkoutUrl, activity)
}

@markmur
markmur requested a review from a team as a code owner July 13, 2026 21:46
@github-actions github-actions Bot added the #gsd:50662 Rebase Checkout Kit on UCP label Jul 13, 2026

markmur commented Jul 13, 2026

Copy link
Copy Markdown
Contributor Author

This stack of pull requests is managed by Graphite. Learn more about stacking.

@github-actions

github-actions Bot commented Jul 13, 2026

Copy link
Copy Markdown

React Native — Coverage Report

Lines Statements Branches Functions
Coverage: 92%
91.85% (327/356) 87.98% (183/208) 100% (86/86)

@github-actions

github-actions Bot commented Jul 13, 2026

Copy link
Copy Markdown

Package Size

Platform Artifact Base Head Delta
Android release AAR 246.4 KiB 257.7 KiB +11.3 KiB
Android file breakdown
File Base Head Delta
classes.jar 260.6 KiB 273.6 KiB +13.0 KiB
res/layout/checkout_view_content.xml 2.2 KiB 2.2 KiB 0 B
res/layout/checkout_sheet_content.xml 2.0 KiB 2.0 KiB 0 B
res/values/values.xml 1.2 KiB 1.2 KiB 0 B
R.txt 1.1 KiB 1.1 KiB 0 B
proguard.txt 798 B 798 B 0 B
AndroidManifest.xml 578 B 578 B 0 B
res/drawable/close.xml 431 B 431 B 0 B
res/menu/checkout_menu.xml 354 B 354 B 0 B
META-INF/com/android/build/gradle/aar-metadata.properties 157 B 157 B 0 B

Measured from the PR base SHA and PR head SHA. The file breakdown shows uncompressed sizes within each package artifact, so individual files do not sum to the compressed artifact total. This comment reports package artifact sizes only; it is not a final app binary-size report.

@markmur
markmur force-pushed the android-preload-observability branch from 87cb6db to 89e6e70 Compare July 13, 2026 21:49
@markmur
markmur changed the base branch from preloading-eviction to graphite-base/451 July 13, 2026 22:10
@markmur
markmur force-pushed the android-preload-observability branch from 89e6e70 to 284f325 Compare July 13, 2026 22:10
@markmur
markmur changed the base branch from graphite-base/451 to swift-preload-per-entry July 13, 2026 22:10
@markmur
markmur force-pushed the android-preload-observability branch from 284f325 to bd21ffc Compare July 15, 2026 12:06
@markmur
markmur force-pushed the swift-preload-per-entry branch from 73a7f40 to 2cac69b Compare July 15, 2026 12:06
@markmur
markmur force-pushed the android-preload-observability branch 2 times, most recently from eeb5755 to 95c911b Compare July 15, 2026 13:08
@markmur
markmur force-pushed the swift-preload-per-entry branch from 2cac69b to 4087102 Compare July 15, 2026 13:08
@markmur
markmur force-pushed the android-preload-observability branch from 95c911b to a0d96f6 Compare July 15, 2026 13:09
@markmur
markmur force-pushed the swift-preload-per-entry branch 2 times, most recently from 0c56668 to 9e05f19 Compare July 15, 2026 14:42
@markmur
markmur force-pushed the android-preload-observability branch 2 times, most recently from 55ef015 to b59bb3e Compare July 15, 2026 15:00
@markmur
markmur force-pushed the swift-preload-per-entry branch 2 times, most recently from 00d2d04 to c8f76e8 Compare July 15, 2026 15:11
@markmur
markmur force-pushed the android-preload-observability branch 2 times, most recently from 381d5ca to 65a51ef Compare July 15, 2026 16:01
@markmur
markmur force-pushed the swift-preload-per-entry branch 2 times, most recently from 2cfbff0 to 912a186 Compare July 16, 2026 13:49
@markmur
markmur force-pushed the android-preload-observability branch 2 times, most recently from 3209276 to ec711d4 Compare July 16, 2026 13:53
@markmur
markmur force-pushed the swift-preload-per-entry branch from 912a186 to 6c6f096 Compare July 16, 2026 13:53
@markmur
markmur force-pushed the android-preload-observability branch from ec711d4 to 16dd07a Compare July 16, 2026 14:18
@markmur
markmur force-pushed the swift-preload-per-entry branch from 6c6f096 to 6876cd9 Compare July 16, 2026 14:18
@markmur
markmur force-pushed the android-preload-observability branch from 16dd07a to 880c148 Compare July 16, 2026 15:09
@markmur
markmur force-pushed the swift-preload-per-entry branch from 6876cd9 to 599dafd Compare July 16, 2026 15:09
@markmur
markmur force-pushed the android-preload-observability branch from 880c148 to f0af5fd Compare July 16, 2026 15:20
@markmur
markmur force-pushed the swift-preload-per-entry branch from 599dafd to aaf688c Compare July 16, 2026 15:20
@markmur
markmur force-pushed the android-preload-observability branch from f0af5fd to 0c29857 Compare July 16, 2026 15:48
Comment thread platforms/android/lib/src/main/java/com/shopify/checkoutkit/ShopifyCheckoutKit.kt Outdated
terminate(if (cached.key == key) PreloadState.Expired else PreloadState.Idle)
null
} else {
ShopifyCheckoutKit.log.d(LOG_TAG, "Returning cached preloaded WebView.")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Re: line +69]

Do we also need a transition here? E.g. a state of PreloadState.consumed or similar

Or from another angle, do we need to actually consume preloads in this way :hmm:

See this comment inline on Graphite.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm just leaving it as .ready across platforms for now. I didn't introduce any state to indicate that it's been presented, but it's worth considering.

Something like this might work:

.idle → .loading → .ready → .presented → (dismiss) .ready → .expired

Comment thread platforms/android/lib/src/main/java/com/shopify/checkoutkit/ShopifyCheckoutKit.kt Outdated
@markmur
markmur force-pushed the swift-preload-per-entry branch from b0aff16 to e67aa39 Compare July 16, 2026 19:16
@markmur
markmur force-pushed the android-preload-observability branch from 0c29857 to 3980889 Compare July 16, 2026 19:16
@markmur
markmur force-pushed the swift-preload-per-entry branch from e67aa39 to a896563 Compare July 16, 2026 19:41
@markmur
markmur force-pushed the android-preload-observability branch from 3980889 to 8410f99 Compare July 16, 2026 19:41
@bitrise

bitrise Bot commented Jul 16, 2026

Copy link
Copy Markdown

Install this build

Open Tophat, select your target device, then click Install. Links open on the Mac running Tophat.

SDK Install
Kotlin Install with Tophat

Checkout Kit E2E results

Status Suite Target Platform OS version tag Device
tests/shared/launch-smoke.yaml kotlin android latest Google Pixel 9
Android 17.0

@markmur
markmur changed the base branch from swift-preload-per-entry to graphite-base/451 July 17, 2026 12:48
@markmur
markmur force-pushed the android-preload-observability branch from f5fa14a to ba71690 Compare July 17, 2026 12:48
@markmur
markmur force-pushed the graphite-base/451 branch from a896563 to 198bbd0 Compare July 17, 2026 12:48
@markmur
markmur changed the base branch from graphite-base/451 to main July 17, 2026 12:48
@markmur
markmur force-pushed the android-preload-observability branch from ba71690 to affdb58 Compare July 17, 2026 14:53
@markmur markmur self-assigned this Jul 17, 2026
Comment thread platforms/android/lib/src/main/java/com/shopify/checkoutkit/CheckoutWebView.kt Outdated
/**
* Called on the main thread whenever the preload state changes.
*/
public var listener: PreloadStateListener? = null

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On iOS, assigning onStateChange immediately replays the current state via a didSet; on Android listener is a plain property with no replay, so a consumer who attaches a listener after obtaining the handle receives nothing until the next transition.

I think this gets us closer to iOS / didSet

public var listener: PreloadStateListener? = null
        set(value) {
            field = value
            value?.onStateChanged(cache.state)
        }

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated. Assigning listener now immediately replays the current state on the main thread, matching iOS.

public fun preload(
checkoutUrl: String,
context: ComponentActivity,
listener: PreloadStateListener? = null,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The cache stores the observer as a WeakReference<CheckoutPreload>, and preload(url, activity) { state -> ...} returns the handle but nothing else retains it.

Only Loading fires synchronously, al the later states are driven by async WebView callbacks.

Because the old API returned Unit, folks migrating might keep that call shape, and add the trailing lambda (i.e. no assigning handle to a variable). That would mean CheckoutPreload is unreferenced and eligible for garbage collection right after the preload call returns.

If it gets collected, consumers might not get the other state transitions.

I wonder if we should keep a handle to CheckoutPreload in the library (until a terminal state / another preload call / preload consumed / preload fails)? Maybe passing the listener into CheckoutWebView.preload(), passing into preloadCache.store() storing as an instance var in the cache, and then setting to null at various transitions

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. The cache now strongly retains the current CheckoutPreload, so callback-only usage continues receiving asynchronous transitions. It releases the observer on Idle, Expired, or Failed, or replaces it when another preload starts. Cleanup is reentrancy-safe, so callbacks can start a replacement preload.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

#gsd:50662 Rebase Checkout Kit on UCP

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants