Rejourney Flutter Analytics in Beta: Session Replay for Flutter GPU and Impeller

mrr73371 pts0 comments

Flutter Session Replay SDK: Rejourney Open Beta<br>Log inGet started

Back to Engineering Log<br>The Rejourney Flutter SDK is now in open beta on pub.dev. The current release is 0.3.1. It supports Flutter 3.22+, Dart 3.3+, iOS 15.1+, and Android API 24+.<br>flutter pub add rejourney

// Configure first. Recording starts only when start() is called.<br>await Rejourney.init('pk_live_your_public_key');<br>await Rejourney.start();<br>01 // THE BRIDGEKeep the Dart side small

Rejourney already had native recorders for iOS and Android. Rewriting their lifecycle, upload, crash recovery, and capture logic in Dart would have created a third recorder with subtly different behavior. The Flutter package instead uses one method channel, co.rejourney.flutter/methods, as a narrow boundary around the native cores.<br>Dart owns the parts that only Flutter can describe well: route changes, framework errors, widget bounds, and the retained render layer. Native code owns session state, remote recording policy, the capture timer, encoding, persistence, and upload. The boundary carries plain maps, lists, byte arrays, and scalar values. Rejourney._channelValue rejects anything the platform channel cannot carry before it reaches Kotlin or Swift.<br>init() also does less than its name might suggest. It validates the project key and options, then configures both native sides. Capture waits for an explicit start(). That split gives an app a clean place to wait for consent. It also lets remote sampling and the recording kill switch run before a framebuffer read begins.<br>Platform boundary<br>abstract class RejourneyPlatform extends PlatformInterface {<br>Future invoke(<br>String method,<br>[Map? arguments],<br>);

Stream> get events;

02 // ANDROID GPU CAPTUREPixelCopy said success. The replay was black.

A Flutter screen on Android is usually presented through a FlutterSurfaceView. Calling draw() on the surrounding Android view hierarchy gets the container, but not the GPU pixels on that surface. We use PixelCopy on the Flutter surface for the normal path.<br>The first beta builds exposed a worse failure mode. On some renderer and device combinations, PixelCopy returned SUCCESS and handed us a correctly sized black bitmap. A toast could make the whole window look non-empty, leaving a replay with one small native overlay floating over a black Flutter app. Checking the result code was not enough.<br>The recorder now samples a 24 by 24 grid and classifies the result. The detector checks near-black ratio, luma range, and how many pixels contain a meaningful signal. One branch requires at least 98.5% near-black samples with a maximum luma below 48. Another catches the sparse-overlay case. The classifier has no Android graphics dependency, so its edge cases run as ordinary JVM tests.<br>False-success classification<br>val nearBlackRatio = nearBlack.toDouble() / visible.toDouble()<br>return (nearBlackRatio >= 0.985 && maximumLuma = 0.975 && nonBlack<br>We also find Flutter by its typed FlutterView hierarchy. Matching a class-name string worked in debug builds and failed after Android minification changed the name. That was a small bug with a very convincing local test result.

03 // RETAINED LAYERSThe fallback asks Dart for pixels

Once the native recorder recognizes a black Flutter surface, the method channel runs in the other direction. Android calls _captureFlutterFrame. Dart reads the root OffsetLayer from Flutter's RenderView and rasterizes that retained layer with toImage().<br>A request can arrive while Flutter is building a frame. During transient callbacks, mid-frame microtasks, or persistent callbacks, the capture waits for endOfFrame. It then calculates a pixel ratio for the native target dimensions, requests raw RGBA bytes, copies those bytes, and disposes the engine image. The copy matters because an engine may back ByteData with image-owned storage.<br>Retained Flutter scene readback<br>final phase = SchedulerBinding.instance.schedulerPhase;<br>if (phase == SchedulerPhase.transientCallbacks ||<br>phase == SchedulerPhase.midFrameMicrotasks ||<br>phase == SchedulerPhase.persistentCallbacks) {<br>await SchedulerBinding.instance.endOfFrame;

final layer = renderView.layer as OffsetLayer;<br>final image = await layer.toImage(bounds, pixelRatio: scale);<br>final bytes = await image.toByteData(format: ui.ImageByteFormat.rawRgba);<br>Android turns the returned bytes into a bitmap, composites any native sheet roots, applies redaction, and sends JPEG work to its single-thread encode executor. The retained-layer request uses half the usual replay width and height. That cuts the readback to one quarter of the pixels on the devices already struggling with the normal path.<br>Compatibility capture also runs less often. Its idle heartbeat is 15 seconds. A high-importance visual change can request a frame after a 5-second minimum interval, and navigation waits 2.5 seconds for the destination to settle. An in-flight guard drops overlapping ticks. A five-second timeout releases the guard if Dart never answers. The app's live...

flutter rejourney android native dart layer

Related Articles