Google introduced Material Design on June 25th, 2014, at that year’s Google I/O conference. It’s a list guidelines essentially describing the appearance displayed as well as the actions performed, as it pertains to a mobile app’s user interface. Of course, the goal of introducing such guidelines was to influence all the platforms Google was involved in at the time and in the future. It was to provide their unique and consistent take on the ‘user experience.’ Their brand of UX.
In any event, the MaterialApp widget is the means to follow the Material Design guidelines in Flutter. The idea is, if you want to make a mobile app in Flutter, you’d first insert the MaterialApp widget at the root of the widget tree to get started. Consequently, your app now uses Material Design. Ta-da!
The MaterialApp class itself has a long list of named parameters conveying its influence and importance in making such apps in Flutter. It’s the MaterialApp widget that defines the theme (the ‘look and feel’) of your app. It’s to the MaterialApp object where you specify the list of ‘routes’ to any and all the separate screens that will make up your app. You can also tell the MaterialApp object, which will be the first screen (the ‘home’ screen) when your app first starts up. It’s to the MaterialApp object where you give a title to your app. Further, it’s to the Material object where you specify what locale to use to for your app. In other words, how to display values that are location-specific. Different countries and regions of the World use different formats for dates for example. They use different ways of presenting their currency and their very text. Of course, even the direction the text is read. Either from ‘left-to-right’ or ‘right-to-left.’
On the more technical side, it’s to the MaterialApp object where the MediaQuery widget is first instantiated. It’s to negotiate with the particular model of phone on how the app will appear and function. The Scaffold widget is also involved with a MaterialApp — designed to provide the very elements of Material Design. The Scaffold widget allows you to present an AppBar class and or a Drawer class as part of the user experience. To provide snack bars and bottom sheets. To use a BottomAppBar class, a TabBar class, a BottomNavigationBar class and or a FloatingActionButton class. All of which is described in the Material Design guidelines in one fashion or another.
The plan for this article is to address the many named parameters found in the MaterialApp class. It’s hoped that'll effectively explain its use. It’s a rather ‘busy’ class, and this article will undoubtedly miss aspects of it, but that’s what ‘follow up’ articles are for. As usual, we will be using specific yet simple examples to illustrate certain functions and features.
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 frankly. However, you can click or tap on these screenshots to see the code they represent 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.
Some Material Things Are Necessary
Having glanced at the screenshot of the MaterialApp class above, you’ve no doubt noticed there are a number of name parameters that must be provided values when instantiating a MaterialApp object. Look again below, and note the little red arrows. Instead of starting from the top (or first parameter), we’ll start our examination of the MaterialApp widget by looking at these required attributes.
As a quick aside, note the MaterialApp widget is a StatefulWidget. That tells you things can change in such an object. It retains a state, but that state is free to change. An important fact to keep in mind with regards to its function.
Ok, we’ll look at the ten of the twenty-five named parameters available to you that must not be null. They must be assigned values when you instantiate a MaterialApp object, and there’s a number of ‘assert’ statements to enforce that fact. If you don’t supply values explicitly, they’re supplied default values. Looking at the parameters for left-to-right (or top to bottom as they’re formatted), the first one we encounter assigned a default value, routes.
What Route To Take?
To ‘move about’ to more than one screen in a Flutter app, the concept of ‘routes’ is utilized. The MaterialApp requires the named parameter, routes, not be null and so it’s assigned, by default, what’s simply an empty Map object. The key apparently is a String object, and its value is of the function-type (see typedef) called, WidgetBuilder.
As always, I like to use established examples. Where better to find such examples but in the ‘Flutter Cookbook.’ Let’s look into routing by examining the Cookbook example, Navigate with named routes.
Your Initial Route
Note, in the example an ‘initial route’ is specified with the named parameter, initialRoute. This will indicate what the first screen will be when the app first starts up.
As it happens, this example is a little misleading, you need not explicitly specify the forward slash, `/` as it’s the default value even if the named parameter, initialRoute, was not provided. And so, commenting it out will bring about the same results. Anyway, for our endeavours, the real interest in this example is the routes parameter. It’s a Map object listing all the screens one can navigate to in this app. Again, the `/` signifies the ‘home screen.’ In this example, the home screen is named, `/`, and the other screen is named, `/second.`
And so, in this example, since an `/` entry was found in the parameter, routes. The class, FirstScreen, is instantiated and presented to the user when the app starts up. Pressing the button labelled, ‘Launch screen’, in the class, FirstScreen, will then use that other named route to get to the second screen.
I’ve changed my mind. Let’s explain this widget another way. While we’re here, let’s look at all seven of the named parameters in the MaterialApp class, from navigatorKey to builder. That’s because they all pertain to Flutter’s navigating mechanism, and all are interrelated in a way. Let’s see how this approach works.
For example, the assigning of a default value to the routes parameter implies, routes need not be specified. And so, what happens then if you don’t provide the parameter, routes, a Map of named WidgetBuilders?? It defaults to an empty Map object, I know, but then what?
There’s No Place Like Home…Sometimes
Now, the app has got to start somewhere. A ‘first screen’ must be specified. As it happens, there’s another ‘Cookbook’ example available to us to illustrate a possible alternative: Navigate to a new screen and back
Admittedly, again, we’re really just interested in a part of the example — in the main() function in this case. It contains the MaterialApp object after all. Now in this example, there’s no ‘routes’ parameter. However, there is the named parameter called, home, being used instead. What does that do? Judging from the name, ‘home’, you’d guess correctly it specifies the first screen to display.
In the documentation, it stipulates that at least one of the following named parameters must be passed to the MaterialApp object and must not be null: home, routes, onGeneratedRoute, or builder. Further, as we’ve already seen, if only ‘routes’ is provided, it must include an entry for the `/` so to present a screen when the app is first launched.
In fact, there is an order to things in this regard. If the parameter, home, is not null, then it provides the ‘home screen’. Simple enough. Otherwise, if the parameter, routes, was provided and there’s a forward slash entry (`/`), then that’s your ‘home screen.’ However, if such an entry doesn’t exist, and no parameter, home, the Flutter framework looks to the fourth of the seven parameters indicated above. The callback function, onGenerateRoute. If provided, it’s called upon to return the first screen. Finally, if the parameter, onGenerateRoute, is not provided and the parameter, builder, is not provided, then the fifth parameter, onUnknownRoute, is called upon — if provided. Note, the parameter, onUnknownRoute, is called if at any time there’s an ‘unknown route’ event in your app. Now, after that, if it’s not there either…well, you’re then typically presented with a blank screen at startup. Ta-da!
Something’s Got To Give
And so, deep in the framework, a String object is assigned the value, name, from the class object, RouteSettings. In most cases, that value will be the forward slash, `/.` As you can see in the screenshot above, if the property, home, is not null it's assigned to the variable, contentPageBuilder, as a class type, WidgetBuilder. If an entry other than the Navigator.defaultRouteName (the forward slash, `/`) is the default home screen (i.e. the parameter, initialRoute, was used to indicate otherwise) or if ‘home’ is null, then the code returns the WidgetBuilder object from the Map object, routes, by the ‘name’ entry. Lastly, as you see in the screenshot above, if there are no ‘routes’ entry and no ‘home’ property, then the property, onGenerateRoute, is called if not null. Otherwise, the whole thing simply returns null.
Make Your Own Route
By the way, there’s a Cookbook example that explicitly uses the callback function, onGenerateRoute if you wanted an example of it being used. It’s called, Alternatively, extract the arguments using onGenerateRoute. A screenshot of part of this example is displayed below.
Its MaterialApp widget uses the parameter, home, and so we know the first screen is the widget called, HomeScreen. The parameter, settings, is of the class type, RouteSettings. Such objects contain information about a particular route. The route’s name and additional arguments for example. Such objects are used to instantiate Route objects to then ‘move about’ the app.
The App’s Navigator
We’ve talked about Flutter’s concept of ‘routes’ used to create the separate ‘screens’ or ‘pages’ of the app. Manipulating these routes is the responsibility of the Navigator object. We’ll now look at two of the MaterialApp’s parameters that involve the Navigator object: navigatorKey and navigatorObservers.
The Key To Routing
Traditionally, the BuildContext object is used to get to one ‘route’ and or back to another. Below are some arbitrary examples. All are using the ‘context’ object to first find its Navigator object and then work with its routes.
However, there may times where you want a direct reference to the navigator. If so, you would then use the MaterialApp’s first parameter, navigatorKey. Below is an example of how you would implement this key.
In the screenshots above, a GlobalKey is passed to the MaterialApp widget. As you see above, retaining that key gives you access to the navigator allowing you to, for example, go to the screen by a more direct means.
Above is a screenshot of the framework, you can see the Navigator object instantiated in the _WidgetAppState build() function receives the ‘navigatorkey’ like any Widget with a key parameter as a value of type, Key. In the screenshot below, if the parameter, navigatorKey, is not specified and instead equals null, then that key is instead created.
Note, it may be beneficial to provide the MaterialApp’s navigator its own key. I see in the code, if it’s null, then a new GlobalObjectKey is created when the widget is moved around the widget tree. Looks expensive. I’ll have to experiment with that parameter myself. I’ll get back to you.
Observe The Navigation
There is another parameter that can’t be null. If used, it will contain a List of classes of type, NavigatorObserver. As you can see below, if it’s not used, it’s assigned an empty List of class type, NavigatorObserver. What is this class?
Above is a screenshot of the NavigatorObserver class. At first glance, we see it has a getter referencing a library-private instance variable of type, NavigatorState, and six functions all with Route parameters and soon-to-be very telling function names. Those function names describe the very actions commonly taken by the Navigator going from screen to screen within an app.
The screenshot above demonstrates how one would, as an example, create a ‘Navigator Observer’ that would then be called with every action performed by the Navigator. It’s from a working example, where the class, NavListener, is eventually passed to a MaterialApp object. Note, how it’s enclosed in square brackets instantiating the class within a List object.
As you see above, the class, NavListener, extends the NavigatorObserver class. Within this class, every overridden function will have its print() function called. Well, almost. Note the if statement in the function, didPush().
It’s within the MaterialApp’s State object, _MaterialAppState, where the parameter, navigatorObservers, is accessed, and consequently, the instance variable, _navigatorObservers, is assigned its own list of NavigatorObservers. Instead of directly referencing the MaterialApp’s own list of observers, the List constructor, from(), is used to make a separate instance of that list. Now, why do you think it’s done like that?
The next two screenshots below depict what happens when the Navigator is called upon to ‘follow’ a named route. The screenshot above shows the Navigator object’s function, pushNamed(), being called supplied with the named route, “/add.” As a consequence, the function, push(), is called.
This function has a for loop that goes through any and all ‘NavigatorObserver’ objects and calls their didPush() functions. See how things are working now?
And so, in our little example code with its print() function and its if statement, we can see using the List constructor, from(), allows access to the _WidgetsAppState’s library-private instance variable of type NavigatorState, _navigator. Placing a breakpoint there, we can readily see the getter successfully retrieves the Navigator object — that print() function will not fire.
Build The Material
The named parameter, builder, describes a function used to override the implicit characteristics of the MaterialApp object. It’s not used actually very much — only for special circumstances. Its type definition is TransitionBuilder, and takes in two parameters, the BuildContext and the Navigator widget.
With these two parameters, a developer has the means to override a number of characteristics and elements normally reserved for the framework itself such as the Navigator, the MediaQuery, or even the internationalization of the app. Again, it’s really only used for special circumstances, however.
One special circumstance, for example, is if no routes are provided using all the other parameters: home, routes, onGenerateRoute, or onUnknownRoute. If the builder is provided, it’s to then perform the routing. In fact, if they’re all null, and builder is not, the Navigator is not even created.
In the screenshot above, we see a portion of the build() function found in the MaterialApp’s State object. The variable, child, is the created Navigator object, but, as you see in the logic, it’s instead passed to the builder() function if one was defined and passed to the named parameter, builder.
Chase The Title
Your app’s got to have a title. I mean, that seems reasonable. If a title is not explicitly provided, an ‘empty string’ is provided instead. A title is usually displayed in the top left corner when your app starts up.
Again, deep in the framework, that title is eventually supplied to a…well…a Title widget. It’s a Stateless widget and takes in the named parameter, title. Note, however, there’s another way to generate a title for your app. You can instead call a function to generate the title.
Right next to the parameter, title, is the callback function, onGenerateTitle. As you see below, it’s a function that returns a String object intended to be then the title of the app. Even if the other parameter, title, is also supplied, this function takes precedence and will ultimately provide the app’s title.
The function takes in the ‘context‘ object, allowing you to, for example, create a localized title. In other words, using a function, you have a means to dynamically generate a title depending on any number of factors. One being locale.
Localize Your Title
Seth Ladd, Product Manager at Google, is actively involved in the development of Flutter. Below he’s offers an example of how to, in fact, generate a localized app titles. In his example, the title will be in Spanish or in English depending on where you’re running the app in World. Neat, no?
Further, we see in his example that two additional named parameters of the MaterialApp class are involved in implementing this localized title for the app: localizationsDelegates and supportedLocales. At the start, a Flutter app resolves what locales it will support using the property, supportedLocales. It too is one of those ten parameters that can’t be null, and so defaults to US English.
Seth Ladd then delegated his own custom class, DemoLocalizationsDelegate, to handle any ‘interpretations’ that may be necessary in his little example. And, of course, you can see a Scaffold object makes two such requests in the screenshot of the example above.
His DemoLocalizationsDelegate class then loads into the framework yet another custom class, DemoLocalization, as a localized resource. You can see this other class defines a getter called, title, to return a String object labelled, title, depending upon the app’s locale. It’s that getter that’s eventually called with every interpretation request. I’ll let you follow up on that example yourself. Needless to say, a MaterialApp object is involved in this ability.
Locate Your Locale
There’s actually two named parameters between localizationsDelegates and supportedLocales. Now they are not required parameters and so their values can be null, but if provided they’re involved in helping to determine the app’s locale. Both are typedefs representing callback functions and are called, localeListResolutionCallback and localeResolutionCallback.
These callback functions, if defined and provided, are involved in choosing the app’s locale when it first starts up, or whenever the user explicitly changes the device’s locale while running the app.
The function, LocaleListResolutionCallback, is called first. If not provided, or it returns null, the next function, LocaleResolutionCallback, is called. It’s said in the documentation the first function, LocaleListResolutionCallback, is preferred because it takes a list of preferred locales in its attempt to choose a locale, while second function, LocaleResolutionCallback, only takes one default locale.
You can see right from the start, in the MaterialApp’s State object, inside its initState() function, the locale is resolved right away. Note, how the function, _resolveLocales(), takes in the phones own determined locale and the named parameter, supportedLocales, mentioned earlier.
Inside the function, _resolveLocales(), if both callback functions, localeResolutionCallback and localeListResolutionCallback, are null or return null, the function, basicLocaleListResolution(), takes in the phones own determined locale and the named parameter, supportedLocales,
And so, if all these fail, the Flutter framework turns to the phone’s operating system and to the default value(s) supplied by the parameter, supportedLocales, to determine the locale. You yourself can turn to Internationalizing Flutter apps for more information on the subject.
What’s Your Colour?
By the way, I’m Canadian if you were wondering. You’ve, no doubt, noticed my spelling of the word, ‘colour’, and yet the parameter, color, is spelled without a ‘u’ — a result of the American Revolution & the first Webster’s Dictionary in 1806. If you would allow me, I would like to retain my country’s orthography and continue with my u’s, with my centre’s, etc.
Regardless! The parameter, color, indicates to the app what to use as the ‘primary colour’ for the user interface. What colour selective elements of the interface shall be, like the AppBar, the FlatButton, the RaisedButton, etc.
The default colour, following the Material style, is simply blue. You can literally see that colour being assigned inside the MaterialApp’s State object’s build() function if both the parameters, color and theme in the MaterialApp widget are not specified.
The App’s Theme
Your app can display a general ‘theme’ with regard to its colour and font style. There’s a widget for that of course. It’s a Statelesswidget, and of a class of type, Theme. However, to specifically supply ‘a theme’ to your app, you would assign yet another class to your MaterialApp. After all, it’s the MaterialApp that’s traditionally at the root of the widget tree.
You would assign an object of the class type, ThemeData, to the parameter, theme, for your MaterialApp. It contains the colour and typography values that will propagate throughout your app. If you don’t specify the parameter, theme, and the MaterialApp widget will default to the colour scheme and such specified in Material Design.
The cookbook example above can be found on the page, Using Themes to share colors and font styles. An example of how one would define a theme.
Go To The Dark Side
At the Google I/O 2019, the Dark Theme support was introduced in Android Q — a system-wide dark UI mode that can be toggled on and off by the user. It’s implemented with the MaterialApp parameter called, darkTheme.
The app defaults to the value of ThemeData.light() when both darkTheme and theme are null.
Develop and Debug
The last six parameters of the MaterialApp object all require they not be null. They all must be provided boolean values, and they all are involved in the debugging of your app or in assisting you while developing it.
Grid Your App
As describes in the documentation, a Material grid is to help developers when they wish to align elements in their app’s user interface. A grid is literally laid over the screen by the MaterialApp object. In the screenshot below, in the Flutter framework, the GridPaper object is instantiated and subsequently applied if the property, debugShowMaterialGrid, is set to true.
Note, that if statement is enclosed in the assert() function. That means that bit of code is not executed when you’ve released your app into production. All assert() functions are actually removed from the codebase when you produce a ‘release’ version of your app. Thus, indicating to you this parameter serves you only when you’re developing your app. I’ve taken our previous ‘themes’ example and simply introduced the parameter, debugShowMaterialGrid, setting it to true as a demonstration. Peachy.
Show Your Performance
The parameter that follows is called, showPerformanceOverlay, and it too is set to false by default. It displays two graphs over the screen itself. The one on top, is the GPU thread showing how hard the graphic engine is working, and the one on the bottom is the UI thread conveying how hard the actual CPU is working. The vertical green is a point of reference and represents the current frame.
Only On Release
It’s encouraged to, in fact, only view these graphs when you’ve created a ‘release’ version of your app. It should be running in ‘release mode.’ Running this performance overlay during development would have your app running in ‘debug mode.’ Debug mode, of course, takes up more resources and so would only produce misleading results in the graphs. Turn to the webpage, Debugging Flutter apps — PerformanceOverlay, for an introduction to the performance overlay, and to the webpage, Flutter performance profiling — The performance overlay, for further information on its use.
As you see in the very first line of the framework code above, setting the parameter, showPerformanceOverlay, to true instantiates an object from the PerformanceOverlay class. It is that object that takes in the next two parameters that would follow in the MaterialApp class constructor, checkerboardRasterCacheImages and checkerboardOffscreenLayers.
Cache Your Images
First, there’s the parameter, checkerboardRasterCacheImages. Now, creating or ‘painting’ the screen on phones is expensive. It draws the most power from the battery for example and takes up the most memory cycles at times. If there are parts of the interface that don’t change, then don’t ‘repaint’ it. Those parts of the interface are thus placed into cache memory to help save on resources. The framework makes an effort to do that, but some parts are sometimes missed.
A developer has the means to find those parts and place them in cache memory manually. The parameter, checkerboardRasterCacheImages, highlights those ‘static parts’ in a checker pattern. And so, if those checker patterns ‘flicker’ when running your app, then they’re still being repainted and possibly should be placed in cache memory. See Checking for non-cached images.
Rendering the screen every 16ms is expensive. Many many algorithms have been conceived in an effort to reduce the expense. Clipping is a common technique where the rendering engine will only “paint” the pixels in defined areas leaving the rest. Further, whole layers of rendering is saved in memory to be later restored. All in the attempt to optimize the rendering of the screen. Flutter has the SaveLayer() method, for example, to do just that. However, saving and restoring the whole layers of rendering is relatively expensive.
Setting the parameter, checkerboardOffscreenLayers, to true will encase widgets on your screen in checkered boxes (see above). Now, while running, if those checkered boxes are flickering, that image is being re-rendered. Possibly, apply opacity to each individual widget inside that checkered box would be beneficial. See. Identifying problems in the GPU graph and Checking for offscreen layers.
Debug The Semantics
The next parameter, showSemanticsDebugger, when set to true displays an overlay on the screen that shows the accessibility information reported by the Flutter framework. Information describing the elements displayed on the screen. Such information is usually audibly delivered to those using the app who happens to be visually impaired.
Semantics Over Performance
Looking at the code, you’ll notice the ‘SemanticsDebugger Overlay’ if applied encloses the ‘Performance Overlay’ if it too is applied. Just follow the variable, result, and see where it goes and when it’s assigned again and again.
Debug Banner, No Debug Banner
The last parameter in the list of parameters for the MaterialApp widget is, debugShowCheckedModeBanner. It’s the only one of the last six parameters that default to true and not to false. You can see it’s used in the last if statement in the screenshot above. Guess what it does.
You got it. Removes that little banner on the top right-hand corner labelled, ‘DEBUG.’ That’s all. Makes for taking ‘snapshots’ of your app while still in development a little more professional. You don’t have that banner displayed there in those cases. Also, note that if statement is also enclosed in an assert function. Therefore, that little stretch of code goes away when the app is finally released.
Others Brand of UX
You’re not just required to use Material Design. Flutter gives you an out — you could impose your own ‘look and feel.’ However, you’d have to build it yourself. Further, you could use what the MaterialApp object uses. After all, upon close inspection, it’s discovered the MaterialApp object merely ‘passes on’ that long list of parameter values to yet another class object that then conveys the nuances found in the Material Design guidelines. Conceivably, you could do the same.
You can see in the MaterialApp’s State class, _MaterialAppState, it takes those many parameters into its build() function and instantiates yet another object: The WidgetsApp object. The WidgetsApp, as described in the documentation, ‘is a widget that wraps a number of widgets that are commonly required for an application.’
Looking at the WidgetsApp constructor itself, most of its parameters are those passed on from the MaterialApp object. Conceivably, one could include the WidgetsApp widget with their own versions of the ‘Scaffold’, the ‘AppBar’ and the ‘Drawer’ class types and make up their own design specifications.
I had mentioned other design guidelines are out there. Near the end of the article, Bye bye Material Design, the author, Emin Durak, was impressed with one which appears to be growing in popularity called, Ant Design. Admittedly, I’ve yet to see it or any other ‘design’ implemented in Flutter, but possibly I haven’t looked hard enough. Maybe someone will be inspired to do so upon reading this article.
So there you have it. A little run-through of the MaterialApp widget. It serves as the bases of your typical Flutter app. Time will tell if alternatives will arise to do the same.
#Source code as of March 18, 2019
*Source code as of April 05, 2019
^Source code as of May 09, 2019