flutter_app_watermark 1.1.0 copy "flutter_app_watermark: ^1.1.0" to clipboard
flutter_app_watermark: ^1.1.0 copied to clipboard

Watermark your entire Flutter app in one line, or scope it to a single widget. Text or image, with configurable angle, opacity and spacing. Pure Dart, no dependencies.

flutter_app_watermark

Watermark your entire Flutter app in one line.
Text or image. Dialogs, bottom sheets and pushed routes included — nothing covers it.

pub package zero dependencies all platforms MIT license

▶ Try it live  ·  简体中文

flutter_app_watermark demo


Why #

Screenshots of internal dashboards, contracts and customer data leak all the time. A watermark carrying who was looking and when turns an anonymous screenshot into a traceable one — and makes people think twice before taking it.

Doing that properly is trickier than it looks: the watermark has to survive route changes, sit above modals, never swallow a tap, never get read aloud by a screen reader, and not cost you frames. This package does all of it in one widget.

One line, whole app Wrap WatermarkScope outside MaterialApp — every route, dialog, bottom sheet and page transition is covered
Or just one widget Watermark scopes it to a single card without touching its layout
Text or an image Tile a string, or tile any ImageProvider — an asset logo, a network badge
Live updates Change the text after sign-in; only the watermark layer repaints, your widget tree doesn't rebuild
Crop-resistant Staggered rows plus a per-user offset, so a cropped screenshot still points back to one person
Invisible to input IgnorePointer plus a hitTest override — buttons underneath stay tappable
Cheap Covers only the bounding box the rotation needs, lays out the text once, caches the layer
Zero dependencies Pure Dart. No native code, no plugins, works on mobile, web and desktop

A watermark deters screenshots and makes leaks traceable — it does not block screen capture. If you need FLAG_SECURE-style blocking, pair this with a dedicated plugin.

Install #

dependencies:
  flutter_app_watermark: ^1.1.0

Requires Flutter >=3.27.0.

Quick start #

import 'package:flutter_app_watermark/flutter_app_watermark.dart';

final watermark = WatermarkController();

void main() {
  runApp(
    WatermarkScope(              // ← outside MaterialApp
      controller: watermark,
      child: const MyApp(),
    ),
  );
}

// after sign-in
watermark.update(lines: [
  '${user.name} ${user.maskedPhone}',
  DateFormat('yyyy-MM-dd HH:mm').format(DateTime.now()),
]);

That's the whole integration. Everything below is optional.

Scope it to one widget #

Watermark(
  lines: const ['Jane Doe · #10086', 'Internal use only'],
  style: const WatermarkStyle(opacity: 0.22, rotation: -25),
  child: const ContractCard(),
)

Watermark sizes itself to its child and never stretches it. Watermark.text('...') is the single-line shorthand, and enabled: false hides it without changing the tree.

Tile an image instead #

WatermarkScope.image(
  const AssetImage('assets/logo.png'),
  width: 72,                 // height follows the original aspect ratio
  style: const WatermarkStyle(opacity: 0.08, rotation: 0),
  child: const MyApp(),
)

Watermark.image(...) does the same for a single widget, and controller.updateImage(...) swaps it at runtime. Any ImageProvider works — AssetImage, NetworkImage, MemoryImage, FileImage. The package never touches the asset pipeline itself.

Text and image are separate content channels: one scope shows one or the other, and the constructors don't mix — there is no call where you could pass both and wonder what happens. Want both? Nest two scopes, and give each its own spacing — a sparse logo under a dense name-and-timestamp grid:

WatermarkScope(
  lines: ['Jane Doe · #10086', '2026-08-20 14:30'],
  child: WatermarkScope.image(
    const AssetImage('assets/logo.png'),
    style: const WatermarkStyle(rowSpacing: 220, columnSpacing: 260),
    child: const MyApp(),
  ),
)

