Flutter

Impeller on Android by Default: What Breaks in Flutter 3.47

Impeller has been the default renderer on Android since Flutter 3.27. Every stable release since then has moved another group of devices onto it, or narrowed the way back to Skia. This post is for Flutter developers who ship an Android app. Maybe you ignored the change so far, or maybe something looked odd after an upgrade. Either way, it covers Impeller on Android as it stands in Flutter 3.47.6: which renderer each class of device gets, whether the opt-out still works, and the specific things that break. Every version claim here traces to the release index, to the engine source at the tagged releases, or to a probe app that ran on two Flutter versions on the same day. The output appears below.

Versions Checked, and How

The timeline below comes from the Flutter release blog posts and the release index on the day of writing. The renderer selection rules do not come from the docs, which still describe the 3.27 behaviour in places. Instead, they come from the engine source at tags 3.35.2, 3.41.0, 3.44.0 and 3.47.0. A release build of a probe app then confirmed them on an emulator, once per Flutter SDK.

Checked 2026-10-07
Windows 11 Pro 25H2 (build 26200), AMD Ryzen 5 8600G, 15.2 GB RAM
Flutter 3.35.2 stable (Dart 3.9.0, engine a8bfdfc394)
Flutter 3.47.6 stable (Dart 3.13.5, engine 692136cb65), current stable, released 2026-10-01
Android Emulator 35.5.10, Pixel 9 Pro AVD, Android 15 (API 35) x86_64 Google Play image, -gpu swiftshader_indirect
3.35.2 template: Gradle 8.12, Android Gradle Plugin 8.9.1, Kotlin 2.1.0
3.47.6 template: Gradle 9.3.1, Android Gradle Plugin 9.1.0, Kotlin 2.4.0
JDK 21 (Android Studio bundled runtime) for both
Release index   https://storage.googleapis.com/flutter_infra_release/releases/releases_windows.json
Engine source   https://github.com/flutter/flutter/tree/3.47.0/engine/src/flutter/shell/platform/android
Next scheduled re-check: 2027-01-07, or when a stable release removes the Android opt-out

The probe is a stock flutter create app with no plugins. Each SDK built it twice as a release APK for android-x64. One build keeps the template manifest, and the other sets the EnableImpeller meta-data to false. The results are log lines and string counts, not timings, so they do not vary between runs. One limitation applies throughout: everything ran on the emulator, which the engine deliberately keeps on OpenGL ES. Consequently, the Vulkan section below rests on the source and on issue reports, not on a log from this machine.

When Did Impeller on Android Become the Default?

Impeller became the default on Android in Flutter 3.27, released 2024-12-11. It applied to devices on API 29 and newer with a working Vulkan driver. Each release since then changed who gets what. The table lists only the changes that affect an Android app, with the official source for each.

FlutterReleasedWhat changed for Android renderingSource
3.272024-12-11Impeller on Vulkan by default on API 29+. Everything else stays on Skia. Opt-out via --no-enable-impeller or the manifestWhat’s new in 3.27
3.292025-02-12Devices without a working Vulkan driver move from Skia to Impeller on OpenGL ES. Emulators move to Impeller GLES. MediaTek and PowerVR lose Vulkan. iOS loses Skia entirelyWhat’s new in 3.29
3.29.3 and 3.322025-04-14 and 2025-05-20API 28 and older go back to Skia regardless of flags. Flutter announces removal of the opt-out “in an upcoming stable release”What’s new in 3.32
3.382025-11-12Opting out now prints an “[Action Required]” warning. Exynos 9820 and 9825 lose Vulkan. Java 17 becomes the minimumPR 173375, 3.38 notes
3.442026-05-18Manifest flags move into a new FlutterEngineFlags class. EnableImpeller stays allowed in release, while release builds drop debug-only flagsFlutterEngineFlags.java at 3.44.0
3.472026-08-12OpenGL ES now stores render-to-texture content top-down, which changes custom shaders. MediaTek Vulkan now needs vendor SDK 32. Impeller becomes the default on desktop tooBreaking change, PR 186405
3.47.62026-10-01Current stable. The engine still honours the opt-out, with the warning, as measured belowThis post

