DEV Community

Cover image for Prevent Screenshot and Screen Recording in Flutter (Android + iOS)
Gulshan Yadav
Gulshan Yadav

Posted on Originally published at misar.blog

Prevent Screenshot and Screen Recording in Flutter (Android + iOS)

So, in this article, I will be showing you how you can prevent screenshots and screen recording in your Flutter app on both Android and iOS.

I ran into this the hard way. I was building a money-transfer flow for a fintech client, and one afternoon their product head asked me, very calmly, "Can someone screenshot the OTP screen?" The honest answer was yes. My app showed a one-time code on screen, the phone's screenshot shortcut was one button away, and nothing in Flutter stopped it by default. We fixed it in an afternoon — and then I learned the uncomfortable truth that the fix looks very different on the two platforms.

So let me show you what I shipped, the code that makes it work, and the platform limits you need to know before you promise anyone anything.

Why You Need This (and Why It Isn't a Silver Bullet)

Screen capture protection is used for payment screens, OTPs, private documents, NDA-protected content, and healthcare data. The mechanism that actually stops screenshots is not a Flutter feature at all — it is a native OS flag. Flutter cannot do it from Dart alone, because Dart has no access to the window-level flags that control whether the OS allows a screen capture.

On Android the flag is FLAG_SECURE. On iOS there is no equivalent public API, and this asymmetry is the single most important thing to understand about this feature. Android gives you a real, documented flag. iOS gives you a workaround that Apple has never officially blessed. Plan for that difference.

Step 1: Add the Dependency

The fastest reliable path is the flutter_secure_screen package, which wraps the native code for you. Add it to your pubspec.yaml:

dependencies:
  flutter:
    sdk: flutter
  flutter_secure_screen: ^3.0.0
Enter fullscreen mode Exit fullscreen mode

Then run flutter pub get.

If you would rather not depend on a third-party package (a legitimate choice for a security-sensitive codebase), skip the package and do it natively — that is the next two steps.

Step 2: Android — Use FLAG_SECURE

On Android, the way to block screenshots, screen recording, and even the recent-apps preview thumbnail is to set FLAG_SECURE on the window. With the package, it is two lines:

import 'package:flutter_secure_screen/flutter_secure_screen.dart';

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  FlutterSecureScreen.secureScreen(
    isEnable: true,            // false to disable protection
    isEnableOCR: false,        // blocks OCR of the view on Android 14+
    isEnableAccessibility: true,
  );
  runApp(const MyApp());
}
Enter fullscreen mode Exit fullscreen mode

The isEnableOCR flag matters. On Android 14 and newer, apps that re-scan the screen (OCR-style screen readers and password managers) can still extract text from a protected window unless you enable OCR blocking. If your screen contains a password or OTP, set it to false, which prevents OCR-based extraction too.

If you prefer the native approach, set the flag in MainActivity.kt before the view renders:

class MainActivity : FlutterActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        window.setFlags(
            WindowManager.LayoutParams.FLAG_SECURE,
            WindowManager.LayoutParams.FLAG_SECURE
        )
    }
}
Enter fullscreen mode Exit fullscreen mode

Either version does the same thing at the window level, and that is where the OS enforces it. Screenshots return a black frame, screen recording captures a black view, and the recents thumbnail is hidden.

Step 3: iOS — The Honest Version

Now the part nobody spells out clearly. iOS has no public API equivalent to FLAG_SECURE. Apple does not give third-party apps a documented "block screenshot" flag. The workaround that ships inside packages like flutter_secure_screen is a trick: it adds a hidden UITextField to the root view. iOS treats secure-text fields as sensitive and refuses to capture them, so the rest of the screen — the part rendered behind the invisible field — also comes out black in screenshots.

It works, but be aware of what it is. It is a trick built on undocumented behavior, not a guaranteed OS guarantee. Apple can change it in any iOS release, and App Store review has occasionally flagged apps that interfere with capture. Treat it as "best-effort on iOS," not "guaranteed."

The package handles the iOS side for you — you do not write Swift for the basic case. If you need to control it per-screen (which you usually do), wrap the secure flag in a service that you toggle in lifecycle callbacks:

class ScreenProtector {
  static void enable() => FlutterSecureScreen.secureScreen(
        isEnable: true,
        isEnableOCR: false,
        isEnableAccessibility: true,
      );

  static void disable() => FlutterSecureScreen.secureScreen(
        isEnable: false,
        isEnableOCR: true,
        isEnableAccessibility: true,
      );
}
Enter fullscreen mode Exit fullscreen mode

Enable it when the sensitive screen opens, disable it when it closes. Running FLAG_SECURE on every screen hurts nothing functionally, but on Android it also kills the recents preview of those screens, which is often not what you want for your main UI.

Using It Per-Screen (the Pattern I Ship)

Blanket protection is usually wrong. Your OTP and payment screens need it; your home feed does not. The pattern I ship wires the flag to widget lifecycle:

class OtpScreen extends StatefulWidget {
  const OtpScreen({super.key});
  @override
  State<OtpScreen> createState() => _OtpScreenState();
}