Two things to know:

  • The text fields of WatermarkStyle (color, fontSize, fontWeight, fontFamily, lineHeight, textAlign) don't apply to an image. Everything geometric — opacity, rotation, spacing, staggered, offset, maxTiles — works the same for both.
  • Decoding is asynchronous, so the very first frame has no watermark on it. For a NetworkImage that gap is as long as the request. Pass onImageError to get load failures; without it they go to FlutterError, same as an Image widget with no errorBuilder.

An image carries no per-user information. A logo proves whose system this is, not who took the screenshot. Keep a text layer if you need to trace a leak back to a person.

Read the controller from anywhere #

WatermarkScope.of(context).update(lines: ['Jane Doe · +1 555 0100']);
WatermarkScope.of(context).update(enabled: false);   // e.g. internal accounts

Styling #

const WatermarkStyle(
  opacity: 0.15,
  rotation: -30,
  fontSize: 14,
  columnSpacing: 80,
  rowSpacing: 60,
  staggered: true,
)
All WatermarkStyle fields
Field Type Default Description
opacity double 0.15 Opacity 0–1, multiplied with the alpha already in color
rotation double -30 Degrees, negative is counter-clockwise, around the canvas center
color Color 0xFF9E9E9E Text color
fontSize double 14 Font size
fontWeight FontWeight normal Font weight
fontFamily String? null Font family
lineHeight double 1.35 Line-height multiplier for multi-line text
textAlign TextAlign center Alignment of the lines inside one tile
rowSpacing double 60 Vertical gap between rows
columnSpacing double 80 Horizontal gap between columns
staggered bool true Shift odd rows by half a tile (crop resistance)
offset Offset zero Offsets the whole pattern — useful to shift it per user
maxTiles int 800 Tile budget per paint; exceeding it thins the grid and warns in debug

WatermarkStyle is an immutable value object — use copyWith for partial changes.

Making leaks traceable #

Put three things in the watermark: who, a unique key, and when.

watermark.update(lines: [
  '${user.name} ${user.maskedPhone}',
  'ID ${user.employeeId}',
  DateFormat('yyyy-MM-dd HH:mm').format(DateTime.now()),
]);

Then shift the pattern per user. Even if most of a screenshot is cropped away, where the remaining tiles sit still identifies one account:

final seed = user.id.hashCode;
style.copyWith(offset: Offset((seed % 37).toDouble(), (seed % 23).toDouble()));

What it covers #

WatermarkScope lives at the very top of the widget tree.

Covered — every route including during transitions · showDialog / showModalBottomSheet / SnackBar · the status bar area and safe areas · Flutter layers above platform views such as WebView and maps.

Not covered — system permission dialogs and the IME panel · the system's post-screenshot preview screen · anything after the user leaves the app. All of it lives outside Flutter, so no pure-Dart package can reach it.

Performance #

  • Fills only the bounding box the rotation actually needs (w·|cosθ| + h·|sinθ| × w·|sinθ| + h·|cosθ|) instead of naively covering twice the diagonal — the naive approach draws roughly 4–5× more tiles
  • Text is laid out once per painter and reused; scrolling and rotation don't re-run layout
  • The layer sits behind a RepaintBoundary and is marked isComplex + !willChange, so app repaints don't drag it along
  • maxTiles is a backstop, so one mistyped font size can't tank the frame rate

On a 390×844 phone with default settings that's about 110 tiles in a single cached layer.

Example app #

cd example && flutter run

A small sales app that adapts from phone to desktop: switch between text and image content, drag angle, opacity, size and spacing live, and check that dialogs, bottom sheets and pushed routes are all covered. Ships with 13 locales, RTL included.

Tests #

flutter test

Support #

If this package saved you some time, a 👍 on pub.dev or a ⭐ on GitHub helps other people find it.

Hit a bug, or have a case it doesn't cover yet? Open an issue — happy to talk it through. PRs welcome too.

License #

MIT

1
likes
160
points
214
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Watermark your entire Flutter app in one line, or scope it to a single widget. Text or image, with configurable angle, opacity and spacing. Pure Dart, no dependencies.

Repository (GitHub)
View/report issues

Topics

#watermark #screenshot #dlp #widget

License

MIT (license)

Dependencies

flutter

More

Packages that depend on flutter_app_watermark