Flutter Widget In Detail: MaterialApp
Detailed Explanation of MaterialApp Widget
Introduction:
- MaterialApp is Futter's one of the most powerful widgets. if you create a basic Flutter app then the first widget you'll see is MaterialApp
- MaterialApp wraps a number of widgets that are commonly required for material design applications.
- By wrapping your application inside the MaterialApp, you're telling your app to use Android's Material Design, which is a design system created by Google to help teams build high-quality digital experiences for Android, iOS, Flutter, and the web.

- More about Material-Design
- But if you want to follow iOS design patterns, then you have to wrap your app inside CupertinoApp. There are many widgets provided by flutter to design your app for iOS platform.

Another thing I want to point out is that MaterialApp and CupertinoApp are built upon WidgetApp.
- Let's understand the MaterialApp widget and its properties in detail with some examples.
MaterialApp
- We can consider this as an application that uses material design.
- Before creating MaterialApp we have to import material package which is provided by flutter SDK.
- To import material package :
import 'package:flutter/material.dart';- This package provides us all the widgets that we can use in our application. For example:
AppBar,Scaffold,BottomNavigationBar,Card,Chip,BottomSheet, etc. - MaterialApp must have at least one of
home,routes,onGenerateRoute, orbuilderproperties non-null. Without it you will get an error. import 'package:flutter/material.dart'; class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp(); } }- Output :

Now let's take a deep dive into all the properties, and understand what each and every property does.
home:- This is a default
routeof an app. - It means whatever is defined here is the first thing you will see on the screen.
- It takes
Widgetas an input. - Usually, we define home, signIn, signUp, splash screens, but you can put any widget here.
MaterialApp( home: MyFirstPage(), );- Output :

