S. Sinha - Beginning Flutter 3.0 With Dart. A Beginner To Pro. Learn How To Build Advanced Flutter Apps (2022) (0401-0500)
S. Sinha - Beginning Flutter 3.0 With Dart. A Beginner To Pro. Learn How To Build Advanced Flutter Apps (2022) (0401-0500)
1 import 'dart:ui';
2
3 import 'package:flutter/material.dart';
4
5 class MaterialDesign extends StatelessWidget {
6 const MaterialDesign({Key? key}) : super(key: key);
7
8 @override
9 Widget build(BuildContext context) {
10 return MaterialApp(
11 title: 'Better Flutter - Essential Widgets',
12 home: MDFirstPage(),
13 initialRoute: '/second',
14 onGenerateRoute: _getSecondPageFirst,
15 );
16 }
17
18 Route<dynamic>? _getSecondPageFirst(RouteSettings settings) {
19 if (settings.name != '/second') {
20 return null;
21 }
22
9. Everything about Flutter Navigation and Route 392
23 return MaterialPageRoute<void>(
24 settings: settings,
25 builder: (BuildContext context) => MDSecondPage(),
26 fullscreenDialog: true,
27 );
28 }
29 }
Watch the above code. Although the home property indicates to the first page, the initialRoute
property navigates to the second page.
However, we need to be careful about one thing. It should return a non-null value.
The rest is quite simple. Now we can design our first page and second page.
In any case, the app will open the second page first.
If we want to make this page a log in and registration page, we can design that too.
In addition, if the user doesn’t want to log in or register, she can touch the cross icon. In that case,
the home property comes into effect, opening the first page as usual.
1 import 'package:flutter/material.dart';
2
3 void main() {
4 runApp(const MyApp());
5 }
6
7 class MyApp extends StatelessWidget {
8 const MyApp({Key? key}) : super(key: key);
9
10 // This widget is the root of your application.
11 @override
12 Widget build(BuildContext context) {
13 return MaterialApp(
14 routes: <String, WidgetBuilder>{
15 '/': (BuildContext context) => const HomePage(),
16 },
17 title: 'Flutter Demo',
18 theme: ThemeData(
19 primaryColor: const Color(0xFF3EBACE),
20 backgroundColor: const Color(0xFFF3F5F7),
21 primarySwatch: Colors.indigo,
22 ),
23 );
24 }
25 }
As we can see, the route widget takes us to the Home Page. However, it also carries the context.
Next, we need to go to the second page.
9. Everything about Flutter Navigation and Route 394
Because, it uses the Navigator widget push method. And after that, it passes the same context.
1 import 'package:flutter/material.dart';
2
3 class Category {
4 final String id;
5 final String title;
6 final Color color;
7
8 const Category({
9 required this.id,
10 required this.title,
11 this.color = Colors.orangeAccent,
12 });
13 }
14 And with the category class, we need some dummy category data like the following cod\
15 e.
16
17 import 'package:flutter/material.dart';
9. Everything about Flutter Navigation and Route 397
18
19 import 'category.dart';
20
21 const DUMMY_CATEGORIES = const [
22 Category(
23 id: 'c1',
24 title: 'Health',
25 color: Colors.red,
26 ),
27 Category(
28 id: 'c2',
29 title: 'Wellness',
30 color: Colors.deepOrange,
31 ),
32 Category(
33 id: 'c3',
34 title: 'Politics',
35 color: Colors.black54,
36 ),
37 Category(
38 id: 'c4',
39 title: 'Travel',
40 color: Colors.green,
41 ),
42 Category(
43 id: 'c5',
44 title: 'Internet',
45 color: Colors.yellow,
46 ),
47 Category(
48 id: 'c6',
49 title: 'Lifestyle',
50 color: Colors.indigo,
51 ),
52 Category(
53 id: 'c7',
54 title: 'Headlines',
55 color: Colors.pink,
56 ),
57 Category(
58 id: 'c8',
59 title: 'Sports',
60 color: Colors.orange,
9. Everything about Flutter Navigation and Route 398
61 ),
62 Category(
63 id: 'c9',
64 title: 'Science',
65 color: Colors.blueAccent,
66 ),
67 Category(
68 id: 'c10',
69 title: 'Environemnt',
70 color: Colors.redAccent,
71 ),
72 ];
To display these categories in the opening page, we need route widget which defines the navigation
pattern.
Since the related code is too long, please visit the respective GitHub repository. Above all this
repository also connects you to the book Better Flutter in Leanpub.
This repository will also give an idea how from the current route we move to the detail page.
As we can see that the current route takes us to the detail page where the data are coming from the
dummy News class.
If we take a look at the News class, we will understand how it works.
9. Everything about Flutter Navigation and Route 399
1 enum Nature {
2 hard,
3 soft,
4 }
5
6 class News {
7 final String id;
8 final List<String> categories;
9 final String title;
10 final String detail;
11 final String imageURL;
12 final Nature nature;
13
14 const News({
15 required this.id,
16 required this.categories,
17 required this.title,
18 required this.detail,
19 required this.imageURL,
20 required this.nature,
21 });
22 }
At the same time, we pass the data from page one to the second page like this:
1 body: GridView(
2 gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
3 maxCrossAxisExtent: 200,
4 crossAxisSpacing: 20.0,
5 mainAxisSpacing: 20.0,
6 ),
7 children: DUMMY_CATEGORIES.map(
8 (e) {
9 return AllCategories(
10 id: e.id,
11 title: e.title,
12 color: e.color,
13 );
14 },
15 ).toList(),
16 ),
17
18 // code is incomplete for brevity, please consult the GitHub repository
9. Everything about Flutter Navigation and Route 400
1 routes: {
2 '/': (context) => const FirstPageBody(),
3 '/categories': (context) => SecondPage(),
4 },
As a result it becomes easier for us to catch the data in the second page.
To get the data the modal route class uses a static method “of” that passes the context. And the
context also defines the location of this widget in the widget tree.
In the coming flutter tutorials we’ll discuss more about passing data through route and context.
1 enum Nature {
2 hard,
3 soft,
4 }
5
6 class News {
7 final String id;
8 final List<String> categories;
9 final String title;
10 final String detail;
11 final String imageURL;
12 final Nature nature;
13
14 const News({
15 required this.id,
16 required this.categories,
17 required this.title,
18 required this.detail,
19 required this.imageURL,
20 required this.nature,
21 });
22 }
Next, we’ll use the Enums in the display page as string data. To use Enums as string data we need
to use switch case.
9. Everything about Flutter Navigation and Route 402
Now, at the top of the display page, we can use that Enums like the following images.
1 body: ListView.builder(
2 itemBuilder: (context, index) {
3 return Column(
4 children: [
5 Container(
6 margin: const EdgeInsets.all(10.0),
7 padding: const EdgeInsets.all(10.0),
8 child: Text(
9 natureText,
10 style: const TextStyle(
11 fontSize: 20.0,
12 ),
13 ),
14 ),
15
16 // code is incomplete for brevity
Hopefully, this makes sense. Moreover, we’ll not stop here. We’ll build the News app so that it looks
more attractive than this.
That means, we’ll design our Flutter News app better than this one.
And at the same time, to pass data we’ll use route and navigation in a different way.
1 theme: ThemeData(
2 primarySwatch: Colors.pink,
3 primaryColor: Colors.amber,
4 canvasColor: const Color.fromRGBO(255, 254, 229, 1),
5 fontFamily: 'Raleway',
6 textTheme: ThemeData.light().textTheme.copyWith(
7 bodyText2: const TextStyle(
8 color: Color.fromRGBO(20, 51, 51, 1),
9 ),
10 bodyText1: const TextStyle(
11 color: Color.fromRGBO(20, 51, 51, 1),
12 ),
13 headline6: const TextStyle(
14 fontSize: 20,
15 fontFamily: 'RobotoCondensed',
16 fontWeight: FontWeight.bold,
17 )),
18 ),
For brevity, we cannot display the full code snippet that involves route name and passing arguments.
Please visit the respective code repository in GitHub.
How do we can use this route name? To do that we need to use the route widget in our Material
App widget.
In case, our route name doesn’t work, we can also use a fallback.
1 onUnknownRoute: (settings) {
2 return MaterialPageRoute(
3 builder: (ctx) => const CategoriesScreen(),
4 );
5 },
As a result, if we cannot go the Categories Detail Screen page, and our route name fails, it always
stays in the Home page.
In our News App we can display every category using a dummy data.
In the updated Flutter version, the null checking is mandatory. So keep that in mind. We need to
add an exclamatory sign after the Modal route of method that passes the context.
As a result, whenever we click any category, it takes us to static name of route that we’ve defined
inside the class.
As we can see, in the Health category, we have two news items. If we have a look at the dummy
data, we’ll find that.
1 const dummyNews = [
2 News(
3 id: 'b1',
4 categories: [
5 'c1',
6 'c4',
7 ],
8 title: 'Global Worming fuels disaster',
9
10 ....
11
12 News(
13 id: 'b5',
14 categories: [
9. Everything about Flutter Navigation and Route 407
15 'c1',
16 'c5',
17 ],
18 title: 'Take more outdoor walks',
19
20 // code is incomplete
As in two news items we find this category, therefore, it displays them on the category news screen.
However, to display the route items in the right manner, we take help from a controller.
As a matter of fact, we define the select category method here and push the route name and
arguments here.
To make the whole News App complete we need to do many more things. Consequently that also
makes the route name and passing arguments process complete.
9. Everything about Flutter Navigation and Route 408
1 Route
2 Navigator
3 List and Map
Where should we mention the route? At our Material App widget. The route indicates where we
want to go from the home screen or page.
Not only that, when we move from one screen to another, we’ll also pass the data. So, in the second
screen, we display that data.
Above all, when we go from the home screen, we must use a method that will send us along with
the data. And based on that data we can display other data on the second screen.
To explain the whole process of passing data from one screen to another, we need to show some
code. However, for brevity, we cannot show the full code.
For the full code snippets, please visit the respective GitHub repository.
In the News App stateless widget we define our Material App widget where we use the route widget.
As we can see, the initial route is Categories Screen stateless widget. This widget must return some
data. Here all the categories of the News Items like Health, Politics, Internet and many more.
To get that data we need Category class and dummy data in our model folder. Please visit the GitHub
repository to have an idea.
1 body: GridView(
2 padding: const EdgeInsets.all(25),
3
4 /// the first page displays CategoryItem controller
5 children: dummyCategories
6 .map(
7 (catData) => CategoryItem(
8 id: catData.id,
9 title: catData.title,
10 color: catData.color,
11 ),
12 )
13 .toList(),
14
15 ...
16 // code is not complete
The dummy categories are a few constant data where we’ve defined the id, title and the color of the
category.
1 const dummyCategories = [
2 Category(
3 id: 'c1',
4 title: 'Health',
5 color: Colors.red,
6 ),
7 Category(
8 id: 'c2',
9 title: 'Wellness',
10 color: Colors.deepOrange,
11 ),
12 ....
13 // code is not complete
Actually, the Category Item widget will help to display all the categories and at the same time it will
let us allow to click each category to see what kind of news items belong to that category.
9. Everything about Flutter Navigation and Route 411
We’ve passed all data through the Class Constructor and then we define a method select Category.
Inside that method, Navigator widget uses a chain of static methods through which we pass the
context and a list of arguments.
Because of this widget we can now see all the categories.
However, if we click any category the method fires and push the Navigator to another stateless
widget Category News Screen.
Let us see a few part of Category News Screen widget code.
9. Everything about Flutter Navigation and Route 412
We’ve already sent the arguments as a List. Where we have sent ID and Title of Category class and
dummy category data.
Now according to the News class and dummy data we can now show the News item that belongs
to that category.
Now we can click the title or the special enum to view the News in detail.
1 child: Column(
2 children: <Widget>[
3 Stack(
4 children: <Widget>[
5 ClipRRect(
6 borderRadius: const BorderRadius.only(
7 topLeft: Radius.circular(15),
8 topRight: Radius.circular(15),
9 ),
10 child: Image.network(
11 imageUrl,
12 height: 250,
13 width: double.infinity,
14 fit: BoxFit.cover,
15 ),
16 ),
17 Positioned(
18 bottom: 20,
19 right: 10,
20 child: Container(
21 width: 300,
22 color: Colors.black54,
23 padding: const EdgeInsets.symmetric(
24 vertical: 5,
25 horizontal: 20,
26 ),
27 child: Text(
28 title,
29 style: const TextStyle(
30 fontSize: 26,
31 color: Colors.white,
32 ),
33 softWrap: true,
34 overflow: TextOverflow.fade,
35 ),
36 ),
37 )
9. Everything about Flutter Navigation and Route 415
38 ],
39 ),
40 Padding(
41 padding: const EdgeInsets.all(20),
42 child: Row(
43 mainAxisAlignment: MainAxisAlignment.spaceAround,
44 children: <Widget>[
45 Row(
46 children: <Widget>[
47 const Icon(
48 Icons.work,
49 ),
50 const SizedBox(
51 width: 6,
52 ),
53 Text(natureText),
54 ],
55 ),
56 ],
57 ),
58 ),
59 ],
60 ),
The above method with the help of “on Tap” void function return the “select News” method with
the context as its parameter.
17 return Container(
18 decoration: BoxDecoration(
19 color: Colors.white,
20 border: Border.all(color: Colors.grey),
21 borderRadius: BorderRadius.circular(10),
22 ),
23 margin: const EdgeInsets.all(10),
24 padding: const EdgeInsets.all(10),
25 height: 150,
26 width: 300,
27 child: child,
28 );
29 }
30
31 @override
32 Widget build(BuildContext context) {
33 final mealId = ModalRoute.of(context)!.settings.arguments as String;
34 final selectedNews = dummyNews.firstWhere((meal) => meal.id == mealId);
35 return Scaffold(
36 appBar: AppBar(
37 title: Text(selectedNews.title),
38 ),
39 body: SingleChildScrollView(
40 child: Column(
41 children: <Widget>[
42 SizedBox(
43 height: 300,
44 width: double.infinity,
45 child: Image.network(
46 selectedNews.imageURL,
47 fit: BoxFit.cover,
48 ),
49 ),
50 buildSectionTitle(context, 'News Detail'),
51 Text(selectedNews.detail),
52 ],
53 ),
54 ),
55 );
56 }
57 }
10. More on Flutter UI, List, Map, and
Provider Best Practices
In this chapter we’ll take a close look at everything we’ve learned so far. However, we must remember
one thing, Flutter changes with the time.
The upgraded Flutter comes with more features and at the same time, discards many faetures that
we’ve used before.
Consequently, we’re trying to evolve our knowledge with the time.
²³https://sanjibsinha.com/decoration-in-a-container-in-flutter/
10. More on Flutter UI, List, Map, and Provider Best Practices 419
1 decoration: BoxDecoration(
2 gradient: LinearGradient(
3 colors: [
4 color.withOpacity(0.7),
5 color,
6 ],
7 begin: Alignment.topLeft,
8 end: Alignment.bottomRight,
9 ),
10 borderRadius: BorderRadius.circular(15),
11 ...
12 // the code is incomplete for brevity
1 Container(
2 padding: const EdgeInsets.all(15),
3 child: Text(
4 title,
5 style: Theme.of(context).textTheme.headline6,
6 ),
7 /* decoration: BoxDecoration(
8 gradient: LinearGradient(
9 colors: [
10 color.withOpacity(0.7),
11 color,
12 ],
13 begin: Alignment.topLeft,
14 end: Alignment.bottomRight,
15 ),
16 borderRadius: BorderRadius.circular(15),
17 ), */
18 alignment: Alignment.center,
19 //color: Colors.blue,
20 // we can adjust width and height
21 // to do that we need to commented out the constraints
22 width: 350.00,
23 height: 350.00,
24 // to skew the container
25 // transform: Matrix4.rotationZ(0.1),
26 decoration: BoxDecoration(
27 color: Colors.blue,
28 border: Border.all(
29 color: Colors.red,
30 width: 2.0,
31 style: BorderStyle.solid,
32 ),
33 borderRadius: const BorderRadius.all(Radius.circular(40.0)),
34 boxShadow: const [
35 BoxShadow(
36 color: Colors.black54,
37 blurRadius: 20.0,
38 spreadRadius: 20.0,
39 ),
40 ],
41 gradient: const LinearGradient(
42 begin: Alignment.centerLeft,
43 end: Alignment.centerRight,
10. More on Flutter UI, List, Map, and Provider Best Practices 421
44 colors: [
45 Colors.red,
46 Colors.white,
47 ],
48 ),
49 ),
50 ),
51 ...
52 // the code is incomplete for brevity
To outline, we’ve used border property and supply the color outline, which is red.
In the same vein, we’ve also modified gradient property, changed the box shadow and used border
radius.
However, we’ve commented out the transform property only.
Why?
Because we will use that to tweak the look later.
1 transform: Matrix4.rotationZ(0.1),
You can read more about the transform class and widget in Flutter documentation.
In addition, the TextSpan widget has a Gesture Detector property called recognizer. Using recognizer
we can easily build a navigation link in that particular part of paragraph.
In short, RichText widget gives us multiple advantages to style a paragraph of Text.
How we can do that?
We’ll see in a minute.
1 import 'package:flutter/gestures.dart';
2 import 'package:flutter/material.dart';
3 import 'package:news_app_extended/views/local_news.dart';
4
5 class FavoritesScreen extends StatelessWidget {
6 const FavoritesScreen({Key? key}) : super(key: key);
7
8 @override
9 Widget build(BuildContext context) {
10 return _richTextController(context);
11 }
12 }
13
14 Widget _richTextController(BuildContext context) => Container(
15 color: Colors.black12,
16 padding: const EdgeInsets.all(10),
17 child: Center(
18 child: RichText(
19 text: TextSpan(
20 text: 'This News App is inspired by the principle of free'
21 ' Journalism. You can select ',
10. More on Flutter UI, List, Map, and Provider Best Practices 423
65 style: TextStyle(
66 color: Colors.green,
67 fontSize: 20,
68 fontWeight: FontWeight.bold,
69 ),
70 )
71 ],
72 ),
73 ),
74 ),
75 );
76
77 // Although we have the full code snippet of Favorites screen, but to understand the\
78 architecture of News App, please visit the respective GitHub repository
If we don’t get a glimpse of this page, we cannot understand how RichText widget helps us to add
styling to a whole paragraph of text.
1 TextSpan(
2 text: 'Local',
3 style: const TextStyle(
4 color: Colors.deepOrange,
5 fontSize: 20,
6 fontWeight: FontWeight.bold,
7 decoration: TextDecoration.underline,
8 ),
9 recognizer: TapGestureRecognizer()
10 ..onTap = () {
11 Navigator.of(context).pushNamed(LocalNews.routeName);
12 },
13 ),
The above code snippet allows us to add the navigation link to another page, or screen.
10. More on Flutter UI, List, Map, and Provider Best Practices 425
As an example, we’ve only created a Local News Page, where we can navigate from this page.
To do that, we have added a route name in Material Design page first.
1 routes: {
2 '/': (ctx) => const TabsScreen(),
3 CategoryNewsScreen.routeName: (ctx) => const CategoryNewsScreen(),
4 NewsDetailScreen.routeName: (ctx) => const NewsDetailScreen(),
5 LocalNews.routeName: (ctx) => const LocalNews(),
6 },
After that, we’ve added a static route name property in the Local News screen widget.
1 recognizer: TapGestureRecognizer()
2 ..onTap = () {
3 Navigator.of(context).pushNamed(LocalNews.routeName);
4 },
Now we can tap the link and the TapGestureRecognizer handles the event.
And, finally we reach to a new screen.
To sum up, with the help of RichText widget and by returning several TextSpan widgets we’ve added
more styling to our News App.
To have a look at the screenshots we’ve used in this section, please visit this LINK²⁴
1 import 'package:flutter/material.dart';
2
3 void main() {
4 runApp(MyApp());
5 }
6
7 class MyApp extends StatelessWidget {
8 // This widget is the root of your application.
9 @override
10 Widget build(BuildContext context) {
11 return MaterialApp(
12 title: 'Flutter Demo',
13 theme: ThemeData(
14 primarySwatch: Colors.blue,
15 ),
16 home: MyHomePage(title: 'Flutter Demo Home Page'),
17 );
18 }
19 }
20
21 class MyHomePage extends StatefulWidget {
22 MyHomePage({Key? key, required this.title}) : super(key: key);
23
24 final String title;
25
26 @override
27 _MyHomePageState createState() => _MyHomePageState();
28 }
10. More on Flutter UI, List, Map, and Provider Best Practices 427
29
30 class _MyHomePageState extends State<MyHomePage> {
31 int _counter = 0;
32
33 void _incrementCounter() {
34 setState(() {
35 _counter++;
36 });
37 }
38
39 @override
40 Widget build(BuildContext context) {
41
42 return Scaffold(
43 appBar: AppBar(
44 title: Text(widget.title),
45 ),
46 body: Center(
47 child: Column(
48 mainAxisAlignment: MainAxisAlignment.center,
49 children: <Widget>[
50 Text(
51 'You have pushed the button this many times:',
52 ),
53 Text(
54 '$_counter',
55 style: Theme.of(context).textTheme.headline4,
56 ),
57 ],
58 ),
59 ),
60 floatingActionButton: FloatingActionButton(
61 onPressed: _incrementCounter,
62 tooltip: 'Increment',
63 child: Icon(Icons.add),
64 ),
65 );
66 }
67 }
Let us press the button for five times and side by side watch the flutter performance.
Now, let us take a closer look and the screenshot looks like the following image.
10. More on Flutter UI, List, Map, and Provider Best Practices 428
It clearly shows that after pressing the button for five times, on the far right side of the screen each
widget shows the number 6.
That means a stateful way always requires more memory than the stateless way.
In fact that is clearly visible in the first image, because the red colored bar represents memory usage.
This is the reason why we should use a combination of Provider package and stateless widgets.
It makes our flutter app more performant.
1 dependencies:
2 provider: ^6.0.0
Secondly, we import the provider package just like any other packages.
1 import 'package:provider/provider.dart';
Finally, we need to notify the listeners that we’re going to change the state.
We can define that method in our model folder, from where our data come.
1 import 'package:flutter/foundation.dart';
2 import 'package:flutter/material.dart';
3 import 'package:provider/provider.dart';
4
5 class Counter with ChangeNotifier {
6 int _count = 0;
7
8 int get count => _count;
9
10 void increment() {
11 _count++;
12 notifyListeners();
13 }
14 }
Next, to run the app we must place providers above the Counter App. Not only that, we need to use
other methods so that we can read and watch the change.
10. More on Flutter UI, List, Map, and Provider Best Practices 429
1 import 'package:flutter/foundation.dart';
2 import 'package:flutter/material.dart';
3 import 'package:provider/provider.dart';
4 import 'package:shop_app/models/counter.dart';
5 void main() {
6 runApp(
7 MultiProvider(
8 providers: [
9 ChangeNotifierProvider(create: (_) => Counter()),
10 ],
11 child: const CounterApp(),
12 ),
13 );
14 }
15
16 class CounterApp extends StatelessWidget {
17 const CounterApp({Key? key}) : super(key: key);
18 @override
19 Widget build(BuildContext context) {
20 return const MaterialApp(
21 home: CounterHomePage(),
22 );
23 }
24 }
25
26 class CounterHomePage extends StatelessWidget {
27 const CounterHomePage({Key? key}) : super(key: key);
28 @override
29 Widget build(BuildContext context) {
30 final subtree = ConstantWidget(
31 child: const Text("Hello World")
32 );
33 final anotherSubtree = _widget();
34 return Scaffold(
35 appBar: AppBar(
36 title: const Text('A Stateless Counter App'),
37 ),
38 body: Center(
39 child: Column(
40 mainAxisSize: MainAxisSize.min,
41 mainAxisAlignment: MainAxisAlignment.center,
42 children: <Widget> [
43 Text('You have pushed the button this many times:'),
10. More on Flutter UI, List, Map, and Provider Best Practices 430
44 Text(
45 '${context.watch<Counter>().count}',
46 key: const Key('counterState'),
47 style: Theme.of(context).textTheme.headline4,
48 ),
49 SizedBox(width: 10.0,),
50 subtree,
51 SizedBox(width: 10.0,),
52 anotherSubtree,
53 ],
54 ),
55 ),
56 floatingActionButton: FloatingActionButton(
57 key: const Key('increment_floatingActionButton'),
58 onPressed: () => context.read<Counter>().increment(),
59 tooltip: 'Increment',
60 child: const Icon(Icons.add),
61 ),
62 );
63 }
64 /// Just to test rebuild
65 Widget _widget() => const Text('Hi');
66
67 ConstantWidget({required Text child}) {
68 /// Just to test rebuild
69 return Text('Hello');
70 }
71 }
1 '${context.watch<Counter>().count}',
2 onPressed: () => context.read<Counter>().increment(),
We call context.watch to make the count property of Type Counter rebuild when the counter
changes.
However, inside the floating action button on press method we return context.read.
It stops the floating action button widget from rebuilding itself.
Now, as a result, we get a different screenshot when we press the button.
With the help of Provider package, we have kept the requirement of memory usage much lower than
the stateful widget.
As we can see, there are no red bars.
Not only that, now we can take a closer look at the flutter performance.
As we keep on pressing the button, only two widgets rebuild themselves without affecting the others.
Moreover, now the state change centers on only two text widgets.
The parent tree remains unaffected.
²⁶https://sanjibsinha.com/gridtile-flutter/
10. More on Flutter UI, List, Map, and Provider Best Practices 432
As we can see, the Books overview screen displays the book items using three main stateless widgets.
They are GridTile, GridTileBar and GridView.
We’re going to the glimpse of the code snippets. For full code, please visit the respective GitHub
repository.
Firstly take a look at how we have used GridTile.
18 product.imageUrl,
19 fit: BoxFit.cover,
20 ),
21 ),
22 footer: GridTileBar(
23 backgroundColor: Colors.black87,
24 leading: Consumer<Book>(
25 builder: (ctx, product, _) => IconButton(
26 icon: Icon(
27 product.isFavorite ? Icons.favorite : Icons.favorite_border,
28 ),
29 color: Theme.of(context).primaryColor,
30 onPressed: () {
31 product.toggleFavoriteStatus();
32 },
33 ),
34 ),
35 title: Text(
36 product.title,
37 textAlign: TextAlign.center,
38 ),
39 trailing: IconButton(
40 icon: const Icon(
41 Icons.shopping_cart,
42 ),
43 onPressed: () {},
44 color: Theme.of(context).primaryColor,
45 ),
46 ),
47 ),
48 );
49 }
50 }
As we mentioned earlier, any GridTile must have a child and a GridTileBar widget either in header
or footer.
In the above code, we’ve placed it in the footer.
Inside the GridTileBar, we’ve used two IconButton widgets. One represents a favorite symbol and
the other displays a shopping cart.
Since we can use each IconButton to press and fire a function, therefore, we’ve used notify listeners
to change the color of favorite sign.
10. More on Flutter UI, List, Map, and Provider Best Practices 434
As a result, we can have an image like the following where we’ve clicked favorites sign for three
times.
We have managed the state in the stateless widgets by using Provider package.
Certainly we’ll discuss that in a separate article. But before that, let us see how we’ve used GridView
widget to display the clicked favorite book items.
1 import 'package:flutter/foundation.dart';
2
3 class Book with ChangeNotifier {
4 final String id;
5 final String title;
6 final String description;
7 final double price;
8 final String imageUrl;
9 bool isFavorite;
10
11 Book({
12 required this.id,
13 required this.title,
14 required this.description,
15 required this.price,
16 required this.imageUrl,
17 this.isFavorite = false,
18 });
19
20 void toggleFavoriteStatus() {
21 isFavorite = !isFavorite;
22 notifyListeners();
23 }
24 }
²⁷https://sanjibsinha.com/change-notifier-provider-flutter/
10. More on Flutter UI, List, Map, and Provider Best Practices 436
The above code means when we click the favorite icon on the Book item, the ChangeNotifier notify
the listeners.
Now, question is what is the correct way to create a ChangeNotifier?
According to the documentation provided by the Provider package ChangeNotifierProvider, the
following way is the correct way.
1 void main() {
2 runApp(
3 MultiProvider(
4 providers: [
5 ChangeNotifierProvider(
6 create: (_) => Books(),
7 ),
8 ChangeNotifierProvider(
9 create: (_) => Cart(),
10 ),
11 ChangeNotifierProvider(
12 create: (_) => Orders(),
13 ),
14 ],
15 child: const BookApp(),
16 ),
17 );
18 }
19 class BookApp extends StatelessWidget {
20 const BookApp({Key? key}) : super(key: key);
21
22 @override
23 Widget build(BuildContext context) {
24 return MaterialApp(
25 title: 'Book Shop',
26 theme: ThemeData(
27 primarySwatch: Colors.purple,
28 primaryColor: Colors.deepOrange,
29 ),
30 home: const BooksOverviewScreen(),
31 routes: {
32 BookDetailScreen.routeName: (ctx) => const BookDetailScreen(),
33 },
34 );
35 }
36 }
10. More on Flutter UI, List, Map, and Provider Best Practices 437
37
38 // for full code please visit Book Shopping Cart flutter app
We must place the Providers above the Book App. As a result, we can read and watch the change of
state in Book Overview screen.
From Book overview screen we can go the book detail page quite easily now.
The book detail screen works on product ID.
10. More on Flutter UI, List, Map, and Provider Best Practices 438
44 loadedProduct.description,
45 textAlign: TextAlign.center,
46 softWrap: true,
47 ),
48 )
49 ],
50 ),
51 ),
52 );
53 }
54 }
55 // for full code please visit Book Shopping Cart flutter app
If we take a detail look at the Book Shopping Cart flutter app, we will find how ChangeNotifier-
Provider creates the ChangeNotifier and how that notifies the listeners where it is needed.
Now, our unique flutter fonts has been added globally. We can add any one of these flutter font
anywhere in our app.
Consequently, we can add this font to our Book or product detail screen. What font has flutter?
If we don’t want to change the look, flutter manages it.
That’s not a problem.
But, if we want to change the flutter font, then we have to through the above steps that we’ve already
mentioned.
Moreover, to add some special flutter font effects to any page or screen of our flutter app, we need
to mention our unique flutter font name inside TestStyle widget.
1 import 'package:flutter/material.dart';
2
3 import 'package:provider/provider.dart';
4
5 import '../models/books.dart';
6
7 class BookDetailScreen extends StatelessWidget {
8 static const routeName = '/product-detail';
9
10 const BookDetailScreen({Key? key}) : super(key: key);
11
12 @override
13 Widget build(BuildContext context) {
14 final productId =
10. More on Flutter UI, List, Map, and Provider Best Practices 441
58 ),
59 const SizedBox(
60 height: 10,
61 ),
62 Container(
63 padding: const EdgeInsets.symmetric(horizontal: 10),
64 width: double.infinity,
65 child: Text(
66 loadedProduct.description,
67 textAlign: TextAlign.center,
68 softWrap: true,
69 style: TextStyle(
70 fontFamily: 'Anton',
71 fontSize: 20.0,
72 ),
73 ),
74 ),
75 ],
76 ),
77 ),
78 );
79 }
80 }
81 // for full code please visit respective GitHub Repository.
As we can see that in the above code snippet, these two Container widget are responsible to display
the flutter font Anton and Lato.
Watch this part carefully:
1 Container(
2 padding: const EdgeInsets.symmetric(horizontal: 10),
3 width: double.infinity,
4 child: Text(
5 loadedProduct.title,
6 textAlign: TextAlign.center,
7 softWrap: true,
8 style: TextStyle(
9 fontFamily: 'Lato',
10 fontSize: 30.0,
11 ),
12 ),
13 ),
10. More on Flutter UI, List, Map, and Provider Best Practices 443
For the product title we’ve used Lato. And, for the product description, we’ve used Anton.
As a result, we’ve got this unique look of the product detail screen.
Please compare this image with the previous image we’ve displayed in this page before.
The unique flutter font has changed the look of the product detail screen completely.
Now, let us start learning how to store persistent data in flutter. At the very beginning, let me tell
you, the full code snippet is available in the respective GitHub Repository.
We cannot use full code snippets. As they are too long. Therefore, we must use part of them for
brevity.
Above all, we’ll follow MVC or Model-view-controller pattern.
So data comes from the models folder.
1 import 'package:flutter/foundation.dart';
2
3 class Book with ChangeNotifier {
4 final String id;
5 final String title;
6 final String description;
7 final double price;
8 final String imageUrl;
9 bool isFavorite;
10
11 Book({
12 required this.id,
13 required this.title,
14 required this.description,
15 required this.price,
16 required this.imageUrl,
17 this.isFavorite = false,
18 });
19
20 void toggleFavoriteStatus() {
21 isFavorite = !isFavorite;
22 notifyListeners();
23 }
24 }
In the above code, we have defined a Book class whose constructor would pass different properties
including a boolean value isFavorite.
We’ve used Dart mixin to use ChangeNotifier, so that we can use ChangeNotifierProvider from
provider package.
Now, we must have another data model which is actually our local storage. Based on the product
ID, we can retrieve data from that storage.
10. More on Flutter UI, List, Map, and Provider Best Practices 445
1 import 'package:flutter/material.dart';
2
3 import 'book.dart';
4
5 class Books with ChangeNotifier {
6 final List<Book> _items = [
7 Book(
8 id: 'p1',
9 title: 'Beginning Flutter With Dart',
10 description: 'You can learn Flutter as well Dart.',
11 price: 9.99,
12 imageUrl:
13 'https://cdn.pixabay.com/photo/2014/09/05/18/32/old-books-436498_960_720.jpg\
14 ',
15 ),
16 Book(
17 id: 'p2',
18 title: 'Flutter State Management',
19 description: 'Everything you should know about Flutter State.',
20 price: 9.99,
21 imageUrl:
22 'https://cdn.pixabay.com/photo/2016/09/10/17/18/book-1659717_960_720.jpg',
23 ),
24 ...
25 // code is incomplete for brevity
1 void main() {
2 runApp(
3 MultiProvider(
4 providers: [
5 ChangeNotifierProvider(
6 create: (_) => Books(),
7 ),
8 ChangeNotifierProvider(
9 create: (_) => Cart(),
10 ),
11 ChangeNotifierProvider(
12 create: (_) => Orders(),
13 ),
14 ],
15 child: const BookApp(),
16 ),
17 );
18 }
19
20 class BookApp extends StatelessWidget {
21 const BookApp({Key? key}) : super(key: key);
22
23 @override
24 Widget build(BuildContext context) {
25 return MaterialApp(
26 title: 'Book Shop',
27 theme: ThemeData(
28 primarySwatch: Colors.purple,
29 primaryColor: Colors.deepOrange,
30 ),
31 home: const BooksOverviewScreen(),
32 routes: {
33 BookDetailScreen.routeName: (ctx) => const BookDetailScreen(),
34 //CartScreen.routeName: (ctx) => CartScreen(),
35 },
36 );
37 }
38 }
The home page route points to the books overview screen. However, with the help of other controllers
we will be able to navigate to the book detail page,or screen.
Now, in our material design we’ve defined the route, so one click on the image will take us to the
respective book detail screen.
10. More on Flutter UI, List, Map, and Provider Best Practices 447
In our controllers folder, we have two widgets that act as consumers who will listen to the
notification sent by the ChangeNotifier.
The following code snippet will give us an idea.
As we can see, the body of the books overview screen points to books grid widget. Therefore, we
can have a look at that widget in controllers folder.
1 @override
2 Widget build(BuildContext context) {
3 final productsData = Provider.of<Books>(context);
4 final products = showFavs ? productsData.favoriteItems : productsData.items;
5 return GridView.builder(
6 padding: const EdgeInsets.all(10.0),
7 itemCount: products.length,
8 itemBuilder: (ctx, i) => ChangeNotifierProvider.value(
9 // builder: (c) => products[i],
10 value: products[i],
11 child: const BookItem(
12
13 ),
14 ),
15 ..
16 // incomplete code for brevity
To persist data, we’ve used Provider.of method which passes context as parameter and uses the type
Book class.
Here that plays the crucial role.
Not only that, the following part of the above code also helps us to persist data.
Although the Book App is in primary stage, still it’s worth taking a look at the GitHub Repository.
Moreover, we’ll keep building that e-commerce Book App in the future.
So, stay tuned and get in touch with all Flutter articles that always gives you updated information
on Flutter.
10. More on Flutter UI, List, Map, and Provider Best Practices 449
1 class Book {
2 final String id;
3 final String title;
4 final String description;
5 final double price;
6 final String imageUrl;
7 bool isFavorite;
8
9 Book({
10 required this.id,
11 required this.title,
12 required this.description,
13 required this.price,
14 required this.imageUrl,
15 this.isFavorite = false,
16 });
17 }
We keep this data defining class in our models folder. Now, to add some using the constructor of
this Book class, we can adopt two ways.
Either we can keep that data in our models folder, or we can create a list of books in our stateless
book overview screen.
³⁰https://sanjibsinha.com/data-model-flutter/
10. More on Flutter UI, List, Map, and Provider Best Practices 450
1 import 'package:flutter/material.dart';
2 import 'package:shop_app_primary/data-model/controllers/book_item.dart';
3 import 'package:shop_app_primary/data-model/models/book.dart';
4
5 class BooksOverviewScreen extends StatelessWidget {
6 BooksOverviewScreen({Key? key}) : super(key: key);
7
8 final List<Book> books = [
9 Book(
10 id: 'p1',
11 title: 'Beginning Flutter With Dart',
12 description: 'You can learn Flutter as well Dart.',
13 price: 9.99,
14 imageUrl:
15 'https://cdn.pixabay.com/photo/2014/09/05/18/32/old-books-436498_960_720.jpg\
16 ',
17 ),
18 Book(
19 id: 'p2',
20 title: 'Flutter State Management',
21 description: 'Everything you should know about Flutter State.',
22 price: 9.99,
23 imageUrl:
24 'https://cdn.pixabay.com/photo/2016/09/10/17/18/book-1659717_960_720.jpg',
25 ),
26 Book(
27 id: 'p3',
28 title: 'WordPress Coding',
29 description:
30 'WordPress coding is not difficult, in fact it is interesting.',
31 price: 9.99,
32 imageUrl:
10. More on Flutter UI, List, Map, and Provider Best Practices 451
33 'https://cdn.pixabay.com/photo/2015/11/19/21/10/glasses-1052010_960_720.jpg',
34 ),
35 Book(
36 id: 'p4',
37 title: 'PHP 8 Standard Library',
38 description: 'PHP 8 Standard Library has made developers life easier.',
39 price: 9.99,
40 imageUrl:
41 'https://cdn.pixabay.com/photo/2015/09/05/21/51/reading-925589_960_720.jpg',
42 ),
43 Book(
44 id: 'p5',
45 title: 'Better Flutter',
46 description:
47 'Learn all the necessary concepts of building a Flutter App.',
48 price: 9.99,
49 imageUrl:
50 'https://cdn.pixabay.com/photo/2015/09/05/07/28/writing-923882_960_720.jpg',
51 ),
52 Book(
53 id: 'p6',
54 title: 'Discrete Mathematical Data Structures and Algorithm',
55 description:
56 'Discrete mathematical concepts are necessary to learn Data Structures and A\
57 lgorithm.',
58 price: 9.99,
59 imageUrl:
60 'https://cdn.pixabay.com/photo/2015/11/19/21/14/glasses-1052023_960_720.jpg',
61 ),
62 ];
63
64 @override
65 Widget build(BuildContext context) {
66 return Scaffold(
67 appBar: AppBar(
68 title: Text('MyShop'),
69 ),
70 body: GridView.builder(
71 padding: const EdgeInsets.all(10.0),
72 itemCount: books.length,
73 itemBuilder: (ctx, i) => BookItem(
74 books[i].id,
75 books[i].title,
10. More on Flutter UI, List, Map, and Provider Best Practices 452
76 books[i].imageUrl,
77 ),
78 gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
79 crossAxisCount: 2,
80 childAspectRatio: 3 / 2,
81 crossAxisSpacing: 10,
82 mainAxisSpacing: 10,
83 ),
84 ),
85 );
86 }
87 }
Although the code snippet is a little bit longer but that is due to the list of books we’ve added as our
local storage.
However, display of data has been managed by another custom widget BookItem.
Watch this part of code:
1 body: GridView.builder(
2 padding: const EdgeInsets.all(10.0),
3 itemCount: books.length,
4 itemBuilder: (ctx, i) => BookItem(
5 books[i].id,
6 books[i].title,
7 books[i].imageUrl,
8 ),
The item builder of Grid view builder passes the context and the index of the list. Next, through
BookItem stateless widget constructor we can pass that data.
Now, we keep that stateless custom widget BookItem in our controllers folder.
In addition, the BookItem widget extracts the data from the data model and sends that back to the
book overview screen.
All together, the MVC structure in our small flutter app works fine. The data comes from the model
class, the controller extracts that data and sends that data to the view.
The picture of data flow becomes clear.
Finally we take a look at the controller, which extracts data from the model class.
1 import 'package:flutter/material.dart';
2
3 class BookItem extends StatelessWidget {
4 final String id;
5 final String title;
6 final String imageUrl;
7
8 BookItem(this.id, this.title, this.imageUrl);
9
10 @override
11 Widget build(BuildContext context) {
12 return ClipRRect(
13 borderRadius: BorderRadius.circular(10),
14 child: GridTile(
15 child: GestureDetector(
16 onTap: () {},
17 child: Image.network(
18 imageUrl,
19 fit: BoxFit.cover,
20 ),
21 ),
22 footer: GridTileBar(
23 backgroundColor: Colors.black87,
24 leading: IconButton(
25 icon: const Icon(Icons.favorite),
26 color: Theme.of(context).primaryColor,
27 onPressed: () {},
28 ),
29 title: Text(
30 title,
31 textAlign: TextAlign.center,
32 ),
33 trailing: IconButton(
34 icon: const Icon(
35 Icons.shopping_cart,
36 ),
37 onPressed: () {},
10. More on Flutter UI, List, Map, and Provider Best Practices 454
38 color: Theme.of(context).primaryColor,
39 ),
40 ),
41 ),
42 );
43 }
44 }
1 import 'package:flutter/foundation.dart';
2
3 class Book with ChangeNotifier {
4 final String id;
5 final String title;
6 final String description;
7 final double price;
8 final String imageUrl;
9 bool isFavorite;
10
11 Book({
12 required this.id,
13 required this.title,
14 required this.description,
15 required this.price,
³¹https://sanjibsinha.com/pass-data-between-screens-flutter/
10. More on Flutter UI, List, Map, and Provider Best Practices 455
16 required this.imageUrl,
17 this.isFavorite = false,
18 });
19
20 void toggleFavoriteStatus() {
21 isFavorite = !isFavorite;
22 notifyListeners();
23 }
24 }
Next, we need a local storage of data where we must fill in with some dummy data.
For images, we could have used local assets. However, we prefer image network instead.
Consequently, let’s get a glimpse of the dummy data.
1 import 'package:flutter/material.dart';
2
3 import 'book.dart';
4
5 class Books with ChangeNotifier {
6 final List<Book> _items = [
7 Book(
8 id: 'p1',
9 title: 'Beginning Flutter With Dart',
10 description: 'You can learn Flutter as well Dart.',
11 price: 9.99,
12 imageUrl:
13 'https://cdn.pixabay.com/photo/2014/09/05/18/32/old-books-436498_960_720.jpg\
14 ',
15 ),
16 Book(
17 id: 'p2',
18 title: 'Flutter State Management',
19 description: 'Everything you should know about Flutter State.',
20 price: 9.99,
21 imageUrl:
22 'https://cdn.pixabay.com/photo/2016/09/10/17/18/book-1659717_960_720.jpg',
23 ),
24 ...
25 // code incomplete for brevity
Now, we can have a books overview screen that displays all the book items. Similarly, if we click
any image that will navigate to the detail screen.
10. More on Flutter UI, List, Map, and Provider Best Practices 456
The Books Grid widget, on the other hand, have products data and a child widget Book Items where
it passes the data.
As a result, the Books item child widget plays the crucial role to get the products id and pass it to
the book detail screen.
Let’ see how it has done that.
10. More on Flutter UI, List, Map, and Provider Best Practices 457
1 @override
2 Widget build(BuildContext context) {
3 final product = Provider.of<Book>(context, listen: false);
4 return ClipRRect(
5 borderRadius: BorderRadius.circular(10),
6 child: GridTile(
7 child: GestureDetector(
8 onTap: () {
9 Navigator.of(context).pushNamed(
10 BookDetailScreen.routeName,
11 arguments: product.id,
12 );
13 },
14 child: Image.network(
15 product.imageUrl,
16 fit: BoxFit.cover,
17 ),
18 ),
19 ...
In the Books item widget on tap method, we use Navigator push named method, and as an argument
we’ve passed the product ID.
How did we get the product ID?
Very simple.
Since as a type Provider of method has used Book class, now we can get any book property anywhere
in our flutter app.
1 import 'package:flutter/material.dart';
2
3 import 'package:provider/provider.dart';
4
5 import '../models/books.dart';
6
7 class BookDetailScreen extends StatelessWidget {
8 static const routeName = '/product-detail';
9
10 const BookDetailScreen({Key? key}) : super(key: key);
11
12 @override
13 Widget build(BuildContext context) {
14 final productId =
15 ModalRoute.of(context)!.settings.arguments as String; // is the id!
16 final loadedProduct = Provider.of<Books>(
17 context,
18 listen: false,
19 ).findById(productId);
20 return Scaffold(
21 appBar: AppBar(
22 title: Text(loadedProduct.title),
23 ),
24 body: SingleChildScrollView(
25 child: Column(
26 children: <Widget>[
27 SizedBox(
28 height: 300,
29 width: double.infinity,
30 child: Image.network(
31 loadedProduct.imageUrl,
32 fit: BoxFit.cover,
33 ),
34 ),
35 const SizedBox(height: 10),
36 Text(
37 '\$${loadedProduct.price}',
38 style: const TextStyle(
39 color: Colors.grey,
40 fontSize: 20,
41 ),
42 ),
43 const SizedBox(
10. More on Flutter UI, List, Map, and Provider Best Practices 459
44 height: 10,
45 ),
46 Container(
47 padding: const EdgeInsets.symmetric(horizontal: 10),
48 width: double.infinity,
49 child: Text(
50 loadedProduct.title,
51 textAlign: TextAlign.center,
52 softWrap: true,
53 style: TextStyle(
54 fontFamily: 'Lato',
55 fontSize: 30.0,
56 ),
57 ),
58 ),
59 const SizedBox(
60 height: 10,
61 ),
62 Container(
63 padding: const EdgeInsets.symmetric(horizontal: 10),
64 width: double.infinity,
65 child: Text(
66 loadedProduct.description,
67 textAlign: TextAlign.center,
68 softWrap: true,
69 style: TextStyle(
70 fontFamily: 'Anton',
71 fontSize: 20.0,
72 ),
73 ),
74 ),
75 ],
76 ),
77 ),
78 );
79 }
80 }
In the above code, these two lines have played crucial roles. In fact, due to these lines, we can display
any book based on the product ID.
10. More on Flutter UI, List, Map, and Provider Best Practices 460
1 final productId =
2 ModalRoute.of(context)!.settings.arguments as String; // is the id!
3 final loadedProduct = Provider.of<Books>(
4 context,
5 listen: false,
6 ).findById(productId);
1 class Book {
2 final String id;
3 final String title;
4 final String description;
5 final double price;
6 final String imageUrl;
7 bool isFavorite;
8
9 Book({
10 required this.id,
11 required this.title,
12 required this.description,
13 required this.price,
14 required this.imageUrl,
15 this.isFavorite = false,
16 });
17 }
³²https://sanjibsinha.com/pass-data-with-provider-flutter/
10. More on Flutter UI, List, Map, and Provider Best Practices 461
Now, based on that, we have some dummy data also so we can display the Book items on the books
overview screen.
The above code is quite simple. Because we pass data between classes through class constructor.
The following code snippet handles that passage of data.
Now we can have a Book Item controller, where we can request that data through class constructor.
In addition, we can display the title, image etc.
1 import 'package:flutter/material.dart';
2
3 class BookItem extends StatelessWidget {
4 final String id;
5 final String title;
6 final String imageUrl;
7
8 BookItem(this.id, this.title, this.imageUrl);
9
10 @override
11 Widget build(BuildContext context) {
12 return ClipRRect(
13 borderRadius: BorderRadius.circular(10),
14 child: GridTile(
15 child: GestureDetector(
16 onTap: () {},
17 child: Image.network(
18 imageUrl,
19 fit: BoxFit.cover,
20 ),
21 ),
22 footer: GridTileBar(
23 backgroundColor: Colors.black87,
24 leading: IconButton(
25 icon: const Icon(Icons.favorite),
26 color: Theme.of(context).primaryColor,
27 onPressed: () {},
28 ),
29 title: Text(
30 title,
10. More on Flutter UI, List, Map, and Provider Best Practices 464
31 textAlign: TextAlign.center,
32 ),
33 trailing: IconButton(
34 icon: const Icon(
35 Icons.shopping_cart,
36 ),
37 onPressed: () {},
38 color: Theme.of(context).primaryColor,
39 ),
40 ),
41 ),
42 );
43 }
44 }
However, the above data model represents the whole book items. Not a single book.
How we can achieve that?
Here Provider package, and ChangeNotifierProvider come to our rescue.
If we add Provider package ChangeNotifierProvider with our book class, we can easily notify the
listener widget in our widget tree.
1 import 'package:flutter/foundation.dart';
2
3 class Book with ChangeNotifier {
4 final String id;
5 final String title;
6 final String description;
7 final double price;
8 final String imageUrl;
9 bool isFavorite;
10
11 Book({
12 required this.id,
13 required this.title,
14 required this.description,
15 required this.price,
16 required this.imageUrl,
17 this.isFavorite = false,
18 });
19 }
1 import 'package:flutter/material.dart';
2 import 'package:provider/provider.dart';
3
4 import '../views/book_detail_screen.dart';
5 import '../models/book.dart';
6
7 class BookItem extends StatelessWidget {
8 const BookItem({Key? key}) : super(key: key);
9
10 @override
11 Widget build(BuildContext context) {
12 final product = Provider.of<Book>(context, listen: false);
13 return ClipRRect(
14 borderRadius: BorderRadius.circular(10),
15 child: GridTile(
16 child: GestureDetector(
17 onTap: () {
18 Navigator.of(context).pushNamed(
19 BookDetailScreen.routeName,
10. More on Flutter UI, List, Map, and Provider Best Practices 466
20 arguments: product.id,
21 );
22 },
23 child: Image.network(
24 product.imageUrl,
25 fit: BoxFit.cover,
26 ),
27 ),
28 footer: GridTileBar(
29 backgroundColor: Colors.black87,
30 leading: Consumer<Book>(
31 builder: (ctx, product, _) => IconButton(
32 icon: Icon(
33 product.isFavorite ? Icons.favorite : Icons.favorite_border,
34 ),
35 color: Theme.of(context).primaryColor,
36 onPressed: () {
37 product.toggleFavoriteStatus();
38 },
39 ),
40 ),
41 title: Text(
42 product.title,
43 textAlign: TextAlign.center,
44 ),
45 trailing: IconButton(
46 icon: const Icon(
47 Icons.shopping_cart,
48 ),
49 onPressed: () {},
50 color: Theme.of(context).primaryColor,
51 ),
52 ),
53 ),
54 );
55 }
56 }
And in the Books overview screen we also have not sent all books data through Book Item
constructor. Instead, we’ve used another widget Book Grid as the body.
After that, in that Book Grid widget, we return a GridView builder like the following.
Now as a ChangeNotifierProvider value we pass one product object from the instantiated books or
products.
This part is little bit tricky, but based on that the Book Item widget now can provide one product
object as an argument, while navigating to the books detail page.
1 child: GridTile(
2 child: GestureDetector(
3 onTap: () {
4 Navigator.of(context).pushNamed(
5 BookDetailScreen.routeName,
6 arguments: product.id,
7 );
8 },
9 child: Image.network(
10 product.imageUrl,
11 fit: BoxFit.cover,
12 ),
13 ),
Since, we’ve building this Book shopping cart together, let us keep in touch with the further progress.
Until now, you’ll get the full code snippet at this GitHub Repository.
10. More on Flutter UI, List, Map, and Provider Best Practices 468
1 import 'package:flutter/foundation.dart';
2
3 class Product with ChangeNotifier {
4 final String id;
5 final String title;
6 final String description;
7 final double price;
8 final String imageUrl;
9
10 Product({
11 required this.id,
12 required this.title,
13 required this.description,
14 required this.price,
15 required this.imageUrl,
³³https://sanjibsinha.com/provider-flutter-example/
10. More on Flutter UI, List, Map, and Provider Best Practices 469
16 });
17 }
The Products file is too long, so we cut it short for brevity. The list of products has actually
instantiated the product object several times.
1 import 'package:flutter/material.dart';
2
3 import 'product.dart';
4
5 class Products with ChangeNotifier {
6 final List<Product> products = [
7 Product(
8 id: 'p1',
9 title: 'Classic Watch',
10 description: 'A Classic Watch accessorized with style.',
11 price: 9.99,
12 imageUrl:
13 'https://cdn.pixabay.com/photo/2018/02/24/20/39/clock-3179167_960_720.jpg',
14 ),
15 Product(
16 id: 'p1',
17 title: 'Shoe with Gears',
18 description: 'Shoes paired with excersise accessories.',
19 price: 9.99,
20 imageUrl:
21 'https://cdn.pixabay.com/photo/2017/07/02/19/24/dumbbells-2465478_960_720.jp\
22 g',
23 ),
24 ...
The question is how we can provide this data model to our home screen. When should I use provider
in Flutter?
We use provider for many purposes. However, at present we want to count how many products
objects are there in our local storage.
Let’s just count the length of the products.
To do that, we must place our ChangeNotifierProvider on the top of our main app.
10. More on Flutter UI, List, Map, and Provider Best Practices 470
1 import 'package:flutter/foundation.dart';
2 import 'package:flutter/material.dart';
3 import 'package:provider/provider.dart';
4 import '../models/products.dart';
5
6 void main() {
7 runApp(
8 MultiProvider(
9 providers: [
10 ChangeNotifierProvider(
11 create: (_) => Products(),
12 ),
13 ],
14 child: const ShopAppWithProvider(),
15 ),
16 );
17 }
We’re interested about Product objects that have already been instantiated. As a result ChangeNoti-
fierProvider create named parameter returns Products.
Subsequently, we can retrieve all the products by two ways.
Let’s see the first way.
20 child: Column(
21 mainAxisSize: MainAxisSize.min,
22 mainAxisAlignment: MainAxisAlignment.center,
23 children: <Widget>[
24 const Text('All products length:'),
25 Text(
26 '${context.watch<Products>().products.length}',
27 key: const Key('productKeyOne'),
28 style: Theme.of(context).textTheme.headline4,
29 ),
30 ],
31 ),
32 ),
33 );
34 }
35 }
Now, directly context can watch the products length and give us a nice output like the following
image.
However, we can count the products length by using Provider also.
In the above code, a few lines are worth noting. The first one is the usage of Provider.
Next, the provider provides the current data model products to the Text widget.
10. More on Flutter UI, List, Map, and Provider Best Practices 473
1 Text(
2 '${products.length}',
3 key: const Key('productKeyTwo'),
4 style: Theme.of(context).textTheme.headline4,
5 ),
Therefore, the next image shows two numbers of products length, instead of one.
And, finally, you may have noticed that each Text widget has different keys.
1 Text(
2 '${context.watch<Products>().products.length}',
3 key: const Key('productKeyOne'),
4 style: Theme.of(context).textTheme.headline4,
5 ),
6 const SizedBox(
7 height: 10.0,
8 ),
9 Text(
10 '${products.length}',
11 key: const Key('productKeyTwo'),
12 style: Theme.of(context).textTheme.headline4,
13 ),
1 import 'package:flutter/foundation.dart';
2
3 class Product with ChangeNotifier {
4 final String id;
5 final String title;
6 final String description;
7 final double price;
8 final String imageUrl;
9
10 Product({
11 required this.id,
12 required this.title,
13 required this.description,
14 required this.price,
15 required this.imageUrl,
16 });
17 }
Next, we need to instantiate the product objects in a separate class Products. So that ChangeNoti-
fierProvider can use value method to get the existing value.
1 import 'package:flutter/material.dart';
2
3 import 'product.dart';
4
5 class Products with ChangeNotifier {
6 final List<Product> products = [
7 Product(
8 id: 'p1',
9 title: 'Classic Watch',
10 description: 'A Classic Watch accessorized with style.',
11 price: 9.99,
12 imageUrl:
13 'https://cdn.pixabay.com/photo/2018/02/24/20/39/clock-3179167_960_720.jpg',
14 ),
15 Product(
16 id: 'p1',
10. More on Flutter UI, List, Map, and Provider Best Practices 475
Now, as a rule, we’ll keep the ChangeNotifierProvider create named parameter points to the Products
class. So that later we can use that type.
1 void main() {
2 runApp(
3 MultiProvider(
4 providers: [
5 ChangeNotifierProvider(
6 create: (_) => Products(),
7 ),
8 ],
9 child: const ShopAppWithProvider(),
10 ),
11 );
12 }
We have used the default constructor because to create a value we should always use the default
constructor. According to the documentation, we cannot create the instance inside build method.
It will lead to memory leaks and potentially undesired side-effects. Where is ChangeNotifierProvider
used?
We use ChangeNotifierProvider here for showing existing data model. Therefore,the Shop App With
Provider stateless widget will show the products overview screen.
Or, we may consider it as the home page.
Actually, we’ve already instantiated the product object. Therefore, we need the object individually.
That means, if we can get the product ID, our job is done.
Based on that ID, we can display all the products in a scrollable Grid. Each product ID points to the
respective image and title.
Now, with the help of ChangeNotifierProvider, we can use three ways to display them.
Let’s see the first one’s code snippet.
10. More on Flutter UI, List, Map, and Provider Best Practices 476
44 crossAxisCount: 2,
45 childAspectRatio: 3 / 2,
46 crossAxisSpacing: 10,
47 mainAxisSpacing: 10,
48 ),
49 ),
50 );
51 }
52 }
The next line, which plays a key role is the following one:
20 fit: BoxFit.cover,
21 ),
22 ),
23 footer: GridTileBar(
24 backgroundColor: Colors.black87,
25 title: Text(
26 product[i].title,
27 textAlign: TextAlign.center,
28 ),
29 ),
30 ),
31 ),
32 // ),
33 gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
34 crossAxisCount: 2,
35 childAspectRatio: 3 / 2,
36 crossAxisSpacing: 10,
37 mainAxisSpacing: 10,
38 ),
39 ),
40 );
41 }
42 }
We have used Provider of context method where we’ve mentioned the type. And then get all the
products.
Further, we can also use ChangeNotifierProvider value and change the above code.
All these three code snippet of products home page will display the products in the same way.
Furthermore, we can use the Gesture Detector on tap method to pass the product ID as an argument.
As a result, we can display the product detail in a separate screen.
In the next article we will discuss that. So stay tuned to get updated on building a flutter shopping
app.
10. More on Flutter UI, List, Map, and Provider Best Practices 480
³⁵https://sanjibsinha.com/changenotifierprovider-value/
10. More on Flutter UI, List, Map, and Provider Best Practices 481
1 import 'package:flutter/foundation.dart';
2
3 class Product with ChangeNotifier {
4 final String id;
5 final String title;
6 final String description;
7 final double price;
8 final String imageUrl;
9
10 Product({
11 required this.id,
12 required this.title,
13 required this.description,
14 required this.price,
15 required this.imageUrl,
16 });
17 }
We don’t have any method yet. Although we use dart mixin, to use ChangeNotifier.
Next, we instantiate a dew product object and keep both classes at models folder.
1 import 'package:flutter/material.dart';
2
3 import 'product.dart';
4
5 class Products with ChangeNotifier {
6 final List<Product> products = [
7 Product(
8 id: 'p1',
9 title: 'Classic Watch',
10 description: 'A Classic Watch accessorized with style.',
11 price: 9.99,
12 imageUrl:
13 'https://cdn.pixabay.com/photo/2018/02/24/20/39/clock-3179167_960_720.jpg',
14 ),
15 Product(
16 id: 'p1',
17 title: 'Shoe with Gears',
18 description: 'Shoes paired with excersise accessories.',
19 price: 9.99,
20 imageUrl:
21 'https://cdn.pixabay.com/photo/2017/07/02/19/24/dumbbells-2465478_960_720.jp\
10. More on Flutter UI, List, Map, and Provider Best Practices 482
22 g',
23 ),
24 ...
As a result, our data model is now ready. Meanwhile, we can now use “ChangeNotifierProvider
create” to create a ChangeNotifier and place it over the top of our shopping cart app.
1 void main() {
2 runApp(
3 MultiProvider(
4 providers: [
5 ChangeNotifierProvider(
6 create: (_) => Products(),
7 ),
8 ],
9 child: const ShopAppWithProvider(),
10 ),
11 );
12 }
13 ...
Next in that widget tree comes ShopHomePage() stateless widget. This home page, or screen,
whatever we wan to call it, will display the products object in a Grid View.
Therefore, we must have two separate controllers, which will not only display the products, but also,
at the same time, navigate to the products detail page.
Now, in the ProductView() controller we can use ChangeNotifierProvider value to provide an
existing ChangeNotifier.
18 childAspectRatio: 3 / 2,
19 crossAxisSpacing: 10,
20 mainAxisSpacing: 10,
21 ),
22 );
23 }
24 }
The above code snippet is self explanatory. We can use the context and index of all products and
provide ChangeNotifier to the child widget ProductItem().
As a result, now we can easily access each product property through the instantiated product objects;
as we’ve defined them in our Product class.
30 subtitle: Text(
31 product.description,
32 textAlign: TextAlign.center,
33 ),
34 ),
35 ),
36 );
37 }
38 }
Not only that, we can now pass the product ID as an argument of of the Navigator class.
Now we can click any image on the product home page, and get to the detail page.
In the next article we’ll try to build the product detail page, so that it displays the product details,
instead of showing a text.
1 void main() {
2 runApp(
3 MultiProvider(
4 providers: [
5 ChangeNotifierProvider(
6 create: (_) => Products(),
7 ),
8 ],
9 child: const ShopAppWithProvider(),
10 ),
11 );
12 }
In the above code snippet, these two lines are extremely important. In those lines, we have defined
the routes.
We should always define our routes in MaterialApp widget.
1 routes: {
2 ShopProductDetailScreen.routename: (context) =>
3 const ShopProductDetailScreen(),
4 },
In our Shop Product Detail Screen we’ve declared a static constant property routename. Moreover,
it’s a string that we can find in the second image that displays the product detail screen.
As we’ve defined the routes, subsequently we must have a method that should use Navigator widget.
Meanwhile, the Navigator widget uses pushnamed method so that we can pass the Shop Detail
Screen routename along with the product ID.
That’s the technique we have adopted in our Book Item controller, which is responsible for displaying
all the products on the home page.
Therefore, we can use Gesture Detector on tap method, so that user can tap the image and reach the
product detail screen, or page.
In that on tap method, we define that Navigator widget methods.
1 child: GridTile(
2 child: GestureDetector(
3 onTap: () {
4 Navigator.of(context).pushNamed(
5 ShopProductDetailScreen.routename,
6 arguments: product.id,
7 );
8 },
9 child: Image.network(
10 product.imageUrl,
11 fit: BoxFit.cover,
12 ),
13 ),
14 ...
15 // code is incomplete for brevity
In our cases, we need to pass arguments to the named route, and navigated to the /product-details
route.
The Products Home Page will show all Products.
We can see all product items at a glance here. Moreover, we can have title and subtitle that gives us
an idea about the product.
However, to get the details of the product we need to navigate to another page. And for that we’ll
use Navigator widget pushNamed and pass the product ID as an argument.
To navigate to a new page we must define the routes first in our MaterialApp widget.
Before that, we need to declare a static constant string as routename in the Shop Product Detail
Screen widget.
12 child: Text(
13 'We will get product detail here',
14 style: TextStyle(
15 fontSize: 30.0,
16 ),
17 ),
18 ),
19 );
20 }
21 }