Secure-screen protection for Flutter apps on Android and iOS.
flutter_defender is a general security layer for apps that handle sensitive
data (finance, healthcare, enterprise, identity, and more). Guarded screens can:
- hide Android recents/screenshot content with
FLAG_SECURE - react to screenshot and live-capture events
- conceal sensitive content immediately when iOS loses focus
- enforce OTP/session background timeouts
- block release builds on emulators/simulators
- harden Android guarded screens against overlay-based tapjacking
This package uses explicit guard widgets:
FlutterDefenderSensitiveGuardFlutterDefenderOtpGuard
There is no route-observer setup. A guarded screen protects itself before the sensitive child is revealed.
dependencies:
flutter_defender: ^0.3.0enableEmulatorDetectionRelease blocks guarded Flutter screens in release
builds. If you need the stricter policy where a release APK is blocked before
Flutter starts, make the package guard activity your Android launcher and point
it at your real Flutter activity:
<activity
android:name="aleem.flutter.defender.ReleaseEmulatorGuardActivity"
android:exported="true"
android:launchMode="singleTask"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
tools:replace="android:exported">
<meta-data
android:name="aleem.flutter.defender.TARGET_ACTIVITY"
android:value=".MainActivity" />
<!-- Optional text overrides:
<meta-data
android:name="aleem.flutter.defender.BLOCK_TITLE"
android:value="Unsupported device" />
<meta-data
android:name="aleem.flutter.defender.BLOCK_SUBTITLE"
android:value="Security protection is enabled" />
<meta-data
android:name="aleem.flutter.defender.BLOCK_MESSAGE"
android:value="This release build cannot run on emulators." />
<meta-data
android:name="aleem.flutter.defender.BLOCK_BUTTON"
android:value="Close app" />
-->
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<!-- Keep your existing MainActivity settings, but remove MAIN/LAUNCHER from it. -->
<activity
android:name=".MainActivity"
android:exported="false"
android:theme="@style/LaunchTheme" />No Gradle change is required. Debug and profile builds remain runnable on
emulators; non-debuggable release-like builds are blocked at launch when an
emulator is detected. Android can still install a release APK on a compatible
emulator, so this is launch-time enforcement rather than install prevention. If
your manifest does not already define it, add
xmlns:tools="http://schemas.android.com/tools" to the root <manifest> tag.
If TARGET_ACTIVITY is wrong, the native guard shows a configuration error and
logs the missing activity instead of crashing.
Initialize once before runApp:
import 'package:flutter/widgets.dart';
import 'package:flutter_defender/flutter_defender.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await FlutterDefender.instance.init(
otpBackgroundTimeoutSeconds: 60,
authenticatedBackgroundTimeoutSeconds: 120,
onLogoutRequested: () {
// Clear session and return to a safe route.
},
);
runApp(const MyApp());
}Tell the plugin when the authenticated session changes:
FlutterDefender.instance.setAuthenticated(true);
FlutterDefender.instance.setAuthenticated(false);Wrap sensitive screens directly:
class StatementPage extends StatelessWidget {
const StatementPage({super.key});
@override
Widget build(BuildContext context) {
return const FlutterDefenderSensitiveGuard(
child: StatementView(),
);
}
}
class OtpPage extends StatelessWidget {
const OtpPage({super.key});
@override
Widget build(BuildContext context) {
return const FlutterDefenderOtpGuard(
child: OtpView(),
);
}
}Options:
otpBackgroundTimeoutSecondsauthenticatedBackgroundTimeoutSecondsenableForegroundCheckenableEmulatorDetectionReleaseenableRootDetection(defaults totruein release,falsein debug/profile)enableProxyVpnDetection(defaults totruein release,falsein debug/profile)enableRaspDetection(defaults totruein release,falsein debug/profile)enableSecureStorageHelper(defaultfalse)clearSecureStorageOnLogout(defaultfalse)onLogoutRequestedonRootDetectedonProxyOrVpnDetectedonTamperingDetectedblockingScreenBuilderuiThemeblockingLocalemessageResolverblockingTitleResolver
All advanced layers are optional and configured at init.
- Android checks common root indicators (for example
su, Magisk paths,test-keys). - iOS checks common jailbreak indicators (for example Cydia paths and sandbox write escape).
- Callback:
onRootDetected - Policy toggle:
enableRootDetection
- Detects active proxy settings and VPN transport/interface indicators.
- Callback:
onProxyOrVpnDetected - Policy toggle:
enableProxyVpnDetection
- Detects debugger attachment and common hooking artifacts (best-effort).
- Callback:
onTamperingDetected - Policy toggle:
enableRaspDetection
- Provides convenience secure key/value methods backed by:
- Android: Keystore-backed encrypted shared preferences
- iOS: Keychain
- Toggle:
enableSecureStorageHelper - Optional lifecycle integration:
clearSecureStorageOnLogout - Failure policy: secure-storage platform errors are fail-fast and throw; only
missing keys return
nullfromsecureRead.
await FlutterDefender.instance.init(
enableSecureStorageHelper: true,
clearSecureStorageOnLogout: true,
);
await FlutterDefender.instance.secureWrite(key: 'token', value: 'abc');
final token = await FlutterDefender.instance.secureRead('token');
await FlutterDefender.instance.secureDelete('token');
await FlutterDefender.instance.secureClearAll();Use for any guarded screen that should:
- enable Android secure-window protection
- react to overlay hardening events on Android
- conceal content immediately when iOS enters
inactive - react to capture/foreground/emulator policy failures
Use for OTP flows. On timeout, only the enclosing OTP route is popped.
Controls the authenticated-session timeout logic. Call:
trueafter successful loginfalseon logout or session clear
authenticatedBackgroundTimeoutSeconds applies to this authenticated-session state.
The older pinBackgroundTimeoutSeconds name is deprecated because the timeout is
not tied to detecting a specific PIN page.
The built-in blocking UI is full-screen and always absorbs interaction.
You can customize the visible content with blockingScreenBuilder, but the plugin still owns the modal barrier and pointer absorption:
await FlutterDefender.instance.init(
blockingScreenBuilder: (message) {
return Center(child: Text(message));
},
);| Capability | Android | iOS |
|---|---|---|
| Secure screenshots / recents | Yes, via FLAG_SECURE |
No direct equivalent |
| Screenshot event | Android 14+ screenshot callback | Post-capture notification only |
| Live capture / mirroring detection | Limited | Yes, across connected screens via UIScreen.isCaptured |
Conceal on focus loss (inactive) |
Lifecycle-driven concealment | Yes, hides guarded content immediately |
| Overlay protection | Mitigation-based hardening | Not supported |
| Emulator / simulator release block | Guarded screens; optional native launch guard | Flutter/Xcode tooling blocks release simulator builds |
| Root / jailbreak detection | Yes (best-effort indicators) | Yes (best-effort indicators) |
| Proxy / VPN detection | Yes | Yes |
| Basic RASP (debugger / hooking) | Yes | Yes |
| Secure storage helper | Yes (Keystore-backed) | Yes (Keychain-backed) |
Important limitations:
- Android overlay defense is mitigation-based. The plugin hardens guarded screens and reports obscured-touch violations; it does not claim perfect detection of every hostile overlay.
- iOS screenshot detection is after capture. The system screenshot has already happened when the notification arrives.
- iOS uses privacy concealment, not hostile-overlay detection. Guarded content is hidden when the app becomes inactive, such as during Control Center, Notification Center, Siri, calls, or app-switcher transitions.
- Release-only emulator/simulator blocking applies on guarded screens when
enableEmulatorDetectionReleaseis enabled. On Android, the optional package launcher guard blocks release-like emulator launches before Flutter starts. On iOS,flutter build ios --simulator --releaseis already rejected by Flutter/Xcode tooling.
- On iOS, guarded content is concealed immediately while the app is
inactiveand revealed again when the app becomes active. - While an
FlutterDefenderOtpGuardscreen is active, background timeout pops only that OTP route. - While
setAuthenticated(true)is active, background timeout callsonLogoutRequested. - Timeout state is persisted across process death and rechecked on the next launch.
Register the package delegates in your app:
MaterialApp(
localizationsDelegates: const [
...FlutterDefenderLocalizations.localizationsDelegates,
],
supportedLocales: mergeFlutterDefenderSupportedLocales(
const [Locale('en')],
),
);Supported built-in locales:
- English
- Arabic
- French
- Spanish
The example/ app demonstrates:
- guarded sensitive screens
- OTP guard behavior
- authenticated timeout wiring
- blocking UI customization profiles (
blockingScreenBuilder,uiTheme,blockingLocale,messageResolver,blockingTitleResolver) - policy toggle profiles for
enableForegroundCheckandenableEmulatorDetectionRelease - advanced-layer profiles for root/jailbreak, proxy/VPN, RASP, and secure storage helper
- manual validation steps for release emulator/simulator checks and capture handling
Run it with:
cd example
flutter runflutter analyze
flutter test
cd example && flutter build apk --release
cd example && flutter build ios --simulator --debug --no-pub
cd example && flutter test
flutter pub publish --dry-runThis repository includes GitHub Actions for CI and publishing:
- Pull requests run package and example analysis plus tests.
- Pushes to
main/masterrerun those checks, verify thatpubspec.yamlcontains a version higher than the previous branch tip, and then create a matching Git tag such asv0.3.0. - Pushing that tag triggers the publish workflow, which runs a final
flutter pub publish --dry-runand then publishes to pub.dev.
Important notes:
- The first release of a new package must still be published manually with
dart pub publish/flutter pub publish. - Pub.dev automated publishing from GitHub Actions only works for workflows triggered by tag pushes, so the main-branch workflow tags the release and the tag workflow performs the actual publish.
- Configure automated publishing for this package on pub.dev and require the
GitHub Actions environment named
pub.devto match the publish workflow.
Apache-2.0