Security
14

Screenshot and Screen Recording Protection in Flutter

Screenshot & Screen Recording Protection in Flutter

March 24, 2026

A banking app shows an account balance. The user glances at the number, then switches to their messaging app to reply to a friend. In that fraction of a second — as the banking app moves to the background — the operating system captures a screenshot of the current screen. That screenshot becomes the thumbnail in the app switcher, the little card the user sees when they swipe through their recent apps. The account balance is now visible in that thumbnail to anyone who picks up the phone.

That is one vector. There are others. The user takes a manual screenshot — the balance is now a file in their photo gallery, backed up to the cloud, potentially shared. A screen recording app captures the entire session. A support agent asks the user to share their screen during a video call — financial data streams live to a third party.

None of these scenarios require malware. None require root access or jailbreaking. They are all normal, expected features of the operating system. And they all expose sensitive content that your app displays on screen.

This post covers what the OS captures, when it captures it, and what you can do about it on both Android and iOS — including the significant differences between the two platforms and the UX tradeoffs that come with every approach.

What the OS captures and when

There are four distinct vectors, and they matter separately because the mitigations are different.

App switcher thumbnail. When your app moves to the background, the OS captures a snapshot of the current screen to use as the preview card in the recent apps view. This happens automatically. The user does not trigger it deliberately. On Android, this is a full-resolution screenshot of the window. On iOS, it is a snapshot stored by the system.

User-initiated screenshots. The user presses the hardware button combination (power + volume down on most Android devices, power + volume up on iPhone). The screenshot is saved to the device gallery. It may be synced to cloud storage automatically.

Screen recording. Both Android and iOS have built-in screen recording features. Third-party apps can also record the screen. The recording captures everything visible, frame by frame.

Screen sharing and casting. AirPlay, Chromecast, video calls with screen sharing enabled. The screen content is streamed to another device or another person in real time.

Each of these is a separate problem. On Android, a single mechanism addresses all four. On iOS, each requires its own approach, and not all of them can be fully prevented.

Android: FLAG_SECURE

Android provides a window flag called FLAG_SECURE. When set on a window, it instructs the system to treat the window's content as secure. The effects are comprehensive:

  • Screenshots are blocked. The user sees a black screen or a system message when they attempt a screenshot.
  • Screen recording captures a black screen instead of the app content.
  • The app switcher thumbnail is blank.
  • Screen casting and screen sharing show a blank area where the app would be.

One flag, four vectors, all addressed. This is unusually clean for mobile security.

Global protection in MainActivity.kt

The simplest implementation sets the flag in your main activity's onCreate method. Every screen in your app is protected.

kotlin
// android/app/src/main/kotlin/com/yourapp/MainActivity.kt

package com.yourapp

import android.os.Bundle
import android.view.WindowManager
import io.flutter.embedding.android.FlutterActivity

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

That is the entire change. When the app launches, the flag is set, and the OS enforces it for the lifetime of the window.

Per-screen protection via method channel

Global protection is heavy-handed. If you want to enable FLAG_SECURE only on specific screens — the one showing a credit card number, the one displaying a bank balance — and leave the rest of the app unprotected, you need to toggle the flag from Dart at runtime. A method channel bridges that gap.

On the Kotlin side, register a method channel that accepts enable and disable commands:

kotlin
// android/app/src/main/kotlin/com/yourapp/MainActivity.kt

package com.yourapp

import android.os.Bundle
import android.view.WindowManager
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel

class MainActivity : FlutterActivity() {
    private val channelName = "com.yourapp/screen_security"

    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)

        MethodChannel(
            flutterEngine.dartExecutor.binaryMessenger,
            channelName
        ).setMethodCallHandler { call, result ->
            when (call.method) {
                "enableSecureMode" -> {
                    window.setFlags(
                        WindowManager.LayoutParams.FLAG_SECURE,
                        WindowManager.LayoutParams.FLAG_SECURE
                    )
                    result.success(true)
                }
                "disableSecureMode" -> {
                    window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
                    result.success(true)
                }
                else -> result.notImplemented()
            }
        }
    }
}

On the Dart side, create a service that calls these methods:

dart
// lib/services/screen_security_service.dart

import 'package:flutter/services.dart';

class ScreenSecurityService {
  static const _channel = MethodChannel('com.yourapp/screen_security');