title:- This takes
Stringas value. - If you put value in
title, you will not see any changes in your app. It will still show an empty blank screen. - You will see this title when you press the "recent apps" button.
- Let's define
titlein MaterialApp MaterialApp( title: "Widget In Detail", home: MyFirstPage(), );
debugShowCheckedModeBanner:- This is a banner that indicates that currently, our app is running in `debug mode.
- The default value of this property is
true. - To remove this banner, simply put
falseinside it. - In release mode, this has no effect.
MaterialApp( debugShowCheckedModeBanner: true, title: "Widget In Detail", home: MyFirstPage(), );- Output :

builder:- A builder that builds a widget given to a child.
builderfunction takes two parametercontextandwidget.- The return type of
builderisWidget.MaterialApp( builder: (context,widget) { return widget; } ); - By using
builderproperty, we can override properties likeNavigator,MediaQuery, orinternationalizationthat is set byMaterialApp - For example, If no routes are provided to the regular
MaterialAppconstructor usinghome,routes,onGenerateRoute, oronUnknownRoute, the child will benull, and it is the responsibility of thebuilderto provide the application'srouting machinery. - If
builderis null,routesmust be provided using one of the other properties (home,routes,onGenerateRoute, oronUnknownRoute,).Use cases :
- To insert widgets above the
Navigator. - To insert widgets above the
Routerbut below the other widgets created by theWidgetsAppwidget - For replacing the
Navigator/Routerentirely.
- To insert widgets above the
- If
Navigatoris not provided in the builder we will not able to useNavigator.push,Navigator.pop,Heroetc. 
- Okay let me take an example. Consider the below code :
class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( builder: (context, child) { return MyHomePage(); }); } }class MyHomePage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( body: Center( child: ElevatedButton( onPressed: () => Navigator.push( context, MaterialPageRoute( builder: (_) => SecondPage(), ), ), child: Text('To Second Screen'), ), ), ); } }
- Now if we press the button, nothing will happen. Why? Because we haven't passed
Navigatorin our app. You can see that in the above code, Inbuilderwe are simply returningMyHomePage(). 
- Let's wrap that child inside
Navigatorand pass the routing information accordingly.MaterialApp( builder: (context, child) { return Navigator( // If you don't know about `initialRoute` and `onGenerateRoute`, I've explained these properties below. initialRoute: "/", onGenerateRoute: (settings) { if (settings.name == '/') { return MaterialPageRoute(builder: (_) => MyHomePage()); } return null; // Let `onUnknownRoute` handle this behavior. }, ); }); - Output :

- So as you can see, now we are successfully navigating to Second Screen.
- Material-specific features such as
showDialogandshowMenu, and widgets such asTooltip,PopupMenuButton, also require aNavigatorto properly function.
routes :
- If you want to navigate via
namedRoutes, you have to first define all the routes in the application's top-level routing table. i.e, inMaterialApp'sroutesproperty. - You can think of
routesas atablewhere eachscreenis binded with a particularpath. For example,"/home"is binded withHomeScreen()widget. - It takes
Map<String, Widget Function(BuildContext)>as an input. Wherekeyis the actualpathName(ex: "/home","/signIn" ,etc), andvalueis actual Widget/Screen (ex: HomeScreen(), SignIn(), etc). - Example :
MaterialApp( routes: { "/": (_)=> MyHomePage(), "/secondScreen": (_) => MySecondPage(), }, ); - Now you can use
Navigator.pushNamed(context, "/secondScreen");for navigation.class HomePage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( body: Center( child: ElevatedButton( onPressed: () => Navigator.pushNamed(context, "/secondScreen"), child: Text('To Second Screen'), ), ), ); } } - Output :

Notice that I've not defined
homeproperty insideMaterialApp. As I've already defined/key in theroutesproperty. TheMaterialAppwill automatically consider/key defined in theroutesmap as aStarting Point of Application. This is not any kind of magic. Behind the scene,Navigator.defaultRouteNamehas/value by default.- If
homeis specified, then it implies an entry in this table for theNavigator.defaultRouteNameroute/.Note: You cannot specify
homeand/key inrouteboth at the same time. It will lead to an error.
onGenerateRoute :
- This is used when the app navigates to the
named route. - If this returns
null, For example :MaterialApp( onGenerateRoute: (settings) { return null; }, home: MyHomePage(), ); - Then all the routes are
discardedand Navigator.defaultRouteName is used instead (/). Which here isMyHomePage(). - Let's see how we can generate route using
onGenerateRoute. - Example :
MaterialApp( onGenerateRoute: (settings) { if (settings.name == "/secondScreen") { return MaterialPageRoute(builder: (_) => MySecondPage()); } }, home: MyHomePage(), );- As you can see in the above snippet, there is one
parameternamedsettings, passed in theonGenerateRoute. Thissettingsis calledRouteSettings, which provides us two things.nameandarguments. nameis the name of aroutename. For example: If we callNavigator.pushNamed(context, "/secondScreen");, thennamegets a value as/secondScreen.argumentsis the data which has been passed through the screen. For example:ElevatedButton( onPressed: () => Navigator.pushNamed(context, '/secondScreen', arguments: 42), // Passing argument child: Text('Go to BarPage'), ),- Here as you can see the
argumentproperty defined inpushNamedconstructor, which later will be assigned to thesettings.arguments - Now you can use
Navigator.pushNamed(context, "/secondScreen");for navigation. class HomePage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( body: Center( child: ElevatedButton( onPressed: () => Navigator.pushNamed(context, "/secondScreen"), child: Text('To Second Screen'), ), ), ); } }
- Then what's the
differencebetweenroutesandonGenerateRoute. Both are doing the same thing, right?. Well YES. Both are used when app navigate via anamedRoute. - BUT, Both has its different use cases. Let's understand..
routesis static. It means it doesn't offer a functionality of passing arguments between screen, or implementing differentPageRoute.- This is why
onGenerateRouteproperty comes into the picture. - With
onGenerateRoute, you can passargumentsbetween routes. Which is not possible inroutes. - Example
MaterialApp( routes: { '/': (_) => HomePage(), '/secondScreen': (_) => SecondPage(), }, onGenerateRoute: (settings) { if (settings.name == '/thirdScreen') { final value = settings.arguments as int; // Retrieve the value. return MaterialPageRoute( builder: (_) => ThirdPage(value)); // Passing the value } return null; }, ),class HomePage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text('HomePage')), body: Center( child: Column( children: [ ElevatedButton( onPressed: () => Navigator.pushNamed(context, '/secondScreen'), child: Text('Go to Second Page'), ), SizedBox(height:10.0), ElevatedButton( onPressed: () => Navigator.pushNamed(context, '/thirdScreen', arguments: 123), child: Text('Go to Third'), ), ], ), ), ); } }class SecondPage extends StatelessWidget { @override Widget build(_) => Scaffold( appBar: AppBar( title: Text('SecondPage'), ), ); }class ThirdPage extends StatelessWidget { final int value; ThirdPage(this.value); @override Widget build(_) => Scaffold( appBar: AppBar( title: Text('ThirdPage, value = $value'), ), ); }- Output :

onGenerateInitialRoutes:
- The routes generator callback used for generating initial routes if
initialRouteis provided. - One use case can be , When you want to navigate user to
IntroPageif he/she is not authorized and toHomePageif authorized. - Example :
MaterialApp( onGenerateInitialRoutes: (route) { if (isAuthorized) { return <Route>[ MaterialPageRoute(builder: (context) => HomePage()) ]; } else { return <Route>[ MaterialPageRoute(builder: (context) => IntroPage()) ]; } }, onGenerateRoute: (settings) { switch (settings.name) { case '/': return MaterialPageRoute(builder: (_) => IntroPage()); case '/homePage': return MaterialPageRoute(builder: (_) => HomePage()); } }, ),
onUnknownRoute :
- This will return a route when
onGenerateRoutefails to generate a route. - This callback is typically used for error handling. For example, this callback might always generate a "not found" page that describes the route that wasn't found.
- Example :
MaterialApp( onUnknownRoute: (RouteSettings settings) { return MaterialPageRoute<void>( settings: settings, builder: (BuildContext context) => Scaffold(body: Center(child: Text('Not Found'))), ); }, home: HomePage(), ),
darkTheme :
- By applying the
ThemeDatain thedarkThemeproperty, we are telling our app to use this particularThemeDatawhen the system requests forDarkTheme. - For example : We have an app where we've provided toggle for
LightModeandDarkMode. Whenever user toggles the theme toDarkTheme, entire app will use theThemeDatathat is specified in thedarkThemeproperty ofMaterialApp. - Example :
MaterialApp( darkTheme: ThemeData( brightness: Brightness.dark ), home: HomePage(), ), ); } - Output :

- Let's tweak the values of
ThemeDatas'primaryColorwhen the app is in dark mode MaterialApp( darkTheme: ThemeData( brightness: Brightness.dark, primaryColor: Colors.red ), home: HomePage(), ),- Output :

- As we can see, the
primaryColorapplied successfully.
theme:
- This is a
defaulttheme that will be applied to our app. This theme will be applied when thethemModevalue islight. i.e.ThemeMode.light - If you want to edit the theme of an app when
themeModeisThemeMode.dark, you have to specifyThemeDataindarkModeproperty as discussed above. - Here we can define default
primaryColor,secondaryColor,buttonColor,etc of our app. - It takes
ThemeDataas an input. - Example :
MaterialApp( themeMode: ThemeMode.light, theme: ThemeData( brightness: Brightness.light, primaryColor: Colors.green ), home: HomePage(), ), - Output :

themeMode:
- This property determines which theme will be used by the application if both
themeanddarkThemeare provided. - The
defaultvalue ofthemeModeisThemeMode.system, which means whatever the theme of the system will be applied by default by our app. ThemeModehas3enums.ThemeMode.dark: Use thethemedefined indarkThemeproperty. It will always use the dark mode (if available) regardless of system preference.ThemeMode.light: Use thethemedefined inthemeproperty. It will always use the light mode regardless of system preference.ThemeMode.system: Use either the light or dark theme based on what the user has selected in the system settings.- Example :
MaterialApp( themeMode: ThemeMode.dark, theme: ThemeData( brightness: Brightness.light, primaryColor: Colors.green ), darkTheme: ThemeData( brightness: Brightness.dark, primaryColor: Colors.red ), home: HomePage(), ), - As shown in the above example, the value of
themeModeisThemeMode.dark. Because of that, thedarkThemewill be applied to our app. If the value isThemeMode.lightthenthemewill be applied to our app. - You can switch between
darkModeandlightModeby toggling the value ofthemeModeusing some kind oflistenerthat willlistento the toggle event and toggles thethemeModevalues accordingly as shown below. 
highContrastDarkTheme:
- When a 'dark mode' and 'high contrast' is requested by the system the theme defined in
highContrastDarkThemewill be applied. - Some host platforms (for example, iOS) allow the users to increase contrast through an accessibility setting.
- You can check whether the user requested a high contrast between foreground and background content by
MediaQueryData.highContrastboolean flag. - This theme should have a
ThemeData.brightnessset toBrightness.dark. - It will use
darkThemewhen null.
highContrastTheme:
- When
high contrast is requested by the system we can use thethemedefined inhighContrastTheme`. - It will use the
themewhen null.
initialRoute:
- The
initialRouteproperty tells our app which is the initial page/widget to load. - The value is a type of
String. Anddefaulttodart:ui.PlatformDispatcher.defaultRouteName. Which we can override too. - Example :
MaterialApp( initialRoute: "/", routes: { '/': (_) => HomePage(), }, ), - Here the app will consider
HomePageasinitial routeas theinitialRouteis/. - You might think what is the difference between
initialRoute,home,onGenerateRoute, andonGenerateInitialRoute.
- There is the only a difference in code readability (but not limited to), see all of them are doing the same job but in different ways:
homes' way to render initial widget:MaterialApp( home: HomePage(), ),initialRoutes' way to render initial widget:MaterialApp( initialRoute: '/', routes: { '/': (_) => HomePage(), }, ),onGenerateRoutes' way to render initial widget :MaterialApp( initialRoute: '/', onGenerateRoute: (settings) { if (settings.name == '/') return MaterialPageRoute(builder: (_) => HomePage()); return MaterialPageRoute(builder: (_) => UnknownPage()); }, ),onGenerateInitialRoutes' way to render initial widget :MaterialApp( onGenerateInitialRoutes: (route) { return [ MaterialPageRoute(builder: (_) => HomePage()) ]; } ),
navigatorKey:
- As we've seen above, we are writing our navigation business logic directly from our UI(in view (if we consider MVC)) page. And that's how we usually do. Because for
navigationwe needBuildContext. Withoutcontextwe can't navigate to other screens. - So Is there any way to write our business logic inside the
modelclass? Is there any way to navigate without usingBuildContext? 
- YES. In Flutter
GlobalKeyscan be used to access the state of a StatefulWidget and that's what we'll use to access theNavigatorStateoutside of the build context. - We can create
NavigationServiceclass that contains theglobal key, we'll set that key on initialization and we'll expose a function on the service to navigate given a name. class NavigationService { final GlobalKey<NavigatorState> navigatorKey = new GlobalKey<NavigatorState>(); Future<dynamic> navigateTo(String routeName) { return navigatorKey.currentState.pushNamed(routeName); } }- Then we register out NavigationService with the locator (here get_it is used for registering).
void setupLocator() { locator.registerLazySingleton(() => NavigationService()); }- In the main file, we then pass our
GlobalKeyas theNavigatorKeyto ourMaterialApp. MaterialApp( navigatorKey: locator<NavigationService>().navigatorKey, onGenerateRoute: (routeSettings) { switch (routeSettings.name) { case 'secondPage': return MaterialPageRoute(builder: (context) => SecondPage()); } }, home: HomePage() );- Now we can navigate by calling the
navigateTofunction by passingpathName. locator<NavigationService>().navigateTo('SecondPage');
navigatorObserver :
- As you know in the flutter navigation is handled by
Navigatorand is also responsible for screen transitions. There are different options likepush,popscreens. - The list of
NavigatorObservercan also be passed toNavigatorto receive events related toscreen-transitions. - A custom
NavigatorObservercan also be used but if the handling of it in the state is required then it is a better option to go with theRouteObserver. - What is RouterObserver?
RouteObserverinforms subscribers whenever a route of typeRis pushed on top of their own route of typeRor popped from it. This is for example useful to keep track of page transitions, e.g. aRouteObserver<PageRoute>will inform subscribedRouteAwareswhenever the user navigates away from the current page route to another page route. - Let's understand how to use RouteObserver in our app,
- We have to extend
RouteObserverfor using3methods,didPush(),didReplace(),didPop(), class MyRouteObserver extends RouteObserver<PageRoute<dynamic>> { void _sendScreenView(PageRoute<dynamic> route) { var screenName = route.settings.name; print('screenName $screenName'); // do something with it, ie. send it to your analytics service collector }@override void didPop(Route<dynamic> route, Route<dynamic>? previousRoute) { super.didPop(route, previousRoute); if (previousRoute is PageRoute && route is PageRoute) { _sendScreenView(previousRoute); } }@override void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) { super.didPush(route, previousRoute); if (route is PageRoute) { _sendScreenView(route); } } } // End of MyRouteObserver class- After that, You need to call this class in main.dart & It will automatically notify all the screen transitions.
final RouteObserver<PageRoute> routeObserver = RouteObserver<PageRoute>();class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( navigatorObservers: [MyRouteObserver()], routes: { 'pageone': (context) => PageOne(), 'pagetwo': (context) => PageTwo() }, home: MyHomePage(), ); } }
locale :
- This property of the
MaterialAppclass allows us to immediately specify what locale we want our app to use. - If the value of this is
nullthen the system's locale will be applied to our app. - This
localeproperty allows us to force the locale of the app to the locale specified inlocale, regardless of the locale of the device. - It takes
Locale(String _languageCode, [String? _countryCode])as an input. - Example :
MaterialApp( locale: Locale('hi', ''), home: HomeScreen() ); - This is the easiest way to define
localeof our app.
localeResolutionCallback :
- This callback is responsible for choosing the app's
localewhen the app is started, and when the user changes the device'slocale. - It is recommended to provide a
localeListResolutionCallbackinstead of alocaleResolutionCallbackwhen possible, aslocaleListResolutionCallbackis in the first priority. - Example :
MaterialApp( localeResolutionCallback: (deviceLocale, supportedLocales) { for (var locale in supportedLocales) { if (locale.languageCode == deviceLocale!.languageCode && locale.countryCode == deviceLocale.countryCode) { return deviceLocale; } } return supportedLocales.first; }, home: HomePage(), ), - What this above code will do is, It will check if the current app supports the device locale or not. If not then, we can simply return the
localefrom thesupportedLocale.
localizationsDelegates :
- If we see the
materialandcupertinowidget, For ex:calender,datePickeretc, there are obviously texts/numbers written on it. 
- Now what if we want to translate those texts?
- So this
localizationsDelegatesprovides us three important in-builtdelegates:GlobalMaterialLocalizations.delegate,GlobalWidgetsLocalizations.delegate,GlobalCupertinoLocalizations.delegate. - These three
delegates are responsible for translating thosematerialandcupertinowidgets. 
- We can also create our own
delegatesto translate our app's texts. I can't explain that here, as it is out of the scope of this blog. I'll explain this in a future blog.
localeListResolutionCallback :
- This callback is responsible for choosing the app's
localewhen the app is started, and when the user changes the device's locale. - When a
localeListResolutionCallbackis provided, Flutter will first attempt to resolve thelocalewith the providedlocaleListResolutionCallback. If the callback or result isnull, it will fallback to trying thelocaleResolutionCallback. If bothlocaleResolutionCallbackandlocaleListResolutionCallbackare leftnullor fail to resolve (returnnull), the a basic fallback algorithm will be used. - The
priorityof each available fallback is: localeListResolutionCallbackis attempted first.localeResolutionCallbackis attempted second.- Flutter's basic resolution algorithm, as described in
supportedLocales, is attempted last. - This callback function takes two arguments.
locale: List of locales.supportedLocale: supportedLocale- Example :
MaterialApp( localeListResolutionCallback: (locales, supportedLocales) { print(locales); print(supportedLocales); return null; }, home: HomePage(), ), - Output :

