cd /news/computer-vision/choosing-between-methodchannel-and-p… · home › topics › computer-vision › article
[ARTICLE · art-139267] src=dev.to ↗ pub= topic=computer-vision verified=true sentiment=· neutral

Choosing Between MethodChannel and PlatformView: A Real Decision, Not a Rule of Thumb

A mobile engineer with seven years of experience built LensBridge, a Flutter app that embeds a native Android camera preview using PlatformView with CameraX and a live ML Kit face-detection overlay drawn by a custom native View, while using MethodChannel for commands. The writeup documents three failure modes encountered during the build, including that CameraX's unbindAll() stops new frames but never clears the existing surface buffer, leaving the last frame frozen on screen, and that adding an overlay view without explicit LayoutParams causes it to measure to 0×0 so onDraw() renders nothing.

by read4 min views1 publishedSep 24, 2026

Seven years into mobile engineering, I wanted to answer a question I've seen a few times: when do you actually need PlatformView instead of MethodChannel? Most answers I've seen are either "PlatformView is for native UI" (true, but not useful) or skip straight to copy-pasteable boilerplate with none of the failure modes that actually teach you anything.

Over the weekend, I built a small Flutter app that embeds a native Android camera preview with a live ML Kit face-detection overlay, drawn natively, not through a Flutter widget stacked on top. This article is the build and, more usefully, the three things that broke along the way and what each one taught me about how Flutter's native bridging actually works under the hood.

Before touching code, it's worth being precise about something I initially framed loosely myself: MethodChannel isn't "too weak" for native work; it's just the wrong tool for a specific job. It's a request/response bridge. Dart calls a method, native code handles it, returns a result. For one-off calls, biometric auth, reading an NFC tag, taking a photo, it's genuinely the right tool, no asterisk needed.

What it categorically cannot do is put a native View inside your Flutter widget tree. It moves data, not UI. The moment you need an actual native surface rendering live inside your layout- a camera preview, a map SDK's own view- you need PlatformView. That's not a performance tradeoff or a style preference. It's the only mechanism Flutter gives you for that job.

So the real architecture question for LensBridge wasn't "MethodChannel or PlatformView". It was "PlatformView for the pixels, MethodChannel for the commands," running side by side, each doing what it's actually built for.

PlatformView`` PreviewView (CameraX) wrapped in a FrameLayout, with a second custom View stacked on top for drawing detection boxesImageAnalysis`` Preview) feeds frames to ML Kit's on-device face detector MethodChannel

class CameraPlatformView(
    private val context: Context,
    viewId: Int,
    private val lifecycleOwner: LifecycleOwner
) : PlatformView {

    private val previewView: PreviewView = PreviewView(context).apply {
        implementationMode = PreviewView.ImplementationMode.COMPATIBLE
    }
    private val overlayView: OverlayView = OverlayView(context)
    private val container: FrameLayout = FrameLayout(context).apply {
        addView(previewView, FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT))
        addView(overlayView, FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT))
    }

    override fun getView(): View = container
    // ...
}

Two lines in there, implementationMode and the explicit LayoutParams, look unremarkable. They cost me two separate debugging sessions. More on both below.

unbindAll() doesn't clear the surface After wiring MethodChannel controls for Stop, hitting it left the last camera frame frozen on screen instead of going blank. Not a bug. PreviewView is backed by a SurfaceView/ TextureView, and unbindAll() stops new frames from being pushed but never explicitly clears the existing buffer. Nothing tells the GPU to draw anything else over it, so the last frame just sits there.

Confirmed it was genuinely stopped (not stalled) by waving a hand in front of the camera. No update. Fixed it by explicitly toggling previewView.visibility on stop/start rather than relying on frame delivery to communicate state.

Once ML Kit was wired up and logcat confirmed detection was running (face_count=1 in the stats), nothing appeared on screen. No crash, no warning. The overlay simply didn't render.

The cause: I added the overlay view to its container with addView(view) and no explicit LayoutParams. Default behavior is WRAP_CONTENT, and since OverlayView has no intrinsic content, it's just a custom View doing manual canvas drawing, it measured out to 0×0. onDraw() was being called the whole time, just onto a canvas with no area to draw into.

addView(
    overlayView,
    FrameLayout.LayoutParams(
        FrameLayout.LayoutParams.MATCH_PARENT,
        FrameLayout.LayoutParams.MATCH_PARENT
    )
)

This is the one I'd flag hardest to anyone stacking a custom-drawn view over a CameraX preview: the pipeline can be entirely correct and you'll still see nothing, because the bug isn't in your detection logic at all.

Even after fixing #2, the overlay still didn't show, except for a single frame that flashed the instant I hit Stop. That flash was the clue.

PreviewView defaults to ImplementationMode.PERFORMANCE, which renders through a SurfaceView. SurfaceView content composites through a separate hardware layer, outside Android's normal view-drawing pass, meaning it can visually sit above other views in the same layout regardless of their z-order in code. My overlay was drawing correctly the entire time. It was just being painted over, every frame, by the camera feed's hardware-composited layer underneath it.

The fix is a documented tradeoff, not a hack:

previewView.implementationMode = PreviewView.ImplementationMode.COMPATIBLE

COMPATIBLE forces a TextureView instead, which renders as a normal part of the view hierarchy, with predictable layering, at the cost of losing PERFORMANCE mode's hardware-layer fast path. For a demo like this, that's the right trade every time.

If I rebuilt this today, I'd add the COMPATIBLE implementation mode from the start rather than discovering it two bugs deep. It's a known CameraX behavior. I just hadn't hit it firsthand before.

Code's on GitHub: https://github.com/droidchief/lense_bridge

── more in #computer-vision 4 stories · sorted by recency
── more on @flutter 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
→ Live at https://your-agent.zahid.host ✓
Get free account → Pricing
from €0/mo · no card required
LIVE [news/choosing-between-met…] indexed:0 read:4min 2026-09-24 · —