class _OtpScreenState extends State<OtpScreen> {
  @override
  void initState() {
    super.initState();
    ScreenProtector.enable();
  }

  @override
  void dispose() {
    ScreenProtector.disable();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return const Scaffold(body: Center(child: Text('Enter OTP')));
  }
}
Enter fullscreen mode Exit fullscreen mode

initState turns protection on the moment the screen mounts, and dispose releases it when the screen unmounts. This keeps the recents-preview blanking scoped to the screens that actually need it. If the user backgrounds the app mid-flow, AppLifecycleListener lets you re-enable the flag when the app resumes, in case the OS dropped it:

AppLifecycleListener(
  onResume: ScreenProtector.enable, // safe to call twice
  onPause: ScreenProtector.enable,  // secure even while backgrounded
);
Enter fullscreen mode Exit fullscreen mode

The double-call is deliberate. Calling secureScreen is idempotent, and re-enabling on onPause covers the window where Android flattens the window for the task switcher — that snapshot is exactly what you do not want visible in recents.

Android Screen-Recording Detection (a Useful Companion)

Often you do not just want to block recording — you want to know it is happening so you can show a warning. Android exposes this through MediaProjectionManager's addCallback: when another app starts capturing the screen, you get an event. In Flutter this needs a small MethodChannel:

// MainActivity.kt — companion feature
private val projectionManager by lazy {
    getSystemService(MediaProjectionManager::class.java)
}
private val callback = object : MediaProjectionManager.Callback() {
    override fun onStop() { /* recording stopped */ }
}

// expose onStart/onStop through a MethodChannel
Enter fullscreen mode Exit fullscreen mode
// Dart side
const channel = MethodChannel('screen_capture');
channel.setMethodCallHandler((call) async {
  if (call.method == 'onCaptureStart') {
    // Show a banner: "Your screen is being recorded."
  }
});
Enter fullscreen mode Exit fullscreen mode

This does not block the recording — it tells you it is happening. Combined with FLAG_SECURE blanking the content, it is the honest, complete Android story: you cannot always stop the capture, but you can make it capture nothing and warn the user at the same time.

Important Notes and Pitfalls

  1. This does not stop everything. A second physical camera photographing the screen is out of scope. The OS can block software capture; it cannot block a camera. Scope your expectations and tell the business owner the same thing I told the client.

  2. WebViews and native views are separate. If your sensitive content renders inside a WebView, the web content has its own capture rules. A WebView on top of a FLAG_SECURE window does not always inherit protection the way you expect. Test the actual content, not the shell.

  3. Screen recording is caught on Android, partially on iOS. Android's FLAG_SECURE blanks the feed to MediaProjection recordings. On iOS, the hidden-textfield trick is less reliable against full-screen recordings; again, best-effort.

  4. Emulators and adb can still capture. adb screencap on a debuggable build can grab frames in some configurations. FLAG_SECURE is a production feature; do not rely on it in debug builds and do not test it on an emulator alone.

  5. Don't use this for DRM. If your goal is preventing content piracy, screenshots are the least of your problems. This is for privacy and accidental capture, not for copy protection.

  6. Test on a real device, both platforms. I have seen flutter_secure_screen behave differently across Android OEMs (some manufacturers optimize the screenshot shortcut in ways that ignore the flag). A quick manual test on a Pixel and a Samsung tells you more than a week of reading.

Here is the per-scenario summary, so you know what to promise and what not to:

Scenario Android (FLAG_SECURE) iOS (textfield trick)
Hardware screenshot button Blocked Blocked
MediaProjection screen recording Blocked (black feed) Partial / best-effort
Recent-apps preview thumbnail Hidden Hidden
OCR / accessibility re-scan Blocked only with isEnableOCR Not covered
Second physical camera Not blocked Not blocked
adb screencap on debug build Can bypass Can bypass

The pattern in the last two rows is the one worth internalizing: the OS flag stops software capture of your own window, and it stops nothing physical and nothing privileged. That is not a defect in the feature; it is the definition of the feature.

The Quick Checklist

  • [ ] Confirm your use case is "privacy/accidental capture," not DRM.
  • [ ] Decide package vs. native MainActivity flag — same result, different maintenance.
  • [ ] Enable on sensitive screens only (OTP, payment, documents).
  • [ ] Set isEnableOCR: false for password/OTP screens on Android 14+.
  • [ ] Test Android screenshot, Android recording, and the recents thumbnail.
  • [ ] Test iOS screenshot — expect best-effort, not guarantee.
  • [ ] Tell the product owner what it cannot stop (a second camera, adb on debug builds).

That is the whole fix. Android gets a real OS flag; iOS gets an honest workaround. Know which one you are shipping, and never let a client believe screen protection is absolute — it is not, on either platform, and the moment you set the expectation honestly is the moment you stop being the person who gets the panicked call.

If you have a specific screen or a specific capture scenario the default flag does not cover — comment below with what you are trying to protect, and I'll cover the exact setup next.


*Gulshan Yad

Top comments (0)