A Flutter plugin that allows you to request high refresh rate mode on Android, iOS and macOS devices. This plugin provides a simple API to attempt to enable the highest possible refresh rate for your Flutter application.
-
- 4.1. iOS Setup
- 4.2. Android Setup
- 4.3. macOS Setup
-
- 5.1. Basic Example
- 5.2. Complete Example
-
- 7.1. Properties
- 7.2. Methods
- 7.2.1.
requestHighRefreshRate() - 7.2.2.
stopHighRefreshRate({bool force = false}) - 7.2.3.
getRefreshRateInfo()
- 7.2.1.
-
- 8.1. Android Implementation
- 8.2. iOS Implementation
- 8.3. macOS Implementation
-
- 9.1. Common Issues
- 9.2. Testing
- 9.2.1. Unit Tests
- 9.2.2. Integration Tests
- 9.2.3. Manual Testing Tips
- 9.3. Performance Considerations
| Platform | Support | Min Version | Behavior |
|---|---|---|---|
| Android | ✅ | API 24 (Android 7.0) | Full functionality |
| iOS | ✅ | iOS 13.0 | Full functionality |
| macOS | ✅ | macOS 10.15 (requests need macOS 14) | Full functionality on macOS 14+, info only before that |
| Web | N/A | No-op (returns false/basic info) | |
| Windows | N/A | No-op (returns false/basic info) | |
| Linux | N/A | No-op (returns false/basic info) |
Note: Unsupported platforms will gracefully return false for control methods and basic information for info methods. You can optionally enable exceptions for unsupported platforms (see Exception Handling).
Why no Web, Windows or Linux? There is nothing to request there. Browsers offer no API to ask for a refresh rate, and on Windows and Linux Flutter already renders at whatever rate the monitor is running at; going higher would mean changing the display mode for the whole desktop. getRefreshRateInfo() still reports the current refresh rate on these platforms.
- Request the highest available refresh rate on supported devices
- Stop high refresh rate mode to return to normal power consumption
- Get detailed information about device refresh rate capabilities
- Cross-platform support for Android, iOS and macOS
- Graceful handling of unsupported platforms (no-op by default)
- Optional exception throwing for unsupported platforms
- Low Battery Mode: Most devices disable high refresh rates when battery is low
- Thermal Throttling: Devices may reduce refresh rate when overheating
- Power Management: System may override refresh rate settings to preserve battery
- Display Hardware: Not all devices support high refresh rates
- App Background State: High refresh rates may be disabled when app is not in foreground
While the plugin will not cause problems on unsupported platforms, the behavior is as follows:
- Android: Depends on device manufacturer implementation and Android version (e.g. 90Hz, 120Hz display with "Smooth Display" or manufacturer equivalent enabled)
- iOS: Requires ProMotion displays (iPhone 13 Pro+, iPad Pro models)
- macOS: Requires macOS 14+ and a high refresh rate display (e.g. MacBook Pro with ProMotion). Flutter usually already renders at the display's maximum rate on macOS, so the request mostly keeps it from dropping
- Adaptive Refresh: Some devices use variable refresh rates based on content
Add this to your package's pubspec.yaml file:
dependencies:
flutter_refresh_rate_control: ^0.1.0Then run:
flutter pub getFor iOS, ensure you have the following in your Info.plist to allow high refresh rates: (this should already be included by Flutter)
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>No setup is needed.
On Android 15 (API 35) and newer, the plugin opts the window out of Adaptive Refresh Rate (ARR) while high refresh rate mode is requested, and restores the previous setting when it is stopped. See: Optimize frame rate with adaptive refresh rate for more information.
No setup is needed.
import 'package:flutter_refresh_rate_control/flutter_refresh_rate_control.dart';
void main() async {
final _refreshRateControl = FlutterRefreshRateControl();
// Request high refresh rate
bool requestSuccess = await _refreshRateControl.requestHighRefreshRate();
if (requestSuccess) {
print('High refresh rate enabled');
} else {
print('Failed to enable high refresh rate');
}
// Get refresh rate information
// See lib/flutter_refresh_rate_control.dart: getRefreshRateInfo()
// for more possible values.
Map<String, dynamic> info = await _refreshRateControl.getRefreshRateInfo();
// Available on all platforms:
print('Platform: ${info['platform']}');
print('Supported: ${info['supported']}');
// Android, iOS and macOS only:
if (info['supported'] == true) {
print('Current refresh rate: ${info['currentRefreshRate']}');
print('Maximum refresh rate: ${info['maximumFramesPerSecond']}');
}
// Stop high refresh rate mode
bool stopSuccess = await _refreshRateControl.stopHighRefreshRate();
if (stopSuccess) {
print('Returned to normal refresh rate');
}
}import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_refresh_rate_control/flutter_refresh_rate_control.dart';
class RefreshRateScreen extends StatefulWidget {
@override
_RefreshRateScreenState createState() => _RefreshRateScreenState();
}
class _RefreshRateScreenState extends State<RefreshRateScreen> {
final _refreshRateControl = FlutterRefreshRateControl();
bool _isHighRefreshRate = false;
Map<String, dynamic> _info = {};
@override
void initState() {
super.initState();
_loadRefreshRateInfo();
}
Future<void> _loadRefreshRateInfo() async {
try {
final info = await _refreshRateControl.getRefreshRateInfo();
setState(() {
_info = info;
});
} catch (e) {
print('Error loading refresh rate info: $e');
}
}
Future<void> _toggleRefreshRate() async {
try {
bool success;
if (_isHighRefreshRate) {
success = await _refreshRateControl.stopHighRefreshRate();
} else {
success = await _refreshRateControl.requestHighRefreshRate();
}
if (success) {
setState(() {
_isHighRefreshRate = !_isHighRefreshRate;
});
await _loadRefreshRateInfo();
}
} on PlatformException catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Error: ${e.message}')),
);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Refresh Rate Control')),
body: Padding(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Status: ${_isHighRefreshRate ? "High" : "Normal"} Refresh Rate'),
SizedBox(height: 16),
ElevatedButton(
onPressed: _toggleRefreshRate,
child: Text(_isHighRefreshRate ? 'Disable High Refresh Rate' : 'Enable High Refresh Rate'),
),
SizedBox(height: 16),
Text('Device Information:'),
..._info.entries.map((e) => Text('${e.key}: ${e.value}')),
],
),
),
);
}
}By default, the plugin gracefully handles unsupported platforms by returning false for control methods and empty/informational data for info methods. The same happens when the native side of the plugin is not registered, such as in widget tests. However, you can optionally enable exceptions for these cases:
final _refreshRateControl = FlutterRefreshRateControl();
// On unsupported platforms, these return false/basic info without throwing
bool success = await _refreshRateControl.requestHighRefreshRate(); // Returns false
Map<String, dynamic> info = await _refreshRateControl.getRefreshRateInfo();
// Returns: {'platform': 'web', 'supported': false, 'message': '...', 'currentRefreshRate': 60.0, ...}final _refreshRateControl = FlutterRefreshRateControl();
// Enable exceptions for unsupported platforms
_refreshRateControl.exceptionOnUnsupportedPlatform = true;
try {
bool success = await _refreshRateControl.requestHighRefreshRate();
} on PlatformException catch (e) {
if (e.code == 'UNSUPPORTED_PLATFORM') {
print('Refresh rate control not supported on ${e.details}');
}
}Use default behavior (no exceptions) when:
- You want your app to work seamlessly across all platforms
- You're building a cross-platform app and want to gracefully degrade features
- You prefer to check return values rather than handle exceptions
Use exception mode when:
- You need to explicitly know when a platform is unsupported
- You want to fail fast during development/testing
- You prefer exception-based error handling
Controls whether exceptions should be thrown on unsupported platforms.
Type: bool
Default: false
This setting is shared by all FlutterRefreshRateControl instances.
Usage:
final plugin = FlutterRefreshRateControl();
plugin.exceptionOnUnsupportedPlatform = true; // Enable exceptions
bool isEnabled = plugin.exceptionOnUnsupportedPlatform; // Check current stateAttempts to enable the highest available refresh rate.
Returns: Future<bool> - true if successful, false otherwise
Throws: PlatformException if an error occurs
Stops high refresh rate mode and puts things back to how they were before the request, which normally hands the choice of refresh rate back to the system.
On Android the request changes settings of the app window (the preferred display mode and, on API 35+, Adaptive Refresh Rate). The values from before the request are remembered and restored. If a setting no longer has the value this plugin gave it, something else has changed it in the meantime and it is left as it is, unless force is true. force has no effect on iOS and macOS, where the plugin only removes its own display link.
Returns: Future<bool> - true if successful, false otherwise (on Android and macOS also when high refresh rate mode was not requested)
Throws: PlatformException if an error occurs
Gets detailed information about the device's refresh rate capabilities.
Returns: Future<Map<String, dynamic>> containing:
-
All platforms
platform: (string) The platform the app is running on, e.g. "android", "ios", "macos", "windows", "linux", "web"supported: (bool) Whether refresh rate control is supported on the current platform
-
Unsupported platforms
message: (string) "Refresh rate control not supported on this platform"currentRefreshRate,currentFramesPerSecond: The display refresh rate in Hz as reported by Flutter, if known
-
Android/iOS/macOS:
currentRefreshRate,currentFramesPerSecond: Current refresh rate in Hz. On iOS and macOS this is measured from the plugin's own display link, so it is only present while high refresh rate mode is requested and the display link has fired at least oncemaximumFramesPerSecond: Maximum supported refresh rate in Hz (macOS 12+)
-
iOS/macOS-specific fields (only while high refresh rate mode is requested):
duration: CADisplayLink durationtimestamp: Current timestamptargetTimestamp: Target timestamppreferredFrameRateRange: Frame rate range (iOS 15+)
-
macOS-specific fields:
highRefreshRateEnabled: Whether high refresh rate is currently requestedminimumRefreshInterval,maximumRefreshInterval: Shortest and longest time between screen updates in seconds (macOS 12+)
-
Android-specific fields:
supportedModes: List of all supported display modescurrentMode: Current display mode informationhighRefreshRateEnabled: Whether high refresh rate is currently requestedsurfaceFrameRateRequested: Whether the frame rate is also requested on the surface Flutter renders intoandroidVersion: Android API leveldeviceModel: Device manufacturer and model
Throws: PlatformException if exceptionOnUnsupportedPlatform is enabled and platform is unsupported
The Android implementation uses the following APIs:
- Display.Mode API: For finding the mode with the highest refresh rate at the current resolution
- WindowManager.LayoutParams.preferredDisplayModeId: For requesting that mode, and for putting back the previous value (normally
0, no preference) - Surface.setFrameRate() (API 30+): For requesting the same rate on the surface Flutter renders into. This is skipped when Flutter is not rendering into its own
SurfaceView, seesurfaceFrameRateRequestedingetRefreshRateInfo() - Window.setFrameRatePowerSavingsBalanced() (API 35+): For opting out of Adaptive Refresh Rate
The request belongs to the activity window, so it is made again if the activity is recreated.
Key Android APIs:
Display.ModeWindowManager.LayoutParams.preferredDisplayModeIdSurface.setFrameRate()Window.setFrameRatePowerSavingsBalanced()
Android Documentation:
The iOS implementation uses CADisplayLink for high refresh rate control:
- CADisplayLink: For requesting specific frame rates
- CAFrameRateRange (iOS 15+): For fine-grained frame rate control
- UIScreen.maximumFramesPerSecond: For device capability detection
The plugin keeps a display link running on the main thread that asks for the display's maximum rate as its minimum, maximum and preferred rate. Flutter's own display link allows the system to drop to half of the maximum rate, this one does not. The display link fires on every frame for as long as high refresh rate mode is requested, even when your app is not drawing anything, so call stopHighRefreshRate() when you no longer need it.
Key iOS APIs:
iOS Documentation:
The macOS implementation is the same idea as on iOS, using the display link of the Flutter view:
- NSView.displayLink(target:selector:) (macOS 14+): For requesting specific frame rates
- CAFrameRateRange: For fine-grained frame rate control
- NSScreen.maximumFramesPerSecond (macOS 12+): For display capability detection
Before macOS 14 there is no API for this, so requestHighRefreshRate() returns false.
Key macOS APIs:
High refresh rate not working:
- Check if platform is supported (Android/iOS/macOS only)
- Check if device supports high refresh rates
- Ensure device is not in low battery mode
- Verify app is in foreground
- Check device temperature (thermal throttling)
Methods returning false/empty on supported platforms:
- Ensure proper permissions (should not be needed for this plugin)
- Check platform compatibility
- Verify device display capabilities
- Check if
exceptionOnUnsupportedPlatformis enabled to get detailed error messages
Unsupported platform behavior:
- By default: Methods return
falseor informational data without throwing - With exceptions enabled:
PlatformExceptionwith code'UNSUPPORTED_PLATFORM'is thrown - Use
getRefreshRateInfo()to check platform support status
Run the unit tests to verify platform interface and method channel functionality:
flutter testRun the integration tests from the example directory on a real device to test platform channel communication and to measure the frame rate before, during and after a request:
# Android or iOS device
flutter test integration_test/plugin_integration_test.dart
# macOS
flutter test integration_test/plugin_integration_test.dart -d macosThe measured frame rates are printed, and the test fails if the display supports more than 60Hz but Flutter does not get close to the maximum while high refresh rate mode is requested. Simulators and emulators are limited to 60Hz, so use real hardware for this.
- Use a device with high refresh rate support
- Ensure device is charged and not in power saving mode
- Keep app in foreground during testing
- Monitor device temperature
- Test on both supported (Android/iOS/macOS) and unsupported platforms
- Try enabling/disabling exception mode to test error handling
- High refresh rates consume more battery
- May cause device heating during extended use
- Some devices automatically adjust based on content
- Consider user preferences and battery level in your app
Contributions are welcome! Please read our contributing guidelines and submit pull requests to our GitHub repository.
This project is licensed under the MIT License - see the LICENSE file for details.
See CHANGELOG.md for a detailed history of changes.