  static Future<void> enableSecureMode() async {
    try {
      await _channel.invokeMethod('enableSecureMode');
    } on PlatformException catch (e) {
      // Log the error. Do not crash.
      debugPrint('Failed to enable secure mode: ${e.message}');
    }
  }

  static Future<void> disableSecureMode() async {
    try {
      await _channel.invokeMethod('disableSecureMode');
    } on PlatformException catch (e) {
      debugPrint('Failed to disable secure mode: ${e.message}');
    }
  }
}

Then toggle it when entering and leaving a sensitive screen:

dart
// In a StatefulWidget for a sensitive screen

@override
void initState() {
  super.initState();
  ScreenSecurityService.enableSecureMode();
}

@override
void dispose() {
  ScreenSecurityService.disableSecureMode();
  super.dispose();
}

When the user navigates to this screen, FLAG_SECURE is set. When they navigate away, it is cleared. Every other screen in the app remains screenshot-friendly.

The limitation

FLAG_SECURE is all-or-nothing per window. You cannot protect a single widget — say, the account balance text — while leaving the rest of the screen open for screenshots. The entire window is either secure or it is not. If a screen shows a mix of sensitive and non-sensitive information, you either protect the entire screen or protect none of it.

iOS: A more complex story

iOS has no equivalent of FLAG_SECURE. There is no single flag that blocks screenshots, screen recording, and the app switcher thumbnail simultaneously. Each vector requires its own approach, and the level of control varies.

App switcher thumbnail

When an iOS app moves to the background, the system captures a snapshot for the app switcher. To prevent sensitive data from appearing in that snapshot, you add an overlay — typically a blur or a solid colour — when the app is about to resign active status, and remove it when the app returns.

In your AppDelegate.swift:

swift
// ios/Runner/AppDelegate.swift

import UIKit
import Flutter

@main
@objc class AppDelegate: FlutterAppDelegate {
    private var privacyOverlay: UIView?

    override func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
    ) -> Bool {
        GeneratedPluginRegistrant.register(with: self)
        return super.application(application, didFinishLaunchingWithOptions: launchOptions)
    }

    override func applicationWillResignActive(_ application: UIApplication) {
        super.applicationWillResignActive(application)
        addPrivacyOverlay()
    }

    override func applicationDidBecomeActive(_ application: UIApplication) {
        super.applicationDidBecomeActive(application)
        removePrivacyOverlay()
    }

    private func addPrivacyOverlay() {
        guard privacyOverlay == nil,
              let window = self.window else { return }

        let overlay = UIView(frame: window.bounds)
        overlay.backgroundColor = .white
        overlay.tag = 999

        // Optional: add a blur effect instead of a solid colour
        let blurEffect = UIBlurEffect(style: .regular)
        let blurView = UIVisualEffectView(effect: blurEffect)
        blurView.frame = overlay.bounds
        blurView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        overlay.addSubview(blurView)

        window.addSubview(overlay)
        privacyOverlay = overlay
    }

    private func removePrivacyOverlay() {
        privacyOverlay?.removeFromSuperview()
        privacyOverlay = nil
    }
}

When the user double-taps home or swipes up to open the app switcher, they see a blurred screen instead of the app content. When they return to the app, the overlay disappears instantly.

Screenshots: detection, not prevention

On iOS, you cannot prevent a user from taking a screenshot. The operating system does not offer that capability to third-party apps. What you can do is detect that a screenshot was taken, after the fact.

Register for the notification in your AppDelegate or via a method channel:

swift
// Inside application(_:didFinishLaunchingWithOptions:)

NotificationCenter.default.addObserver(
    self,
    selector: #selector(userDidTakeScreenshot),
    name: UIApplication.userDidTakeScreenshotNotification,
    object: nil
)
swift
@objc private func userDidTakeScreenshot() {
    // Notify the Flutter side via method channel
    guard let controller = window?.rootViewController as? FlutterViewController else { return }
    let channel = FlutterMethodChannel(
        name: "com.yourapp/screen_security",
        binaryMessenger: controller.binaryMessenger
    )
    channel.invokeMethod("onScreenshotTaken", arguments: nil)
}

On the Dart side, listen for this:

dart
// In your Dart code

const channel = MethodChannel('com.yourapp/screen_security');

channel.setMethodCallHandler((call) async {
  if (call.method == 'onScreenshotTaken') {
    // Show a warning, log the event, or take other action
  }
});

