A Few Good CodersStart a project
Flutter Package

refresh_rate

Request high refresh rates in one line of Flutter — with live diagnostics, benchmark sessions, and an overlay HUD.

Updated , by A Few Good Coders

Published 13 sectionsFlutter Package

pub points
160/160
likes
21
downloads, 30 days
3,071
latest version
2.0.1

pub.dev, as of Verify on pub.dev (opens in a new tab)BSD-3-Clause licence (opens in a new tab)

Start here: request a rate, measure the result

Your first working integration

  1. Install and rebuild
  2. Run the minimal app
  3. Check the reported display signals
  4. Measure a repeatable workload
  5. Diagnose unexpected results

Already using this in production? Start with the troubleshooting and migration or release-checklist sections below. Work in a development project first, and keep the reviewed package version visible when comparing these examples with your lockfile.

This guide targets refresh_rate 2.0.1, checked against the published package and repository on 13 October 2026. Start with installation and the minimal app, then add scoped preferences and measurement only where you need them.

The package has four jobs: submit Android refresh preferences, inspect display signals, show a diagnostic overlay, and collect Flutter frame timing sessions. It does not make slow rendering faster by itself. A request accepted by Android is not proof that the panel—or Flutter—is producing 120 frames per second.

Choose your path: use enable() for an app-wide high-rate preference; RefreshRateScope for a visible route; a lease for explicitly owned work; or diagnostics alone on platforms without control.

1. Install and rebuild

Use Flutter 3.24+ and Dart 3.5+. Android support starts at API 23. Run these commands from the Flutter app directory. Commit your application's lockfile so the team measures the same resolved version.

After adding a native plugin, stop the app and rebuild it. Hot reload does not register new native platform code. No Firebase account, API key, or server is required.

If dependency resolution fails, inspect flutter --version and the solver's SDK constraints before adding overrides. Dependencies can impose additional requirements beyond a package's own minimum.

Terminal
flutter --version
flutter pub add refresh_rate:^2.0.1
flutter pub get
flutter run

2. Run a complete minimal app

Replace lib/main.dart in a scratch app with this example. The request happens after the first frame so Flutter has attached a view. Keep your normal application bootstrap when integrating into an existing product.

Inspect the returned status. submitted means the selected backend accepted the preference, not that the requested refresh rate was reached. unsupported is expected for control on Apple, desktop, and web targets; it does not mean their displays lack high-refresh hardware.

Dart
import 'package:flutter/material.dart';
import 'package:refresh_rate/refresh_rate.dart';

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  runApp(const MaterialApp(
    home: Scaffold(body: Center(child: Text('Refresh-rate check'))),
  ));
  WidgetsBinding.instance.addPostFrameCallback((_) async {
    final result = await RefreshRate.enable();
    debugPrint('Request: ${result.status.name}; backend: ${result.backend}');
    final info = await RefreshRate.refresh();
    debugPrint('Reported display Hz: ${info.nativeReportedDisplayHz}');
  });
}

What each platform can actually do

PlatformRefresh controlWhat to validate
Android API 23+Preferences supported; backend varies by OS/deviceDevice mode, surface availability, battery and thermal policy
iOSFlutter-engine control unsupportedProMotion setup, display capability, timing samples
macOSFlutter-engine control unsupportedApp-window display and available power/thermal signals
Windows / LinuxNo rate controlNative display information when available; compositor limitations
WebNo physical display controlBrowser callback cadence and Flutter timing, not panel presentation

RefreshRate.capabilities() describes operation support at runtime. unavailable can mean the native target is not ready; failed is different from unsupported. A content-rate vote targets the Flutter surface, not a video plugin's private playback surface. Widget scopes arbitrate ownership on a shared surface; they do not create independent refresh rates for separate widgets.

3. Own preferences and release them

enable() and preferMax() request the high-rate preference. preferDefault() releases the manual preference; separately owned leases may still be active. Do not repeatedly request rates inside build().

For a route, prefer the scope below: it acquires/releases its lease as visibility and lifecycle change. If custom tab navigation keeps widgets mounted and offstage, pass active: false for hidden content. For explicit jobs, acquire a lease once and release it in finally or the owner's teardown. Higher priority wins; newer requests break ties. Nested scopes add their depth to their priority.

Dart
// Inside a route's build method:
return RefreshRateScope(
  preference: const RatePreference.high(),
  child: Scaffold(
    appBar: AppBar(title: const Text('Smooth scrolling')),
    body: ListView.builder(
      itemCount: 200,
      itemBuilder: (_, index) => ListTile(title: Text('Item $index')),
    ),
  ),
);

// For explicitly owned work instead:
// final lease = RefreshRate.request(
//   const RatePreference.high(), owner: 'preview', priority: 100);
// await lease.release(); // when that work finishes

iOS: configure ProMotion without promising a rate

In ios/Runner/Info.plist, place this key/value inside the root dict, then perform a full rebuild. It allows eligible iPhone configurations to use high-refresh timing; it is not a guarantee of a fixed 120 Hz rate. Device support, power state, and the Flutter engine still matter.

Use isProMotionConfigured to inspect the configuration signal and supportsHighRefreshRate for the capability signal. They are different questions. The package does not use process-wide display-link swizzling in v2, and does not claim to control Flutter's engine scheduling on iOS or macOS.

Dart
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>

Read trustworthy diagnostics

RefreshRate.info is a synchronous cached snapshot; call refresh() before making a decision that requires fresh data. Prefer nullable observation fields: missing evidence is not 60 Hz, and an unknown power state is not confirmed normal power.