- I'm executing this code on
dartad.dev(Windows).
restorationScopeId :
- As a developer, we need to take care of the app's user interface by preserving it. By doing this, It creates an illusion that your app is always running.
- Sometimes interruptions can occur on devices and might cause the system to terminate your app to free up resources.
- But the users do not know all these behind the scene activities. They only expect your app to be in the same state as when they left.
- For that
State PreservationandRestorationconcepts are used. It ensures that the app returns to its previous state when it launches again. - Flutter has the
RestorationManagerwhich is responsible for handling all the state restoration work. We don't usually use it directly. RestorationBucketis used to hold the piece of the restoration data that our app needs to restore its state later.RestorationScopeis used to provide a scopedRestorationBucketto its descendants.- If the
restorationScopeIdparameter isnullthen, the restoration is disabled for its descendants. RestorationMixinis the one that is used by our widget's state. It provides use an API to save and restore our state.- And finally, we have to use
restorable properties, which are used to represent the data to be stored in the buckets. 
- First of all we have to provide a
restorationScopeIdto ourMaterialApp. MaterialApp( restorationScopeId: 'root', //default value if null. home: HomePage(), );- Then use
RestorationMixinmixed-in withHomePage class _HomePageState extends State<HomePage> with RestorationMixin { // ..... }- After that create
restorableproperties that we want to restore if something went wrong. final RestorableInt _index = RestorableInt(0);- The final step is to resgister our restorable properties for restoration.
@override // The restoration bucket id for current page String get restorationId => 'home_page'; @override void restoreState(RestorationBucket? oldBucket, bool initialRestore) { // Register our property to be saved every time it changes, // and to be restored every time our app is killed by the OS! registerForRestoration(_index, 'nav_bar_index'); }- If you want to test this code. First enable the
Don't keep activitiesfrom the mobile'sDeveloper Options. - Now lets see the output :

- As you can see that the selected setting option is still selected.
- But if you try this without
restorationyou will notice that the index will always come back toHome. 
shortcuts:
- We can add shortcut keys to perform certain tasks by using the
shortcutproperty. - It takes
Mapof typeLogicalKeyState. LogicalKeyStateis a set ofLogicalKeyboardKeysthat can be used as the keys in a map.- Example :
class AddIntent extends Intent {}MaterialApp( shortcuts: { LogicalKeySet(LogicalKeyboardKey.arrowUp): AddIntent(), }, home: MyHomePage(), ); - Now we have to wrap the widget tree inside
Actions. This will dispatch the actions when you press the shortcut key provided inshortcutproperty. class _MyHomePageState extends State<MyHomePage> { int _number = 0; changeNumber() { setState((){ _number += 1; }); }@override Widget build(BuildContext context) { return Scaffold( body: Actions( actions: { AddIntent: CallbackAction<AddIntent>( onInvoke: (intent) => changeNumber(), ), }, child: Center( child: Container( height:100, width:100, color:Colors.red, child: Focus( autofocus: true, child: Center( child: Text("$_number") ), ) ), ) ), ); } }- Output :

scaffoldMessengerKey :
- A key to use when building the ScaffoldMessenger.
- If a scaffoldMessengerKey is specified, the ScaffoldMessenger can be directly manipulated without first obtaining it from a BuildContext via ScaffoldMessenger.of: from the scaffoldMessengerKey, use the GlobalKey.currentState getter.
THAT'S IT
- That's all you need to know about the
MaterialAppwidget. - I know that, it's a lot of properties and a lot of stuff is going on. But if you try and practice it, you will remember it easily.

Previous Widget In Detail : AlertDialog

