Home › Articles › Shipping blendMode to RawImage
One Parameter, 75 Days: Shipping blendMode to Flutter's RawImage
The framework already knew how to blend images. The widgets just never asked. The anatomy of my tenth merged Flutter PR.
On July 17, 2026, PR #185938 merged into flutter/flutter. It adds exactly one parameter: blendMode on RawImage and its render object, RenderImage.
Seventy three lines added. Two removed. Four files. Seventy five days from opening the PR to seeing the purple merged badge.
That ratio, tiny diff to long journey, is the most honest picture of framework contribution I can give you. So this article is a walkthrough of the whole thing: the gap I found, how the API was designed so it could not break anyone, the tests, the review, and the patterns that make a first framework PR realistic for anyone reading this from Karachi, Lahore, or anywhere else.
The gap: capability without a doorway
Deep in Flutter's painting layer there is a workhorse function called paintImage. Every image you see in a Flutter app eventually funnels through it. And paintImage has accepted a blendMode parameter for years: it controls the Paint.blendMode used when the image is composited onto the canvas.
RawImage, the low-level widget that paints a dart:ui image directly, delegates its painting to exactly that function. So does RenderImage, the render object underneath it.
Here is the strange part: neither of them exposed the parameter. The blend mode was permanently stuck on the default, BlendMode.srcOver. The capability existed one layer down, fully implemented, fully tested, and the public API simply never handed you the key.
This is what issue #145614 was about. The concrete pain: crossfading two images. The classic fade-between-photos effect looks wrong with two stacked srcOver fades, because mid-transition both images are semi-transparent and the result visibly dips toward the background. The clean fix is additive blending, BlendMode.plus, where the outgoing and incoming images sum to full brightness through the whole transition. The painting layer could do it. The widget would not let you.
Lesson one: the framework often already knows how. Look for parameters that stop one layer short of the surface.
Designing a boring API, on purpose
The fastest way to get a framework PR rejected is to be creative. The fastest way to get it merged is to be boring in exactly the way the codebase is already boring.
RenderImage already had a template for this situation: filterQuality, another non-nullable enum property that affects painting only. So the new property copies that pattern line for line:
- Constructor argument with a default:
this.blendMode = BlendMode.srcOver, matching thepaintImagedefault, so every existing caller behaves identically. - Private field with a getter and setter. The setter short-circuits if the value is unchanged, then calls
markNeedsPaint(). - Forwarded in
paint()straight into the existingpaintImage(blendMode: ...)call. - Reported in
debugFillPropertiesso DevTools and widget inspector dumps show it like every other property.
One detail worth pausing on, because reviewers absolutely check it: the setter calls markNeedsPaint(), not markNeedsLayout(). Changing a blend mode cannot change the size or position of anything. It only changes pixels. Signalling the cheaper invalidation is not a micro-optimization, it is a statement to the reviewer that you understand the rendering pipeline's phases.
Using it looks like this:
RawImage(
image: frame.image,
fit: BoxFit.cover,
// NEW in Flutter after PR #185938:
blendMode: BlendMode.plus, // additive crossfade, no mid-fade dip
)
And because the default is BlendMode.srcOver, the diff is invisible to the entire existing Flutter ecosystem. Zero migrations, zero deprecations, zero behavior changes unless you opt in. For a framework used by millions of apps, default-preserving is not a nice-to-have, it is the price of entry.
Tests at both layers
The test additions mirror how the framework thinks about its own layering:
- Widget layer (
widgets/image_test.dart): build aRawImagewith no blend mode and assert the createdRenderImagereportsBlendMode.srcOver; build one withBlendMode.plusand assert it forwards. - Rendering layer (
rendering/image_test.dart): the same pair of assertions directly onRenderImage, without a widget tree in between.
Testing the default explicitly matters as much as testing the custom value. The default test is the regression guard that proves you did not silently change behavior for the millions of existing RawImage users. That is the test a reviewer looks for first on a PR like this.
The review: one bot, two humans, seventy five days
The PR went up on May 3. First feedback came from gemini-code-assist, the AI reviewer that now does a first pass on Flutter PRs. Then the humans: justinmc from the Flutter team and loic-sharma reviewed, commented, and eventually approved.
Most of the elapsed time was not back-and-forth. It was queueing: reviewer bandwidth, tree status, the reality that Flutter lands hundreds of commits a week and a small API addition is never anyone's emergency. The work on my side was patience discipline: keep the branch green against main, respond quickly when feedback arrived, and resist the urge to ping more than once a week.
If you have read my first contribution playbook, this is the part that does not get easier with experience. PR number ten waits in the same queue as PR number one.
What it unlocks
Small parameter, real use cases:
- Additive crossfades. The original motivation:
BlendMode.pluscrossfades between images without the mid-transition brightness dip. - Tint and mask effects without extra layers. Modes like
multiply,screen, oroverlayapplied at composite time, in the same paint call that draws the image, with nosaveLayerand no wrapper widget. - Custom image pipelines. If you are building your own frame-by-frame image presentation on top of
RawImage(video thumbnails, camera previews, sprite sheets), you now control the compositing math directly.
The repeatable playbook
Strip the specifics away and this PR is a formula you can reuse:
- Hunt for buried capability. Grep the layer below the API you use. When a lower-level function accepts a parameter the public widget does not expose, you have found a candidate feature that is almost pre-approved, because the hard part already exists.
- Anchor on an issue. A feature request with real use cases (#145614 here) turns "random API addition" into "closing a known gap".
- Copy the house pattern. Find the nearest existing property of the same shape (
filterQuality) and match it exactly: naming, defaults, invalidation, debug properties. - Preserve every default. Your diff should be invisible until someone opts in.
- Test both layers, both directions. Default stays default, custom forwards through.
- Then wait well. Green CI, fast responses, no nagging.
That formula is how a two-line-of-actual-logic change becomes a permanent part of a framework running on hundreds of millions of devices. And it is repeatable from anywhere: this one was written in Karachi.
Want to land your first Flutter framework PR?
I mentor engineers on exactly this path: finding the right first issue, surviving review, and building a contribution track record. 13+ years of engineering, 10 merged PRs into the Flutter ecosystem, and the scars to prove the queue is survivable.
Get in touch →FAQ
What does the blendMode parameter on RawImage do?
It controls the Paint.blendMode used when the image is composited onto the canvas. The default stays BlendMode.srcOver, so nothing changes for existing code, but you can now pick modes like BlendMode.plus for additive crossfades or multiply for tinting, directly on RawImage and RenderImage.
Why not just wrap the image in ColorFiltered?
ColorFiltered applies its effect through an extra layer, which costs a saveLayer and only covers color-filter style effects. blendMode on RawImage changes how the image itself is composited, in the same paint call that draws it: no extra layer, no wrapper widget, and it handles compositing effects like additive blending that ColorFiltered is not designed for.
How long does a Flutter framework PR take to merge?
This one took 75 days: opened May 3, merged July 17, 2026. Small, default-preserving API additions with tests tend to move faster than big features, but reviewer bandwidth varies, so patience and polite, well-timed follow-ups are part of the job.
How do I find my own first framework contribution?
Look for capability that stops one layer short of the public API, like paintImage's blendMode did. Search the issue tracker for feature requests that map to an existing lower-layer parameter, copy the surrounding code patterns exactly, keep defaults unchanged, and ship tests with the first push.
Source of record:
Related reading
- → How a Pakistani Engineer Got 6 PRs Merged Into Flutter : the original framework contribution playbook
- → Flutter's Three-Tree Architecture Explained : why RenderImage is where painting decisions live
- → Building Production Flutter Plugins (156-likes case study) : shipping at the plugin layer instead
Ishaq Hassan is a Flutter Framework Contributor with 10 merged PRs across the Flutter ecosystem, Engineering Manager at DigitalHire, and creator of the 35-video Urdu Flutter course listed on docs.flutter.dev. Karachi, Pakistan. ishaqhassan.dev.