screenshot_shield 0.1.6
screenshot_shield: ^0.1.6 copied to clipboard
Detect user screenshots and optionally prevent screen capture on Android and iOS.
example/lib/main.dart
import 'dart:async';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:screenshot_shield/screenshot_shield.dart';
final RouteObserver<ModalRoute<void>> routeObserver =
RouteObserver<ModalRoute<void>>();
void main() {
runApp(
ScreenshotShieldScope(
shield: ScreenshotShield(),
routeObserver: routeObserver,
child: const MaterialApp(
title: 'Screenshot Shield Demo',
home: HomeScreen(),
),
),
);
}
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
final ScreenshotShield shield = ScreenshotShield();
StreamSubscription<bool>? screenRecordingSubscription;
bool preventCapture = true;
bool detectScreenshots = true;
bool backgroundBlur = false;
DateTime? lastDetected;
Uint8List? lastImage;
bool isScreenRecording = false;
bool hasScreenRecordingState = false;
@override
void initState() {
super.initState();
screenRecordingSubscription = shield.onScreenRecordingChanged.listen((
value,
) {
if (!mounted) {
return;
}
setState(() {
isScreenRecording = value;
hasScreenRecordingState = true;
});
});
}
@override
void dispose() {
screenRecordingSubscription?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return ScreenshotShieldRouteGuard(
preventCapture: preventCapture,
detectScreenshots: detectScreenshots,
captureOnScreenshot: true,
onScreenshotDetected: (image) {
setState(() {
lastDetected = DateTime.now();
lastImage = image;
});
},
child: Scaffold(
appBar: AppBar(title: const Text('Screenshot Shield Demo')),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
const _SectionLabel('Guarded content'),
Container(
height: 180,
alignment: Alignment.center,
decoration: BoxDecoration(
color: Colors.indigo,
borderRadius: BorderRadius.circular(12),
),
child: const Text(
'This screen is protected.\nTry taking a screenshot.',
textAlign: TextAlign.center,
style: TextStyle(color: Colors.white, fontSize: 16),
),
),
const SizedBox(height: 16),
SwitchListTile(
title: const Text('Prevent capture'),
subtitle: const Text(
'Blanks the captured frame (Android and iOS)',
),
value: preventCapture,
onChanged: (value) => setState(() => preventCapture = value),
),
SwitchListTile(
title: const Text('Detect screenshots'),
subtitle: const Text('Report onScreenshotDetected events'),
value: detectScreenshots,
onChanged: (value) => setState(() => detectScreenshots = value),
),
SwitchListTile(
title: const Text('Background blur'),
subtitle: const Text('Blur the app in the app switcher'),
value: backgroundBlur,
onChanged: (value) async {
setState(() => backgroundBlur = value);
await shield.setProtection(backgroundBlur: value);
},
),
const SizedBox(height: 16),
const _SectionLabel('Detection'),
Card(
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
lastDetected == null
? 'No screenshot detected yet.'
: 'Detected at ${lastDetected!.toLocal()}',
),
if (lastImage != null) ...[
const SizedBox(height: 8),
Image.memory(
lastImage!,
height: 160,
fit: BoxFit.contain,
),
],
],
),
),
),
const SizedBox(height: 16),
const _SectionLabel('Screen recording'),
Card(
child: ListTile(
leading: Icon(
isScreenRecording
? Icons.fiber_manual_record
: Icons.videocam_off_outlined,
color: isScreenRecording ? Colors.red : null,
),
title: Text(
!hasScreenRecordingState
? 'Waiting for a platform event...'
: isScreenRecording
? 'The screen is being recorded'
: 'The screen is not being recorded',
),
subtitle: const Text(
'Android 15+, iOS; best-effort on Windows and Linux',
),
),
),
const SizedBox(height: 16),
FilledButton.tonal(
onPressed: () => Navigator.of(context).push(
MaterialPageRoute<void>(builder: (_) => const SecondScreen()),
),
child: const Text('Open granular protection demo'),
),
],
),
),
);
}
}
class _SectionLabel extends StatelessWidget {
const _SectionLabel(this.text);
final String text;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Text(text, style: Theme.of(context).textTheme.titleMedium),
);
}
}
class SecondScreen extends StatelessWidget {
const SecondScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Granular protection')),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
const Text(
'This route is not guarded, so whole-window protection is off. Only '
'the region below is excluded from captures.',
),
const SizedBox(height: 16),
const Text('Not protected (visible in a screenshot):'),
const SizedBox(height: 8),
Container(
height: 80,
alignment: Alignment.center,
color: Colors.teal,
child: const Text(
'Ordinary content',
style: TextStyle(color: Colors.white, fontSize: 18),
),
),
const SizedBox(height: 24),
const Text('Protected region (blank in a screenshot):'),
const SizedBox(height: 8),
ScreenshotShieldSensitiveView(
refreshInterval: const Duration(milliseconds: 250),
child: Container(
height: 120,
alignment: Alignment.center,
color: Colors.deepOrange,
child: const Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Sensitive region',
style: TextStyle(color: Colors.white, fontSize: 18),
),
SizedBox(height: 4),
Text(
'Expires 09/29 · CVV 123',
style: TextStyle(color: Colors.white),
),
],
),
),
),
const SizedBox(height: 24),
const Text(
'iOS only. On a device, take a screenshot: the teal block above is '
'captured, the orange region is blank. `xcrun simctl io screenshot` '
'cannot show this, because it reads the simulator framebuffer '
'without applying capture exclusion.',
),
],
),
);
}
}