Do Not Use debugPrint In Flutter
Google has been caught using debugPrint in a library, here is why you should not do that.
Found Google using debugPrint in a production library, so now I have to explain why we never ever should do that in a library or a production flutter apps.
Fact of job hunting for front-end designers, code terms get the recruiter's attention, but actual design gets you hired. Does it make sense to read Flutter GDE content where they are asking to use legacy UseCase, Repository, and Datasource Class abstraction when no Flutter app designer uses that crap anymore?
I source top design tricks and techniques from the top presenters at Google IO such as GSkinner, VeryGoodVentures, RydMike, and many others to make sure you have the top flutter app design tips and tricks to make your flutter app design awesome. Because, design and UX is the damn point!
Why not join Miguel Angel Toscan, Roman Roan, etc. in sharing this post?
Oh Google What Have You Done
Here is what I found in the Dynamic Color package:
import 'package:flutter/material.dart'; | |
import 'package:flutter/services.dart'; | |
import 'package:material_color_utilities/material_color_utilities.dart'; | |
import 'corepalette_to_colorscheme.dart'; | |
import 'dynamic_color_plugin.dart'; | |
/// A stateful builder widget that provides a light and dark [ColorScheme]. | |
/// | |
/// Android: the [ColorScheme]s are constructed from the [CorePalette] provided | |
/// by the Android OS. | |
/// | |
/// macOS, Windows and Linux: the [ColorScheme]s are constructed from the accent | |
/// [Color] provided by the system. | |
/// | |
/// See also: | |
/// | |
/// * [DynamicColorBuilder example](https://github.com/material-foundation/flutter-packages/tree/main/packages/dynamic_color/example/lib/dynamic_color_builder_example.dart) | |
/// * [Complete example](https://github.com/material-foundation/flutter-packages/tree/main/packages/dynamic_color/example/lib/complete_example.dart) | |
/// for obtaining dynamic colors and creating a harmonized color scheme | |
/// * [DynamicColorPlugin.getCorePalette] for requesting the [CorePalette] | |
/// directly, asynchronously. | |
/// * [DynamicColorPlugin.getAccentColor] for requesting the accent [Color] | |
/// [ColorScheme] directly, asynchronously. | |
class DynamicColorBuilder extends StatefulWidget { | |
const DynamicColorBuilder({ | |
Key? key, | |
required this.builder, | |
}) : super(key: key); | |
/// Builds the child widget of this widget, providing a light and dark [ColorScheme]. | |
/// | |
/// The [ColorScheme]s will be null if dynamic color is not supported on the | |
/// platform, or if the OS has yet to respond. | |
final Widget Function( | |
ColorScheme? lightDynamic, | |
ColorScheme? darkDynamic, | |
) builder; | |
@override | |
DynamicColorBuilderState createState() => DynamicColorBuilderState(); | |
} | |
class DynamicColorBuilderState extends State<DynamicColorBuilder> { | |
ColorScheme? _light; | |
ColorScheme? _dark; | |
@override | |
void initState() { | |
super.initState(); | |
initPlatformState(); | |
} | |
// Platform messages are asynchronous, so we initialize in an async method. | |
Future<void> initPlatformState() async { | |
// Platform messages may fail, so we use a try/catch PlatformException. | |
try { | |
CorePalette? corePalette = await DynamicColorPlugin.getCorePalette(); | |
// If the widget was removed from the tree while the asynchronous platform | |
// message was in flight, we want to discard the reply rather than calling | |
// setState to update our non-existent appearance. | |
if (!mounted) return; | |
if (corePalette != null) { | |
debugPrint('dynamic_color: Core palette detected.'); | |
setState(() { | |
_light = corePalette.toColorScheme(); | |
_dark = corePalette.toColorScheme(brightness: Brightness.dark); | |
}); | |
return; | |
} | |
} on PlatformException { | |
debugPrint('dynamic_color: Failed to obtain core palette.'); | |
} | |
try { | |
final Color? accentColor = await DynamicColorPlugin.getAccentColor(); | |
// Likewise above. | |
if (!mounted) return; | |
if (accentColor != null) { | |
debugPrint('dynamic_color: Accent color detected.'); | |
setState(() { | |
_light = ColorScheme.fromSeed( | |
seedColor: accentColor, | |
brightness: Brightness.light, | |
); | |
_dark = ColorScheme.fromSeed( | |
seedColor: accentColor, | |
brightness: Brightness.dark, | |
); | |
}); | |
return; | |
} | |
} on PlatformException { | |
debugPrint('dynamic_color: Failed to obtain accent color.'); | |
} | |
debugPrint('dynamic_color: Dynamic color not detected on this device.'); | |
} | |
@override | |
Widget build(BuildContext context) { | |
return widget.builder(_light, _dark); | |
} | |
} |
It is of course wrong to use either print or debugPrint. Let me show you why, here is how print is implemented:
void print(Object? object) { | |
String line = "$object"; | |
var toZone = printToZone; | |
if (toZone == null) { | |
printToConsole(line); | |
} else { | |
toZone(line); | |
} | |
} |
Why is this wrong? Because it forces the end developer to sanitize each line in a print statement. And what is worse, in production ready packages we have no control over whether the package author has properly sanitized the input that now appears in the console.
And debugPrint is named wrong, it's not debug mode. Instead, this is what it does:
DebugPrintCallback debugPrint = debugPrintThrottled; | |
typedef DebugPrintCallback = void Function(String? message, { int? wrapWidth }); | |
void debugPrintThrottled(String? message, { int? wrapWidth }) { | |
final List<String> messageLines = message?.split('\n') ?? <String>['null']; | |
if (wrapWidth != null) { | |
_debugPrintBuffer.addAll(messageLines.expand<String>((String line) => debugWordWrap(line, wrapWidth))); | |
} else { | |
_debugPrintBuffer.addAll(messageLines); | |
} | |
if (!_debugPrintScheduled) { | |
_debugPrintTask(); | |
} |
In only throttles the printing to console and modifies the width.
Now, let me show you what to use instead.
How To Log
Log simply means emit an event:
external void log( | |
String message, { | |
DateTime? time, | |
int? sequenceNumber, | |
int level = 0, | |
String name = '', | |
Zone? zone, | |
Object? error, | |
StackTrace? stackTrace, | |
}); |
Notice the function is not returning anything. How does the emitting of the log event happen?
Or in simple terns, will the log by itself emit events? No, it has to be configured by the end developer of where and when those log events should be emitted.
What is happening is that the Logging package maps the log emit events to the DevTools LoggingView or in the Web platform case to the Web the DartDev console.
That means app logging wise, the user would not see the logs of the app unless you made an act to show them to the app user. On the web platform, we can only see those logs from the DartDev console.
Now, here is where I take issue with Google, as the Logging package directions and example are still wrong. You do not effing use the Dart print function with the logging package! It's damn it 2024, fix the damn docs already! So let me show you a correct example of configuring the logging package:
// Copyright 2023 Fredrick Allan Grott. All rights reserved. | |
// Use of this source code is governed by a BSD-style | |
// license that can be found in the LICENSE file. | |
// ignore_for_file: unused_local_variable | |
import 'dart:developer'; | |
import 'package:logging/logging.dart'; | |
import 'package:logging_appenders/logging_appenders.dart'; | |
/// The app Global logger. | |
final appLogger = Logger("LoggingDemo"); | |
/// AppLogging class initializes the logging set up at object creation. | |
/// To call it is just AppLogging() within the main function block. | |
/// | |
/// Note, this an obseverable log that operates in all modes including | |
/// release which means we do not use print but instead use log to output. | |
/// | |
/// @author Fredrick Allan Grott. | |
class AppLogging { | |
factory AppLogging() { | |
return _appLogging; | |
} | |
AppLogging._internal() { | |
_init(); | |
} | |
static final AppLogging _appLogging = AppLogging._internal(); | |
void _init() { | |
// disable hierarchical logger | |
hierarchicalLoggingEnabled = false; | |
Logger.root.level = Level.ALL; | |
// stack traces are expensive so we turn this on for | |
// severe and above | |
recordStackTraceAtLevel = Level.SEVERE; | |
// just so during a log level change that we know about it. | |
// log is used instead of print as this is for observable | |
// logging even in release mode | |
Logger.root.onLevelChanged.listen((event) { | |
log('${event?.name} changed'); | |
}); | |
// now to get the log output | |
Logger.root.onRecord.listen((record) { | |
if (record.error != null && record.stackTrace != null) { | |
log( | |
'${record.level.name}: ${record.loggerName}: ${record.time}: ${record.message}: ${record.error}: ${record.stackTrace}', | |
); | |
log( | |
'level: ${record.level.name} loggerName: ${record.loggerName} time: ${record.time} message: ${record.message} error: ${record.error} exception: ${record.stackTrace}', | |
); | |
} else if (record.error != null) { | |
log( | |
'level: ${record.level.name} loggerName: ${record.loggerName} time: ${record.time} message: ${record.message} error: ${record.error}', | |
); | |
} else { | |
log( | |
'level: ${record.level.name} loggerName: ${record.loggerName} time: ${record.time} message: ${record.message}', | |
); | |
} | |
}); | |
// Appenders set up for color logs | |
PrintAppender(formatter: const ColorFormatter()).attachToLogger(Logger.root); | |
// Any appendeing logs to 3rd party observable logging services | |
// would be here using the logging appenders other appenders | |
} | |
} |
It’s a singleton that sets up the logging package configuration to log in debug and production modes with using the logging appenders package to print color output to DevTools logging view.
Do not effing ever print to console to server as debugging! It's the first level of how developers security audit someone else's code.
It's not just flutter app design tricks and tips I am curating for this flutter newsletter audience. There is a collection of must-have plugins that make our flutter app designs and UX more powerful in the eyes of our app users.
And, not every plugin is endorsed by Google, like go-router. I am talking about using Flex Color Scheme to better customize the Color Scheme generation and generate the component themes as just one branding empowerment example. It's all about improving the UX and design while at the same time giving them the device-platform experience they are accustomed to on their device.
Why not join Roman Roan, Miguel Angel Toscan, etc. in subscribing today.