What you do with this information is up to your application's requirements. Some banking apps show a warning dialog. Some log the event on the server. Some financial messaging apps (like certain stock trading platforms) watermark the screen with the user's ID so that leaked screenshots can be traced.

But the screenshot itself has already been taken. You are reacting, not preventing.

Screen recording: real-time detection

Screen recording is a different story. iOS does notify your app when screen recording starts and stops, in real time. This means you can hide sensitive content while recording is active.

swift
// Inside application(_:didFinishLaunchingWithOptions:)

NotificationCenter.default.addObserver(
    self,
    selector: #selector(screenCaptureDidChange),
    name: UIScreen.capturedDidChangeNotification,
    object: nil
)
swift
@objc private func screenCaptureDidChange() {
    let isCaptured = UIScreen.main.isCaptured

    guard let controller = window?.rootViewController as? FlutterViewController else { return }
    let channel = FlutterMethodChannel(
        name: "com.yourapp/screen_security",
        binaryMessenger: controller.binaryMessenger
    )
    channel.invokeMethod("onScreenCaptureChanged", arguments: isCaptured)

    if isCaptured {
        addPrivacyOverlay()
    } else {
        removePrivacyOverlay()
    }
}

When screen recording starts, UIScreen.main.isCaptured becomes true. You overlay the screen. The recording captures the overlay, not the content beneath it. When recording stops, you remove the overlay and the user sees their content again.

This also covers screen mirroring via AirPlay and other casting mechanisms — isCaptured is true for those as well.

The UITextField secure entry trick

There is one additional technique for iOS that deserves mention. The UITextField class has a property called isSecureTextEntry — the same property that turns a text field into a password field with dots instead of characters. When this property is set to true, the system renders the field's content through a secure layer that is excluded from screenshots and screen recordings.

The trick is to create a UITextField with isSecureTextEntry = true, make it invisible, and use its secure layer as a container for other views:

swift
// This is a simplified illustration of the technique

let secureField = UITextField()
secureField.isSecureTextEntry = true

// Access the secure container layer
if let secureContainer = secureField.layer.sublayers?.first {
    // Add your sensitive view's layer to this container
    secureContainer.addSublayer(sensitiveView.layer)
}

This approach allows you to protect individual views rather than the entire screen. It is undeniably a hack — it relies on undocumented behaviour of how UITextField renders secure content — and Apple could change this in any iOS release. But it is widely used, including by some well-known banking and financial apps, and it has remained functional across multiple iOS versions.

For production use, you would want to wrap this in a native iOS view and expose it to Flutter through a platform view. The complexity is significant, and it is the kind of thing where using a well-maintained package (discussed below) may be more practical than rolling your own.

Per-screen vs global protection

You have two broad strategies, and the right choice depends on what your app does.

Global protection means setting FLAG_SECURE in onCreate on Android and keeping the privacy overlay active for all background transitions on iOS. Every screen is protected. The implementation is minimal — a few lines of native code, no method channels needed. This is appropriate for apps where the entire experience is sensitive: banking apps, medical records, enterprise apps with confidential data.

Per-screen protection means toggling the security measures on and off as the user navigates between screens. The credit card details screen is protected. The order confirmation screen is not. This requires the method channel pattern shown above, and your Dart code needs to manage the toggling carefully.

A clean way to handle per-screen toggling in Flutter is to create a wrapper widget or mixin:

dart
// lib/mixins/secure_screen_mixin.dart

import 'package:flutter/widgets.dart';
import '../services/screen_security_service.dart';

mixin SecureScreenMixin<T extends StatefulWidget> on State<T> {
  @override
  void initState() {
    super.initState();
    ScreenSecurityService.enableSecureMode();
  }

  @override
  void dispose() {
    ScreenSecurityService.disableSecureMode();
    super.dispose();
  }
}

Then any screen that needs protection simply mixes it in:

dart
class _CardDetailsScreenState extends State<CardDetailsScreen>
    with SecureScreenMixin {
  // The screen is automatically protected when mounted
  // and unprotected when disposed

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      // ... your UI
    );
  }
}

This keeps the security logic out of your screen's build methods and ensures it is applied consistently.

