Rivora Labz Narayan Dhingra
All ventures

Snook A Look, Compose Multiplatform components

Five pieces of production code from the live Snook A Look app, extracted and written up as standalone components.

App
Snook A Look, iOS and Android
Stack
Kotlin Multiplatform, Compose Multiplatform, Decompose
Source
Open on GitHub
Two Snook A Look iPhone screens: clubs on the map, and a club's booking page

The components

iOS-like predictive back

compose-predictive-back-parallax

Decompose's predictive back gesture driving an iOS-style transition: the screen follows the finger, the previous screen parallaxes in from minus 30 percent, and a scrim fades out. A cancelled swipe springs back from exactly where the finger left.

Code and write-up on GitHub
IosLikePredictiveBackAnimatable.kt
override val exitModifier: Modifier
    get() = Modifier.graphicsLayer {
        // Active screen follows finger: 0% → 100% of its own width
        translationX = rawProgress * size.width
    }

override val enterModifier: Modifier
    get() = Modifier
        .graphicsLayer {
            // Previous screen parallaxes from -30% at rest toward 0 on complete
            translationX = (1f - rawProgress) * (-0.30f * size.width)
        }
        .drawWithContent {
            drawContent()
            // Scrim fades from 40% at rest to 0 as gesture progresses
            val scrimAlpha = (1f - rawProgress) * scrimColor.alpha
            if (scrimAlpha > 0f) {
                drawRect(color = scrimColor.copy(alpha = scrimAlpha))
            }
        }

Lifecycle-safe iOS camera bridge

kmp-ios-camera-bridge

The native UIImagePickerController bridged into Compose: the delegate is retained in Compose state so Kotlin/Native cannot collect it, state is written only after the dismiss animation finishes, and resizing runs off the main thread.

Code and write-up on GitHub
CameraCaptureBridge.ios.kt
override fun imagePickerController(
    picker: UIImagePickerController,
    didFinishPickingMediaWithInfo: Map<Any?, *>,
) {
    val image = didFinishPickingMediaWithInfo[UIImagePickerControllerOriginalImage] as? UIImage
    // Capture image ref BEFORE dismiss; move delegate cleanup + encode INSIDE
    // completion so capturedBytes is only written after the dismiss animation
    // fully finishes, prevents UIKit "presentation in progress" collision
    // when Compose consent AlertDialog tries to present mid-dismiss.
    picker.dismissViewControllerAnimated(true) {
        picker.delegate = null
        retainedDelegate = null
        if (image != null) {

Mapbox in a WKWebView, sharing KMP state

kmp-ios-mapbox-webview-bridge

mapbox-gl-js inside a WKWebView instead of the native SDK: markers, theme and user location flow in from shared Kotlin state through JavaScript, so the camera never reloads mid-flight.

Code and write-up on GitHub
MapViewBridge.ios.kt
val html = remember(isLight) { buildMapHtml(isLight, MapPoseSession.last) }
val readyFlow = remember { MutableStateFlow(false) }
var webViewRef by remember { mutableStateOf<WKWebView?>(null) }

val currentUserLoc by rememberUpdatedState(userLocation)
val currentIsOutsideUae by rememberUpdatedState(isOutsideUae)

// Inject / refresh markers when centers change. Map state preserved.
LaunchedEffect(centers) {
    readyFlow.first { it }
    val webView = webViewRef ?: return@LaunchedEffect
    val markersJson = buildMarkersJson(centers)
    webView.evaluateJavaScript("snookSetMarkers($markersJson);", null)
}

Cinematic map intro, as a state machine

kmp-map-choreography

A four-beat Mapbox camera intro, from country overview to the Dubai cluster to the user, that survives late GPS, users outside the UAE and mid-flight drags. Camera requests are serialised through one channel that defers to the intro.

Code and write-up on GitHub
MapCameraRequestChannel.kt
object MapCameraRequestChannel {
    data class Target(
        val lat: Double,
        val lng: Double,
        val zoom: Double,
        val durationMs: Int,
    )

    private val _requests = MutableSharedFlow<Target>(
        replay = 0,
        extraBufferCapacity = 4,
        onBufferOverflow = BufferOverflow.DROP_OLDEST,
    )

    val requests: SharedFlow<Target> = _requests.asSharedFlow()

    fun request(lat: Double, lng: Double, zoom: Double = 14.0, durationMs: Int = 2000) {
        _requests.tryEmit(Target(lat, lng, zoom, durationMs))
    }

Apple Sign-In on Android

kmp-android-apple-signin

Sign in with Apple through Chrome Custom Tabs: shared code suspends on a continuation keyed by the OAuth state, and the redirect resumes it, even if the OS killed the app in between.

Code and write-up on GitHub
AppleAuthRegistry.kt
object AppleAuthRegistry {

    private val pending = ConcurrentHashMap<String, CancellableContinuation<AppleAuthArtifact>>()

    fun newState(): String = UUID.randomUUID().toString()

    suspend fun await(state: String): AppleAuthArtifact = suspendCancellableCoroutine { cont ->
        pending[state] = cont
        cont.invokeOnCancellation { pending.remove(state) }
    }

    fun resume(state: String, artifact: AppleAuthArtifact) {
        pending.remove(state)?.resume(artifact)
    }

    fun cancel(state: String) {
        pending.remove(state)?.resumeWithException(
            OAuthFlowException(OAuthErrorCode.USER_CANCELLED),
        )
    }

    fun isPending(state: String): Boolean = pending.containsKey(state)
}