Part of the ‘A Work in Progress’ Series
Today, I’m demonstrating how the MVC framework library package is adaptive and flexible in its implementation of specific needs. In this case, I’ll show you how the framework easily incorporates Firebase’s Crashlytics for its error handling. Of course, it has its own error handling routines, but the developer has the means to incorporate their own or third-party options.
A Work In Progress
This is part of the ‘A Work in Progress’ series of articles covering the progress of a simple ‘ToDo’ app called WorkingMemory. The intent of this series is to document the implementation of every and all aspects of this app and its construction as well as its use of the MVC framework library package, mvc_application.
I Like Screenshots. Click For Gists.
As always, I prefer using screenshots over gists to show concepts rather than just show code in my articles. I find them easier to work with, and easier to read. However, you can click/tap on them to see the code in a gist or in Github. Ironically, it’s better to read this article about mobile development on your computer than on your phone. Besides, we program on our computers; not on our phones. For now.
Let’s start from the beginning, and when it comes to Flutter apps, that means starting in the function, main(). You can see the app, WorkingMemory, is being passed to the runApp() function as usual. Note, this app extends the class, App. At startup, the class, App, does a lot of stuff ‘behind the scenes’ to set up and ready the application. It provides the many functions and features common to all Flutter apps. Further, it has the function, createView(), so a can ‘hot reload’ can be performed to run the class, View, again for any reason.
If you’re familiar with my past work, you know I use the MVC design pattern in most of my apps. I’d know the View is the ‘interface’, the Model is the ‘data’, and the Controller is the logic component of the app. If you like, there is the free article, Flutter + MVC at Last!, to introduce you to how MVC implemented in Flutter using the library packages, mvc_application, and mvc_pattern.
A further collection of free articles on the subject are listed in the article, MVC in Flutter. Simply put, when it came to the architectures and design patterns currently offered to the Flutter community, I found MVC met my needs.
Let’s get back to it. Note the import statements in the main.dart file. You’ll see such import statements throughout the source code. With the file, view.dart concerned the app’s interface, and the file, controller.dart, concerned with the logic involved in the app. For example, you can see the class, View, is coming from the file, view.dart. Very reasonable — it’s concerned with the interface. Further, the function, runApp(), is coming from the file, controller.dart. However, that’s not the runApp() function you may be familiar with.
Run Run Run An App
The screenshot below presents the screen of my IDE. That’s so you can get an appreciation of how the code is organized in this app. The Controller, in simple terms, is to ‘control’ what happens when something…happens. As pertaining to this article, when the app starts up, the Firebase Crashlytics routine is to handle and record any and all exceptions that may occur.
If you look closely, the runApp() function is found in the ‘Controller’ file situated at the ‘app level.’ In other words, in the file, controller.dart, found in the directory, app. The code in that directory and in that file is concerned with the app ‘on the whole.’ It’s not concerned, for example, with the home screen for this particular app. The code for the home screen is instead be found in the directory, home. This is all in the attempt to organize the code.
Again, looking closely at the runApp() function, we see, if nothing is passed as parameters, the Firebase Crashlytics routines are assigned to handle and report exceptions. The runApp() function displayed above then calls the runApp() functions displayed below. Yet, it too is not the function you’re familiar with. However, this second runApp() function does eventually call the original runApp() function supplied by Flutter, but not before wrapping your app around an error handler. How this error handler works is what we’re going to examine, today.
In The Zone
We’re going to walk-through the code and examine the series of events that occur when an error is triggered in the app. This error will occur when the runApp() function is running and the app is first starting up. In the screenshot above, you can see the error routine, runZonedError, is highlighted. It’s called when an error occurs at startup.
In the screenshot below, you’ll notice I’ve commented-out the Firebase Crashlytics routines for now. Hence, they won't be used in this instance. Let’s instead see what the ‘default behaviour’ is when an error occurs at startup.
Below, is a screenshot of the error routine, runZonedError. When an exception does occur, it merely passes the generated Exception and StackTrace objects to the routine, _debugReportException(). It is this routine that then produces a ‘FlutterErrorDetails’ object and finally passes it to the Flutter framework’s error handler. Of course, since this app uses the MVC framework, the developer is able to define their own error handler.
To Make An Error
To cause the error, I’ve decided to mess with the very framework I use with all my apps, and change some code to intentionally cause an error — I’ll be sure to correct it after we’re done with this exercise.
Listen To The Connection
In this framework, you can assign ‘listeners’ to be triggered if and when the device’s connectivity status changes for one reason or another. For example, when you turn off the wifi or your phone. The class, App, has such a routine to add a listener, and it’s found right in its constructor. As your recall, the App class is extended and called in the main.dart file. A screenshot of this class and its constructor is displayed below.
I made a quick change to the code to cause an error at startup. The function, addConnectivityListener(), is called every time the Flutter app starts up. In most instances, there won’t be a ‘listener’ specified and so there’s an if statement there to prevent any problems. You see, one can’t assign null as a listener, but I’ve commented out that if statement. The app is not going to like that.
Report The Error
And so, in the screenshot below, we’re back where the app is just about to call Flutter’s own error handler. It had started up but quickly encountered that error while running Flutter’s runApp() function. In the screenshot below, we see the FlutterErrorDetails object was created and, again, is just about to be passed to Flutter’s reportError() function. As you know from your own experiences with Flutter, this usually results in the ‘Red Screen of Death’ or at the very least, the error message and stack trace displayed in your IDE’s console screen.
Let’s that a quick peek inside Flutter’s reportError() function. You can see that it, in turn, calls the routine assigned to the static variable, onError. Note, the reportError() function is called almost every time an exception occurs in your Flutter app.
Dump The Error
By design, the default behavior for the static function, FlutterError.onError, is to call yet another static function, dumpErrorToConsole. A very descriptive name — you’re familiar with this as it lists the error right out on the IDE’s console screen. In fact, below is a screenshot of this original routine.
However, in this framework, it’s the function below that’s called instead Below is a screenshot with a breakpoint stopping the running code. In this MVC framework, error handling is to be as versatile as possible. For example, the onError() function below could have been overridden by the developer to customize the error handling. However, you can see the ‘App Controller’, con, has its own error handler — a developer could have customized that error handler instead. Options.
Let’s now look at that AppController’s error handler. We can see you if you didn’t override it with your own error routine, it turns to it’s associated State object and it’s error handler. At last count, that’s three places in the framework for error handling to be adapted to specific requirements of a Flutter app. Three places the developer or developers could have overridden. Good to have options.
By design, in this framework, each State object can have its own error handler. Each State object can have any number of Controllers associated with it, and it’s standard for each Controller to then rely on the State object’s error handler — and that’s what you’re seeing in this code. At this point, the State object can either have its own error handler defined or will simply use the ‘old’ or ‘default’ error handler. As it happens, in this case, it’s the old static function, dumpErrorToConsole, that’s the default error handler. Let’s see how this all works.
And so, when the instance variable, handler, is executed in the screenshot above, the old static function, dumpErrorToConsole, is indeed called and the error listed in the IDE’s console window as you see below.
However, we want the developer to have options, and in this case, the developer wants to use Firebase Crashlytics. Let’s uncomment those two lines and now allow Firebase Crashlytics to take over any and all exceptions that may occur. And so, Crashlytics is now assigned as the error handler as well as assigned to report any exception that may occur.
In this second round, the instance variable, _reportError, in the MVC framework is now not null this time. It now contains a reference to the function, reportError(), in the Crashlytics class.
Below is a screenshot of that very routine in the Crashlytics class. A breakpoint highlights in blue where I’ve stopped the execution. The two red arrows highlight the two functions that were assigned to handle errors. Note, the first arrow conveys the fact Crashlytics’ error handler calls the standard ‘dump to Console’ if an exception occurs.
Send To Crashlytics
Note, setting a particular property to true while in development will send the error information up to your Firebase console as it would do if the app was running in production. And so, it does this even when you're in development and nothing is now displayed on your IDE’s console. Below is a screenshot with the property, endableInDevMode, now set to true.
There’s not much now displayed in the IDE console. Instead, it announces the error information is sent up into the cloud to your Firebase console.
Mind you, if I had commented out the runApp() routine in this example, the second version of runApp() function (the one that then calls Flutter’s own version) is instead used. And don’t forget, you’re free to impose your own error handling with each StateMVC object you use to display each screen (each View) in your app. Good to have options — especially in your error handling.
Let’s leave it at that. Fork the WorkingMemory app and follow along if you like. Mind you, you’ll have to set up your own Firebase account and database to run it — I’ll help you with that. As things progress, I will be writing supplementary articles about specific aspects of this Flutter app while it uses the MVC design pattern through the library package, mvc_application.