flutter_onscreen_logger 3.2.0
flutter_onscreen_logger: ^3.2.0 copied to clipboard
A Flutter package that allows you to display logs on the screen of your app for easier debugging.
Flutter On-Screen Logger #
A Flutter package that displays logs over your app for easier debugging and can capture a session without an overlay for sharing through your support flow.

Features #
- Log exceptions and errors in a user-friendly way.
- Easily integrate with Dio for HTTP request logging.
- Simple API for logging custom messages.
- Customizable display options for logs.
- Programmatic capture controls for development, production, and runtime settings.
- Export retained session logs directly as text or UTF-8 bytes for feedback attachments.
Getting Started #
-
Add the package to your
pubspec.yamlfile:dependencies: flutter_onscreen_logger: ^3.2.0 -
Configure your
main.dartto integrate the library:- Wrap your
runApp()method inmain()withrunZonedGuarded()to handle errors globally. - Optionally call
OnScreenLog.init()to choose capture and scrolling options. - Call
OnScreenLog.onError()during startup to capture Flutter framework errors. - Log asynchronous errors received by the zone's error callback.
void main() { runZonedGuarded(() { WidgetsFlutterBinding.ensureInitialized(); OnScreenLog.init( enabled: true, enabledTypes: LogItemType.values.toSet(), autoScroll: true, ); OnScreenLog.onError(); //...other code... runApp(MyApp()); }, (error, stack) { OnScreenLog.e(title: error.toString(), message: stack.toString()); }); }- Add the overlay using your app's
builder, so it inherits the active app theme. - Return a
Stackcontaining the builder'schildfirst andLoggerOverlayWidget()last. - Show the overlay when useful for your app. Its visibility is independent of capture. This example displays it only in debug mode (
kDebugModecomes frompackage:flutter/foundation.dart).
MaterialApp( // ...your theme, routes, and other app properties... home: MyHomePage(title: 'MyApp'), builder: (context, child) => Stack( fit: StackFit.expand, children: [ child ?? const SizedBox.shrink(), if (kDebugMode) LoggerOverlayWidget(), ], ), ); - Wrap your
App theme integration #
Use the same builder setup with MaterialApp, MaterialApp.router, or
GetMaterialApp. The logger reads Flutter's inherited Theme, so it follows
the active theme and runtime light/dark changes without depending on how the
app manages theme state. The handle and jump button use ThemeData.primaryColor,
and the options popup inherits the app's theme, including popupMenuTheme.
Message cards and quick filters retain their type colors and black background.
Quick filters use a fixed style with 8-pixel corners, independent of the app's
chip shape, typography, and spacing. Accessibility text scaling still applies.
If your app already has a builder, keep its existing content in the stack along
with the logger. Keep the supplied child in the tree so navigation still works.
Migrating an existing integration: move the logger from the outer
Stack(children: [MaterialApp(...), LoggerOverlayWidget()]) into the app's
builder. A sibling cannot inherit the theme inside MaterialApp or
GetMaterialApp. The package's earlier setup instructions placed it outside
that theme.
For custom placement outside the themed subtree, supply an explicit override:
LoggerOverlayWidget(theme: activeAppTheme)
Rebuild with the updated ThemeData whenever that external theme changes. The
override takes precedence over the inherited theme. Without an override or an
inherited theme, Flutter supplies its fallback theme.
Usage #
You can use the on-screen logger to log your own custom message throughout your project using the following methods:
e() - Log Errors #
Logs error messages with a red color.
OnScreenLog.e(
title: 'Network Error',
message: 'Unable to fetch data from server.',
);
i() - Log Info #
Logs info messages with a white color.
OnScreenLog.i(
title: 'API Response Received!',
message: '* API Response:\n$data',
);
s() - Log Success #
Logs success messages with a green color.
OnScreenLog.s(
title: 'Login Successful',
message: 'User has logged in successfully.',
);
w() - Log Warnings #
Logs warning messages with a orange color.
OnScreenLog.w(
title: 'Low Disk Space',
message: 'Disk space is running low, consider cleaning up.',
);
exportReport() - Export without files or a share sheet #
Available in 3.1.0. Retrieve a session report directly for an in-app feedback
attachment. Import only package:flutter_onscreen_logger/flutter_onscreen_logger.dart:
OnScreenLog.init(enabled: true);
// Mounting the overlay is optional, including in release builds.
OnScreenLog.i(
title: 'Budget',
message: 'Budget screen opened',
);
try {
final LogReport report = await OnScreenLog.exportReport();
final bytes = report.toUtf8Bytes();
// Application code decides where to send the attachment:
await feedbackRepository.uploadLog(
bytes: bytes,
filename: 'session.log',
contentType: 'text/plain; charset=utf-8',
);
} catch (error) {
// Handle an export or upload failure in your application's feedback flow.
}
feedbackRepository belongs to your application; the library adds no uploader
dependency. See the runnable export example
for a version requiring no application services.
The public immutable LogReport contains text, UTC generatedAt, entryCount,
and nullable firstEntryAt / lastEntryAt (the first and last entries in capture
order, not a timestamp sort). toUtf8Bytes() returns a fresh UTF-8 buffer without
a byte-order mark; modifying it cannot change the report. Generation and entry
timestamps in the text use UTC ISO 8601, retaining subsecond precision. Titles,
types, multiline descriptions, Unicode, stack traces, and already-captured
structured HTTP details use the same report formatter as Share all.
- Export snapshots all retained entries when invoked, in capture order, even when timestamps are identical. Later additions or clearing cannot change that report; concurrent calls are independent. Flush any logging deferred by application code before requesting the snapshot.
- Display filters do not limit export. Capture-excluded entries and messages discarded while paused cannot be reconstructed. Exporting while paused or disabled includes previously retained entries without resuming capture.
- An empty capture returns a readable empty report,
entryCount: 0, and null first/last entry timestamps. It is a successful result. - Formatting failures complete the Future with an error. Handle them with
try/catch; they are not converted into empty reports or logged recursively. - Export works without a widget
BuildContextor mounted overlay in debug, profile, and release builds. It does not change navigation, focus, scrolling, visibility, capture settings, or the retained buffer. - Export and UTF-8 conversion perform no file, share-sheet, clipboard, storage
permission, gallery, upload, or network operations. Only the application's
uploader in the example accesses the network.
shareAll()continues to create a temporary file and open native sharing, using the same formatter.
Only retained content and basic report metadata are included; export does not collect device identifiers, account data, or environment information. Avoid logging secrets and private content. Existing network redaction applies before storage; exporting adds no new automatic redaction for application-written logs. Large sessions still require memory for the retained entries, report text, and encoded bytes. Optional retention limits are available from 3.2.0 (below).
captureSnapshot() - Freeze now, export or retry later #
Available in 3.2.0. Freeze the logs when a feedback flow opens, even if the user has not yet chosen whether to attach them:
final LogSnapshot snapshot = OnScreenLog.captureSnapshot();
// Later, including after logging, clearing, pausing or changing limits:
try {
final report = await OnScreenLog.exportReport(snapshot: snapshot);
final bytes = report.toUtf8Bytes();
// Pass bytes to your application's attachment uploader.
} catch (error) {
// Retry export with the same snapshot to use the original captured logs.
}
Snapshot capture is synchronous and does not format a report or perform I/O.
It owns immutable entries, structured details, original entry timestamps,
capturedAt, retention counters, captureEnabled, enabledTypes, and retention.
It does not retain application callbacks or overlay state. Flush any deferred
logging before capturing. Snapshot-creation errors propagate synchronously;
formatting errors complete exportReport with an error and leave the snapshot
usable. A deterministic formatting failure may fail again on retry.
Each retry uses the snapshot's original UTC timestamp as LogReport.generatedAt,
producing identical text and UTF-8 bytes. No live controller is needed when
exporting a supplied snapshot. Calling exportReport() without an argument still
captures the current logs immediately, and Share all uses the same formatter.
The library keeps no snapshot history. Your app owns snapshot lifetime: release references when the feedback flow ends, and handle account/session boundaries in your app. Clearing the live logger cannot invalidate a snapshot you already hold.
configureRetention() - Optional rolling capture limits #
Available in 3.2.0. All limits are unlimited by default. Configure any combination before logging, or change it later:
OnScreenLog.configureRetention(
maxEntries: 500,
maxUtf8Bytes: 1024 * 1024,
maxEntryCharacters: 4000,
oversizedEntryPolicy: OversizedEntryPolicy.truncate,
);
These are example values, not a prescribed production preset. Each call replaces
all retention settings; omitted limits become unlimited. Inspect the current
immutable settings through OnScreenLog.retention.
// Count-only retention does not truncate messages.
OnScreenLog.configureRetention(maxEntries: 1000);
// Restore unlimited capture without clearing entries or counters.
OnScreenLog.resetRetention();
- Limits apply when an entry is accepted, and stricter settings trim existing entries immediately. Oldest entries are evicted first, preserving capture order. Removing limits cannot recover discarded data.
maxEntriesmust be positive.maxEntryCharactersmust be positive and limits combined textual field content in user-perceived Unicode characters (grapheme clusters), including the visible…truncation marker. It preserves joined emoji and combining marks. Plain entries count title plus description, including stack traces. HTTP entries count method, URL, request ID, error, headers and body once; generated labels and duplicate display/export representations are instead covered by the byte budget. Identifying fields take priority over large bodies.maxUtf8Bytesbounds the complete UTF-8 report, including separators, timestamps and retention headers. A conservative 512-byte metadata reserve makes 512 the minimum accepted limit; the remaining budget holds formatted entries. A metadata-only budget can retain no entries. All entry fields and generated labels count toward this limit. Your application's extra diagnostic header is outside the report and must have its own allowance.- An entry larger than the available byte budget is shortened by default. Choose
OversizedEntryPolicy.dropto discard it instead. An explicit character limit still applies first. If even a shortened entry cannot fit, it is dropped without unnecessarily evicting useful history. HTTP cards and exports both use shortened copies; the logger does not retain their original oversized payloads. - Invalid limits throw
ArgumentErrorbefore changing settings. Retention failures propagate without recursively logging or showing UI. Capture remains independent of the overlay and build mode; display filters do not affect retention or export.
LogReport and LogSnapshot expose cumulative omittedEntries and
truncatedEntries since clearAll(). Omitted entries were accepted then evicted,
or dropped because they could not fit. Truncated entries were shortened, counted
once each even if shortened again. An entry shortened and later dropped/evicted
can count in both; these counters can exceed the current entryCount. Paused or
capture-excluded events do not increment counters. Exported text includes both
counters when limits are active or either counter is nonzero.
clearAll() clears entries and counters, while preserving capture and retention
settings. It does not change already-frozen snapshots or reports. New LogReport
constructor fields default to zero for compatibility with existing callers.
The byte budget is not a Dart heap limit. Strings, structured card data, formatting, UTF-8 conversion, and caller-held snapshots/reports use additional memory. Retention bounds the live buffer, not snapshots held by your app, and cannot prevent allocations made by application code before calling the logger.
shareAll() - Share All Logs #
Allows you to share all the log entries that have been captured so far. This method will gather the logs and open a sharing interface for the user to share the log messages.
OnScreenLog.shareAll();
clearAll() - Clear All Logs #
Clears all log entries currently stored in the logger, effectively resetting the log data. This is useful if you want to start a new logging session or remove sensitive log data.
OnScreenLog.clearAll();
Configure capture and scrolling #
Call OnScreenLog.init() before your first log to choose the session's initial
settings. Initialization is optional: the logger still initializes lazily with
capture enabled, every message type enabled, and auto-scroll enabled.
OnScreenLog.init(
enabled: true,
enabledTypes: {LogItemType.error, LogItemType.warning},
autoScroll: false,
);
| Option | Default | Effect |
|---|---|---|
enabled |
true |
Whether new entries are captured at all, including when the overlay is absent. |
enabledTypes |
Every LogItemType |
Types accepted into the session. An empty set captures nothing. |
autoScroll |
true |
Whether the overlay follows new entries while already at the bottom. |
networkLogOptions |
NetworkLogOptions() |
Which HTTP headers and bodies connectors capture and the redactor applied before storage. |
Disabling capture discards future entries from OnScreenLog methods, the default
Dio/HTTP connectors, and installed Flutter error capture. Existing entries remain
available to share or clear. Resuming records new entries only; discarded entries
are never replayed. Removing or collapsing the overlay does not change these
settings. Connectors using a custom log callback have their own sink; these
settings apply when that callback submits entries to OnScreenLog.
Change individual settings at runtime without resetting the others:
OnScreenLog.configure(enabled: false); // Stop capture, retain existing logs.
OnScreenLog.configure(enabled: true); // Resume capture.
OnScreenLog.configure(
enabledTypes: {LogItemType.error, LogItemType.warning},
autoScroll: false,
);
OnScreenLog.configure(enabledTypes: LogItemType.values.toSet());
configure() updates only the supplied options. Calling init() again applies
its complete settings, including defaults for omitted options, to the existing
session. Neither call clears captured logs or changes the overlay's quick filters.
The menu's pause/resume and auto-scroll actions use the same state as these APIs.
Read OnScreenLog.isEnabled, OnScreenLog.isAutoScrollEnabled, and the immutable
OnScreenLog.enabledTypes set to inspect the current settings. Use
OnScreenLog.isEnabledFor(type) before preparing expensive log data:
if (OnScreenLog.isEnabledFor(LogItemType.info)) {
OnScreenLog.i(message: buildDiagnosticReport());
}
Capture only during development #
Choose this if release builds should capture no session logs. Existing logging calls can stay in your application code:
OnScreenLog.init(enabled: kDebugMode);
// In the Stack returned by your app's builder:
if (kDebugMode) LoggerOverlayWidget(),
Capture production support sessions without an overlay #
Keep capture enabled and display the overlay only during development. Your production settings or support page can offer a share button:
OnScreenLog.init(enabled: true);
// In the Stack returned by your app's builder:
if (kDebugMode) LoggerOverlayWidget(),
// In your settings/support page:
TextButton.icon(
onPressed: OnScreenLog.shareAll,
icon: const Icon(Icons.share),
label: const Text('Share session logs'),
),
To reduce production capture to warnings and errors while retaining every type during development:
OnScreenLog.init(
enabled: true,
enabledTypes: kDebugMode
? LogItemType.values.toSet()
: {LogItemType.warning, LogItemType.error},
autoScroll: kDebugMode,
);
// Later, apply an app setting without changing the type selection:
OnScreenLog.configure(enabled: captureEnabledByUser);
Quick filters and options #
Tap the info, success, warning, and error chips to include or
exclude types independently. All types start selected; deselecting every type
shows an empty view. These filters only affect the view, so Share all still
shares the complete captured session. In contrast, enabledTypes controls which
new entries are stored in the first place. The top-right Logger options menu contains
Pause logging / Resume logging, Auto-scroll (configurable at startup),
Share all, and Clear all. Pausing discards incoming entries, including
connector logs, while keeping existing entries available. It remains paused
when the overlay is collapsed; resuming captures only new entries and does not
replay skipped messages. This is independent of auto-scroll.
Filter chips use the same colors as their message types: outlined when excluded and filled when selected. Every menu action has an icon; toggle icons reflect the current logging/auto-scroll state.
When newer messages are below the viewport, a small button at the bottom right shows how many filtered messages are below the last visible entry. Partially visible entries count as visible. Tap it to jump to the actual latest entry; it disappears at the bottom. Scrolling up suspends automatic following so new messages do not interrupt reading. Returning to the bottom resumes following if auto-scroll is enabled. Jumping does not change the auto-scroll preference.
Automatic network logging #
Two independent connector packages live in packages/. They use the same
logger session whether or not LoggerOverlayWidget is mounted.
Dio #
import 'package:dio/dio.dart';
import 'package:flutter_onscreen_logger_dio/flutter_onscreen_logger_dio.dart';
final dio = Dio();
dio.interceptors.add(OnScreenLoggerInterceptor());
package:http #
HTTP uses a client wrapper because it has no Dio-style interceptor API. Make
requests through the wrapper (top-level http.get calls are not intercepted).
import 'package:http/http.dart' as http;
import 'package:flutter_onscreen_logger_http/flutter_onscreen_logger_http.dart';
final client = OnScreenLoggerClient(http.Client());
final response = await client.get(Uri.parse('https://example.com'));
client.close(); // Also closes the wrapped client.
For local development, add the desired connector with a path dependency:
dependencies:
flutter_onscreen_logger:
path: /path/to/flutter_onscreen_logger
flutter_onscreen_logger_dio:
path: /path/to/flutter_onscreen_logger/packages/flutter_onscreen_logger_dio
# Or flutter_onscreen_logger_http with its corresponding path.
dependency_overrides:
flutter_onscreen_logger:
path: /path/to/flutter_onscreen_logger
The connectors log methods, URL paths, response status, and failures. URL credentials and query values are omitted. Headers and bodies stay off by default. Enable selected fields and redact their formatted text before it enters the session:
OnScreenLog.init(
networkLogOptions: NetworkLogOptions(
includeRequestHeaders: true,
includeResponseHeaders: true,
includeRequestBody: true,
includeResponseBody: true,
redactor: LogRedactor.redact, // Your app's redactor, e.g. token/password rules.
),
);
NetworkLogOptions can also be passed to an individual
OnScreenLoggerInterceptor(options: ...) or OnScreenLoggerClient(options: ...)
to override the global setting. Redaction runs before the entry is stored, so
the overlay, copy action, and shared session use the same sanitized snapshot.
Implement the callback to return safe text, for example by redacting sensitive
JSON keys and token patterns. includeResponseBody captures decoded Dio
responses; the HTTP connector does not read or buffer response streams. It
captures buffered http.Request bodies only when enabled. Each connector
accepts an optional log: (LogItem item) { ... } callback for a custom sink.
Logging failures do not interrupt requests.
Capturing a session without the overlay #
Keep OnScreenLog enabled and call OnScreenLog.i/e/w/s or use either connector
without adding the widget. Configure enabledTypes to choose what is captured.
Offer OnScreenLog.shareAll() in your app's support flow to export the captured
session. Logs are held in memory for the current process, and written to a
temporary file when shared; they are not persisted across app restarts. This
does not schedule background execution or collect logs from other isolates.
Developing the packages #
Run flutter pub get, flutter analyze, and flutter test in the repository
root and in each connector directory. Connector pubspec_overrides.yaml files
resolve the core package locally and are excluded from publication by pub.
The connector packages must be published separately before hosted dependencies
can be used by consumers.
Version 3 toolchain and migration #
Version 3 requires Flutter 3.44.3 or newer and Dart 3.12 or newer.
The repository pins Flutter 3.44.3 in .fvmrc; run fvm use 3.44.3 to install/select
that SDK on another machine. SDK installations are excluded from Git and pub
archives. The .fvm/flutter_sdk link is generated locally by FVM.
The Android example uses Gradle 9.7.1, Android Gradle Plugin 9.3.3, Kotlin 2.4.20, and Java 17 source/target compatibility. Run Gradle with JDK 17 or newer (Flutter can use Android Studio's bundled JDK). The iOS example targets iOS 15 or newer. Update consuming applications' native build settings before upgrading from version 2, since the latest plugin dependencies have higher platform/toolchain requirements.
Direct dependencies are updated to fluttertoast 10.0.0, get 4.7.3, intl 0.20.3,
path_provider 2.1.6, and share_plus 13.3.0. Sharing uses the current
SharePlus.instance.share(ShareParams(...)) API; the public OnScreenLog calls
remain compatible. Some transitive dependencies are constrained by Flutter or
upstream plugins; this project does not force incompatible dependency overrides.
Before publishing, run the following with the selected SDK:
fvm flutter pub get
fvm flutter analyze
fvm dart format --output=none --set-exit-if-changed lib test example/lib example/test packages
fvm flutter test
fvm dart pub outdated --no-dev-dependencies --up-to-date --no-dependency-overrides
fvm dart pub publish --dry-run
Run analysis and tests in example/ and each connector directory too. Publish
the core version 3.0.0 package before the connectors, which depend on ^3.0.0.
Pub.dev assigns the final hosted score after publication and reanalysis.
The root .pubignore keeps the nested connector sources out of the core package
archive. Export each connector directory outside the repository before
publishing it, so it is checked as its own package:
mkdir -p /tmp/logger-release
git archive HEAD packages/flutter_onscreen_logger_dio | tar -x -C /tmp/logger-release
cd /tmp/logger-release/packages/flutter_onscreen_logger_dio
fvm dart pub publish --dry-run
Repeat with flutter_onscreen_logger_http in the archive path and working
directory for the HTTP connector. Publish the core package first so the
connectors' ^3.0.0 dependency is available.
Try both connectors in the example #
Run fvm flutter pub get and fvm flutter run from example/. Its local
pubspec_overrides.yaml selects the connector packages from this repository.
Tap Test Dio requests or Test HTTP requests. Each makes two GET requests
to JSONPlaceholder: /posts/1
(200) and /posts/0 (404), demonstrating automatic request, success, and error
logs. These calls need an internet connection and send no credentials. The Dio
demo enables body logging for this public test data; the HTTP demo logs metadata.
Both clients are closed when the example screen is disposed. Network button
tests use mock transports, so the automated suite runs offline.
Reading network logs #
Dio and HTTP entries use dedicated network cards. The collapsed summary shows the method, request/response/failure stage, HTTP status when available, endpoint, elapsed time, and a request ID shared by related entries. Timing measures the Dio response or failure, and HTTP time to response headers; it is not a measure of downloading an HTTP response stream.
Tap a card's header to inspect the full captured URL, failure category, and
payload. Dio bodies captured with logBodies: true are formatted as indented
JSON when possible. Large bodies scroll inside a bounded panel, so the Copy
log action stays accessible. Copying and sharing include the entire captured
body, not just the visible portion. HTTP streams remain unbuffered and its
connector continues to log metadata only. Headers, credentials, and query values
remain omitted by the connectors.
You can also submit structured network entries from a custom integration:
OnScreenLog.log(LogItem.network(
type: LogItemType.success,
details: HttpLogDetails(
method: 'GET',
url: 'https://api.example.com/orders/42',
phase: HttpLogPhase.response,
requestId: 'orders-42',
statusCode: 200,
duration: const Duration(milliseconds: 120),
body: HttpLogDetails.formatBody({'id': 42, 'status': 'delivered'}),
),
));
Custom integrations choose which URL and payload information to capture.
LogItem.network keeps the card and its text export consistent. Existing
OnScreenLog.i/e/w/s calls and plain LogItem entries keep their original layout.