├── .gitignore
├── LICENSE
├── README.md
├── images
├── adventure_avatar.png
├── adventure_workshop.png
└── cover.png
└── workshop
├── 01_introduction
├── instructions.md
├── snippet.dart
└── solution.dart
├── 02.1_themedata
├── instructions.md
├── snippet.dart
└── solution.dart
├── 02.2_themedata
├── instructions.md
├── snippet.dart
└── solution.dart
├── 03.1_texts_icons
├── instructions.md
├── snippet.dart
└── solution.dart
├── 03.2_texts_icons
├── instructions.md
├── snippet.dart
└── solution.dart
├── 04.1_buttons
├── instructions.md
├── snippet.dart
└── solution.dart
├── 04.2_buttons
├── instructions.md
├── snippet.dart
└── solution.dart
├── 05_inputs
├── instructions.md
├── snippet.dart
└── solution.dart
├── 06_screens
├── instructions.md
├── snippet.dart
└── solution.dart
├── 07_conclusion
├── instructions.md
└── snippet.dart
└── meta.yaml
/.gitignore:
--------------------------------------------------------------------------------
1 | **/.DS_Store
2 | **/.idea
3 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Anna (Domashych) Leushchenko
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Consistent design with Flutter Theme
2 |
3 | Source code for the "Consistent design with Flutter Theme" DartPad workshop by [Anna Leushchenko 👩💻💙📱🇺🇦](https://github.com/foxanna)
4 |
5 | > Consistency is a winning strategy for a good application UI. And Flutter developers don’t have to be professional designers to achieve it. As usual, there is a widget for that - Theme! Find more details about effortless consistent Flutter application design in this workshop.
6 |
7 | [View in DartPad](https://dartpad.dev/workshops.html?webserver=https://raw.githubusercontent.com/foxanna/flutter_theme_workshop/main/workshop)
8 |
9 | 
10 |
11 | ## Google I/O 2022 Adventure
12 |
13 | This workshop was featured in Google I/O 2022 Adventure world in Flutter workshops zone.
14 |
15 | 
16 |
17 | 
18 |
--------------------------------------------------------------------------------
/images/adventure_avatar.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/foxanna/flutter_theme_workshop/195c1f2aa15442cd8af6b183d487f7c8f04d78d1/images/adventure_avatar.png
--------------------------------------------------------------------------------
/images/adventure_workshop.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/foxanna/flutter_theme_workshop/195c1f2aa15442cd8af6b183d487f7c8f04d78d1/images/adventure_workshop.png
--------------------------------------------------------------------------------
/images/cover.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/foxanna/flutter_theme_workshop/195c1f2aa15442cd8af6b183d487f7c8f04d78d1/images/cover.png
--------------------------------------------------------------------------------
/workshop/01_introduction/instructions.md:
--------------------------------------------------------------------------------
1 | # Introduction
2 |
3 | Welcome to the ***Consistent design with Flutter Theme*** workshop by [*Anna Leushchenko* 👩💻💙📱🇺🇦](https://github.com/foxanna)!
4 |
5 | >Consistency is a winning strategy for a good application UI. And Flutter developers don’t have to be professional designers to achieve it. As usual, there is a widget for that - `Theme`! Find more details about effortless consistent Flutter application design in this workshop.
6 |
7 | This workshop only assumes knowledge of commonly used Flutter material widgets like `Text`, `ElevatedButton`, `TextField`, etc., so everyone is welcomed!
8 |
9 | ## Contents
10 |
11 | * Step 1. Introduction
12 | * Step 2 & 3. ThemeData
13 | * Step 4. Styling icons
14 | * Step 5. Styling texts
15 | * Step 6 & 7. Styling buttons
16 | * Step 8. Styling inputs
17 | * Step 9. Styling screens
18 | * Step 10. Conclusion
19 |
20 | ## About author
21 |
22 | [*Anna Leushchenko* 👩💻💙📱🇺🇦](https://github.com/foxanna) is a mobile development expert, passionate about quality software, focused on Flutter. Anna is a Google Developer Expert in Dart and Flutter, conference speaker and tech writer, mentor and OSS contributor. Senior Staff Mobile Engineer at [*Tide*](https://www.tide.co/careers/).
23 |
24 | ## Let's go!
25 |
26 | Take a look at the code snippet on the right. `ExampleApp` is a `MaterialApp` with `ExamplePage` as `home` page. `ExamplePage` consists of a `Scaffold` with an `AppBar` and `ExampleWidget` as `body`. This widgets structure will remain the same over the span of this workshop, but the content inside the `ExampleWidget` will change from step to step.
27 |
28 | To start with, `ExampleWidget` consists of three similar `ElevatedButton` widgets placed in a `Column`:
29 |
30 | ```dart
31 | Column(
32 | mainAxisAlignment: MainAxisAlignment.spaceEvenly,
33 | children: [
34 | ElevatedButton(child: Text('first'), onPressed: () {}),
35 | ElevatedButton(child: Text('second'), onPressed: () {}),
36 | ElevatedButton(child: Text('third'), onPressed: () {}),
37 | ],
38 | )
39 | ```
40 |
41 | Run the code snippet on the right. You will see three similarly-looking pale purple (blue in pre-Material3 theme) buttons with deep purple text.
42 |
43 | To make UI more custom, let's design a lime button with white text:
44 |
45 | ```dart
46 | ElevatedButton(
47 | style: ElevatedButton.styleFrom(
48 | backgroundColor: Colors.lime,
49 | foregroundColor: Colors.white,
50 | ),
51 | ...
52 | )
53 | ```
54 |
55 | To make this styling consistent across the application, such a style should be applied to every button, which would lead to code duplication. There are of course ways around it, like creating extensions or custom buttons. But this workshop is about effortless UI consistency, which can be achieved with the `Theme` widget.
56 |
57 | ## Theme widget
58 |
59 | The `Theme` widget applies a given `ThemeData`, which describes the colors and typographic choices of an application, to descendant widgets.
60 |
61 | Going back to the previous example, the `style` field value can be moved from the `ElevatedButton` widget up to the `ElevatedButtonThemeData`, which is a part of `ThemeData` provided to the `Theme` widget:
62 |
63 | ```dart
64 | Theme(
65 | data: ThemeData(
66 | elevatedButtonTheme: ElevatedButtonThemeData(
67 | style: ElevatedButton.styleFrom(
68 | backgroundColor: Colors.lime,
69 | foregroundColor: Colors.white,
70 | ),
71 | ),
72 | ),
73 | child: ElevatedButton(...),
74 | )
75 | ```
76 |
77 | The result is the same: a lime button with white text. But now, when placed closer to the top of the widgets tree, this `Theme` widget can help to consistently style all buttons on the screen.
78 |
79 | ## Your turn
80 |
81 | 1. Apply the `Theme` from the code snippet above to the `ExampleWidget`. All three buttons should become lime
.
82 |
--------------------------------------------------------------------------------
/workshop/01_introduction/snippet.dart:
--------------------------------------------------------------------------------
1 | // ignore_for_file: prefer_const_constructors, prefer_const_literals_to_create_immutables, curly_braces_in_flow_control_structures
2 |
3 | import 'package:flutter/material.dart';
4 |
5 | void main() {
6 | runApp(ExampleApp());
7 | }
8 |
9 | class ExampleApp extends StatelessWidget {
10 | @override
11 | Widget build(BuildContext context) {
12 | return MaterialApp(
13 | debugShowCheckedModeBanner: false,
14 | home: ExamplePage(),
15 | );
16 | }
17 | }
18 |
19 | class ExamplePage extends StatelessWidget {
20 | @override
21 | Widget build(BuildContext context) {
22 | return Scaffold(
23 | appBar: AppBar(
24 | title: Text('Consistent design with Flutter Theme'),
25 | actions: [IconButton(icon: Icon(Icons.account_circle), onPressed: () {})],
26 | ),
27 | body: Padding(
28 | padding: EdgeInsets.all(20.0),
29 | child: Center(
30 | // TODO 1: Apply a Theme widget over ExampleWidget
31 |
32 | // Tip: Place your cursor over the ExampleWidget text below and hit
33 | // alt + enter on Windows/Linux or option + return on Mac. Then,
34 | // select "Wrap with widget..." from the dropdown menu that appears.
35 | child: ExampleWidget(),
36 | ),
37 | ),
38 | );
39 | }
40 | }
41 |
42 | class ExampleWidget extends StatelessWidget {
43 | @override
44 | Widget build(BuildContext context) {
45 | return Column(
46 | mainAxisAlignment: MainAxisAlignment.spaceEvenly,
47 | children: [
48 | ElevatedButton(child: Text('first'), onPressed: () {}),
49 | ElevatedButton(child: Text('second'), onPressed: () {}),
50 | ElevatedButton(child: Text('third'), onPressed: () {}),
51 | ],
52 | );
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/workshop/01_introduction/solution.dart:
--------------------------------------------------------------------------------
1 | // ignore_for_file: prefer_const_constructors, prefer_const_literals_to_create_immutables, curly_braces_in_flow_control_structures
2 |
3 | import 'package:flutter/material.dart';
4 |
5 | void main() {
6 | runApp(ExampleApp());
7 | }
8 |
9 | class ExampleApp extends StatelessWidget {
10 | @override
11 | Widget build(BuildContext context) {
12 | return MaterialApp(
13 | debugShowCheckedModeBanner: false,
14 | home: ExamplePage(),
15 | );
16 | }
17 | }
18 |
19 | class ExamplePage extends StatelessWidget {
20 | @override
21 | Widget build(BuildContext context) {
22 | return Scaffold(
23 | appBar: AppBar(
24 | title: Text('Consistent design with Flutter Theme'),
25 | actions: [IconButton(icon: Icon(Icons.account_circle), onPressed: () {})],
26 | ),
27 | body: Padding(
28 | padding: EdgeInsets.all(20.0),
29 | child: Center(
30 | child: Theme(
31 | data: ThemeData(
32 | elevatedButtonTheme: ElevatedButtonThemeData(
33 | style: ElevatedButton.styleFrom(
34 | backgroundColor: Colors.lime,
35 | foregroundColor: Colors.white,
36 | ),
37 | ),
38 | ),
39 | child: ExampleWidget(),
40 | ),
41 | ),
42 | ),
43 | );
44 | }
45 | }
46 |
47 | class ExampleWidget extends StatelessWidget {
48 | @override
49 | Widget build(BuildContext context) {
50 | return Column(
51 | mainAxisAlignment: MainAxisAlignment.spaceEvenly,
52 | children: [
53 | ElevatedButton(child: Text('first'), onPressed: () {}),
54 | ElevatedButton(child: Text('second'), onPressed: () {}),
55 | ElevatedButton(child: Text('third'), onPressed: () {}),
56 | ],
57 | );
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/workshop/02.1_themedata/instructions.md:
--------------------------------------------------------------------------------
1 | # ThemeData
2 |
3 | Actually, the main focus of this workshop is not on the `Theme` widget itself, but on the `ThemeData` object, which is passed to the `Theme` widget `data` field. `ThemeData` is a container that aggregates styles for all kinds of widgets.
4 |
5 | Previously you saw how an `ElevatedButton` can be customized by specifying `elevatedButtonTheme` field:
6 |
7 | ```dart
8 | Theme(
9 | data: ThemeData(
10 | elevatedButtonTheme: ElevatedButtonThemeData(
11 | style: ElevatedButton.styleFrom(
12 | backgroundColor: Colors.lime,
13 | foregroundColor: Colors.white,
14 | ),
15 | ),
16 | ),
17 | child: ExampleWidget(),
18 | )
19 | ```
20 |
21 | The `ThemeData` class constructor accepts over 70 parameters dedicated to customizing color palette, typography, and components. In the next steps of this workshop you will take a deeper look into these fields:
22 |
23 | ```dart
24 | ThemeData(
25 | colorScheme: ColorScheme(...),
26 | iconTheme: IconThemeData(...),
27 | textTheme: TextTheme(...),
28 | elevatedButtonTheme: ElevatedButtonThemeData(...),
29 | outlinedButtonTheme: OutlinedButtonThemeData(...),
30 | textButtonTheme: TextButtonThemeData(...),
31 | inputDecorationTheme: InputDecorationTheme(...),
32 | textSelectionTheme: TextSelectionThemeData(...),
33 | appBarTheme: AppBarTheme(...),
34 | scaffoldBackgroundColor: Color(...),
35 | )
36 | ```
37 |
38 | ## Predefined ThemeData
39 |
40 | Obviously, specifying values for all `ThemeData` fields would be tedious. There are additional constructors for obtaining a well-made predefined collections of styles:
41 |
42 | * `ThemeData.light()` returns a default light purple theme
43 | * `ThemeData.dark()` returns a default dark purple theme
44 |
45 | They can be used as a basis for a good application design, and can be further customized with `copyWith` method:
46 |
47 | ```dart
48 | Theme(
49 | data: ThemeData.light().copyWith(
50 | elevatedButtonTheme: ElevatedButtonThemeData(...),
51 | ),
52 | ...
53 | )
54 | ```
55 |
56 | ## Generating ThemeData
57 |
58 | The `ColorScheme` is a set of colors
59 | based on the [*Material spec*](https://m3.material.io/styles/color/the-color-system/color-roles) that can be used to configure the color properties of most components. It is a powerful tool to consistently customize application colors with a well-made color palette.
60 |
61 | One way to initialize a `ThemeData` object with a `ColorScheme` is to provide the `colorSchemeSeed` field and the `brightness`. A generated color scheme will be based on the tones of `colorSchemeSeed` color and all of its contrasting colors will meet accessibility guidelines for readability:
62 |
63 | ```dart
64 | Theme(
65 | data: ThemeData(
66 | colorSchemeSeed: Colors.green,
67 | brightness: Brightness.light,
68 | ),
69 | ...
70 | )
71 | ```
72 |
73 | The `Brightness.light` is the default one if the `brightness` field value was not specified.
74 |
75 | If you want to customize a generated color scheme, you can use
76 | `ColorScheme.fromSeed` and then override any colors that need to be replaced directly or with `copyWith` method:
77 |
78 | ```dart
79 | Theme(
80 | data: ThemeData(
81 | colorScheme: ColorScheme.fromSeed(
82 | seedColor: Colors.green,
83 | onPrimary: Colors.yellow,
84 | ).copyWith(...),
85 | ),
86 | ...
87 | )
88 | ```
89 |
90 | ## Your turn
91 |
92 | 1. Provide `colorScheme` field to the `ThemeData` constructor. Use `ColorScheme.fromSeed` constructor with `Colors.green` color. No changes happen to the style of the buttons because it is explicitly customized with `ElevatedButtonThemeData`. If it is removed, buttons become green thanks to the `colorScheme` provided. Try it out with and without `elevatedButtonTheme` field set
.
93 |
--------------------------------------------------------------------------------
/workshop/02.1_themedata/snippet.dart:
--------------------------------------------------------------------------------
1 | // ignore_for_file: prefer_const_constructors, prefer_const_literals_to_create_immutables, curly_braces_in_flow_control_structures
2 |
3 | import 'package:flutter/material.dart';
4 |
5 | void main() {
6 | runApp(ExampleApp());
7 | }
8 |
9 | class ExampleApp extends StatelessWidget {
10 | @override
11 | Widget build(BuildContext context) {
12 | return MaterialApp(
13 | debugShowCheckedModeBanner: false,
14 | home: ExamplePage(),
15 | );
16 | }
17 | }
18 |
19 | class ExamplePage extends StatelessWidget {
20 | @override
21 | Widget build(BuildContext context) {
22 | return Scaffold(
23 | appBar: AppBar(
24 | title: Text('Consistent design with Flutter Theme'),
25 | actions: [IconButton(icon: Icon(Icons.account_circle), onPressed: () {})],
26 | ),
27 | body: Padding(
28 | padding: EdgeInsets.all(20.0),
29 | child: Center(
30 | child: Theme(
31 | data: ThemeData(
32 | // TODO 1: Provide colorScheme value
33 | elevatedButtonTheme: ElevatedButtonThemeData(
34 | style: ElevatedButton.styleFrom(
35 | backgroundColor: Colors.lime,
36 | foregroundColor: Colors.white,
37 | ),
38 | ),
39 | ),
40 | child: ExampleWidget(),
41 | ),
42 | ),
43 | ),
44 | );
45 | }
46 | }
47 |
48 | class ExampleWidget extends StatelessWidget {
49 | @override
50 | Widget build(BuildContext context) {
51 | return Column(
52 | mainAxisAlignment: MainAxisAlignment.spaceEvenly,
53 | children: [
54 | ElevatedButton(child: Text('first'), onPressed: () {}),
55 | ElevatedButton(child: Text('second'), onPressed: () {}),
56 | ElevatedButton(child: Text('third'), onPressed: () {}),
57 | ],
58 | );
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/workshop/02.1_themedata/solution.dart:
--------------------------------------------------------------------------------
1 | // ignore_for_file: prefer_const_constructors, prefer_const_literals_to_create_immutables, curly_braces_in_flow_control_structures
2 |
3 | import 'package:flutter/material.dart';
4 |
5 | void main() {
6 | runApp(ExampleApp());
7 | }
8 |
9 | class ExampleApp extends StatelessWidget {
10 | @override
11 | Widget build(BuildContext context) {
12 | return MaterialApp(
13 | debugShowCheckedModeBanner: false,
14 | home: ExamplePage(),
15 | );
16 | }
17 | }
18 |
19 | class ExamplePage extends StatelessWidget {
20 | @override
21 | Widget build(BuildContext context) {
22 | return Scaffold(
23 | appBar: AppBar(
24 | title: Text('Consistent design with Flutter Theme'),
25 | actions: [IconButton(icon: Icon(Icons.account_circle), onPressed: () {})],
26 | ),
27 | body: Padding(
28 | padding: EdgeInsets.all(20.0),
29 | child: Center(
30 | child: Theme(
31 | data: ThemeData(
32 | colorScheme: ColorScheme.fromSeed(seedColor: Colors.green),
33 | elevatedButtonTheme: ElevatedButtonThemeData(
34 | style: ElevatedButton.styleFrom(
35 | backgroundColor: Colors.lime,
36 | foregroundColor: Colors.white,
37 | ),
38 | ),
39 | ),
40 | child: ExampleWidget(),
41 | ),
42 | ),
43 | ),
44 | );
45 | }
46 | }
47 |
48 | class ExampleWidget extends StatelessWidget {
49 | @override
50 | Widget build(BuildContext context) {
51 | return Column(
52 | mainAxisAlignment: MainAxisAlignment.spaceEvenly,
53 | children: [
54 | ElevatedButton(child: Text('first'), onPressed: () {}),
55 | ElevatedButton(child: Text('second'), onPressed: () {}),
56 | ElevatedButton(child: Text('third'), onPressed: () {}),
57 | ],
58 | );
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/workshop/02.2_themedata/instructions.md:
--------------------------------------------------------------------------------
1 | # Global ThemeData
2 |
3 | In the previous workshop steps the `ThemeData` configuration was applied only to a part of the widget tree wrapped with the `Theme` widget. Other parts of the screen or other screens were not styled in the same way.
4 |
5 | To make the styling consistent across the entire application, the `MaterialApp` widget exposes a special `theme` field of `ThemeData` type. When set, the given `ThemeData` configuration is applied to all application screens:
6 |
7 | ```dart
8 | MaterialApp(
9 | theme: ThemeData(
10 | colorScheme: ColorScheme.fromSeed(seedColor: Colors.green),
11 | elevatedButtonTheme: ElevatedButtonThemeData(...),
12 | ),
13 | home: ExamplePage(),
14 | )
15 | ```
16 |
17 | In fact, under the hood, the `MaterialApp` widget passes the given `theme` value to the inner `Theme` widget, which wraps all application screens.
18 |
19 | It's worth mentioning that if no `theme` value is provided to `MaterialApp` widget, the default `ThemeData.light()` is used. That is why the three buttons were purple without any customization.
20 |
21 | ## Dark theme
22 |
23 | Additionally, the `MaterialApp` widget exposes `darkTheme` field, which is used to provide a dark version of the user interface. The `themeMode` field controls which theme is used if a `darkTheme` is provided:
24 |
25 | ```dart
26 | MaterialApp(
27 | darkTheme: ThemeData.dark(),
28 | themeMode: ThemeMode.dark,
29 | ...
30 | )
31 | ```
32 |
33 | ## Accessing ThemeData
34 |
35 | No matter how a `ThemeData` was provided, either through the `Theme` widget or through the `MaterialApp`, it can be accessed from the widgets below with:
36 |
37 | ```dart
38 | ThemeData theme = Theme.of(context);
39 | ```
40 |
41 | ## Your turn
42 |
43 | 1. Copy the `data` field value from the `Theme` widget into the `theme` field of the `MaterialApp`. The `AppBar` background color should change to green.
44 | 2. Remove `Theme` widget around `ExampleWidget`. All three buttons should remain lime.
45 | 3. Set the `darkTheme` field of the `MaterialApp` to the predefined dark theme. Try different values for the `themeMode` field
.
46 |
--------------------------------------------------------------------------------
/workshop/02.2_themedata/snippet.dart:
--------------------------------------------------------------------------------
1 | // ignore_for_file: prefer_const_constructors, prefer_const_literals_to_create_immutables, curly_braces_in_flow_control_structures
2 |
3 | import 'package:flutter/material.dart';
4 |
5 | void main() {
6 | runApp(ExampleApp());
7 | }
8 |
9 | class ExampleApp extends StatelessWidget {
10 | @override
11 | Widget build(BuildContext context) {
12 | return MaterialApp(
13 | debugShowCheckedModeBanner: false,
14 | // TODO 1: Provide theme field value
15 | // TODO 3: Provide darkTheme field value, specify themeMode
16 | home: ExamplePage(),
17 | );
18 | }
19 | }
20 |
21 | class ExamplePage extends StatelessWidget {
22 | @override
23 | Widget build(BuildContext context) {
24 | return Scaffold(
25 | appBar: AppBar(
26 | title: Text('Consistent design with Flutter Theme'),
27 | actions: [IconButton(icon: Icon(Icons.account_circle), onPressed: () {})],
28 | ),
29 | body: Padding(
30 | padding: EdgeInsets.all(20.0),
31 | child: Center(
32 | // TODO 2: Remove the Theme widget
33 |
34 | // Tip: Once again, you can place your cursor over the "Theme" widget below
35 | // and hit alt + enter on Windows/Linux or option + return on Mac.
36 | // Then, select "Remove this widget" from the dropdown menu that appears.
37 | child: Theme(
38 | data: ThemeData(
39 | colorScheme: ColorScheme.fromSeed(seedColor: Colors.green),
40 | elevatedButtonTheme: ElevatedButtonThemeData(
41 | style: ElevatedButton.styleFrom(
42 | backgroundColor: Colors.lime,
43 | foregroundColor: Colors.white,
44 | ),
45 | ),
46 | ),
47 | child: ExampleWidget(),
48 | ),
49 | ),
50 | ),
51 | );
52 | }
53 | }
54 |
55 | class ExampleWidget extends StatelessWidget {
56 | @override
57 | Widget build(BuildContext context) {
58 | return Column(
59 | mainAxisAlignment: MainAxisAlignment.spaceEvenly,
60 | children: [
61 | ElevatedButton(child: Text('first'), onPressed: () {}),
62 | ElevatedButton(child: Text('second'), onPressed: () {}),
63 | ElevatedButton(child: Text('third'), onPressed: () {}),
64 | ],
65 | );
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/workshop/02.2_themedata/solution.dart:
--------------------------------------------------------------------------------
1 | // ignore_for_file: prefer_const_constructors, prefer_const_literals_to_create_immutables, curly_braces_in_flow_control_structures
2 |
3 | import 'package:flutter/material.dart';
4 |
5 | void main() {
6 | runApp(ExampleApp());
7 | }
8 |
9 | class ExampleApp extends StatelessWidget {
10 | @override
11 | Widget build(BuildContext context) {
12 | return MaterialApp(
13 | debugShowCheckedModeBanner: false,
14 | theme: ThemeData(
15 | colorScheme: ColorScheme.fromSeed(seedColor: Colors.green),
16 | elevatedButtonTheme: ElevatedButtonThemeData(
17 | style: ElevatedButton.styleFrom(
18 | backgroundColor: Colors.lime,
19 | foregroundColor: Colors.white,
20 | ),
21 | ),
22 | ),
23 | darkTheme: ThemeData.dark(),
24 | themeMode: ThemeMode.light,
25 | home: ExamplePage(),
26 | );
27 | }
28 | }
29 |
30 | class ExamplePage extends StatelessWidget {
31 | @override
32 | Widget build(BuildContext context) {
33 | return Scaffold(
34 | appBar: AppBar(
35 | title: Text('Consistent design with Flutter Theme'),
36 | actions: [IconButton(icon: Icon(Icons.account_circle), onPressed: () {})],
37 | ),
38 | body: Padding(
39 | padding: EdgeInsets.all(20.0),
40 | child: Center(
41 | child: ExampleWidget(),
42 | ),
43 | ),
44 | );
45 | }
46 | }
47 |
48 | class ExampleWidget extends StatelessWidget {
49 | @override
50 | Widget build(BuildContext context) {
51 | return Column(
52 | mainAxisAlignment: MainAxisAlignment.spaceEvenly,
53 | children: [
54 | ElevatedButton(child: Text('first'), onPressed: () {}),
55 | ElevatedButton(child: Text('second'), onPressed: () {}),
56 | ElevatedButton(child: Text('third'), onPressed: () {}),
57 | ],
58 | );
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/workshop/03.1_texts_icons/instructions.md:
--------------------------------------------------------------------------------
1 | # Styling icons
2 |
3 | Though providing a `ColorScheme` is a powerful consistent way to style most of the application components, there might be the desire to tweak some widgets individually. This and the next workshop steps are dedicated to customizing some of the most used widgets.
4 |
5 | Check the code snippet on the right. The `ExampleWidget` content changed to `Icon` and `IconButton` widgets.
6 |
7 | ## Icon style
8 |
9 | The `Icon` widget exposes several fields that control its style:
10 |
11 | ```dart
12 | Icon(
13 | Icons.email,
14 | color: Colors.lime,
15 | size: 36.0,
16 | opacity: 0.5,
17 | )
18 | ```
19 |
20 | They can be customized individually for each `Icon`, but typically all application icons have the same look. To reach the icons
21 | design consistency, meet the `IconThemeData`.
22 |
23 | ## IconThemeData
24 |
25 | `IconThemeData` is an object similar to the `ThemeData` but dedicated to styling icons. And the `IconTheme` widget is an analog of the `Theme` widget, but again, dedicated to styling icons only, so its `data` field type is `IconThemeData`:
26 |
27 | ```dart
28 | IconTheme(
29 | data: IconThemeData(
30 | color: Colors.lime,
31 | size: 36.0,
32 | opacity: 0.5,
33 | ),
34 | child: Icon(Icons.email),
35 | )
36 | ```
37 |
38 | Similarly, the icons style can be customized globally through `iconTheme` field of the `ThemeData` object:
39 |
40 | ```dart
41 | MaterialApp(
42 | theme: ThemeData(
43 | iconTheme: IconThemeData(
44 | color: Colors.lime,
45 | size: 36.0,
46 | opacity: 0.5,
47 | ),
48 | ),
49 | ...
50 | )
51 | ```
52 |
53 | With this change, all application icons would become lime and half-transparent.
54 |
55 | ## IconButton style
56 |
57 | `Icon` widgets are not interactive. For interactive icons, Flutter has a dedicated `IconButton` widget:
58 |
59 | ```dart
60 | IconButton(
61 | icon: Icon(Icons.language),
62 | onPressed: () {},
63 | )
64 | ```
65 |
66 | Customizing `IconButton` widgets is done through the same `iconTheme` field of the global `ThemeData` object. So with the setup above any `IconButton` would also become lime and half-transparent.
67 |
68 | ## AppBar Icon style
69 |
70 | By default, customizations of the global theme `iconTheme` field also affect `IconButton` widgets inside the `AppBar`, except for their `color`. With global `iconTheme` customized as above, any button like this would be half-transparent and of size `36`:
71 |
72 | ```dart
73 | AppBar(
74 | actions: [
75 | IconButton(
76 | icon: Icon(Icons.account_circle),
77 | onPressed: () {}
78 | ),
79 | ],
80 | )
81 | ```
82 |
83 | Next steps of this workshop show how to customize icons located in the `AppBar` more precisely.
84 |
85 | ## Your turn
86 |
87 | 1. Customize global `iconTheme` as shown above. The email and globe icons should become lime and half-transparent. The account icon in the top corner should become half-transparent
.
88 |
--------------------------------------------------------------------------------
/workshop/03.1_texts_icons/snippet.dart:
--------------------------------------------------------------------------------
1 | // ignore_for_file: prefer_const_constructors, prefer_const_literals_to_create_immutables, curly_braces_in_flow_control_structures
2 |
3 | import 'package:flutter/material.dart';
4 |
5 | void main() {
6 | runApp(ExampleApp());
7 | }
8 |
9 | class ExampleApp extends StatelessWidget {
10 | @override
11 | Widget build(BuildContext context) {
12 | return MaterialApp(
13 | debugShowCheckedModeBanner: false,
14 | theme: ThemeData(
15 | colorScheme: ColorScheme.fromSeed(seedColor: Colors.green),
16 | // TODO 1: Provide iconTheme field value
17 | ),
18 | home: ExamplePage(),
19 | );
20 | }
21 | }
22 |
23 | class ExamplePage extends StatelessWidget {
24 | @override
25 | Widget build(BuildContext context) {
26 | return Scaffold(
27 | appBar: AppBar(
28 | title: Text('Consistent design with Flutter Theme'),
29 | actions: [IconButton(icon: Icon(Icons.account_circle), onPressed: () {})],
30 | ),
31 | body: Padding(
32 | padding: EdgeInsets.all(20.0),
33 | child: Center(
34 | child: ExampleWidget(),
35 | ),
36 | ),
37 | );
38 | }
39 | }
40 |
41 | class ExampleWidget extends StatelessWidget {
42 | @override
43 | Widget build(BuildContext context) {
44 | return Column(
45 | mainAxisAlignment: MainAxisAlignment.spaceEvenly,
46 | children: [
47 | Icon(Icons.email),
48 | IconButton(icon: Icon(Icons.language), onPressed: () {}),
49 | ],
50 | );
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/workshop/03.1_texts_icons/solution.dart:
--------------------------------------------------------------------------------
1 | // ignore_for_file: prefer_const_constructors, prefer_const_literals_to_create_immutables, curly_braces_in_flow_control_structures
2 |
3 | import 'package:flutter/material.dart';
4 |
5 | void main() {
6 | runApp(ExampleApp());
7 | }
8 |
9 | class ExampleApp extends StatelessWidget {
10 | @override
11 | Widget build(BuildContext context) {
12 | return MaterialApp(
13 | debugShowCheckedModeBanner: false,
14 | theme: ThemeData(
15 | colorScheme: ColorScheme.fromSeed(seedColor: Colors.green),
16 | iconTheme: IconThemeData(
17 | color: Colors.lime,
18 | size: 36.0,
19 | opacity: 0.5,
20 | ),
21 | ),
22 | home: ExamplePage(),
23 | );
24 | }
25 | }
26 |
27 | class ExamplePage extends StatelessWidget {
28 | @override
29 | Widget build(BuildContext context) {
30 | return Scaffold(
31 | appBar: AppBar(
32 | title: Text('Consistent design with Flutter Theme'),
33 | actions: [IconButton(icon: Icon(Icons.account_circle), onPressed: () {})],
34 | ),
35 | body: Padding(
36 | padding: EdgeInsets.all(20.0),
37 | child: Center(
38 | child: ExampleWidget(),
39 | ),
40 | ),
41 | );
42 | }
43 | }
44 |
45 | class ExampleWidget extends StatelessWidget {
46 | @override
47 | Widget build(BuildContext context) {
48 | return Column(
49 | mainAxisAlignment: MainAxisAlignment.spaceEvenly,
50 | children: [
51 | Icon(Icons.email),
52 | IconButton(icon: Icon(Icons.language), onPressed: () {}),
53 | ],
54 | );
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/workshop/03.2_texts_icons/instructions.md:
--------------------------------------------------------------------------------
1 | # Styling texts
2 |
3 | Check the code snippet on the right. The `ExampleWidget` content changed to `Text` widgets with different styles.
4 |
5 | ## Text style
6 |
7 | Typically, applications use a variety of text styles. Thus, `Text` widgets are usually explicitly customized with some text style:
8 |
9 | ```dart
10 | Text(
11 | 'example',
12 | style: TextStyle(
13 | color: Colors.red,
14 | fontSize: 18.0,
15 | fontWeight: FontWeight.bold,
16 | letterSpacing: 0.8,
17 | ...
18 | ),
19 | )
20 | ```
21 |
22 | ## TextTheme
23 |
24 | Instead of configuring individual `Text` widget styles, the consistent design can be achieved by customizing a global set of text styles. The `TextTheme` object, containing definitions for the various typographical styles, can be provided to the global `ThemeData` object:
25 |
26 | ```dart
27 | MaterialApp(
28 | theme: ThemeData(
29 | textTheme: TextTheme(...),
30 | ),
31 | ...
32 | )
33 | ```
34 |
35 | The `TextTheme` allows defining the following text styles:
36 |
37 | ```dart
38 | TextTheme(
39 | displayLarge: TextStyle(...),
40 | displayMedium: TextStyle(...),
41 | displaySmall: TextStyle(...),
42 | headlineLarge: TextStyle(...),
43 | headlineMedium: TextStyle(...),
44 | headlineSmall: TextStyle(...),
45 | titleLarge: TextStyle(...),
46 | titleMedium: TextStyle(...),
47 | titleSmall: TextStyle(...),
48 | bodyLarge: TextStyle(...),
49 | bodyMedium: TextStyle(...),
50 | bodySmall: TextStyle(...),
51 | labelLarge: TextStyle(...),
52 | labelMedium: TextStyle(...),
53 | labelSmall: TextStyle(...),
54 | )
55 | ```
56 |
57 | However, describing all `TextStyle` fields of `TextTheme` is unnecessary. As with the `ThemeData` object, there are predefined `TextTheme` instances that implement the typography styles in the material design specification.
58 |
59 | ## Predefined TextTheme
60 |
61 | * `Typography().black` - a material design text theme with dark glyphs
62 | * `Typography().white` - a material design text theme with light glyphs
63 |
64 | These should be used as a starting point and further customized with `copyWith` or `apply` methods:
65 |
66 | ```dart
67 | ThemeData(
68 | textTheme: Typography().black
69 | .apply(
70 | displayColor: Colors.greenAccent,
71 | bodyColor: Colors.green,
72 | )
73 | .copyWith(
74 | displayLarge: TextStyle(
75 | color: Colors.lightGreen,
76 | fontSize: 18.0,
77 | fontWeight: FontWeight.bold,
78 | letterSpacing: 0.8,
79 | ),
80 | ),
81 | )
82 | ```
83 |
84 | While the `copyWith` method creates a copy of this text theme but with the given fields replaced with the new values, the `apply` method creates a copy of this text theme but with the given field replaced in **each** of the individual text styles.
85 |
86 | It's worth mentioning that by default, the `ThemeData` object already defines an instance of `TextTheme` with either dark or light glyphs depending on the `ThemeData` brightness.
87 |
88 | ## Using TextTheme
89 |
90 | Now, `TextStyle`s from above can be obtained with `Theme.of(context).textTheme` and provided to individual `Text` widgets:
91 |
92 | ```dart
93 | Text(
94 | 'example',
95 | style: Theme.of(context).textTheme.displayLarge,
96 | )
97 | ```
98 |
99 | If no explicit style is provided to a `Text` widget, it implicitly uses the `bodyMedium` style:
100 |
101 | ```dart
102 | Text('bodyMedium')
103 | ```
104 |
105 | In fact, providing global `TextTheme` is also a way to implicitly customize some other widgets. For example, `ElevatedButton`, `TextButton`, and `OutlinedButton` widgets by default use `labelLarge` text style, `titleLarge` style is used for `AlertDialog.title` and `AppBar.title`, etc.
106 |
107 | ## Your turn
108 |
109 | 1. Customize `TextTheme` globally with the `Typography().black` instance.
110 | 2. Apply customizations with `.apply` and `.copyWith` methods
.
111 |
--------------------------------------------------------------------------------
/workshop/03.2_texts_icons/snippet.dart:
--------------------------------------------------------------------------------
1 | // ignore_for_file: prefer_const_constructors, prefer_const_literals_to_create_immutables, curly_braces_in_flow_control_structures
2 |
3 | import 'package:flutter/material.dart';
4 |
5 | void main() {
6 | runApp(ExampleApp());
7 | }
8 |
9 | class ExampleApp extends StatelessWidget {
10 | @override
11 | Widget build(BuildContext context) {
12 | return MaterialApp(
13 | debugShowCheckedModeBanner: false,
14 | theme: ThemeData(
15 | colorScheme: ColorScheme.fromSeed(seedColor: Colors.green),
16 | // TODO 1: Set textTheme field value
17 | // TODO 2: Use .apply and .copyWith methods
18 | ),
19 | home: ExamplePage(),
20 | );
21 | }
22 | }
23 |
24 | class ExamplePage extends StatelessWidget {
25 | @override
26 | Widget build(BuildContext context) {
27 | return Scaffold(
28 | appBar: AppBar(
29 | title: Text('Consistent design with Flutter Theme'),
30 | actions: [IconButton(icon: Icon(Icons.account_circle), onPressed: () {})],
31 | ),
32 | body: Padding(
33 | padding: EdgeInsets.all(20.0),
34 | child: Center(
35 | child: ExampleWidget(),
36 | ),
37 | ),
38 | );
39 | }
40 | }
41 |
42 | class ExampleWidget extends StatelessWidget {
43 | @override
44 | Widget build(BuildContext context) {
45 | return Column(
46 | mainAxisAlignment: MainAxisAlignment.spaceEvenly,
47 | children: [
48 | Text('displayLarge', style: Theme.of(context).textTheme.displayLarge),
49 | Text('explicit bodyMedium', style: Theme.of(context).textTheme.bodyMedium),
50 | Text('implicit bodyMedium'),
51 | Text('labelSmall', style: Theme.of(context).textTheme.labelSmall),
52 | ],
53 | );
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/workshop/03.2_texts_icons/solution.dart:
--------------------------------------------------------------------------------
1 | // ignore_for_file: prefer_const_constructors, prefer_const_literals_to_create_immutables, curly_braces_in_flow_control_structures
2 |
3 | import 'package:flutter/material.dart';
4 |
5 | void main() {
6 | runApp(ExampleApp());
7 | }
8 |
9 | class ExampleApp extends StatelessWidget {
10 | @override
11 | Widget build(BuildContext context) {
12 | return MaterialApp(
13 | debugShowCheckedModeBanner: false,
14 | theme: ThemeData(
15 | colorScheme: ColorScheme.fromSeed(seedColor: Colors.green),
16 | textTheme: Typography().black
17 | .apply(
18 | displayColor: Colors.greenAccent,
19 | bodyColor: Colors.green,
20 | )
21 | .copyWith(
22 | displayLarge: TextStyle(
23 | color: Colors.lightGreen,
24 | fontSize: 18.0,
25 | fontWeight: FontWeight.bold,
26 | letterSpacing: 0.8,
27 | ),
28 | ),
29 | ),
30 | home: ExamplePage(),
31 | );
32 | }
33 | }
34 |
35 | class ExamplePage extends StatelessWidget {
36 | @override
37 | Widget build(BuildContext context) {
38 | return Scaffold(
39 | appBar: AppBar(
40 | title: Text('Consistent design with Flutter Theme'),
41 | actions: [IconButton(icon: Icon(Icons.account_circle), onPressed: () {})],
42 | ),
43 | body: Padding(
44 | padding: EdgeInsets.all(20.0),
45 | child: Center(
46 | child: ExampleWidget(),
47 | ),
48 | ),
49 | );
50 | }
51 | }
52 |
53 | class ExampleWidget extends StatelessWidget {
54 | @override
55 | Widget build(BuildContext context) {
56 | return Column(
57 | mainAxisAlignment: MainAxisAlignment.spaceEvenly,
58 | children: [
59 | Text('displayLarge', style: Theme.of(context).textTheme.displayLarge),
60 | Text('explicit bodyMedium', style: Theme.of(context).textTheme.bodyMedium),
61 | Text('implicit bodyMedium'),
62 | Text('labelSmall', style: Theme.of(context).textTheme.labelSmall),
63 | ],
64 | );
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/workshop/04.1_buttons/instructions.md:
--------------------------------------------------------------------------------
1 | # Styling buttons
2 |
3 | Check the code snippet on the right. The `ExampleWidget` content changed to buttons of different types.
4 |
5 | ## ElevatedButton style
6 |
7 | Earlier you saw how to customize some `ElevatedButton` style properties. `ElevatedButtonThemeData` object is created out of a `ButtonStyle`, and is provided to the global `ThemeData` configuration:
8 |
9 | ```dart
10 | MaterialApp(
11 | theme: ThemeData(
12 | elevatedButtonTheme: ElevatedButtonThemeData(
13 | style: ElevatedButton.styleFrom(
14 | backgroundColor: Colors.lime,
15 | foregroundColor: Colors.white,
16 | ),
17 | ),
18 | ),
19 | ...
20 | )
21 | ```
22 |
23 | ## ButtonStyle
24 |
25 | The `ButtonStyle` is the object that accommodates all button style settings. The `ElevatedButton.styleFrom()` is a static convenience method that constructs an `ElevatedButton` style from simple values like `primary` for button background color and `onPrimary` for text and icon color of type `Color`. However, `ButtonStyle()` default constructor allows a more granular customization, because all of its parameters are of type `MaterialStateProperty`:
26 |
27 | ```dart
28 | ButtonStyle(
29 | MaterialStateProperty? textStyle,
30 | MaterialStateProperty? backgroundColor,
31 | MaterialStateProperty? foregroundColor,
32 | MaterialStateProperty? overlayColor,
33 | ...
34 | )
35 | ```
36 |
37 | ## MaterialStateProperty
38 |
39 | Some interactive material widgets like buttons or text inputs when receiving input from the user can be characterized by zero or more [*material states*](https://material.io/design/interaction/states.html). The `MaterialState` is an enum representing such states:
40 |
41 | * `hovered` - when the user moves their mouse cursor over a given widget
42 | * `focused` - when the user navigates with the keyboard to a given widget
43 | * `pressed` - when the user is actively tapping or clicking on a given widget
44 | * `dragged` - when the user moves a given widget from one place to another
45 | * `selected` - when a given widget has been toggled on, such as a checkbox or radio button
46 | * `scrolledUnder` - when a given widget overlaps the content of a scrollable below
47 | * `disabled` - when a given widget cannot be interacted with
48 | * `error` - when a given widget has entered some form of invalid state
49 |
50 | The `MaterialStateProperty` is an interface for classes that resolve to a value of type `T` based on a widget's interactive states set. It's easier to understand by looking at the example.
51 |
52 | Let's say the button should have an overlay of different shades of green when hovered and pressed. It means, the `overlayColor` field of type `MaterialStateProperty` should resolve to different `Color` values based on the content of the `Set states` parameter:
53 |
54 | ```dart
55 | ButtonStyle(
56 | overlayColor: MaterialStateProperty.resolveWith((states) {
57 | if (states.contains(MaterialState.hovered))
58 | return Colors.greenAccent;
59 | if (states.contains(MaterialState.pressed))
60 | return Colors.lightGreenAccent;
61 | return null;
62 | }),
63 | )
64 | ```
65 |
66 | At the same time, a button should always have lime background and white text. It means the `backgroundColor` and `foregroundColor` fields of type `MaterialStateProperty` should resolve to a single value for all states:
67 |
68 | ```dart
69 | ButtonStyle(
70 | backgroundColor: MaterialStateProperty.all(Colors.lime),
71 | foregroundColor: MaterialStateProperty.all(Colors.white),
72 | )
73 | ```
74 |
75 | ## Text style
76 |
77 | In the previous step of this workshop, it was mentioned that `ElevatedButton`, `TextButton`, and `OutlinedButton` widgets' text style can be customized globally by providing a value for `labelLarge` field of the global `TextTheme` object. In addition, `ButtonStyle` object exposes `textStyle` field for more granular customization. For example, this code changes the font weight to bold when the button is in a pressed state:
78 |
79 | ```dart
80 | ButtonStyle(
81 | textStyle: MaterialStateProperty.resolveWith((states) {
82 | return states.contains(MaterialState.pressed)
83 | ? TextStyle(fontWeight: FontWeight.bold, inherit: false)
84 | : null;
85 | }),
86 | )
87 | ```
88 |
89 | ## Your turn
90 |
91 | 1. Customize the global theme `elevatedButtonTheme` field with `ElevatedButtonThemeData` object. Create its `style` using `ButtonStyle()` default constructor. Apply customizations of `backgroundColor`, `foregroundColor`, `overlayColor`, and `textStyle` provided above
.
92 |
--------------------------------------------------------------------------------
/workshop/04.1_buttons/snippet.dart:
--------------------------------------------------------------------------------
1 | // ignore_for_file: prefer_const_constructors, prefer_const_literals_to_create_immutables, curly_braces_in_flow_control_structures
2 |
3 | import 'package:flutter/material.dart';
4 |
5 | void main() {
6 | runApp(ExampleApp());
7 | }
8 |
9 | class ExampleApp extends StatelessWidget {
10 | @override
11 | Widget build(BuildContext context) {
12 | return MaterialApp(
13 | debugShowCheckedModeBanner: false,
14 | theme: ThemeData(
15 | colorScheme: ColorScheme.fromSeed(seedColor: Colors.green),
16 | // TODO 1: Provide elevatedButtonTheme field value
17 | ),
18 | home: ExamplePage(),
19 | );
20 | }
21 | }
22 |
23 | class ExamplePage extends StatelessWidget {
24 | @override
25 | Widget build(BuildContext context) {
26 | return Scaffold(
27 | appBar: AppBar(
28 | title: Text('Consistent design with Flutter Theme'),
29 | actions: [IconButton(icon: Icon(Icons.account_circle), onPressed: () {})],
30 | ),
31 | body: Padding(
32 | padding: EdgeInsets.all(20.0),
33 | child: Center(
34 | child: ExampleWidget(),
35 | ),
36 | ),
37 | );
38 | }
39 | }
40 |
41 | class ExampleWidget extends StatelessWidget {
42 | @override
43 | Widget build(BuildContext context) {
44 | return Column(
45 | mainAxisAlignment: MainAxisAlignment.spaceEvenly,
46 | children: [
47 | ElevatedButton(child: Text('ElevatedButton'), onPressed: () {}),
48 | OutlinedButton(child: Text('OutlinedButton'), onPressed: () {}),
49 | TextButton(child: Text('TextButton'), onPressed: () {}),
50 | ],
51 | );
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/workshop/04.1_buttons/solution.dart:
--------------------------------------------------------------------------------
1 | // ignore_for_file: prefer_const_constructors, prefer_const_literals_to_create_immutables, curly_braces_in_flow_control_structures
2 |
3 | import 'package:flutter/material.dart';
4 |
5 | void main() {
6 | runApp(ExampleApp());
7 | }
8 |
9 | class ExampleApp extends StatelessWidget {
10 | @override
11 | Widget build(BuildContext context) {
12 | return MaterialApp(
13 | debugShowCheckedModeBanner: false,
14 | theme: ThemeData(
15 | colorScheme: ColorScheme.fromSeed(seedColor: Colors.green),
16 | elevatedButtonTheme: ElevatedButtonThemeData(
17 | style: ButtonStyle(
18 | backgroundColor: MaterialStateProperty.all(Colors.lime),
19 | foregroundColor: MaterialStateProperty.all(Colors.white),
20 | overlayColor: MaterialStateProperty.resolveWith((states) {
21 | if (states.contains(MaterialState.hovered))
22 | return Colors.greenAccent;
23 | if (states.contains(MaterialState.pressed))
24 | return Colors.lightGreenAccent;
25 | return null;
26 | }),
27 | textStyle: MaterialStateProperty.resolveWith((states) {
28 | return states.contains(MaterialState.pressed)
29 | ? TextStyle(fontWeight: FontWeight.bold, inherit: false)
30 | : null;
31 | }),
32 | ),
33 | ),
34 | ),
35 | home: ExamplePage(),
36 | );
37 | }
38 | }
39 |
40 | class ExamplePage extends StatelessWidget {
41 | @override
42 | Widget build(BuildContext context) {
43 | return Scaffold(
44 | appBar: AppBar(
45 | title: Text('Consistent design with Flutter Theme'),
46 | actions: [IconButton(icon: Icon(Icons.account_circle), onPressed: () {})],
47 | ),
48 | body: Padding(
49 | padding: EdgeInsets.all(20.0),
50 | child: Center(
51 | child: ExampleWidget(),
52 | ),
53 | ),
54 | );
55 | }
56 | }
57 |
58 | class ExampleWidget extends StatelessWidget {
59 | @override
60 | Widget build(BuildContext context) {
61 | return Column(
62 | mainAxisAlignment: MainAxisAlignment.spaceEvenly,
63 | children: [
64 | ElevatedButton(child: Text('ElevatedButton'), onPressed: () {}),
65 | OutlinedButton(child: Text('OutlinedButton'), onPressed: () {}),
66 | TextButton(child: Text('TextButton'), onPressed: () {}),
67 | ],
68 | );
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/workshop/04.2_buttons/instructions.md:
--------------------------------------------------------------------------------
1 | # Styling buttons
2 |
3 | ## OutlinedButton style
4 |
5 | Styling of the `OutlinedButton` is very similar to `ElevatedButton`. The `ThemeData` object exposes `outlinedButtonTheme` field of type `OutlinedButtonThemeData`, which is created out of a `ButtonStyle` object. Similarly, there is a static convenience method `OutlinedButton.styleFrom` that constructs an outlined button style from simple values:
6 |
7 | ```dart
8 | MaterialApp(
9 | theme: ThemeData(
10 | outlinedButtonTheme: OutlinedButtonThemeData(
11 | style: OutlinedButton.styleFrom(
12 | foregroundColor: Colors.green,
13 | side: BorderSide(color: Colors.green, width: 2),
14 | ),
15 | ),
16 | ),
17 | ...
18 | )
19 | ```
20 |
21 | And similarly, a `ButtonStyle` object can be created with the default `ButtonStyle` constructor, that allows more granular configuration:
22 |
23 | ```dart
24 | ButtonStyle(
25 | foregroundColor: MaterialStateProperty.all(Colors.green),
26 | side: MaterialStateProperty.all(BorderSide(color: Colors.green, width: 2)),
27 | overlayColor: MaterialStateProperty.resolveWith((states) {
28 | if (states.contains(MaterialState.hovered))
29 | return Colors.greenAccent;
30 | if (states.contains(MaterialState.pressed))
31 | return Colors.lightGreenAccent;
32 | return null;
33 | }),
34 | textStyle: MaterialStateProperty.resolveWith((states) {
35 | return states.contains(MaterialState.pressed)
36 | ? TextStyle(fontWeight: FontWeight.bold, inherit: false)
37 | : null;
38 | }),
39 | ),
40 | ```
41 |
42 | ## Reusing MaterialStateProperty
43 |
44 | Have you noticed that in the code snippet above the `overlayColor` and `textStyle` values are exactly the same as for the `ElevatedButton` from the previous workshop step? It may happen that different widget types are required to have the same styling of some UI aspects and thus should share the same `MaterialStateProperty` logic. To make the implementation easy and consistent, inheritors of `MaterialStateProperty` can be declared:
45 |
46 | ```dart
47 | class ButtonOverlayColor implements MaterialStateProperty {
48 | @override
49 | Color? resolve(Set states) {
50 | if (states.contains(MaterialState.hovered))
51 | return Colors.greenAccent;
52 | if (states.contains(MaterialState.pressed))
53 | return Colors.lightGreenAccent;
54 | return null;
55 | }
56 | }
57 | ```
58 | ```dart
59 | class ButtonTextStyle implements MaterialStateProperty {
60 | @override
61 | TextStyle? resolve(Set states) {
62 | return states.contains(MaterialState.pressed)
63 | ? TextStyle(fontWeight: FontWeight.bold, inherit: false)
64 | : null;
65 | }
66 | }
67 | ```
68 |
69 | And used in multiple styles:
70 |
71 | ```dart
72 | ThemeData(
73 | elevatedButtonTheme: ElevatedButtonThemeData(
74 | style: ButtonStyle(
75 | overlayColor: ButtonOverlayColor(),
76 | textStyle: ButtonTextStyle(),
77 | ),
78 | ),
79 | outlinedButtonTheme: OutlinedButtonThemeData(
80 | style: ButtonStyle(
81 | overlayColor: ButtonOverlayColor(),
82 | textStyle: ButtonTextStyle(),
83 | ),
84 | ),
85 | )
86 | ```
87 |
88 | ## TextButton style
89 |
90 | Everything mentioned above is valid for customizing `TextButton` style. The `ThemeData` object exposes `textButtonTheme` field of type `TextButtonThemeData`, which can be created out of `ButtonStyle` instance:
91 |
92 | ```dart
93 | MaterialApp(
94 | theme: ThemeData(
95 | textButtonTheme: TextButtonThemeData(
96 | style: TextButton.styleFrom(
97 | foregroundColor: Colors.lightGreen,
98 | ),
99 | ),
100 | ),
101 | ...
102 | )
103 | ```
104 |
105 | ## Your turn
106 |
107 | 1. Declare `ButtonOverlayColor` and `ButtonTextStyle` classes to reuse their `MaterialStateProperty` logic in multiple styles.
108 | 2. Update the `ElevatedButtonThemeData` fields: `overlayColor` to a `ButtonOverlayColor` instance, `textStyle` to a `ButtonTextStyle` instance.
109 | 3. Define the global `outlinedButtonTheme` with the `style` created using `ButtonStyle()` default constructor. Apply customizations of `foregroundColor` and `side` fields provided above. Set `overlayColor` and `textStyle` field values the same way as `ElevatedButtonThemeData` fields.
110 | 4. Customize the global `textButtonTheme` using the `style` obtained with `TextButton.styleFrom` method, apply customization of `primary` color provided above
.
111 |
--------------------------------------------------------------------------------
/workshop/04.2_buttons/snippet.dart:
--------------------------------------------------------------------------------
1 | // ignore_for_file: prefer_const_constructors, prefer_const_literals_to_create_immutables, curly_braces_in_flow_control_structures
2 |
3 | import 'package:flutter/material.dart';
4 |
5 | void main() {
6 | runApp(ExampleApp());
7 | }
8 |
9 | // TODO 1: Declare ButtonOverlayColor and ButtonTextStyle classes
10 |
11 | class ExampleApp extends StatelessWidget {
12 | @override
13 | Widget build(BuildContext context) {
14 | return MaterialApp(
15 | debugShowCheckedModeBanner: false,
16 | theme: ThemeData(
17 | colorScheme: ColorScheme.fromSeed(seedColor: Colors.green),
18 | elevatedButtonTheme: ElevatedButtonThemeData(
19 | style: ButtonStyle(
20 | backgroundColor: MaterialStateProperty.all(Colors.lime),
21 | foregroundColor: MaterialStateProperty.all(Colors.white),
22 | // TODO 2: Replace overlayColor field value with an instance of ButtonOverlayColor
23 | overlayColor: MaterialStateProperty.resolveWith((states) {
24 | if (states.contains(MaterialState.hovered))
25 | return Colors.greenAccent;
26 | if (states.contains(MaterialState.pressed))
27 | return Colors.lightGreenAccent;
28 | return null;
29 | }),
30 | // TODO 2: Replace textStyle field value with an instance of ButtonTextStyle
31 | textStyle: MaterialStateProperty.resolveWith((states) {
32 | return states.contains(MaterialState.pressed)
33 | ? TextStyle(fontWeight: FontWeight.bold, inherit: false)
34 | : null;
35 | }),
36 | ),
37 | ),
38 | // TODO 3: Provide outlinedButtonTheme field value
39 | // TODO 4: Provide textButtonTheme field value
40 | ),
41 | home: ExamplePage(),
42 | );
43 | }
44 | }
45 |
46 | class ExamplePage extends StatelessWidget {
47 | @override
48 | Widget build(BuildContext context) {
49 | return Scaffold(
50 | appBar: AppBar(
51 | title: Text('Consistent design with Flutter Theme'),
52 | actions: [IconButton(icon: Icon(Icons.account_circle), onPressed: () {})],
53 | ),
54 | body: Padding(
55 | padding: EdgeInsets.all(20.0),
56 | child: Center(
57 | child: ExampleWidget(),
58 | ),
59 | ),
60 | );
61 | }
62 | }
63 |
64 | class ExampleWidget extends StatelessWidget {
65 | @override
66 | Widget build(BuildContext context) {
67 | return Column(
68 | mainAxisAlignment: MainAxisAlignment.spaceEvenly,
69 | children: [
70 | ElevatedButton(child: Text('ElevatedButton'), onPressed: () {}),
71 | OutlinedButton(child: Text('OutlinedButton'), onPressed: () {}),
72 | TextButton(child: Text('TextButton'), onPressed: () {}),
73 | ],
74 | );
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/workshop/04.2_buttons/solution.dart:
--------------------------------------------------------------------------------
1 | // ignore_for_file: prefer_const_constructors, prefer_const_literals_to_create_immutables, curly_braces_in_flow_control_structures
2 |
3 | import 'package:flutter/material.dart';
4 |
5 | void main() {
6 | runApp(ExampleApp());
7 | }
8 |
9 | class ButtonOverlayColor implements MaterialStateProperty {
10 | @override
11 | Color? resolve(Set states) {
12 | if (states.contains(MaterialState.hovered))
13 | return Colors.greenAccent;
14 | if (states.contains(MaterialState.pressed))
15 | return Colors.lightGreenAccent;
16 | return null;
17 | }
18 | }
19 |
20 | class ButtonTextStyle implements MaterialStateProperty {
21 | @override
22 | TextStyle? resolve(Set states) {
23 | return states.contains(MaterialState.pressed)
24 | ? TextStyle(fontWeight: FontWeight.bold, inherit: false)
25 | : null;
26 | }
27 | }
28 |
29 | class ExampleApp extends StatelessWidget {
30 | @override
31 | Widget build(BuildContext context) {
32 | return MaterialApp(
33 | debugShowCheckedModeBanner: false,
34 | theme: ThemeData(
35 | colorScheme: ColorScheme.fromSeed(seedColor: Colors.green),
36 | elevatedButtonTheme: ElevatedButtonThemeData(
37 | style: ButtonStyle(
38 | backgroundColor: MaterialStateProperty.all(Colors.lime),
39 | foregroundColor: MaterialStateProperty.all(Colors.white),
40 | overlayColor: ButtonOverlayColor(),
41 | textStyle: ButtonTextStyle(),
42 | ),
43 | ),
44 | outlinedButtonTheme: OutlinedButtonThemeData(
45 | style: ButtonStyle(
46 | foregroundColor: MaterialStateProperty.all(Colors.green),
47 | side: MaterialStateProperty.all(BorderSide(color: Colors.green, width: 2)),
48 | overlayColor: ButtonOverlayColor(),
49 | textStyle: ButtonTextStyle(),
50 | ),
51 | ),
52 | textButtonTheme: TextButtonThemeData(
53 | style: TextButton.styleFrom(
54 | foregroundColor: Colors.lightGreen,
55 | ),
56 | ),
57 | ),
58 | home: ExamplePage(),
59 | );
60 | }
61 | }
62 |
63 | class ExamplePage extends StatelessWidget {
64 | @override
65 | Widget build(BuildContext context) {
66 | return Scaffold(
67 | appBar: AppBar(
68 | title: Text('Consistent design with Flutter Theme'),
69 | actions: [IconButton(icon: Icon(Icons.account_circle), onPressed: () {})],
70 | ),
71 | body: Padding(
72 | padding: EdgeInsets.all(20.0),
73 | child: Center(
74 | child: ExampleWidget(),
75 | ),
76 | ),
77 | );
78 | }
79 | }
80 |
81 | class ExampleWidget extends StatelessWidget {
82 | @override
83 | Widget build(BuildContext context) {
84 | return Column(
85 | mainAxisAlignment: MainAxisAlignment.spaceEvenly,
86 | children: [
87 | ElevatedButton(child: Text('ElevatedButton'), onPressed: () {}),
88 | OutlinedButton(child: Text('OutlinedButton'), onPressed: () {}),
89 | TextButton(child: Text('TextButton'), onPressed: () {}),
90 | ],
91 | );
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/workshop/05_inputs/instructions.md:
--------------------------------------------------------------------------------
1 | # Styling inputs
2 |
3 | Check the code snippet on the right. The `ExampleWidget` content changed to `TextField` widgets in different states with various decorations.
4 |
5 | ## TextField style
6 |
7 | Hopefully, by now it's no surprise for you that the `ThemeData` object exposes a field to customize the look of the `TextField` widget - `inputDecorationTheme` of type `InputDecorationTheme`:
8 |
9 | ```dart
10 | MaterialApp(
11 | theme: ThemeData(
12 | inputDecorationTheme: InputDecorationTheme(...),
13 | ),
14 | ...
15 | )
16 | ```
17 |
18 | The `InputDecorationTheme` class has about 30 fields dedicated to different aspects of `TextField` UI and behavior. Here are some of them:
19 |
20 | ```dart
21 | InputDecorationTheme(
22 | errorStyle: TextStyle(
23 | fontStyle: FontStyle.italic,
24 | ),
25 | floatingLabelStyle: TextStyle(
26 | fontWeight: FontWeight.bold,
27 | color: Colors.lightGreen,
28 | ),
29 | hintStyle: TextStyle(
30 | fontStyle: FontStyle.italic,
31 | fontSize: 14.0,
32 | ),
33 | suffixIconColor: Colors.greenAccent,
34 | )
35 | ```
36 |
37 | Additionally, text style of the `hint` and of the `text` entered in the `TextField` can be implicitly controlled by the `titleMedium` field of the `TextTheme`, error style - by `bodySmall`.
38 |
39 | ## TextField borders
40 |
41 | `TextField` borders deserve a special attention. They are:
42 |
43 | * `enabledBorder` - displayed when the widget is enabled and is not showing an error;
44 | * `focusedBorder` - displayed when the widget has the focus and is not showing an error;
45 | * `errorBorder` - displayed when the widget does not have the focus and is showing an error;
46 | * `focusedErrorBorder` - displayed when the widget has the focus and is showing an error;
47 | * `disabledBorder` - displayed when the widget is disabled and is not showing an error;
48 | * `border` - this property is only used when the appropriate one of the above is not specified.
49 |
50 | These values default to `UnderlineInputBorder` which draws a horizontal line at the bottom of a widget. Instead, let's define a custom set of borders using an `OutlineInputBorder` which draws a rounded rectangle around the widget. It should be twice thicker when focused, light green when enabled, red in case of error, and grey when disabled:
51 |
52 | ```dart
53 | InputDecorationTheme(
54 | enabledBorder: OutlineInputBorder(borderSide: BorderSide(color: Colors.lightGreen)),
55 | focusedBorder: OutlineInputBorder(borderSide: BorderSide(color: Colors.lightGreen, width: 2)),
56 | errorBorder: OutlineInputBorder(borderSide: BorderSide(color: Colors.red)),
57 | focusedErrorBorder: OutlineInputBorder(borderSide: BorderSide(color: Colors.red, width: 2)),
58 | disabledBorder: OutlineInputBorder(borderSide: BorderSide(color: Colors.grey)),
59 | )
60 | ```
61 |
62 | That is a lot of similar code to write. Would not it be cool to leverage the `MaterialStateProperty` mechanism and react to different `TextField` material states in a single callback?
63 |
64 | ## MaterialStateProperty compatibility
65 |
66 | There are material widgets with fields that can accept both values of simple types you are used to like `Color` or `TextStyle`, and the `MaterialStateProperty` variant of that type. It is possible thanks to compatibility bridge classes like:
67 |
68 | ```dart
69 | class MaterialStateColor extends Color implements MaterialStateProperty {
70 | ...
71 | }
72 | ```
73 |
74 | An instance of `MaterialStateColor` class can be assigned to a widget's field of type `Color`. At the same time a widget can accept a `MaterialStateProperty` value and benefit from reacting to interactive material states. There are several compatibility bridges implemented:
75 |
76 | * `MaterialStateColor`
77 | * `MaterialStateMouseCursor`
78 | * `MaterialStateBorderSide`
79 | * `MaterialStateOutlinedBorder`
80 | * `MaterialStateTextStyle`
81 | * `MaterialStateOutlineInputBorder`
82 | * `MaterialStateUnderlineInputBorder`
83 |
84 | Back to styling `TextField` borders, the documentation for the `border` field of `InputDecorationTheme`states:
85 |
86 | ```dart
87 | class InputDecorationTheme {
88 | /// The shape of the border to draw around the decoration's container.
89 | ///
90 | /// If [border] is a [MaterialStateUnderlineInputBorder]
91 | /// or [MaterialStateOutlineInputBorder], then the effective border can depend on
92 | /// the [MaterialState.focused] state, i.e. if the [TextField] is focused or not.
93 | final InputBorder? border;
94 | }
95 | ```
96 |
97 | This means that to repeat the borders styling above, instead of providing five different borders, it is possible to provide just one of type `MaterialStateOutlineInputBorder`:
98 |
99 | ```dart
100 | InputDecorationTheme(
101 | border: MaterialStateOutlineInputBorder.resolveWith((states) {
102 | final isFocused = states.contains(MaterialState.focused);
103 | final isDisabled = states.contains(MaterialState.disabled);
104 | final hasError = states.contains(MaterialState.error);
105 |
106 | final color = isDisabled ? Colors.grey : hasError ? Colors.red : Colors.lightGreen;
107 | final width = isFocused ? 2.0 : 1.0;
108 |
109 | return OutlineInputBorder(borderSide: BorderSide(color: color, width: width));
110 | }),
111 | )
112 | ```
113 |
114 | ## Text selection
115 |
116 | There is another important aspect of `TextField` behavior, which is cursor and text selection style. It is regulated by the `textSelectionTheme` field of the `ThemeData` object:
117 |
118 | ```dart
119 | MaterialApp(
120 | theme: ThemeData(
121 | textSelectionTheme: TextSelectionThemeData(
122 | cursorColor: Colors.lightGreen,
123 | selectionColor: Colors.lime,
124 | selectionHandleColor: Colors.lightGreen,
125 | ),
126 | ),
127 | ...
128 | )
129 | ```
130 |
131 | ## Your turn
132 |
133 | 1. Define global `inputDecorationTheme` using `InputDecorationTheme` with all customizations given above. Use `MaterialStateOutlineInputBorder` to define the `border` field value. Enter some text into input fields, switch between fields to see different UI when they are focused or not.
134 | 2. Define global `textSelectionTheme` using customizations given above. Select entered texts to see the effect
.
135 |
--------------------------------------------------------------------------------
/workshop/05_inputs/snippet.dart:
--------------------------------------------------------------------------------
1 | // ignore_for_file: prefer_const_constructors, prefer_const_literals_to_create_immutables, curly_braces_in_flow_control_structures
2 |
3 | import 'package:flutter/material.dart';
4 |
5 | void main() {
6 | runApp(ExampleApp());
7 | }
8 |
9 | class ExampleApp extends StatelessWidget {
10 | @override
11 | Widget build(BuildContext context) {
12 | return MaterialApp(
13 | debugShowCheckedModeBanner: false,
14 | theme: ThemeData(
15 | colorScheme: ColorScheme.fromSeed(seedColor: Colors.green),
16 | // TODO 1: Provide inputDecorationTheme field value
17 | // TODO 2: Provide textSelectionTheme field value
18 | ),
19 | home: ExamplePage(),
20 | );
21 | }
22 | }
23 |
24 | class ExamplePage extends StatelessWidget {
25 | @override
26 | Widget build(BuildContext context) {
27 | return Scaffold(
28 | appBar: AppBar(
29 | title: Text('Consistent design with Flutter Theme'),
30 | actions: [IconButton(icon: Icon(Icons.account_circle), onPressed: () {})],
31 | ),
32 | body: Padding(
33 | padding: EdgeInsets.all(20.0),
34 | child: Center(
35 | child: ExampleWidget(),
36 | ),
37 | ),
38 | );
39 | }
40 | }
41 |
42 | class ExampleWidget extends StatelessWidget {
43 | @override
44 | Widget build(BuildContext context) {
45 | return Column(
46 | mainAxisAlignment: MainAxisAlignment.spaceEvenly,
47 | children: [
48 | TextField(
49 | decoration: InputDecoration(
50 | hintText: 'enabled',
51 | labelText: 'label',
52 | suffixIcon: Icon(Icons.email),
53 | ),
54 | ),
55 | TextField(
56 | decoration: InputDecoration(
57 | hintText: 'enabled error',
58 | errorText: 'error',
59 | ),
60 | ),
61 | TextField(
62 | decoration: InputDecoration(
63 | hintText: 'disabled',
64 | ),
65 | enabled: false,
66 | ),
67 | ],
68 | );
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/workshop/05_inputs/solution.dart:
--------------------------------------------------------------------------------
1 | // ignore_for_file: prefer_const_constructors, prefer_const_literals_to_create_immutables, curly_braces_in_flow_control_structures
2 |
3 | import 'package:flutter/material.dart';
4 |
5 | void main() {
6 | runApp(ExampleApp());
7 | }
8 |
9 | class ExampleApp extends StatelessWidget {
10 | @override
11 | Widget build(BuildContext context) {
12 | return MaterialApp(
13 | debugShowCheckedModeBanner: false,
14 | theme: ThemeData(
15 | colorScheme: ColorScheme.fromSeed(seedColor: Colors.green),
16 | inputDecorationTheme: InputDecorationTheme(
17 | errorStyle: TextStyle(
18 | fontStyle: FontStyle.italic,
19 | ),
20 | floatingLabelStyle: TextStyle(
21 | fontWeight: FontWeight.bold,
22 | color: Colors.lightGreen,
23 | ),
24 | hintStyle: TextStyle(
25 | fontStyle: FontStyle.italic,
26 | fontSize: 14.0,
27 | ),
28 | suffixIconColor: Colors.greenAccent,
29 | border: MaterialStateOutlineInputBorder.resolveWith((states) {
30 | final isFocused = states.contains(MaterialState.focused);
31 | final isDisabled = states.contains(MaterialState.disabled);
32 | final hasError = states.contains(MaterialState.error);
33 |
34 | final color = isDisabled ? Colors.grey : hasError ? Colors.red : Colors.lightGreen;
35 | final width = isFocused ? 2.0 : 1.0;
36 |
37 | return OutlineInputBorder(borderSide: BorderSide(color: color, width: width));
38 | }),
39 | ),
40 | textSelectionTheme: TextSelectionThemeData(
41 | cursorColor: Colors.lightGreen,
42 | selectionColor: Colors.lime,
43 | selectionHandleColor: Colors.lightGreen,
44 | ),
45 | ),
46 | home: ExamplePage(),
47 | );
48 | }
49 | }
50 |
51 | class ExamplePage extends StatelessWidget {
52 | @override
53 | Widget build(BuildContext context) {
54 | return Scaffold(
55 | appBar: AppBar(
56 | title: Text('Consistent design with Flutter Theme'),
57 | actions: [IconButton(icon: Icon(Icons.account_circle), onPressed: () {})],
58 | ),
59 | body: Padding(
60 | padding: EdgeInsets.all(20.0),
61 | child: Center(
62 | child: ExampleWidget(),
63 | ),
64 | ),
65 | );
66 | }
67 | }
68 |
69 | class ExampleWidget extends StatelessWidget {
70 | @override
71 | Widget build(BuildContext context) {
72 | return Column(
73 | mainAxisAlignment: MainAxisAlignment.spaceEvenly,
74 | children: [
75 | TextField(
76 | decoration: InputDecoration(
77 | hintText: 'enabled',
78 | labelText: 'label',
79 | suffixIcon: Icon(Icons.email),
80 | ),
81 | ),
82 | TextField(
83 | decoration: InputDecoration(
84 | hintText: 'enabled error',
85 | errorText: 'error',
86 | ),
87 | ),
88 | TextField(
89 | decoration: InputDecoration(
90 | hintText: 'disabled',
91 | ),
92 | enabled: false,
93 | ),
94 | ],
95 | );
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/workshop/06_screens/instructions.md:
--------------------------------------------------------------------------------
1 | # Styling screens
2 |
3 | The last styling task for today is configuring the screen skeleton: `Scaffold` and `AppBar` widgets.
4 |
5 | Check the code snippet on the right. The `ExampleWidget` content changed to a scrollable list of lime boxes.
6 |
7 | ## Scaffold style
8 |
9 | `Scaffold` is a fundamental widget responsible for screen layout and behaviour, and it does not have much styling possibilities in it. The individual `Scaffold` background can be changed with:
10 |
11 | ```dart
12 | Scaffold(
13 | backgroundColor: Colors.green[50],
14 | body: ...
15 | )
16 | ```
17 |
18 | And to apply such configuration to all `Scaffold` widgets, you have to modify `scaffoldBackgroundColor` field value:
19 |
20 | ```dart
21 | MaterialApp(
22 | theme: ThemeData(
23 | scaffoldBackgroundColor: Colors.green[50],
24 | ),
25 | ...
26 | )
27 | ```
28 |
29 | ## AppBar style
30 |
31 | To consistently apply a particular style to all `AppBar` widgets, you can specify the `appBarTheme` field of the global `ThemeData` object:
32 |
33 | ```dart
34 | MaterialApp(
35 | theme: ThemeData(
36 | appBarTheme: AppBarTheme(...),
37 | ),
38 | ...
39 | )
40 | ```
41 |
42 | Some of customizable `AppBarTheme` properties are:
43 |
44 | ```dart
45 | AppBarTheme(
46 | backgroundColor: Colors.lime,
47 | foregroundColor: Colors.white,
48 | elevation: 8.0,
49 | actionsIconTheme: IconThemeData(color: Colors.white),
50 | centerTitle: false,
51 | )
52 | ```
53 |
54 | `AppBarTheme` gives a chance to override the look of `IconButton` widgets inside the `AppBar` with `actionsIconTheme` if you have earlier provided the global `iconTheme` value.
55 |
56 | Remember the `MaterialStateColor` class from previous workshop step? The one that enables providing `MaterialStateProperty` to a field of type `Color`. The `AppBar` background can change responding to `scrolledUnder` material state:
57 |
58 | ```dart
59 | AppBarTheme(
60 | backgroundColor: MaterialStateColor.resolveWith((states) {
61 | return states.contains(MaterialState.scrolledUnder)
62 | ? Colors.limeAccent
63 | : Colors.lime;
64 | }),
65 | )
66 | ```
67 |
68 | ## Your turn
69 |
70 | 1. Modify global `ThemeData` by providing `scaffoldBackgroundColor` field value.
71 | 2. Define global `appBarTheme` using customizations given above. Set `backgroundColor` field of the `AppBarTheme` to a `MaterialStateColor` instance. Scroll the list of lime boxes to see the `AppBar` background change
.
72 |
--------------------------------------------------------------------------------
/workshop/06_screens/snippet.dart:
--------------------------------------------------------------------------------
1 | // ignore_for_file: prefer_const_constructors, prefer_const_literals_to_create_immutables, curly_braces_in_flow_control_structures
2 |
3 | import 'package:flutter/material.dart';
4 |
5 | void main() {
6 | runApp(ExampleApp());
7 | }
8 |
9 | class ExampleApp extends StatelessWidget {
10 | @override
11 | Widget build(BuildContext context) {
12 | return MaterialApp(
13 | debugShowCheckedModeBanner: false,
14 | theme: ThemeData(
15 | colorScheme: ColorScheme.fromSeed(seedColor: Colors.green),
16 | // TODO 1: Provide scaffoldBackgroundColor field value
17 | // TODO 2: Provide appBarTheme field value
18 | ),
19 | home: ExamplePage(),
20 | );
21 | }
22 | }
23 |
24 | class ExamplePage extends StatelessWidget {
25 | @override
26 | Widget build(BuildContext context) {
27 | return Scaffold(
28 | appBar: AppBar(
29 | title: Text('Consistent design with Flutter Theme'),
30 | actions: [IconButton(icon: Icon(Icons.account_circle), onPressed: () {})],
31 | ),
32 | body: Padding(
33 | padding: EdgeInsets.all(20.0),
34 | child: Center(
35 | child: ExampleWidget(),
36 | ),
37 | ),
38 | );
39 | }
40 | }
41 |
42 | class ExampleWidget extends StatelessWidget {
43 | @override
44 | Widget build(BuildContext context) {
45 | final screenHeight = MediaQuery.of(context).size.height;
46 |
47 | return ListView.builder(
48 | itemBuilder: (context, index) => Padding(
49 | padding: EdgeInsets.all(20.0) + EdgeInsets.only(left: index.isEven ? 0.0 : 40.0, right: !index.isEven ? 0.0 : 40.0),
50 | child: Container(height: screenHeight / 3, color: Colors.lime),
51 | ),
52 | itemCount: 4,
53 | );
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/workshop/06_screens/solution.dart:
--------------------------------------------------------------------------------
1 | // ignore_for_file: prefer_const_constructors, prefer_const_literals_to_create_immutables, curly_braces_in_flow_control_structures
2 |
3 | import 'package:flutter/material.dart';
4 |
5 | void main() {
6 | runApp(ExampleApp());
7 | }
8 |
9 | class ExampleApp extends StatelessWidget {
10 | @override
11 | Widget build(BuildContext context) {
12 | return MaterialApp(
13 | debugShowCheckedModeBanner: false,
14 | theme: ThemeData(
15 | colorScheme: ColorScheme.fromSeed(seedColor: Colors.green),
16 | scaffoldBackgroundColor: Colors.green[50],
17 | appBarTheme: AppBarTheme(
18 | backgroundColor: MaterialStateColor.resolveWith((states) {
19 | return states.contains(MaterialState.scrolledUnder)
20 | ? Colors.limeAccent
21 | : Colors.lime;
22 | }),
23 | foregroundColor: Colors.white,
24 | elevation: 8.0,
25 | actionsIconTheme: IconThemeData(color: Colors.white),
26 | centerTitle: false,
27 | ),
28 | ),
29 | home: ExamplePage(),
30 | );
31 | }
32 | }
33 |
34 | class ExamplePage extends StatelessWidget {
35 | @override
36 | Widget build(BuildContext context) {
37 | return Scaffold(
38 | appBar: AppBar(
39 | title: Text('Consistent design with Flutter Theme'),
40 | actions: [IconButton(icon: Icon(Icons.account_circle), onPressed: () {})],
41 | ),
42 | body: Padding(
43 | padding: EdgeInsets.all(20.0),
44 | child: Center(
45 | child: ExampleWidget(),
46 | ),
47 | ),
48 | );
49 | }
50 | }
51 |
52 | class ExampleWidget extends StatelessWidget {
53 | @override
54 | Widget build(BuildContext context) {
55 | final screenHeight = MediaQuery.of(context).size.height;
56 |
57 | return ListView.builder(
58 | itemBuilder: (context, index) => Padding(
59 | padding: EdgeInsets.all(20.0) + EdgeInsets.only(left: index.isEven ? 0.0 : 40.0, right: !index.isEven ? 0.0 : 40.0),
60 | child: Container(height: screenHeight / 3, color: Colors.lime),
61 | ),
62 | itemCount: 4,
63 | );
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/workshop/07_conclusion/instructions.md:
--------------------------------------------------------------------------------
1 | # Well done!
2 |
3 | In this workshop you learned about:
4 |
5 | * Styling with `Theme` and `MaterialApp` widgets
6 | * Ways to create and customize `ThemeData` object
7 | * Customizing texts and icons
8 | * Styling buttons and text inputs
9 | * Customizing scaffolds and app bars
10 |
11 | The goal of this workshop was to show how it is possible to customize different widget types. However, you don't necessarily have to do this. The snippet on the right contains all widgets we styled in this workshop. Everything is customized by providing the generated `ColorScheme` only, and it already looks stylish, doesn't it? And if you have access to a professional designer or want to experiment - `ThemeData` is there to help you with consistency.
12 |
13 | ## What's next
14 |
15 | * `ThemeData` object has many more fields dedicated to configuring various aspects of the application design. Check out the [*documentation*](https://api.flutter.dev/flutter/material/ThemeData-class.html) to learn more.
16 | * If the standard fields set it not enough, `ThemeData` object can be enhanced with custom values set through the `ThemeExtension` mechanism. Check out the [*documentation*](https://api.flutter.dev/flutter/material/ThemeExtension-class.html) to learn more.
17 | * Also read about [*Material design*](https://m3.material.io/), especially about [*Color schemes*](https://m3.material.io/styles/color/the-color-system/color-roles).
18 | * Watch the [*"MaterialStateProperties" episode*](https://www.youtube.com/watch?v=CylXr3AF3uU) of the [*Decoding Flutter*](https://www.youtube.com/playlist?list=PLjxrf2q8roU1fRV40Ec8200rX6OuQkmnl) series.
19 |
20 | Good luck in mastering Flutter
!
21 |
22 | > If you are interested in creating DartPad workshops, check the [*Workshop Authoring Guide*](https://github.com/dart-lang/dart-pad/wiki/Workshop-Authoring-Guide).
23 |
24 | > Source code for this workshop can be found in [*flutter_theme_workshop GitHub repository*](https://github.com/foxanna/flutter_theme_workshop).
25 |
--------------------------------------------------------------------------------
/workshop/07_conclusion/snippet.dart:
--------------------------------------------------------------------------------
1 | // ignore_for_file: prefer_const_constructors, prefer_const_literals_to_create_immutables, curly_braces_in_flow_control_structures
2 |
3 | import 'package:flutter/material.dart';
4 |
5 | void main() {
6 | runApp(ExampleApp());
7 | }
8 |
9 | class ExampleApp extends StatelessWidget {
10 | @override
11 | Widget build(BuildContext context) {
12 | return MaterialApp(
13 | debugShowCheckedModeBanner: false,
14 | theme: ThemeData(
15 | colorScheme: ColorScheme.fromSeed(seedColor: Colors.green),
16 | ),
17 | home: ExamplePage(),
18 | );
19 | }
20 | }
21 |
22 | class ExamplePage extends StatelessWidget {
23 | @override
24 | Widget build(BuildContext context) {
25 | return Scaffold(
26 | appBar: AppBar(
27 | title: Text('Consistent design with Flutter Theme'),
28 | actions: [IconButton(icon: Icon(Icons.account_circle), onPressed: () {})],
29 | ),
30 | body: Padding(
31 | padding: EdgeInsets.all(20.0),
32 | child: Center(
33 | child: ExampleWidget(),
34 | ),
35 | ),
36 | );
37 | }
38 | }
39 |
40 | class ExampleWidget extends StatelessWidget {
41 | @override
42 | Widget build(BuildContext context) {
43 | return Column(
44 | mainAxisAlignment: MainAxisAlignment.spaceEvenly,
45 | children: [
46 | Row(
47 | mainAxisAlignment: MainAxisAlignment.center,
48 | children: [
49 | Icon(
50 | Icons.account_circle,
51 | color: Theme.of(context).colorScheme.primary,
52 | ),
53 | Text(
54 | 'Don\'t have an account yet?',
55 | style: Theme.of(context).textTheme.bodyLarge,
56 | ),
57 | ],
58 | ),
59 | TextField(
60 | decoration: InputDecoration(
61 | labelText: 'email',
62 | suffixIcon: Icon(Icons.email),
63 | ),
64 | ),
65 | Row(
66 | mainAxisAlignment: MainAxisAlignment.spaceEvenly,
67 | children: [
68 | TextButton(child: Text('Skip'), onPressed: () {}),
69 | ElevatedButton(child: Text('Register'), onPressed: () {}),
70 | ],
71 | ),
72 | ],
73 | );
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/workshop/meta.yaml:
--------------------------------------------------------------------------------
1 | name: Consistent design with Flutter Theme
2 | type: flutter
3 | steps:
4 | - name: Introduction
5 | directory: 01_introduction
6 | has_solution: true
7 | - name: ThemeData
8 | directory: 02.1_themedata
9 | has_solution: true
10 | - name: Global ThemeData
11 | directory: 02.2_themedata
12 | has_solution: true
13 | - name: Styling texts and icons
14 | directory: 03.1_texts_icons
15 | has_solution: true
16 | - name: Styling texts and icons
17 | directory: 03.2_texts_icons
18 | has_solution: true
19 | - name: Styling buttons
20 | directory: 04.1_buttons
21 | has_solution: true
22 | - name: Styling buttons
23 | directory: 04.2_buttons
24 | has_solution: true
25 | - name: Styling inputs
26 | directory: 05_inputs
27 | has_solution: true
28 | - name: Styling screens
29 | directory: 06_screens
30 | has_solution: true
31 | - name: Conclusion
32 | directory: 07_conclusion
33 | has_solution: false
34 |
--------------------------------------------------------------------------------