Gonçalo Palma
March 17, 2019

debugPrint and the power of hiding and customizing your logs in Dart

When developing and debugging software, we usually log some kind of messages to assert its behaviour. In Dart, we can do this using the print :

flutter: Initializing the build method
flutter: UserId: user908976

This output doesn’t provide us with much information. Besides the printed message and a flutter label, we don’t have an indication of the app name or a timestamp of when the message was printed.

Furthermore, if you use the command flutter logs you will see the output of the print function in all applications you have installed in the phone/emulator. This means that even if the app is in release mode, it will still be printed in the terminal.

This poses two problems:

Let’s tackle the first problem.

Hiding Print Statements in a Production App

In our apps, we can use the concept of Flavors, which can be used to create different version of our application. This can be useful if our API uses different environments for development and production or if we want to specify a different behaviour for our app, as for example hiding the logs in the production app.

To be able to modify the behaviour of our print function, we can take a look at the debugPrint method from the foundation library. From the documentation we can read:

The implementation of this function can be replaced by setting the debugPrint variable to a new implementation that matches the DebugPrintCallback signature. For example, flutter_test does this.

The default value is debugPrintThrottled. For a version that acts identically but does not throttle, use debugPrintSynchronously.

So, by overriding the DebugPrintCallback we can change the behaviour of our printed statements. Before changing this implementation, we need to make sure that every printstatement in our project gets replaced by a debugPrint statement

So, assuming we are using different main.dart files: main.dart for production and main_dev.dart for development, we can set a specific callback for each flavour.

void main() {
  debugPrint = (String message, {int wrapWidth}) {};
  runApp(MyApp()); 
}

Now, when the debugPrint statements in our code are called, they won’t be printed to the console since we gave this function an empty callback.

Creating Custom Print Statements

With this information, we can now look into ways to manipulate the string we use in debugPrint to provide more information when logging. But first, we must look into the documentation of debugPrint again.

The default value is debugPrintThrottled. For a version that acts identically but does not throttle, use debugPrintSynchronously.

As we can see, we have two default callbacks to this function: debugPrintThrottled and debugPrintSynchronously.

Let’s review debugPrintThrottled

Implementation of debugPrint that throttles messages. This avoids dropping messages on platforms that rate-limit their logging (for example, Android).

Though this seems reasonable, we are warned in the debugPrint’s documentation that this method can lead to out-of-order messages in the log if used also with print:

By default, this function very crudely attempts to throttle the rate at which messages are sent to avoid data loss on Android. This means that interleaving calls to this function (directly or indirectly via, e.g., debugDumpRenderTree or debugDumpApp) and to the Dart print method can result in out-of-order messages in the logs.

Alternatively, debugPrintSynchronously has a more straightforward implementation:

Alternative implementation of debugPrint that does not throttle. Used by tests.

So basically, if we want to avoid the printing limit of each OS, we can use debugPrintThrottled, otherwise we can change our implementation to debugPrintSynchronously.

To provide more information about the application being debugged, we can use package_info in order to retrieve the package name, version and build_number. With this, we can now customise our debugPrint statements:

import 'package:flutter/foundation.dart';
import 'package:package_info/package_info.dart';

void main() async {
  var packageInfo = await PackageInfo.fromPlatform();
  var version =
      "${packageInfo.packageName} ${packageInfo.version} (${packageInfo.buildNumber})";
  debugPrint = (String message, {int wrapWidth}) => debugPrintSynchronouslyWithText(message, version, wrapWidth: wrapWidth);
  runApp(MyApp());
}

void debugPrintSynchronouslyWithText(String message, String version,
    {int wrapWidth}) {
  message =
  "[${DateTime.now()} - $version]: $message";
  debugPrintSynchronously(message, wrapWidth: wrapWidth);
}

Which will print each message with the following prefix:

flutter: [2019-03-16 15:35:50.176958 - com.vanethos.logExample 1.0.0 (1)]: Initializing the build method
flutter: [2019-03-16 15:35:50.180908 - com.vanethos.logExample 1.0.0 (1)]: UserId: user908976

And we’re done! 🕺 Now we can fully customise our console logs and hide them from prying eyes.

Want to get the latest articles and news? Subscribe to the newsletter here 👇

Diving Into Flutter

In Diving into Flutter we will discover the Flutter Framework, bit by bit.

From the BuildContext, to pubspec files, going through exotic backend solutions, there won't be a stone left unturned.

Each week I'll also share one or two links for interesting resources I found around the Web

And for other articles, check the rest of the blog! Blog - Gonçalo Palma

Want to get the latest articles and news? Subscribe to Diving Into Flutter

Hey! I'm Gonçalo Palma! 👋

I often share Deep Diving articles on Flutter 💙