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
, orbuilder
properties 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
route
of an app. - It means whatever is defined here is the first thing you will see on the screen.
- It takes
Widget
as an input. - Usually, we define home, signIn, signUp, splash screens, but you can put any widget here.
MaterialApp( home: MyFirstPage(), );
- Output :
title
:- This takes
String
as 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
title
in 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
false
inside 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.
builder
function takes two parametercontext
andwidget.
- The return type of
builder
isWidget
.MaterialApp( builder: (context,widget) { return widget; } );
- By using
builder
property, we can override properties likeNavigator
,MediaQuery
, orinternationalization
that is set byMaterialApp
- For example, If no routes are provided to the regular
MaterialApp
constructor usinghome
,routes
,onGenerateRoute
, oronUnknownRoute
, the child will benull
, and it is the responsibility of thebuilder
to provide the application'srouting machinery
. - If
builder
is null,routes
must 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
Router
but below the other widgets created by theWidgetsApp
widget - For replacing the
Navigator
/Router
entirely.
- To insert widgets above the
- If
Navigator
is not provided in the builder we will not able to useNavigator.push
,Navigator.pop
,Hero
etc. - 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
Navigator
in our app. You can see that in the above code, Inbuilder
we are simply returningMyHomePage()
. - Let's wrap that child inside
Navigator
and 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
showDialog
andshowMenu
, and widgets such asTooltip
,PopupMenuButton
, also require aNavigator
to 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
'sroutes
property. - You can think of
routes
as atable
where eachscreen
is binded with a particularpath
. For example,"/home"
is binded withHomeScreen()
widget. - It takes
Map<String, Widget Function(BuildContext)>
as an input. Wherekey
is the actualpathName
(ex: "/home","/signIn" ,etc), andvalue
is 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
home
property insideMaterialApp
. As I've already defined/
key in theroutes
property. TheMaterialApp
will automatically consider/
key defined in theroutes
map as aStarting Point of Application
. This is not any kind of magic. Behind the scene,Navigator.defaultRouteName
has/
value by default.- If
home
is specified, then it implies an entry in this table for theNavigator.defaultRouteName
route/
.Note: You cannot specify
home
and/
key inroute
both 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
discarded
and 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
parameter
namedsettings
, passed in theonGenerateRoute
. Thissettings
is calledRouteSettings
, which provides us two things.name
andarguments
. name
is the name of aroutename
. For example: If we callNavigator.pushNamed(context, "/secondScreen");
, thenname
gets a value as/secondScreen
.arguments
is 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
argument
property defined inpushNamed
constructor, 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
difference
betweenroutes
andonGenerateRoute
. 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..
routes
is static. It means it doesn't offer a functionality of passing arguments between screen, or implementing differentPageRoute
.- This is why
onGenerateRoute
property comes into the picture. - With
onGenerateRoute
, you can passarguments
between 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
initialRoute
is provided. - One use case can be , When you want to navigate user to
IntroPage
if he/she is not authorized and toHomePage
if 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
onGenerateRoute
fails 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
ThemeData
in thedarkTheme
property, we are telling our app to use this particularThemeData
when the system requests forDarkTheme
. - For example : We have an app where we've provided toggle for
LightMode
andDarkMode
. Whenever user toggles the theme toDarkTheme
, entire app will use theThemeData
that is specified in thedarkTheme
property ofMaterialApp
. - Example :
MaterialApp( darkTheme: ThemeData( brightness: Brightness.dark ), home: HomePage(), ), ); }
- Output :
- Let's tweak the values of
ThemeData
s'primaryColor
when the app is in dark mode MaterialApp( darkTheme: ThemeData( brightness: Brightness.dark, primaryColor: Colors.red ), home: HomePage(), ),
- Output :
- As we can see, the
primaryColor
applied successfully.
theme
:
- This is a
default
theme that will be applied to our app. This theme will be applied when thethemMode
value islight
. i.e.ThemeMode.light
- If you want to edit the theme of an app when
themeMode
isThemeMode.dark
, you have to specifyThemeData
indarkMode
property as discussed above. - Here we can define default
primaryColor
,secondaryColor
,buttonColor
,etc of our app. - It takes
ThemeData
as 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
theme
anddarkTheme
are provided. - The
default
value ofthemeMode
isThemeMode.system
, which means whatever the theme of the system will be applied by default by our app. ThemeMode
has3
enums.ThemeMode.dark
: Use thetheme
defined indarkTheme
property. It will always use the dark mode (if available) regardless of system preference.ThemeMode.light
: Use thetheme
defined intheme
property. 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
themeMode
isThemeMode.dark
. Because of that, thedarkTheme
will be applied to our app. If the value isThemeMode.light
thentheme
will be applied to our app. - You can switch between
darkMode
andlightMode
by toggling the value ofthemeMode
using some kind oflistener
that willlisten
to the toggle event and toggles thethemeMode
values accordingly as shown below.
highContrastDarkTheme
:
- When a 'dark mode' and 'high contrast' is requested by the system the theme defined in
highContrastDarkTheme
will 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.highContrast
boolean flag. - This theme should have a
ThemeData.brightness
set toBrightness.dark
. - It will use
darkTheme
when null.
highContrastTheme
:
- When
high contrast is requested by the system we can use the
themedefined in
highContrastTheme`. - It will use the
theme
when null.
initialRoute
:
- The
initialRoute
property tells our app which is the initial page/widget to load. - The value is a type of
String
. Anddefault
todart:ui.PlatformDispatcher.defaultRouteName
. Which we can override too. - Example :
MaterialApp( initialRoute: "/", routes: { '/': (_) => HomePage(), }, ),
- Here the app will consider
HomePage
asinitial route
as theinitialRoute
is/
. - 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:
home
s' way to render initial widget:MaterialApp( home: HomePage(), ),
initialRoute
s' way to render initial widget:MaterialApp( initialRoute: '/', routes: { '/': (_) => HomePage(), }, ),
onGenerateRoute
s' way to render initial widget :MaterialApp( initialRoute: '/', onGenerateRoute: (settings) { if (settings.name == '/') return MaterialPageRoute(builder: (_) => HomePage()); return MaterialPageRoute(builder: (_) => UnknownPage()); }, ),
onGenerateInitialRoute
s' 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
navigation
we needBuildContext
. Withoutcontext
we can't navigate to other screens. - So Is there any way to write our business logic inside the
model
class? Is there any way to navigate without usingBuildContext
? - YES. In Flutter
GlobalKeys
can be used to access the state of a StatefulWidget and that's what we'll use to access theNavigatorState
outside of the build context. - We can create
NavigationService
class 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
GlobalKey
as theNavigatorKey
to 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
navigateTo
function by passingpathName
. locator<NavigationService>().navigateTo('SecondPage');
navigatorObserver
:
- As you know in the flutter navigation is handled by
Navigator
and is also responsible for screen transitions. There are different options likepush
,pop
screens. - The list of
NavigatorObserver
can also be passed toNavigator
to receive events related toscreen-transitions
. - A custom
NavigatorObserver
can 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?
RouteObserver
informs subscribers whenever a route of typeR
is pushed on top of their own route of typeR
or popped from it. This is for example useful to keep track of page transitions, e.g. aRouteObserver<PageRoute>
will inform subscribedRouteAwares
whenever 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
RouteObserver
for using3
methods,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
MaterialApp
class allows us to immediately specify what locale we want our app to use. - If the value of this is
null
then the system's locale will be applied to our app. - This
locale
property 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
locale
of our app.
localeResolutionCallback
:
- This callback is responsible for choosing the app's
locale
when the app is started, and when the user changes the device'slocale
. - It is recommended to provide a
localeListResolutionCallback
instead of alocaleResolutionCallback
when possible, aslocaleListResolutionCallback
is 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
locale
from thesupportedLocale
.
localizationsDelegates
:
- If we see the
material
andcupertino
widget, For ex:calender
,datePicker
etc, there are obviously texts/numbers written on it. - Now what if we want to translate those texts?
- So this
localizationsDelegates
provides us three important in-builtdelegates
:GlobalMaterialLocalizations.delegate
,GlobalWidgetsLocalizations.delegate
,GlobalCupertinoLocalizations.delegate
. - These three
delegate
s are responsible for translating thosematerial
andcupertino
widgets. - We can also create our own
delegates
to 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
locale
when the app is started, and when the user changes the device's locale. - When a
localeListResolutionCallback
is provided, Flutter will first attempt to resolve thelocale
with the providedlocaleListResolutionCallback
. If the callback or result isnull
, it will fallback to trying thelocaleResolutionCallback
. If bothlocaleResolutionCallback
andlocaleListResolutionCallback
are leftnull
or fail to resolve (returnnull
), the a basic fallback algorithm will be used. - The
priority
of each available fallback is: localeListResolutionCallback
is attempted first.localeResolutionCallback
is 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 Preservation
andRestoration
concepts are used. It ensures that the app returns to its previous state when it launches again. - Flutter has the
RestorationManager
which is responsible for handling all the state restoration work. We don't usually use it directly. RestorationBucket
is used to hold the piece of the restoration data that our app needs to restore its state later.RestorationScope
is used to provide a scopedRestorationBucket
to its descendants.- If the
restorationScopeId
parameter isnull
then, the restoration is disabled for its descendants. RestorationMixin
is 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
restorationScopeId
to ourMaterialApp
. MaterialApp( restorationScopeId: 'root', //default value if null. home: HomePage(), );
- Then use
RestorationMixin
mixed-in withHomePage
class _HomePageState extends State<HomePage> with RestorationMixin { // ..... }
- After that create
restorable
properties 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 activities
from 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
restoration
you will notice that the index will always come back toHome
.
shortcuts
:
- We can add shortcut keys to perform certain tasks by using the
shortcut
property. - It takes
Map
of typeLogicalKeyState
. LogicalKeyState
is a set ofLogicalKeyboardKeys
that 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 inshortcut
property. 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
MaterialApp
widget. - 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