The Skia Removal That Did Not Happen

One correction belongs up front, because the mistake is everywhere. Several articles state that Flutter 3.44 removed Skia from Android 10 and newer and that the opt-out no longer works. The engine source at 3.44.0 and 3.47.0 still contains the Skia OpenGL ES path. The official Can I use Impeller? sheet still lists Android as “Default Impeller” with an opt-out. Moreover, the measured run below shows the opt-out working on 3.47.6. Flutter has announced the removal, not shipped it.

Which Renderer Does Your App Actually Get?

Impeller on Android is not one renderer. The engine chooses between three at startup: Skia on OpenGL ES, Impeller on OpenGL ES, and Impeller on Vulkan. It decides in two steps, and both steps are readable in the source at tag 3.47.0. First, flutter_main.cc decides between Skia and Impeller. Then, android_context_dynamic_impeller.cc decides between Vulkan and OpenGL ES.

Here is the first step, in the order the code checks it:

  1. An app that requests software rendering gets the software renderer. Asking for Impeller at the same time is a fatal check
  2. In debug and profile builds only, an ImpellerBackend value of opengles or vulkan forces that backend
  3. Impeller enabled, API 29 or higher, and no Vivante GPU driver: the app gets Impeller with automatic backend selection
  4. Otherwise the app gets Skia on OpenGL ES

So three things put an app on Skia in 3.47.6: an explicit opt-out, a device on Android 9 or older, or a Vivante GPU. The engine detects Vivante through the ro.hardware.egl property, and nothing else qualifies. Notably, the default Flutter template now sets minSdkVersion to 24. An app built with defaults therefore ships both renderers, and every user still on Android 7 through 9 runs on Skia.

The Vulkan Denylist in Flutter 3.47

The second step is where most surprises come from. Automatic selection tries Vulkan first. However, it skips Vulkan without even creating a context on any of the following, and uses Impeller on OpenGL ES instead:

  • Any emulator, which the engine detects through ro.hardware, a gphone model name, or the /dev/qemu_pipe device
  • Any Huawei device, detected through ro.com.google.clientidbase
  • Any MediaTek platform with a vendor SDK level below 32. PR 186405 raised this from API 31 in 3.47 after a gralloc crash on the MT8788
  • Any board on the hard-coded bad SoC list. In 3.47 that means Exynos 7870, 7880, 7872, 7884, 7885, 7904, 8890, 8895, 9609, 9610, 9611, 9810, 9820, 9825 and rk30sdk

If none of those match, the engine creates a Vulkan context and then inspects the driver. driver_info_vk.cc rejects the context, logs “Known bad Vulkan driver encountered, falling back to OpenGLES”, and uses GLES for:

  • Adreno 650 and older
  • Huawei Maleoon GPUs
  • Samsung Xclipse GPUs that do not report Vulkan 1.3
  • PowerVR GPUs older than the BXE series
  • A Pixel 10 whose PowerVR driver is older than version 25.1

That list is longer than most teams expect. In practice, an app with a broad Android user base runs on all three renderers at once. A rendering bug report therefore needs the device model before it means anything. The next section shows the adb logcat line that names the backend. It is the first thing to ask a reporter for.

Does the Impeller Opt-Out Still Work in Flutter 3.47?

Yes. On 3.47.6 both the manifest entry and the --no-enable-impeller flag still switch an API 35 device to Skia. What changed since 3.35 is that the engine now prints a ten-line warning when you do it. To show this, each SDK built the probe app as a release APK, with and without the manifest opt-out. Each build then launched on the emulator while logcat captured the flutter tag.

