A Flutter file logger (for iOS and Android simulators)

If you need a Flutter file logger for debugging apps on iOS and Android simulators, I just came up with the following approach, which seems to work well. But before I jump into the solution I need to offer a wee bit of background and a few caveats.

Note: I do mean file logger. I found some other Flutter logging solutions, but they all seem to rely on logging to a database, and I wanted a real file logger. While the current approach is extremely basic, it does write to a log file, so I think it can be the start of something better.

Background: My problem

I ran into a Flutter bug that I can only reproduce by following these steps:

  1. Start my app in a simulator
  2. Enable notifications in the app
  3. Quit the app
  4. Tap a notification when it comes up
  5. This restarts the app
  6. Bugs start happening ...

Because I have to stop my app to generate the bugs, this means that the flutter run command I issue at the terminal command line stops at that point, so I can’t see any more debugPrint output after that.

Background: My initial solution attempts

When I looked for Flutter file logging solutions, all I found were loggers that sent log messages to a database. That isn’t what I want, so I started writing my own file-logging solution. The first problem I ran into is synchronization problems when different futures are trying to write to the log file at once, which left me with partially-written lines and gobbled text in my log file.

I then tried to implement a solution with Isolates — figured that all the futures could send messages to an Isolate, and the Isolate would write the file synchronously — but by googling the error messages I kept running into, it looks like Isolates and Flutter don’t play well together (at least not yet).

After those attempts I decided to look into making a logging method synchronized, and that’s the approach I use in the solution below.

Caveats

A few caveats about this solution:

First, I’m only concerned with logging output to a text file on iOS and Android simulators, so I haven’t put any work into “packaging” this solution yet.

Second, because I haven’t put in that work, you’ll probably want to comment-out your logging calls when you send your app into production. Either that, or adjust the code so it doesn’t write to a file.

The next caveat is that I haven’t tested this code on Android yet. As shown below, it seems to be working on iOS.

The final caveat is that I just created this on November 13, 2019, and I’m only using it on one project, so who knows what kind of bugs it may have.

One dependency:

My solution depends on the Flutter synchronized project, so you need to add this to your pubspec.yaml file:

synchronized: 2.1.0+1

Solution: My source code

After that, it turns out that the actual file-logging source code needed isn’t too hard. It’s just this Dart source code, that I put in a file named file_logger.dart:

import 'dart:io';
import 'dart:core';
import 'package:synchronized/synchronized.dart';

class Lager {

    static final _lock = Lock();  // uses the “synchronized” package
    static File _logFile;

    static Future initializeLogging(String canonicalLogFileName) async {
        _logFile = _createLogFile(canonicalLogFileName);
        final text = '${new DateTime.now()}: LOGGING STARTED\n';
        /// per its documentation, `writeAsString` “Opens the file, writes 
        /// the string in the given encoding, and closes the file”
        return _logFile.writeAsString(text, mode: FileMode.write, flush: true);
    }

    static Future log(String s) async {
        final text = '${new DateTime.now()}: $s\n';
        return _lock.synchronized(() async {
            await _logFile.writeAsString(text, mode: FileMode.append, flush: true);
        });
    }

    static File _createLogFile(canonicalLogFileName) => File(canonicalLogFileName);

}

I originally named my class FileLogger, but as I get ready to go to production with my app I can now see that this code would be better if it’s a little more general, meaning:

  • I can disable logging when I go into Production on the iOS App Store and Google Play Store
  • I can change the configuration to log to the console instead of to a file

Therefore, I changed the name from FileLogger to Lager.

Using my Flutter logger

After that, using my Flutter logger is simple. Just do some initial setup work in your Flutter app’s main method:

Future<String> _getDocsDir() async {
    final directory = await getApplicationDocumentsDirectory();
    return directory.path;
}

var _logFilename = 'back_to_now.txt';

void main() async {
    var docsDir = await _getDocsDir();
    String canonFilename = '$docsDir/$_logFilename';
    await Lager.initializeLogging(canonFilename);
    await Lager.log('ENTERED main() ...');

    runApp(new MyApp());
}

Then, wherever you want to write something to the log file, just write a line of code like this:

await Lager.log('this is my log message');

I’m currently doing that from several different Flutter widgets I’ve created, and this approach seems to be working well.

The result

The result of this approach is that you’ll build a text-based log file on your iOS or Android simulator. For example, here are the first twenty lines from my current logfile:

2019-11-13 19:31:14.520972: LOGGING STARTED
2019-11-13 19:31:14.535953: ENTERED main() ...
2019-11-13 19:31:14.632513: ENTERED MyApp::build ...
2019-11-13 19:31:14.788621: ENTERED MyHomePage::createState ...
2019-11-13 19:31:14.789076: ENTERED MyHomePageState constructor ...
2019-11-13 19:31:14.790526: ENTERED MyHomePageState::initState
2019-11-13 19:31:14.793420: ENTERED MyHomePageState::setUpFutureNotifications ...
2019-11-13 19:31:14.804923: ENTERED MyHomePageState::build ...
2019-11-13 19:31:26.233814: ENTERED MyHomePageState::didChangeAppLifecycleState ...
2019-11-13 19:31:26.234337:     state = AppLifecycleState.inactive
2019-11-13 19:31:27.005119: ENTERED MyHomePageState::didChangeAppLifecycleState ...
2019-11-13 19:31:27.005401:     state = AppLifecycleState.resumed
2019-11-13 19:31:27.007917: ENTERED MyHomePageState::build ...
2019-11-13 19:31:27.113310: ENTERED CurrentQuoteWidget constructor ...
2019-11-13 19:31:27.113989: CurrentQuoteWidget(), Case #1
2019-11-13 19:31:27.114520: CurrentQuoteWidget() -- user came here without a tap
2019-11-13 19:31:27.116909: ENTERED CurrentQuoteWidgetState::initState ...
2019-11-13 19:31:27.117530: getRandomQuote(), currentQuoteId = null
2019-11-13 19:31:27.118164: _reallyGetRandomQuote ...
2019-11-13 19:31:27.118742: ENTERED CurrentQuoteWidgetState::build ...

much more output here ...

As that output shows, none of the lines in the logfile are getting truncated or garbled, and everything is printing in order.

The location of the logfile on iOS

I need to update this article with a brief description of where your logfile gets written to, but this image will at least give you a hint of where to find the log file on an iOS simulator:

The directory/location of my log file on an iOS simulator

Per this SO post, since iOS 8 you can point your Mac Finder (or Terminal) to this location to find any files your app is writing:

~/Library/Developer/CoreSimulator/Devices

As shown in my image, the file I write is actually well below that top-level directory. The way I found my file was:

  • Go to that root directory
  • Search beneath that directory for my filename (back_to_now.txt)

Android configuration

I haven’t tested this code on Android yet, but if you want to try it on Android, you’ll probably need to add these configuration/permission lines to your AndroidManifest.xml file:

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>

I’ll update this article after I’ve tested this on Android.

Summary

As a brief summary, if you need a simple file-logging solution when creating and debugging a Flutter app on an iOS (and hopefully Android) simulator, I can say that this approach seems to be working. I just got it working today so it’s still a little crude, but it works.