One subtlety to watch for: if the user navigates from one secure screen to another non-secure screen using a route transition, there is a brief moment where both screens are in the widget tree (during the animation). The dispose of the first screen and the initState of the second screen may interleave. If the first screen calls disableSecureMode in dispose while the second screen does not call enableSecureMode, you get a brief unprotected window. If both screens are secure, the order does not matter. But if a secure screen transitions to a non-secure screen, the disable call from the secure screen's dispose is what you want. Test your specific navigation patterns.

The UX cost

Every security measure has a cost. Screenshot and screen recording protection has a particularly visible one: it directly interferes with things users expect to do.

Users screenshot their confirmation numbers. They screenshot receipts. They share order details with friends. They record their screen to show a colleague how to use an app. When you block these actions, you break expectations. Users do not think "this app is secure." They think "this app is broken."

Banking apps accept this tradeoff because the sensitivity of financial data justifies it. Regulatory requirements in some jurisdictions may even mandate it. But for most apps, global screenshot blocking is too aggressive.

The practical guidance is to be surgical. Protect the screen that displays the full credit card number. Do not protect the screen that shows order history. Protect the screen where the user enters their PIN. Do not protect the settings screen. If a screen shows a mix, consider whether you can restructure it so that the truly sensitive data (the card number, the account balance) is on its own screen that the user visits briefly, while the less sensitive data (transaction history, account name) lives on a screen that remains open.

And consider whether detection plus response is better than prevention. On iOS, where you cannot prevent screenshots anyway, showing a "screenshot detected" message and logging the event server-side may be the right balance. The user gets their screenshot. You get a record that it happened. If the app handles particularly sensitive data, you can notify the account holder or flag the event for review.

Flutter packages

Several packages exist that wrap the native implementations described above. The most commonly referenced is screen_protector, which provides a Dart API for enabling FLAG_SECURE on Android and the privacy overlay on iOS.

These packages can save you time. But there are reasons to understand the native code regardless.

First, packages may not cover all vectors. A package might handle the app switcher thumbnail but not screen recording detection. Read the source code and the issue tracker before relying on one.

Second, packages in this space tend to be maintained by individual developers, not large teams. If a new OS version changes behaviour — iOS is particularly prone to this — the package may lag behind. If you understand the native implementation, you can diagnose issues yourself and apply fixes without waiting for a maintainer.

Third, your specific requirements may not match the package's API. You might need per-screen toggling with a specific lifecycle management pattern. You might need to combine screenshot detection with server-side logging. The method channel approach gives you full control.

Use a package if it fits your needs and is actively maintained. But treat it as a convenience layer, not a black box. The native code underneath is what actually protects your users' data.

What this does not protect against

To be clear about the boundaries: none of these techniques protect against a compromised device. If the device is rooted or jailbroken, FLAG_SECURE can be bypassed. Accessibility services on Android can read screen content regardless of the flag. A custom kernel can ignore the flag entirely. On iOS, a jailbroken device can hook into the rendering pipeline and capture frames before any overlay is applied.

These protections are effective against casual capture — the coworker who picks up the phone, the screen recording that runs in the background, the app switcher that inadvertently displays sensitive data. They are not effective against a determined attacker with physical access to a compromised device. That is a different threat model, and defending against it requires different tools (or the acceptance that some threats cannot be fully mitigated on a device you do not control).

For the majority of apps that handle sensitive data — banking, healthcare, enterprise — protecting against casual capture is the realistic and valuable goal. That is what FLAG_SECURE and its iOS equivalents deliver.

Summary

Android gives you FLAG_SECURE: one flag that blocks screenshots, screen recording, app switcher thumbnails, and screen casting. Set it globally in MainActivity.kt or toggle it per-screen via a method channel.

iOS requires separate handling for each vector. The app switcher thumbnail is addressed with a privacy overlay in applicationWillResignActive. Screenshots can only be detected after the fact, not prevented. Screen recording can be detected in real time via UIScreen.main.isCaptured, allowing you to hide content while recording is active.

Both platforms support per-screen toggling through method channels, which is the right approach for most apps — protecting the screens that show truly sensitive data while leaving the rest of the app unblocked.

The decision of what to protect and how aggressively is a UX decision as much as a security one. Be deliberate about it. Protect what genuinely needs protecting. Leave the rest alone.

Ready to build your app?

Flutter apps built on Clean Architecture — documented, tested, and yours to own. See which plan fits your project.