# Four release APKs, built from the same `flutter create` template with each SDK.
# "optout" builds carry EnableImpeller=false in AndroidManifest.xml.
for apk in 3352-default 3352-optout 3476-default 3476-optout; do
  echo "########## $apk"
  adb -e install -r "apks/$apk.apk" | tail -1
  adb -e logcat -c
  adb -e shell am start -W -n com.example.impeller_probe/.MainActivity | grep "^Status"
  sleep 8
  adb -e logcat -d -s flutter:* > "logcat-$apk.txt"
  echo "flutter-tag lines: $(grep -c 'I flutter' logcat-$apk.txt)"
  grep -E "Impeller|opt-out" "logcat-$apk.txt" | sed -E 's/^[0-9-]+ [0-9:.]+ +[0-9]+ +[0-9]+ //' | head -4
  adb -e uninstall com.example.impeller_probe > /dev/null
done
########## 3352-default
Success
Status: ok
flutter-tag lines: 1
I flutter : [IMPORTANT:flutter/shell/platform/android/android_context_gl_impeller.cc(104)] Using the Impeller rendering backend (OpenGLES).

########## 3352-optout
Success
Status: ok
flutter-tag lines: 0

########## 3476-default
Success
Status: ok
flutter-tag lines: 1
I flutter : [IMPORTANT:flutter/shell/platform/android/android_context_gl_impeller.cc(104)] Using the Impeller rendering backend (OpenGLES).

########## 3476-optout
Success
Status: ok
flutter-tag lines: 10
I flutter : [IMPORTANT:flutter/shell/common/shell.cc(528)] [Action Required]: Impeller opt-out deprecated.
I flutter :     The application opted out of Impeller by either using the
I flutter :     `io.flutter.embedding.android.EnableImpeller` `AndroidManifest.xml` entry.
I flutter :     the explicit opt-out. If you need to opt-out, please report a bug describing

Three things stand out. First, both SDKs put the default build on Impeller with OpenGL ES, because the emulator is on the denylist above. So this is also what every developer sees during day-to-day work. Second, the 3.35.2 opt-out build printed nothing at all, which is what Skia looks like in the log: silence. Third, the 3.47.6 opt-out build printed the warning instead of the Impeller banner, and nothing else. It is on Skia as well.

The loop above keeps only the first four matching lines per build. Here is the complete warning. It comes from a debug flutter run --no-enable-impeller of the 3.47.6 probe with the stock manifest. The manifest route printed exactly the same text:

I flutter : [IMPORTANT:flutter/shell/common/shell.cc(528)] [Action Required]: Impeller opt-out deprecated.
I flutter :     The application opted out of Impeller by either using the
I flutter :     `--no-enable-impeller` flag or the
I flutter :     `io.flutter.embedding.android.EnableImpeller` `AndroidManifest.xml` entry.
I flutter :     These options are going to go away in an upcoming Flutter release. Remove
I flutter :     the explicit opt-out. If you need to opt-out, please report a bug describing
I flutter :     the issue.
I flutter :
I flutter :     https://github.com/flutter/flutter/issues/new?template=02_bug.yml
I flutter :

What the Release Engine Binaries Contain

PR 173375 added the warning on 2025-08-07, and it shipped in 3.38. To confirm which engine binaries carry it, a string search ran over the arm64 release engine from each SDK. GrGLInterface is a Skia OpenGL symbol. Its presence confirms that the Android release engine still contains Skia:

# libflutter.so extracted from bin/cache/artifacts/engine/android-arm64-release/flutter.jar
for v in 3.35.2 3.47.6; do
  echo "=== $v"
  so="libs/$v/lib/arm64-v8a/libflutter.so"
  for s in "Impeller opt-out deprecated" "Using the Impeller rendering backend (OpenGLES)" \
           "Using the Impeller rendering backend (Vulkan)" "Known bad Vulkan driver encountered" "GrGLInterface"; do
    printf '  %-52s %s\n' "$s" "$(grep -a -c -F "$s" "$so")"
  done
done
=== 3.35.2
  Impeller opt-out deprecated                          0
  Using the Impeller rendering backend (OpenGLES)      1
  Using the Impeller rendering backend (Vulkan)        1
  Known bad Vulkan driver encountered                  1
  GrGLInterface                                        11
