Logger for Flutter: how to log your messages in Flutter

A simple package to log your message with low effort

Bernardo Iribarne
Nerd For Tech
4 min readFeb 26, 2024

--

Photo by Amanda Jones on Unsplash

Logs are useful to know about the state of out App. When we are coding new features, logs are essentials, and they also are important when our App is on a production environment and we want to know about the App state into an specific user.

What should we expect for a Logger?

There are common features that a logger has to have:

  1. Write to the console or to some file or database
  2. Write with some kind of level: info, debug, error
  3. Have a date/time property to know when the message has been written
  4. Be accessible through all the code
  5. Be simple

I have written a package that resolves the common logger features.

About FionaLogger

Fiona is my cat so it is not the logger of the Shrek’s girlfriend. FionaLogger contains a few classes that consider those 5 points listed before.

Let’s see a graphic with the involved classes:

Logger class diagram
  1. FionaLogger: is our logger. It contains methods for write a message with differents levels (v: verbose, w:warning, i:info, d:debug and e:error).
  2. LogData: is our log entity. It has the level, the datetime and the message.
  3. LogDataRepository: determines where the message will be posted/saved. This entity opens the package to be customized. You will be able to extend it and save your message where ever you want.

LogData

import 'package:fiona_logger/src/log/fiona_logger.dart';
import 'package:intl/intl.dart';

class LogData{

String message="";
String level;
int datetime=DateTime.now().toUtc().millisecondsSinceEpoch;
int id=0;

LogData(this.message, {this.id=0, required this.level});

LogData.fromMap(Map<String, dynamic> item):level=Level.verbose.toString() {
id=item["id"];
message=item["message"];
datetime=item["datetime"];
}

Map<String, Object> toMap(){
return {'id': id, 'message': message, 'datetime': datetime};
}

String getFormattedDateTime(){
var date = DateTime.fromMillisecondsSinceEpoch(datetime);
return DateFormat('yyyy-MM-dd HH:mm:ss').format(date);
}
}

LogDataRepository


import 'package:fiona_logger/src/domain/log_data.dart';

abstract class LogDataRepository {

Future<List<LogData>> getAll();

Future<void> add(LogData logData);

Future<void> clear();

}

FionaLogger

import 'package:fiona_logger/src/domain/log_data.dart';
import 'package:fiona_logger/src/domain/log_data_repository.dart';
import 'package:get_it/get_it.dart';

enum Level { verbose, debug, info, warning, error }

class FionaLogger {
Level level;
late LogDataRepository logDataRepository;

FionaLogger({this.level = Level.verbose}) {
logDataRepository = GetIt.instance<LogDataRepository>();
}

Future<List<LogData>> getAll() async {
return logDataRepository.getAll();
}

Future<void> clear() async {
logDataRepository.clear();
}

void log(LogData log, Level level) {
int levelIndex = Level.values.indexOf(level);
int logLevelIndex = Level.values.indexOf(this.level);

if (logLevelIndex <= levelIndex) {
_writeLog(log);
}
}

void _writeLog(LogData log) {
logDataRepository.add(log);
}

void v(String message) {
LogData data = LogData(message, level: Level.verbose.toString());
data.datetime = DateTime.now().toUtc().millisecondsSinceEpoch;
log(data, Level.verbose);
}

void w(String message) {
LogData data = LogData(message, level: Level.warning.toString());
data.datetime = DateTime.now().toUtc().millisecondsSinceEpoch;
log(data, Level.warning);
}

void i(String message) {
LogData data = LogData(message, level: Level.info.toString());
data.datetime = DateTime.now().toUtc().millisecondsSinceEpoch;
log(data, Level.info);
}

void d(String message) {
LogData data = LogData(message, level: Level.debug.toString());
data.datetime = DateTime.now().toUtc().millisecondsSinceEpoch;
log(data, Level.debug);
}

void e(String message) {
LogData data = LogData(message, level: Level.error.toString());
data.datetime = DateTime.now().toUtc().millisecondsSinceEpoch;
log(data, Level.error);
}
}

The package includes LogInConsole repository and as you can imagine, it sends the message to the console.

LogInConsole

import 'package:fiona_logger/src/domain/log_data.dart';
import 'package:fiona_logger/src/domain/log_data_repository.dart';
import 'package:flutter/foundation.dart';

class LogInConsole implements LogDataRepository {
List<LogData> logs = List.empty(growable: true);

@override
Future<void> clear() async {
logs = List.empty(growable: true);
}

@override
Future<void> add(LogData logData) async {
logs.add(logData);
debugPrint(logData.message);
}

@override
Future<List<LogData>> getAll() async {
return logs;
}
}

Then you will be able to write LogInFile, LogInDatabase, LogToView, whatever you want, whitout limitations.

Use the data repository you want

As you can see FionaLogger uses GetIt to get the instance of the LogDataRepository.

By default, you can use LogInConsole:

GetIt.instance.registerLazySingleton<LogDataRepository>(() => LogInConsole());

But you will be able to set the LogDataRepository you want to create.

Log to SQLite

For example, I have a project where I use this package and I created a LogDataRepository to use SQLite:


class LogDataSQLite implements LogDataRepository{

SQLiteDatabase _sqlite = DependencyManager().get<SQLiteDatabase>();


@override
Future<void> clear() async {
final Database db = await _sqlite.initializeDB();
try {
await db.delete(SQLiteScheme.logData_tableName);
} catch (err) {

}
}

@override
Future<void> add(LogData logData) async {
final Database db = await _sqlite.initializeDB();
await db.insert(
SQLiteScheme.logData_tableName, logData.toMap(),
conflictAlgorithm: ConflictAlgorithm.replace);
}

@override
Future<List<LogData>> getAll() async {
final Database db = await _sqlite.initializeDB();
final List<Map<String, Object?>> queryResult =
await db.query(SQLiteScheme.logData_tableName, orderBy: 'datetime DESC');
return queryResult.map((e) => LogData.fromMap(e)).toList();
}

Then into my project startup settings , I inject that dependency for the data repository:

GetIt.instance.registerLazySingleton<LogDataRepository>(() => LogDataSQLite());

Conclusion

  1. Every project needs a Logger.
  2. The console log is useful when you are testing with simulators.
  3. The sqlite log is awesome when you are using real device for testing or production. The user will be able to send you the log by email or by the way you define using the device sqlite database.

Thanks for reading, Clap if you like it!

Photo by Wil Stewart on Unsplash

Let me know your comments below.

--

--

Bernardo Iribarne
Nerd For Tech

Passionate about design patterns, frameworks and well practices. Becoming a Flutter expert