How to setup a logging system in Flutter with Talker
One of the most important things we should do when we release an application, no matter what the platform target is, is to keep track of what’s going on when a user interacts with it. The easiest way to acknowledge this is by using a logging system.
In the past few years, I created my custom logging systems by combining multiple different tools to log information in Flutter such as logger and Firebase Crashlytics. The main drawback of using it was strictly related to the hard coupling between the implementation and Firebase Crashlytics: if my application, for any reason, wasn’t meant to include Firebase, I had to change the implementation of the logging system.
Looking for something different
Logger is a fantastic library, but I find it a little bit oldish and tricky when it comes to plugging things. For instance, if I want to intercept network calls and use Logger as a pipe, strings are not printed out in a fancy way and it becomes difficult to understand what’s going on.
I started looking for new libraries on pub.dev and I found Talker, which by the way integrates pretty well with my Flutter stack: it supports a custom logger for Bloc and Dio, and adding Firebase crashlytics is easy peasy. To be honest, I’m really surprised by how this library is not so well known in the Flutter community.
How to integrate it
Let’s get to the point: first of all, we need to add Talker as a flutter dependency:
flutter pub add talker
If you’re following the Pine architecture as I do, we should inject the Talker instance in the widget tree, let’s open the providers.dart file and let’s inject it:
Provider<Talker>(
create: (context) => Talker(),
),
Now that we’ve properly injected the Talker instance in the tree, we can use it in our services, repositories, blocs, and so on. For instance, I created this repository to obtain news from an RSS feed:
class NewsRepository {
final RSSFeed feed;
final Talker logger;
const NewsRepository({
required this.feed,
required this.logger,
});
Future<AtomFeed> get news async {
String response;
try {
logger.info(
'[NewsRepository] Getting news from API',
);
response = await feed.rss();
logger.good(
'[NewsRepository] Got news from API',
);
} catch (error, stackTrace) {
logger.error(
'Thrown exception during news request',
error,
stackTrace,
);
throw RepositoryError(error: error);
}
return AtomFeed.parse(response);
}
}
As you can see, this repository has two dependencies: an RSSFeed instance named feed, which is a service, and a Talker instance known as logger. This class contains a single method that calls the RSS method from the feed service and logs the information of what’s going on.
Before invoking feed.rss() we can spot
logger.info(
'[NewsRepository] Getting news from API',
);
this allows us to log information at the info level. We’re basically telling “Okay, I’m about to call the RSS service”.
If you don’t know what logging levels are, please take a look at this very well-explained document.
Everything is embraced into a try/catch block: if everything went well, we can use the .good() method from the Talker library to express success, this will print out a green text surrounded by a frame.
logger.good(
'[NewsRepository] Got news from API',
);
If something goes south, the code execution will enter the catch block and before handling the exception, we can log the error using of course the .error() method: this will print out a red text surrounded by a frame. We can provide more information when logging the error by passing both the error and the stack trace variables.
logger.error(
'Thrown exception during news request',
error,
stackTrace,
);
We can also use many different methods to log the behavior of our application. In the following image, we can spot the .debug() and the .warning() outputs which respectively print out a white text and an orange text surrounded by a box.
Send the information to Crashlytics
To send the information to Firebase Crashlytics, we need to create and register an observer. We can register a single observer and I hope the maintainers of the library will find a way to allow us to register multiple ones, but as for now, let’s create a new class to log events to crashlytics.
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
import 'package:talker/talker.dart';
class CrashlitycsTalkerObserver extends TalkerObserver {
final FirebaseCrashlytics crashlytics;
const CrashlitycsTalkerObserver({required this.crashlytics,});
@override
void onLog(TalkerDataInterface log) {
crashlytics.log(log.generateTextMessage());
}
@override
void onError(err) {
crashlytics.recordError(
err.error,
err.stackTrace,
reason: err.message,
);
}
@override
void onException(err) {
crashlytics.recordError(
err.exception,
err.stackTrace,
reason: err.message,
);
}
}
First of all, we should create a class that extends a TalkerObserver. It’s not mandatory to override all the methods in the abstract class, we can decide what kind of error we want to send to crashlytics. In my case, I prefer to send everything to better understand what’s happening during the app’s execution flow.
The onLog method will be triggered every time we call .warning(), .debug(), .info(), and .good(). In case of an error or an exception, onError and onException will be called instead.
Now that we have our observer, we need to register it, so let’s go back to the providers.dart file and let’s add it when creating our Talker instance.
Provider<Talker>(
create: (context) => Talker(
observer: CrashlitycsTalkerObserver(
crashlytics: context.read(),
),
),
),
Do not forget to also inject a FirebaseCrashlytics instance in the tree, otherwise, Provider won’t be able to locate the crashlytics service needed by the Observer.
And that’s it, every time we’ll log something with Talker, everything will be also sent to our Firebase Crashlytics console.
Talker third-party integrations
If you also want to integrate a Dio logger and a Bloc logger, simply run
flutter pub add talker_dio_logger
flutter pub add talker_bloc_logger
To add a Dio logger to our Dio instance, simply add a TalkerDioLogger instance in the Dio interceptors. If you’re following the Pine architecture, you can do it in the providers.dart file as follows:
Provider<Dio>(
create: (_) {
final dio = Dio(BaseOptions());
if (kDebugMode) {
dio.interceptors.add(
TalkerDioLogger(
settings: const TalkerDioLoggerSettings(
printRequestHeaders: true,
printResponseHeaders: true,
printResponseMessage: true,
),
),
);
}
return dio;
},
),
In this case, I’m registering the interceptor when we’re running the application in debug mode. For security reasons, I don’t want to log Dio calls when the app is in production.
To register a Bloc logger, simply add this instruction in your main.dart file before executing runApp():
Bloc.observer = TalkerBlocObserver();
And we’re good to go. We finally have control of what’s happening in our application when a user interacts with it.