├── .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 | ![](images/cover.png) 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 | ![](images/adventure_workshop.png) 16 | 17 | ![](images/adventure_avatar.png) 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 limeGoogle Analytics. 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 setGoogle Analytics. 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` fieldGoogle Analytics. 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-transparentGoogle Analytics. 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` methodsGoogle Analytics. 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 aboveGoogle Analytics. 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 aboveGoogle Analytics. 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 effectGoogle Analytics. 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 changeGoogle Analytics. 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 FlutterGoogle Analytics! 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 | --------------------------------------------------------------------------------