Tomáš Repčík - 3. 8. 2025

Beyond print(): Levelling Up Your Flutter Logging

Your guide to diagnosing issues and understanding your app

Phone writing notes

Logging is a crucial aspect of software development, and every framework has its own way of handling it.

Flutter comes with different logging options, and I think there is a missing extensive guide on how to use them properly.

The article will cover the basics of logging and show you some useful tips and tricks to make your logging more effective.

Printing in Flutter

print()

The simplest way to log in Flutter is using the print() function.

print() is coming from Dart, and it is a simple way to output text to the console. It can be useful for quick debugging, but it has some limitations.

The output is printed to the console, but it can be truncated if the message is too long.

Moreover, it does not provide any way to filter or categorize the logs, which can make it difficult to find information in a large output.

debugPrint()

A better alternative to print() is the debugPrint() function.

The debugPrint() function is coming from Flutter foundation, and it is similar to print(), but it has one advantage.

The log messages are not truncated, so you can see the full message in the console.

The most common misconception is that debugPrint() will not print in release mode. This is not true, as debugPrint() will print in all modes. You can accompany it with kDebugMode in an if statement to conditionally print only in debug mode.

if (kDebugMode) {
    debugPrint('This will only print in debug mode');
}

Downside to the print functions

Both print() and debugPrint() are useful for quick debugging, but they have some downsides:

developer.log

In addition to print() and debugPrint(), Flutter provides a more extensive built-in logging mechanism that allows you to log messages - developer.log.

The developer.log provides more input options, such as:

For example:

import 'dart:developer' as developer;

developer.log(
    'Whatever you want to log',
    time: DateTime.now(),
    level: 1000,
    name: 'my.app.logger',
    error: Exception('An error occurred'),
    stackTrace: StackTrace.current, // returns the current stack trace as from catch(error, stackTrace)
);

Afterwards, in devtools, you can filter the logs by name or level, which makes it easier to find relevant information.

For devtools in VSCode, just hit F1 and start typing open devtools.

This already gives you a lot more flexibility than print() or debugPrint(), but still we miss the ability to log to a file or have multiple loggers with different configurations.

📧 Get more content like this in your inbox

Using a Logging Package

This is not coincidental, but the Dart team provides us with this package, which has analogous functionality to developer.log, but with more features and flexibility.

You can see even in the documentation that the developer.log is designed to mimic the logging package, so the migration is easy.

Go to the logging package pub.dev and feel free to add it to your yaml.

dependencies:
  logging: ^1.3.0

Suddenly, you have a lot more options to log messages.

You can create a logger instance:

import 'package:logging/logging.dart';

final log = Logger('my.app.logger');

void main() {
    log.info('Info');
    log.warning('Warning');
    log.severe('Error');
}

You can set up different log levels and also listen to the changes:

Logger.root.level = Level.WARNING; // Set the root logger level to WARNING
Logger.root.onLevelChanged.listen((level) {
    // Handle level changes, e.g., update UI or save to file
});

But you need to add handlers to log messages to different outputs, such as console, files, or even remote servers:

void main() {
    Logger.root.level = Level.ALL; // allow all log levels
    Logger.root.onRecord.listen((record) {
        // handle log records, e.g.
    });
}

The package, however, does not provide any way to do anything with the logs, so you have to implement your own handlers.

You can use the prints, File, sending them to a server, native loggers, etc. Here, some people are stuck with print() or debugPrint(), but you can also use the developer.log function to log messages.

But the implementation can be better.

In an advanced situation, you can send logs, for example, to a remote server or cache them to a file, but this is mostly done in advanced applications and enterprise applications.

For more basic usage, you probably want to implement Sentry or Firebase Crashlytics, which will handle the logs for you.

Logger Package

The logging package is a great start, but what if we want even more features?

The logger package is a popular choice which brings in more features, but it is not maintained by the Dart team.

You can find it on pub.dev.

dependencies:
  logger: ^2.6.1

This package provides a lot of the same features, but it provides them out of the box, so you do not need to be bothered with the implementation:

import 'package:logger/logger.dart';

final logger = Logger(
    level: Level.debug, // Set the minimum log level
    filter: ProductionFilter(), // Filter out debug logs in production
    output: FileOutput(File('logs.txt')), // Print logs to a file or ConsoleOutput() is default
    printer: PrettyPrinter(
        stackTraceBeginIndex: 0,
        methodCount: 2,
        errorMethodCount: 8,
        lineLength: 120,
        colors: true,
        printEmojis: true,
        dateTimeFormat: DateTimeFormat.none,
        excludeBox: const {},
        noBoxingByDefault: false,
        excludePaths: const [],
        levelColors: null,
        levelEmojis: null,
    ), // Format logs for readability
);

void main() {
    logger.i('Info');
    logger.w('Warning');
    logger.e('Error');
}

If you do not want to be bothered by the customizations, you can use the default Logger() constructor, which will use the default settings, and it works just fine for most cases:

// for debugging this is fine
final logger = Logger();

// for production, you want to log less
final logger = Logger(
    level: Level.warning,
    filter: ProductionFilter()
);

As you can see, the logger takes in a lot of formatters, outputs and filters. You can create your own or use the built-in ones.

Final Thoughts

Here are a couple of tips to make your logging more appropriate:

What I do personally, I always have only one logger instance in my application as a mixin:

import 'package:logger/logger.dart';
import 'package:get_it/get_it.dart';

/// somewhere in your dependency injection setup:
GetIt.I.registerSingleton<Logger>(Logger());

mixin class LoggerMixin {
  @protected
  Logger get l {
    try {
      return GetIt.I.get<Logger>();
    } catch (e) {
      return Logger();
    }
  }
}

The reason is simple: I want to have only one logger instance in my application,and I want to be able to use it everywhere.

If I change the logger implementation, all the code will use the same changes. No fuss with multiple loggers.

Then I can use it like this:

class MyClass with LoggerMixin {
  void myMethod() {
    l.i('Info log');
    l.w('Warning log');
    l.e('Error log');
  }
}

Usage of a different logger is also possible, but in most cases, you will not need it. If so, just create another logger instance with a different name or level.

Socials

Thanks for reading this article!

For more content like this, follow me here or on X or LinkedIn.

Subscribe for more
LinkedIn GitHub Medium X