Logging might not seem like a glamorous topic when building apps, but as a developer, I can tell you it's one of the most essential practices for maintaining healthy and maintainable code. Over the years, I've learned that professional logging is more than just printing errors in the console; it's about creating a streamlined, efficient process that not only helps you debug quickly but also improves the overall quality of your app.
In this blog post, I'll walk you through how I handle logging in my Flutter projects—ranging from using a structured logging approach to ensuring it doesn't slow down your app in production. If you're looking to improve your logging strategy and make your codebase more robust, this post is for you!
Why Logging Matters in Flutter Development
As developers, we all know how frustrating it can be to try to track down bugs and issues. Logging provides valuable insights into what’s happening behind the scenes, especially when something goes wrong. Whether you’re working on a small app or a large-scale project, effective logging can save you hours of troubleshooting and debugging time.
Here’s why I consider logging essential in my projects:
- Quick Debugging: When something goes wrong, logs help me pinpoint exactly where and why it happened.
- Better Monitoring: In production, I can monitor app health, track crashes, and get real-time insights into how users are interacting with the app.
- Efficient Troubleshooting: It makes it easier to fix issues without waiting for users to report them.
Step 1: Choose the Right Logging Package
In Flutter, I recommend avoiding simple print
statements. While print
can be useful during development, it lacks the power and flexibility needed for scalable apps. For a more professional approach, I use the logger
package. It offers structured, readable logs with options for different log levels (info, debug, warning, error) and the ability to customize the output.
Here’s how I set it up:
import 'package:logger/logger.dart';
class AppLogger {
static final Logger _logger = Logger(
printer: PrettyPrinter(
methodCount: 2, // Limit the number of method calls to show in logs
errorMethodCount: 8, // Limit the number of error method calls
lineLength: 120, // Max length of a line
colors: true, // Enable/disable colors in logs
printEmojis: true, // Display emojis in logs
),
);
static void logInfo(String message) {
_logger.i(message);
}
static void logError(String message, [dynamic error]) {
_logger.e(message, error);
}
static void logWarning(String message) {
_logger.w(message);
}
static void logDebug(String message) {
_logger.d(message);
}
}
With this setup, I can log messages with different levels, such as i
for info, w
for warnings, and e
for errors. It's simple yet powerful, and it really helps me stay on top of issues.
Step 2: Adjust Logging Based on Environment
One of the most important things I’ve learned is that logging behavior should change depending on the environment (development vs. production). In production, I want to limit the amount of logging to avoid performance overhead and to prevent leaking sensitive information.
Here’s a trick I use to adjust logging levels:
class AppLogger {
static final Logger _logger = Logger(
level: kReleaseMode ? Level.warning : Level.debug, // Adjust log level for production
printer: PrettyPrinter(),
);
}
In this case, in a production environment (kReleaseMode
), I only log warnings and errors, whereas in development, I allow for more detailed logs like debug messages. This ensures that my logs stay relevant and performant across all environments.
Step 3: Use Structured Logging
Structured logging makes it easier to understand the context of the log entry. Instead of just logging a message, I include relevant data, such as user IDs, session IDs, or error details, which can be immensely helpful when tracking issues in production.
class AppLogger {
static void logError(String message, dynamic error, {Map<String, dynamic>? extraData}) {
_logger.e(message, error, extraData);
}
static void logInfo(String message, {Map<String, dynamic>? extraData}) {
_logger.i(message, extraData);
}
}
By passing additional metadata like extraData
in your logs, you can capture more context around the issue. For example, if an error occurs while processing a user’s request, you might include the user’s ID or device information in the log.
Step 4: Capture Unhandled Errors and Exceptions
In production, I want to ensure that my app is as stable as possible. That's why I use global error handling for uncaught exceptions and Flutter errors. If an unhandled error occurs, I can automatically send it to an external service like Firebase Crashlytics or Sentry for better tracking.
void main() {
FlutterError.onError = (FlutterErrorDetails details) {
FirebaseCrashlytics.instance.recordFlutterError(details);
};
runApp(MyApp());
}
By doing this, I capture all unhandled exceptions and send them to a crash reporting service, allowing me to monitor crashes in real-time and react quickly.
Step 5: Log to External Services
For production apps, logging to an external service like Firebase Crashlytics, Sentry, or Loggly is a game changer. These tools provide powerful features like crash reporting, log aggregation, and even performance monitoring.
Here’s a simple integration with Firebase Crashlytics for error logging:
class AppLogger {
static void logError(String message, dynamic error) {
FirebaseCrashlytics.instance.recordError(error, StackTrace.current);
_logger.e(message, error);
}
}
By sending logs to Firebase Crashlytics, I can monitor the app in real-time, track issues, and get a deeper understanding of what’s going wrong in production.
Step 6: Limit Log Size and Rotate Logs in Production
In production, logging should be efficient, so I make sure to rotate logs and store them in an optimized way. For instance, instead of logging everything to the console, I might store logs locally or remotely for easy analysis.
I also make sure logs are capped or deleted after a certain period to prevent unnecessary storage use.
Conclusion
In my experience as a Flutter developer, logging has been an invaluable tool for improving app stability and boosting productivity. By using structured logging, adjusting the log level based on the environment, and integrating external services, I can keep track of issues, improve debugging, and maintain a more efficient development process.
I hope these tips help you set up your own logging strategy in Flutter. A little effort in setting up logging right can save you tons of time and frustration down the line. Happy coding!