=== 3.47.6
  Impeller opt-out deprecated                          1
  Using the Impeller rendering backend (OpenGLES)      1
  Using the Impeller rendering backend (Vulkan)        1
  Known bad Vulkan driver encountered                  1
  GrGLInterface                                        10

The practical reading is simple. The opt-out works today. Flutter has deprecated it and has said twice in writing that it will go. Treat it as a bridge for one or two releases while you fix whatever made you reach for it. Also file the bug the warning asks for. That report is the only thing that can keep the escape hatch open.

What Breaks When Impeller Is the Default on Android

Most apps upgrade and notice nothing. The breakages below are either new in 3.44 and 3.47, or they only show up on a subset of devices. The second kind slips past a developer whose test device sits on the Vulkan path. If your app also has broader frame-time problems after the move, start with the Flutter performance optimization guide. It covers the usual suspects that have nothing to do with the renderer.

Fragment Shaders Flip on OpenGL ES Devices in 3.47

Before 3.47, Impeller’s OpenGL ES backend stored render-to-texture content bottom-up, while Metal and Vulkan stored it top-down. Shader authors worked around that with a conditional flip. As of 3.47, GLES stores it top-down too. The old workaround now flips the image the wrong way on every device on the GLES path. Per the denylist above, that includes every emulator. The breaking change page documents the migration. Here is the pattern before and after:

// shaders/blur_sample.frag, before Flutter 3.47
#include <flutter/runtime_effect.glsl>

uniform vec2 u_size;
uniform sampler2D u_texture;
out vec4 fragColor;

void main() {
  vec2 uv = FlutterFragCoord().xy / u_size;
#ifdef IMPELLER_TARGET_OPENGLES
  uv.y = 1.0 - uv.y;   // Correct on 3.46 and earlier, upside down on 3.47
#endif
  fragColor = texture(u_texture, uv);
}
// shaders/blur_sample.frag, portable across 3.3x and 3.47
#include <flutter/runtime_effect.glsl>

uniform vec2 u_size;
uniform sampler2D u_texture;
out vec4 fragColor;

void main() {
  vec2 uv = FlutterFragCoord().xy / u_size;
  // The macro only exists on releases that store GLES render targets top-down,
  // so the flip still runs on older SDKs and is skipped on 3.47 and later.
#if defined(IMPELLER_TARGET_OPENGLES) && !defined(IMPELLER_OPENGLES_UNFLIPPED_DEPRECATED)
  uv.y = 1.0 - uv.y;
#endif
  fragColor = texture(u_texture, uv);
}

This works because the compiler defines IMPELLER_OPENGLES_UNFLIPPED_DEPRECATED only on the new behaviour. One shader source can therefore serve a package that supports several Flutter versions. If your app pins one SDK, delete the flip instead. Either way, test on the emulator, since it is the one GLES device every developer already has.

A Plugin Manifest Can Switch Your App Back to Skia

Android merges every library’s manifest into yours, and that includes meta-data elements. As a result, a plugin that ships EnableImpeller=false in its own manifest opts your whole app out. No file you own changes. The CAS ads SDK did exactly this. The report shows why it is hard to spot: the app still builds, still launches, and only the log line changes.

To see what actually shipped, dump the merged manifest from the APK rather than reading your source. The command below ran against the opt-out probe build:

BT="$ANDROID_HOME/build-tools/36.0.0"
"$BT/aapt2" dump xmltree --file AndroidManifest.xml apks/3476-optout.apk \
  | grep -A2 "E: meta-data" | sed -E 's/^ +//' | grep -B1 -A1 EnableImpeller
E: meta-data (line=37)
A: http://schemas.android.com/apk/res/android:name(0x01010003)="io.flutter.embedding.android.EnableImpeller" (Raw: "io.flutter.embedding.android.EnableImpeller")
A: http://schemas.android.com/apk/res/android:value(0x01010024)=false

If that element appears and you did not write it, strip it in your own manifest. When the app marks an element with tools:node="remove", the manifest merger drops it from every lower-priority manifest. That restores the default without asserting true yourself:

<!-- android/app/src/main/AndroidManifest.xml -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">
    <application
        android:label="my_app"
        android:name="${applicationName}"
        android:icon="@mipmap/ic_launcher">
        <!-- Drop any EnableImpeller opt-out merged in from a dependency's manifest -->
        <meta-data
            android:name="io.flutter.embedding.android.EnableImpeller"
            tools:node="remove" />
        <!-- activities and the flutterEmbedding meta-data follow as usual -->
    </application>
</manifest>

Then re-run the aapt2 dump and confirm the element no longer appears. Also ask why the plugin did it. An ad or video SDK that disables Impeller usually had a real rendering problem with its platform view. So after removing the override, exercise that SDK’s screens on a Vulkan device before shipping. The platform channels guide explains how such plugins embed native views, which is where those problems live.

Release Builds Ignore ImpellerBackend in the Manifest

The same FlutterEngineFlags class that keeps EnableImpeller also keeps ImpellerBackend, marked as allowed in release. However, the C++ side wraps the backend check in #ifndef FLUTTER_RELEASE. So debug and profile builds honour a manifest entry that pins opengles or vulkan. Release builds ignore it silently and run automatic selection instead. Teams sometimes pin GLES to work around a Vulkan bug and confirm the fix in a debug build. The release they ship still uses Vulkan. Use EnableImpeller or a device-specific bug report for that case, and keep ImpellerBackend for testing both paths on one device.

Release Builds Drop Debug-Only Manifest Flags Since 3.44

Flutter 3.44 moved manifest flag handling into FlutterEngineFlags and gave every flag an allowedInRelease bit. EnableVulkanValidation, EnableOpenGLGPUTracing, EnableVulkanGPUTracing and TraceSkia lack that bit. A release build logs an error naming the key and ignores them. That is a sensible security change. However, if a profiling flag in your manifest fed release-mode traces, those traces quietly lose their GPU events after 3.44. Move those flags to a profile build or pass them through flutter run instead.

Android 9 and Older Still Run Skia

Flutter 3.29.3 put every device below API 29 back on Skia, whatever the flags say, and 3.47.6 still does. With the template minSdkVersion of 24, that means roughly Android 7 through 9. The share depends entirely on your market. The two renderers differ in text rendering, in blur quality, and in how custom shaders behave. A fix you verified on a Pixel can therefore look different on a 2018 phone. The useful habit is a three-device test matrix. Use one Vulkan device, one GLES device such as the emulator, and one Android 9 device or API 28 AVD. The widget and integration testing guide covers golden tests, which catch most of this class of regression without the devices.

Devices on the GLES Path Crash Differently

The GLES path exists for hardware with untrustworthy Vulkan drivers. That hardware is also where new engine bugs surface first. Issue 190640 is a current example. On Flutter 3.44.6 through 3.44.8, Android 10 devices with Mali GPUs abort inside the GLES blit pass while uploading decoded images. The Exynos 9810 and the MediaTek MT6768 lead the list. Both sit on the denylists above, so they run Impeller on GLES by design. The reporter counted 572 events across 113 installations in three weeks, with no reproduction on newer hardware. Disabling Impeller stopped the crash for that cohort. In other words, the opt-out still does real work for some teams. That is exactly what the deprecation warning asks you to report.

How to Migrate an Android App to Impeller Safely

Here is the order that turns the Impeller on Android change from a series of surprises into one planned release:

  1. Search every manifest you ship, including dependencies, for EnableImpeller and ImpellerBackend. Then dump the merged manifest from the release APK with aapt2 to confirm what shipped
  2. Remove any opt-out you cannot justify with an open issue, and add tools:node="remove" if a dependency keeps merging one in
  3. Run the release build on the emulator and read the flutter tag in logcat. An Impeller banner means Impeller, a warning means Skia, and silence on 3.35 means Skia
  4. If you ship custom fragment shaders, add the IMPELLER_OPENGLES_UNFLIPPED_DEPRECATED guard. Compare the output on the emulator and on a Vulkan device
  5. Add an API 28 AVD to your test matrix, because those users stay on Skia
  6. Check crash reports by device model for the Exynos, MediaTek and Mali families listed above. Those devices run GLES and report different stacks
  7. Ship, then watch the next stable release notes for the opt-out removal and plan the final cleanup

