display_brightness
A small Flutter plugin for app-level screen brightness on iOS and Android, built on jnigen and swiftgen bindings instead of method channels.

Juho Torkkeli

Why I built it
I wanted to build something from scratch to learn how Flutter’s new native interop works: calling Kotlin and Swift directly through generated bindings, with jnigen and swiftgen, instead of method channels.
There were already plenty of brightness packages and good examples out there, but I wanted a very small, opinionated API. I don’t see much use for screen brightness outside mobile platforms, so contributing to the popular packages felt like too big a task: I would have had to change their architecture drastically. So I wrote a new one, for iOS and Android only.
It was my first time using this tech, and I plan to do all my native integrations with it from now on.
What it does
The plugin reads and sets the app’s own screen brightness and reports changes. It’s app-level brightness, so it needs no permissions.
final controller = DisplayBrightnessController();
final current = controller.brightness; // 0.0–1.0, or null if it can't be read
controller.setBrightness(1.0);
final subscription = controller.onBrightnessChanged.listen((value) {
print('Brightness changed: $value');
});
// Later:
subscription.cancel();
controller.dispose();
That’s the whole API: a getter, a setter, a change stream and dispose().
How it works
It’s a federated plugin with four packages on pub.dev: the package apps depend on, a platform interface, an Android implementation and an iOS implementation.
- Android: Kotlin, called through bindings generated by jnigen. Brightness is set with
WindowManager.LayoutParams.screenBrightness, and changes come from aContentObserveron the system brightness setting. - iOS: Swift, called through bindings generated by swiftgen over Objective-C and FFI. It uses the active window scene’s screen and listens for
UIScreen.brightnessDidChangeNotification. The iOS package uses Swift Package Manager. - Tests: the platform code sits behind delegates, so the Dart side is unit-tested without a device.
Platform details
- iOS drops same-value writes. A brightness write that equals the value the app last requested is coalesced into a no-op. So if the app set 1.0, the user then lowered the brightness by hand, and the app set 1.0 again, nothing happened. Writes made before the scene is foreground-active are dropped too.
setBrightnessnow ramps from the current value to the target through eight distinct values, about one frame apart. A newer call cancels an older ramp. - Android brightness ranges vary. The system brightness setting doesn’t always go up to 255 (MIUI is one example), so the plugin reads the device’s real maximum from
PowerManager.BRIGHTNESS_ONand falls back to 255. - JNI object lifetime. Before 1.0.0, calling
dispose()left the Android side pointing at a released JNI object, so reusing the plugin afterwards threwUseAfterReleaseError. The delegate is now recreated on the next use. - Generated headers. The swiftgen output imported its Objective-C header from a machine-specific temp path, so the iOS package failed to build anywhere else. The header now ships with the package.
- AGP 9. The Android package has a small placeholder
FlutterPluginclass so Kotlin still builds in an FFI plugin with Android Gradle Plugin 9.
Status
Version 1.0.0 went out on pub.dev on July 20, 2026, five weeks after the first commit. The main package has 160/160 pub points. It’s published under the Mankeli Solutions publisher on pub.dev (mankeli.co) and BSD-3-Clause licensed.
Tech
- Flutter, Dart
- Kotlin, Swift, Objective-C
- jnigen, swiftgen,
dart:ffi, JNI - Melos, Swift Package Manager
- GitHub Actions