nativeReportedDisplayHz is an OS report, displayModeMaxHz is reported capability, and nativeCallbackCadenceHz is observer cadence (for example on web). None is measured physical Flutter presentation FPS. diagnostics() keeps these sources separate; doctor() checks configuration. An explicitly requested native cadence observer adds workload and is not a Flutter presentation measurement.

Cancel onChanged subscriptions when their owner is disposed. Avoid logging device/environment details to production telemetry without a retention and consent policy.

Dart
final info = await RefreshRate.refresh();
debugPrint('Display: ${info.nativeReportedDisplayHz ?? 'unavailable'}');
debugPrint('Maximum mode: ${info.displayModeMaxHz ?? 'unavailable'}');
debugPrint('Low power: ${info.isLowPowerMode ?? 'unknown'}');
debugPrint('Stale snapshot: ${info.isStale}');
final diagnostics = await RefreshRate.diagnostics();
debugPrint('Presented FPS: ${diagnostics.presentedFps}');
final subscription = RefreshRate.onChanged.listen((next) {
  debugPrint('Reported display Hz: ${next.nativeReportedDisplayHz}');
});
// In the owning service's teardown:
await subscription.cancel();

Use the overlay during development

Show the overlay after the app is mounted. Import package:flutter/foundation.dart for kDebugMode if it is not already exported by your UI imports. Pass an expected rate only when the scenario really demands continuous rendering at that rate.

showFPS() shows frame cadence; showHz() shows the display signal; showOverlay() shows the combined diagnostic panel. These are debugging aids, not hardware presentation probes. Turn the overlay off for controlled benchmark comparisons because drawing it changes the workload.

Dart
if (kDebugMode) {
  RefreshRate.showOverlay(expectedFps: 120);
}
// When leaving the diagnostic screen:
RefreshRate.hideOverlay();

Measure a repeatable workload

Run on a physical device in profile or release mode, using the same device settings, app build, workload duration, and initial thermal conditions for each comparison. First warm up the screen; then record an actual continuously animated or scrolling interval. Do not benchmark an idle screen as though it should produce 120 FPS.

Pause/resume intentionally excluded idle portions. Power and thermal changes are recorded as segments rather than discarded to improve the score. Ending a session waits for batched timing delivery (up to 1.1 seconds by default). Missing boundary evidence remains visible in the report.

The example is an integration fragment: place startSession at your scenario's start and end after the interaction has completed, not immediately on the next line.

Dart
final session = RefreshRate.startSession('feed_scroll', expectedFps: 120);
session.setTag('screen', 'feed');
session.mark('scroll_start');
// Perform your repeatable scrolling workload here.
// session.pause(); ... excluded idle ... session.resume();
final report = await session.end();
debugPrint('Flutter cadence: ${report.flutterFrameCadenceFps}');
debugPrint('Raster p99: ${report.percentilesMs['rasterP99']}');
debugPrint('Phase overruns: ${report.phaseOverrunPercent}');
debugPrint('Complete boundaries: ${report.boundaryCoverageComplete}');
final json = report.toJson();

Interpret reports and CI gates

A phase overrun is not a measured missed physical presentation. The legacy missedFramePercent alias refers to phase overruns; observedAvgHz is unavailable for timing-only sessions. Use the named metrics and their evidence rather than relabeling them as panel FPS.

A 1% low requires at least 100 valid intervals and a 0.1% low requires 1,000. Short runs, missing timestamps, debug mode, or incomplete coverage can make a verdict inconclusive. Histograms approximate quantiles; do not present tiny percentile differences as precise hardware improvements.

For CI, evaluate explicit thresholds using report.evaluate(RefreshRateThresholds(...)), inspect both failures and missingEvidence, and compare only matching device/build/scenario identities. A missing measurement must not pass a gate. Choose thresholds from your product's workload; a 120 Hz target is not appropriate for every screen.

Troubleshoot without hiding the constraint

SymptomCheck and next action
Missing plugin / platform exception after installStop and rebuild the native app; confirm the target is supported
Request submitted, still 60 HzInspect capabilities, device high-refresh setting, low-power/thermal state, and actual workload; acceptance is not achieved rate
Apple returns unsupportedExpected for engine control; keep diagnostics and verify plist/hardware separately
Cached values are zero or staleCall refresh(); use nullable fields rather than interpreting zero as a measurement
High FPS badge but poor interactionInspect UI/raster timings and frame gaps; rate alone does not establish responsiveness
Benchmark inconclusiveCheck profile/release mode, expected workload, sample count, end boundary, and excluded idle
Rate remains high after a route closesCheck other active leases and the manual preference, not just the closed route

For an issue, include package/Flutter version, device and OS, request status/backend, reproduction steps, and a redacted diagnostic report. Never replace unknown signals with invented values.

Migration from earlier versions

When upgrading to 2.x, audit assumptions about Apple control, unavailable values, and report meanings. Older code that equates enable() with an achieved 120 Hz rate is misleading. Legacy numeric fields may use zero for unavailable; prefer nullable observations. Treat report schema v2 and source-qualified metrics deliberately when importing existing dashboards.

Keep preference ownership, telemetry collection, and product performance targets separate: they answer different questions.

Source trail and further reference

Verified release: pub.dev 2.0.1 (opens in a new tab). Repository snapshot: d175183 (opens in a new tab).

Consult the public API (opens in a new tab), controller and scopes (opens in a new tab), and measurement implementation (opens in a new tab) when extending this guide. Recheck these against your resolved version before adopting newer repository APIs.