Step 3 is the one that saves the most time. The log line comes from the engine, not from your code. It tells you in one glance which of the three renderers a device picked. For a more detailed view, the DevTools performance page shows the raster thread. That is where the renderer choice shows up as frame time.

Real-World Scenario: An Ad SDK Update Turns Impeller Off

Consider a mid-sized consumer app with 30 to 40 screens and a team of three. They update the ads SDK as part of a routine dependency bump. Nothing in the diff touches rendering. Over the following week, Android users file tickets about blurrier text and stutter on a screen with an animated list. iOS users report nothing. Crash reporting is clean. The team’s Pixels only show the stutter once someone compares the release build side by side with the previous version.

In a setup like this, the cause is the merged manifest. The SDK shipped EnableImpeller=false, and the app inherited it. Every Android user dropped to Skia, including the API 29+ majority who had run on Vulkan for a year. On 3.38 or later the warning sits in logcat from the first launch. However, nobody reads the log of a build that does not crash. The fix is the tools:node="remove" entry and a re-dump of the manifest, which takes an afternoon.

The trade-off is that the SDK vendor usually had a reason. Often it is a platform view that rendered incorrectly under Vulkan on some devices. So the same afternoon has to include a run of the ad units on a Vulkan device and on a GLES device. Catching this earlier is cheap. A CI step that fails on EnableImpeller in the merged manifest would have flagged the update in the pull request.

When to Keep the Impeller Opt-Out on Android

  • A reproducible Impeller bug affects devices you care about, and you have filed the issue the warning asks for
  • A third-party platform view misrenders under Impeller and the vendor has no fix yet
  • You are mid-way through migrating custom shaders and need a known-good build for comparison during the work
  • A GLES-path cohort in your crash data, like the Android 10 Mali devices in issue 190640, only stabilises on Skia

When NOT to Keep the Impeller Opt-Out on Android

  • You cannot name the bug it works around, because an opt-out without an issue number becomes a surprise regression later
  • The problem only reproduces in debug mode, where shader compilation and validation differ from release
  • You want faster frames on a GLES-denylist device, where Skia is slower, not faster
  • The opt-out came in through a dependency’s manifest and nobody on your team chose it

Common Mistakes with Impeller on Android

  • Assuming the emulator shows what users see, when every emulator runs OpenGL ES and most users run Vulkan
  • Reading a blog post that says Flutter removed Skia from Android and skipping the manifest audit as a result
  • Pinning ImpellerBackend in the manifest and verifying the fix in a debug build, when release builds ignore the key
  • Leaving a profiling or validation flag in the manifest after 3.44 and wondering why release traces lost their GPU events
  • Keeping a bottom-up shader flip under IMPELLER_TARGET_OPENGLES after moving to 3.47
  • Testing only on API 33+ devices while a minSdkVersion of 24 keeps Android 7 to 9 users on Skia
  • Blaming your own code for a Mali or Exynos crash whose stack points into the engine’s GLES path
  • Dropping the opt-out without filing the bug, the only signal the Flutter team has for when the flag can go

Next Steps for Impeller on Android

Impeller on Android is already the renderer for most of your users, and Flutter 3.47.6 still lets you leave, loudly. The real work in this upgrade is not flipping a flag. It is auditing merged manifests for an opt-out you did not write and guarding custom shaders for the 3.47 GLES change. It is also testing on the three device classes the engine actually distinguishes. Start today by dumping the merged manifest of your current release APK and reading the flutter tag on first launch. If the move changes your binary size, the Flutter app size measurements show what each package costs. For the frame-time side of the migration, the animation best practices guide is the right next read.