diff --git a/site/lib/_sass/_site.scss b/site/lib/_sass/_site.scss index 3c5ba5f402d..c400cf86d58 100644 --- a/site/lib/_sass/_site.scss +++ b/site/lib/_sass/_site.scss @@ -43,6 +43,7 @@ @use 'components/theming'; @use 'components/tooltip'; @use 'components/trailing'; +@use 'components/tutorial_pages'; // Styles for specific pages, alphabetically ordered. @use 'pages/glossary'; diff --git a/site/lib/_sass/base/_utils.scss b/site/lib/_sass/base/_utils.scss index 6bc90882928..1eb32e68c78 100644 --- a/site/lib/_sass/base/_utils.scss +++ b/site/lib/_sass/base/_utils.scss @@ -58,8 +58,15 @@ main { .simple-border { border: 1px solid var(--site-inset-borderColor); } + + .center { + display: flex; + justify-content: center; + + } } .text-center { text-align: center; } + diff --git a/site/lib/_sass/components/_stepper.scss b/site/lib/_sass/components/_stepper.scss index 41d14406f28..f12524c7199 100644 --- a/site/lib/_sass/components/_stepper.scss +++ b/site/lib/_sass/components/_stepper.scss @@ -1,6 +1,6 @@ .stepper { - details { + >details { position: relative; margin: 0; padding-bottom: 1px; @@ -25,7 +25,7 @@ display: none; } - summary { + >summary { position: relative; display: flex; align-items: center; @@ -52,7 +52,14 @@ } .step-title { - .header-wrapper, h1, h2, h3, h4, h5, h6 { + + .header-wrapper, + h1, + h2, + h3, + h4, + h5, + h6 { margin: 0; } } @@ -66,7 +73,7 @@ } } - &[open] summary { + &[open]>summary { .step-number { background-color: var(--site-primary-color); color: var(--site-onPrimary-color-lightest); @@ -77,7 +84,7 @@ } } - &:not([open]):has(~[open]) summary .step-number { + &:not([open]):has(~[open])>summary .step-number { background-color: var(--site-onPrimary-color-light); color: var(--site-primary-color); } diff --git a/site/lib/_sass/components/_summary-card.scss b/site/lib/_sass/components/_summary-card.scss index 37ee9ff99ff..188ca2964bf 100644 --- a/site/lib/_sass/components/_summary-card.scss +++ b/site/lib/_sass/components/_summary-card.scss @@ -84,4 +84,15 @@ } } } -} + + .summary-card-item-static { + .summary-card-item-details { + padding: .8rem 1.2rem; + color: var(--site-base-fgColor-lighter); + + >:last-child { + margin-bottom: 0; + } + } + } +} \ No newline at end of file diff --git a/site/lib/_sass/components/_tutorial_pages.scss b/site/lib/_sass/components/_tutorial_pages.scss new file mode 100644 index 00000000000..501ee971cb7 --- /dev/null +++ b/site/lib/_sass/components/_tutorial_pages.scss @@ -0,0 +1,40 @@ +// Styles for TutorialLesson component sections. + +.tutorial-lesson { + // Container for the entire tutorial lesson + + .tutorial-intro { + margin: 1rem 0; + + .description { + font-size: 1.25rem; + font-weight: 600; + margin-bottom: 1rem; + } + + .intro-video { + display: flex; + justify-content: center; + background-color: var(--site-diagram-wrap-bgColor); + padding: 1rem; + border-radius: 1rem; + + margin-bottom: 1.5rem; + } + } + + .tutorial-steps { + margin: 2rem; + } + + .tutorial-divider { + border: none; + border-top: 1px solid var(--site-inset-borderColor); + margin: 3rem 1rem; + } + + img { + display:flex; + justify-self: center; + } +} \ No newline at end of file diff --git a/site/lib/src/client/global_scripts.dart b/site/lib/src/client/global_scripts.dart index 248fca1678e..78edaafbe58 100644 --- a/site/lib/src/client/global_scripts.dart +++ b/site/lib/src/client/global_scripts.dart @@ -390,10 +390,18 @@ void _setUpSteppers() { for (var i = 0; i < steppers.length; i++) { final stepper = steppers.item(i) as web.HTMLElement; - final steps = stepper.querySelectorAll('details'); + final children = stepper.childNodes; + final steps = [ + for (var j = 0; j < children.length; j++) + if (children.item(j) case web.Element( + nodeType: web.Node.ELEMENT_NODE, + tagName: 'DETAILS', + )) + children.item(j) as web.HTMLDetailsElement, + ]; for (var j = 0; j < steps.length; j++) { - final step = steps.item(j) as web.HTMLDetailsElement; + final step = steps[j]; step.addEventListener( 'toggle', @@ -401,7 +409,7 @@ void _setUpSteppers() { // Close all other steps when one is opened. if (step.open) { for (var k = 0; k < steps.length; k++) { - final otherStep = steps.item(k) as web.HTMLDetailsElement; + final otherStep = steps[k]; if (otherStep != step) { otherStep.open = false; } @@ -419,7 +427,7 @@ void _setUpSteppers() { step.open = false; _scrollTo(step, smooth: false); if (j + 1 < steps.length) { - final nextStep = steps.item(j + 1) as web.HTMLDetailsElement; + final nextStep = steps[j + 1]; nextStep.open = true; _scrollTo(nextStep, smooth: true); } diff --git a/site/lib/src/components/tutorial/stepper.dart b/site/lib/src/components/tutorial/stepper.dart index 9f2a87d22cb..c621b55f98a 100644 --- a/site/lib/src/components/tutorial/stepper.dart +++ b/site/lib/src/components/tutorial/stepper.dart @@ -39,7 +39,7 @@ class Stepper extends CustomComponent { attributes: {'class': 'header-wrapper'}, children: [final ElementNode heading, ..._], ) when heading.tag == 'h$level') { - steps.add((title: child, content: [])); + steps.add((title: heading, content: [])); } else { if (steps.isEmpty) { throw Exception( diff --git a/site/lib/src/components/tutorial/summary_card.dart b/site/lib/src/components/tutorial/summary_card.dart index 4f38641493f..76b8a2e7dd1 100644 --- a/site/lib/src/components/tutorial/summary_card.dart +++ b/site/lib/src/components/tutorial/summary_card.dart @@ -35,16 +35,25 @@ class SummaryCard extends CustomComponent { model.items.isNotEmpty, 'SummaryCard must contain at least one item.', ); - return SummaryCardComponent(model: model); + + final expandsAttr = node.attributes['expands']; + final expands = expandsAttr != 'false'; + + return SummaryCardComponent(model: model, expands: expands); } return null; } } class SummaryCardComponent extends StatelessComponent { - const SummaryCardComponent({super.key, required this.model}); + const SummaryCardComponent({ + super.key, + required this.model, + this.expands = true, + }); final SummaryCardModel model; + final bool expands; @override Component build(BuildContext context) { @@ -65,6 +74,17 @@ class SummaryCardComponent extends StatelessComponent { Component _buildSummaryItem(SummaryCardItem item) { if (item.details case final d?) { + if (!expands) { + return div(classes: 'summary-card-item-static', [ + div(classes: 'summary-card-item', [ + span([MaterialIcon(item.icon)]), + span(classes: 'summary-card-item-title', [.text(item.title)]), + ]), + div(classes: 'summary-card-item-details', [ + DashMarkdown(content: d), + ]), + ]); + } return details([ summary(classes: 'summary-card-item', [ span([MaterialIcon(item.icon)]), diff --git a/site/lib/src/extensions/registry.dart b/site/lib/src/extensions/registry.dart index 13f168e8019..fb51180b84b 100644 --- a/site/lib/src/extensions/registry.dart +++ b/site/lib/src/extensions/registry.dart @@ -11,6 +11,7 @@ import 'header_extractor.dart'; import 'header_processor.dart'; import 'table_processor.dart'; import 'tutorial_prefetch_processor.dart'; +import 'tutorial_structure_processor.dart'; /// A list of all node-processing, page extensions to applied to /// content loaded with Jaspr Content. @@ -22,4 +23,5 @@ const List allNodeProcessingExtensions = [ CodeBlockProcessor(), GlossaryLinkProcessor(), TutorialNavigationExtension(), + TutorialStructureExtension(), ]; diff --git a/site/lib/src/extensions/tutorial_structure_processor.dart b/site/lib/src/extensions/tutorial_structure_processor.dart new file mode 100644 index 00000000000..ee06b2e7277 --- /dev/null +++ b/site/lib/src/extensions/tutorial_structure_processor.dart @@ -0,0 +1,138 @@ +// Copyright 2025 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:jaspr_content/jaspr_content.dart'; + +/// A page extension for Jaspr Content that enforces the tutorial +/// page structure for all pages in the tutorial. +final class TutorialStructureExtension implements PageExtension { + const TutorialStructureExtension(); + + @override + Future> apply(Page page, List nodes) async { + if (!page.path.startsWith('learn/tutorial/') || + page.path.endsWith('index.md')) { + return nodes; + } + + final dividerIndex = nodes.indexWhere( + (node) => node is ElementNode && node.tag == 'hr', + ); + + List introContent; + List stepsContent; + List trailingContent; + + if (dividerIndex >= 0) { + introContent = nodes.sublist(0, dividerIndex); + + final trailingDividerIndex = nodes.indexWhere( + (node) => node is ElementNode && node.tag == 'hr', + dividerIndex + 1, + ); + + stepsContent = trailingDividerIndex >= 0 + ? nodes.sublist(dividerIndex + 1, trailingDividerIndex) + : nodes.sublist(dividerIndex + 1); + trailingContent = trailingDividerIndex >= 0 + ? nodes.sublist(trailingDividerIndex) + : []; + } else { + introContent = []; + stepsContent = []; + trailingContent = nodes; + } + + return [ + ElementNode( + 'section', + {'class': 'tutorial-lesson'}, + [ + if (introContent.isNotEmpty) + ElementNode( + 'section', + {'class': 'tutorial-intro'}, + _buildIntroContent(introContent), + ), + if (introContent.isNotEmpty && stepsContent.isNotEmpty) + const ElementNode('hr', {'class': 'tutorial-divider'}, []), + if (stepsContent.isNotEmpty) + ElementNode( + 'section', + {'class': 'tutorial-steps'}, + [ + const ElementNode('h2', {}, [TextNode('Steps')]), + ElementNode('Stepper', {'level': '3'}, stepsContent), + ], + ), + ...trailingContent, + ], + ), + ]; + } + + /// Builds the intro content, automatically wrapping: + /// - Paragraph elements (`

`) get 'description' class added + /// - YouTubeEmbed elements in a div with class 'intro-video' + List _buildIntroContent(List content) { + final wrappedContent = []; + + for (final node in content) { + if (node case ElementNode(tag: 'p', :final children)) { + // Check if paragraph contains only a video embed + final videoChild = _findVideoChild(children); + if (videoChild != null) { + // Extract video from paragraph and wrap in intro-video div + wrappedContent.add( + ElementNode('div', {'class': 'intro-video'}, [videoChild]), + ); + } else { + // Regular paragraph - add description class + wrappedContent.add( + ElementNode('p', {'class': 'description'}, children), + ); + } + } else if (_isVideoEmbed(node)) { + // Direct video embed (not wrapped in paragraph) + wrappedContent.add( + ElementNode('div', {'class': 'intro-video'}, [node]), + ); + } else { + // Pass through other elements (SummaryCard, comments, etc.) + wrappedContent.add(node); + } + } + + return wrappedContent; + } + + /// Checks if a node is a YouTube video embed + bool _isVideoEmbed(Node node) { + if (node case ElementNode(tag: final tag)) { + final lowerTag = tag.toLowerCase(); + return lowerTag == 'youtubeembed' || lowerTag == 'lite-youtube'; + } + return false; + } + + /// Finds a video embed child in a list of nodes, returns it if it's the only + /// meaningful content (ignoring whitespace text nodes) + Node? _findVideoChild(List? children) { + if (children == null) return null; + + Node? videoChild; + for (final child in children) { + if (_isVideoEmbed(child)) { + videoChild = child; + } else if (child case TextNode(:final text)) { + // Ignore whitespace-only text nodes + if (text.trim().isNotEmpty) return null; + } else { + // Non-video, non-whitespace content found + return null; + } + } + return videoChild; + } +} diff --git a/site/web/assets/images/docs/tutorial/bounce_in_curve.gif b/site/web/assets/images/docs/tutorial/bounce_in_curve.gif new file mode 100644 index 00000000000..9099183a10e Binary files /dev/null and b/site/web/assets/images/docs/tutorial/bounce_in_curve.gif differ diff --git a/site/web/assets/images/docs/tutorial/linear_curve.gif b/site/web/assets/images/docs/tutorial/linear_curve.gif new file mode 100644 index 00000000000..f540124a8b3 Binary files /dev/null and b/site/web/assets/images/docs/tutorial/linear_curve.gif differ diff --git a/site/web/assets/images/docs/tutorial/rolodex_complete.png b/site/web/assets/images/docs/tutorial/rolodex_complete.png new file mode 100644 index 00000000000..636e5c97134 Binary files /dev/null and b/site/web/assets/images/docs/tutorial/rolodex_complete.png differ diff --git a/site/web/assets/images/docs/tutorial/wikipedia_app.png b/site/web/assets/images/docs/tutorial/wikipedia_app.png new file mode 100644 index 00000000000..5085857955b Binary files /dev/null and b/site/web/assets/images/docs/tutorial/wikipedia_app.png differ diff --git a/src/content/learn/tutorial/adaptive-layout.md b/src/content/learn/tutorial/adaptive-layout.md index 30758719d80..237daa9fad2 100644 --- a/src/content/learn/tutorial/adaptive-layout.md +++ b/src/content/learn/tutorial/adaptive-layout.md @@ -5,6 +5,10 @@ layout: tutorial sitemap: false --- +Learn how to create layouts that adapt to different screen widths. + + + title: What you'll accomplish items: @@ -16,6 +20,10 @@ items: icon: view_sidebar +--- + +### Introduction + Modern apps need to work well on screens of all sizes. On this page, you'll learn how to create layouts that adapt to different screen widths. @@ -28,7 +36,7 @@ Specifically, this app handles two screen sizes: - **Small screens (phones)**: Uses navigation to move between contact groups and details. -## Create the contact groups page +### Create the contact groups page First, create the basic structure of the `ContactGroupsPage` widget for your contact groups screen. @@ -53,7 +61,7 @@ class ContactGroupsPage extends StatelessWidget { } ``` -## Create the contacts page +### Create the contacts page Similarly, create `lib/screens/contacts.dart` to eventually display individual contacts: @@ -82,7 +90,7 @@ The `ContaactsListPage` widget and `ContactGroupsPage` widget are placeholder pages that are needed to implement the adaptive layout widget, which you'll do next. -## Build the adaptive layout foundation +### Build the adaptive layout foundation Create `lib/screens/adaptive_layout.dart` and start with the following basic structure: @@ -156,7 +164,7 @@ you can decide which layout to show. The 600-pixel threshold is a common breakpoint that separates phone-sized screens from tablet-sized screens. -## Update the main app +### Update the main app Update `main.dart` to use the adaptive layout, so you can see your changes: @@ -194,7 +202,7 @@ class RolodexApp extends StatelessWidget { If you're running in Chrome, you can resize the browser window to see layout changes. -## Add list selection functionality +### Add list selection functionality The large screen layout needs to track which contact group is selected. Update the state object with the following code: @@ -244,7 +252,7 @@ class _AdaptiveLayoutState extends State { The `selectedListId` variable tracks the currently selected contact group, and `_onContactListSelected` updates this value when the user makes a choice. -## Build the large screen layout +### Build the large screen layout Now, implement the side-by-side layout for large screens. First, replace the temporary text with a widget that @@ -349,7 +357,7 @@ This layout creates the following: - A 1-pixel divider between the panels. - A details panel that uses an `Expanded` widget to take the remaining space. -## Test the adaptive layout +### Test the adaptive layout Hot reload your app and test the responsive behavior. If you're running in Chrome, you can resize the browser window to @@ -365,6 +373,8 @@ Both the sidebar and main content area show placeholder text for now. In the next lesson, you'll implement slivers to fill in the contact list content. +### Review + title: What you accomplished subtitle: Here's a summary of what you built and learned in this lesson. diff --git a/src/content/learn/tutorial/advanced-ui.md b/src/content/learn/tutorial/advanced-ui.md index 1a50851de1f..812d7b18bf8 100644 --- a/src/content/learn/tutorial/advanced-ui.md +++ b/src/content/learn/tutorial/advanced-ui.md @@ -7,6 +7,10 @@ layout: tutorial sitemap: false --- +Preview the Rolodex app you'll build and set up a Cupertino-based project with data models. + + + title: What you'll accomplish items: @@ -18,19 +22,23 @@ items: icon: data_object +--- + +### Introduction + In this third installment of the Flutter tutorial series, you'll use Flutter's Cupertino library to build a partial clone of the iOS Contacts app. -A screenshot of the completed Rolodex contact
+<img src='/assets/images/docs/tutorial/rolodex_complete.png' class= By the end of this tutorial, you'll have learned how to create adaptive layouts, implement comprehensive theming, build navigation patterns, and use advanced scrolling techniques. -## What you'll learn +#### What you'll learn This tutorial explores the following topics: @@ -45,7 +53,8 @@ This tutorial assumes that you've completed the previous Flutter tutorials and are comfortable with basic widget composition, state management, and the Flutter project structure. -## Create a new Flutter project + +### Create a new Flutter project To build a Flutter app, you first need a Flutter project. You can create a new app with the [Flutter CLI tool][], @@ -63,7 +72,7 @@ uses the minimal "empty" template. [Flutter CLI tool]: /reference/flutter-cli -## Add the Cupertino Icons dependency +### Add the Cupertino Icons dependency This project uses the [`cupertino_icons` package][], an official Flutter package. @@ -73,7 +82,7 @@ Add it as a dependency by running the following command: $ flutter pub add cupertino_icons ``` -## Set up the project structure +### Set up the project structure First, create the basic directory structure for your app. In your project's `lib` directory, create the following folders: @@ -86,7 +95,7 @@ $ mkdir lib/data lib/screens lib/theme This command creates folders to organize your code into logical sections: data models, screen widgets, and theme configuration. -## Replace the starter code +### Replace the starter code In your IDE, open the `lib/main.dart` file, and replace its entire contents with the following starter code: @@ -126,7 +135,7 @@ this app uses `CupertinoApp` instead of `MaterialApp`. The Cupertino design system provides iOS-style widgets and styling, which is perfect for building apps that feel native on Apple devices. -## Run your app +### Run your app In your terminal at the root of your Flutter app, run the following command: @@ -137,17 +146,17 @@ $ flutter run -d chrome The app builds and launches in a new instance of Chrome. It displays "Hello Rolodex!" in the center of the screen. -## Create the data models +### Create the data models Before building the UI, create the data structures and sample data that the app will use. This section is lightly explained because it's not the focus of this tutorial. -### `Contact` data +#### `Contact` data Create a new file, `lib/data/contact.dart`, and add the basic `Contact` class: -```dart title="lib/data/contact.dart" +```dart foldable title="lib/data/contact.dart" class Contact { Contact({ required this.id, @@ -164,6 +173,7 @@ class Contact { final String? suffix; } +[* - final johnAppleseed = Contact(id: 0, firstName: 'John', lastName: 'Appleseed'); final kateBell = Contact(id: 1, firstName: 'Kate', lastName: 'Bell'); final annaHaro = Contact(id: 2, firstName: 'Anna', lastName: 'Haro'); @@ -279,7 +289,9 @@ final jessicaEdwards = Contact( firstName: 'Jessica', lastName: 'Edwards', ); +*] +[* - final Set allContacts = { johnAppleseed, kateBell, @@ -333,12 +345,13 @@ final Set allContacts = { christopherDaniel, jessicaEdwards, }; +*] ``` This sample data includes contacts with and without middle names and suffixes. This gives you a variety of data to work with as you build the UI. -### `ContactGroup` data +#### `ContactGroup` data Now, create the contact groups that organize your contacts into lists. Create a new `lib/data/contact_group.dart` file and @@ -490,7 +503,7 @@ which covers state management. [previous tutorial covering state]: /learn/tutorial/set-up-state-project -## Connect the data to your app +### Connect the data to your app Update your `main.dart` to include the global state and import the new data file: @@ -529,6 +542,8 @@ you'll start building the app in earnest. [`cupertino_icons` package]: {{site.pub-pkg}}/cupertino_icons +### Review + title: What you accomplished subtitle: Here's a summary of what you built and learned in this lesson. diff --git a/src/content/learn/tutorial/change-notifier.md b/src/content/learn/tutorial/change-notifier.md index ef80e96e8de..a2fcfd89f25 100644 --- a/src/content/learn/tutorial/change-notifier.md +++ b/src/content/learn/tutorial/change-notifier.md @@ -5,6 +5,10 @@ layout: tutorial sitemap: false --- +Learn to create a ViewModel with ChangeNotifier and manage loading, success, and error states. + + + title: What you'll accomplish items: @@ -16,6 +20,10 @@ items: icon: notifications_active +--- + +### Introduction + When developers talk about state-management in Flutter, they're essentially referring to the pattern by which your app updates the data it needs to render correctly and then @@ -32,7 +40,7 @@ which triggers UI rebuilds when called. [`ChangeNotifier`]: {{site.api}}/flutter/foundation/ChangeNotifier-class.html -## Create the basic view model structure +### Create the basic view model structure Create the `ArticleViewModel` class with its basic structure and state properties: @@ -54,7 +62,7 @@ The `ArticleViewModel` holds three pieces of state: - `errorMessage`: Any error that occurred during data fetching. - `loading`: A flag to show progress indicators. -## Add constructor initialization +### Add constructor initialization Update the constructor to automatically fetch content when the `ArticleViewModel` is created: @@ -79,7 +87,7 @@ a `ArticleViewModel` object is created. Because constructors can't be asynchronous, it delegates initial content fetching to a separate method. -## Set up the `getRandomArticleSummary` method +### Set up the `getRandomArticleSummary` method Add the `getRandomArticleSummary` that fetches data and manages state updates: @@ -112,7 +120,7 @@ When the operation completes, it toggles the property back. When you build the UI, you'll use this `loading` property to show a loading indicator while fetching a new article. -## Retrieve an article from the `ArticleModel` +### Retrieve an article from the `ArticleModel` Complete the `getRandomArticleSummary` method to fetch an article summary. Use a [try-catch block][] to gracefully handle network errors and @@ -149,7 +157,7 @@ class ArticleViewModel extends ChangeNotifier { [try-catch block]: {{site.dart-site}}/language/error-handling#catch -## Test the ViewModel +### Test the ViewModel Before building the full UI, test that your HTTP requests work by printing results to the console. @@ -204,6 +212,8 @@ Hot reload your app and check your console output. You should see either an article title or an error message, which confirms that your Model and ViewModel are wired up correctly. +### Review + title: What you accomplished subtitle: Here's a summary of what you built and learned in this lesson. diff --git a/src/content/learn/tutorial/create-an-app.md b/src/content/learn/tutorial/create-an-app.md index c1db6baa7f8..a0cff4ddcba 100644 --- a/src/content/learn/tutorial/create-an-app.md +++ b/src/content/learn/tutorial/create-an-app.md @@ -5,9 +5,9 @@ layout: tutorial sitemap: false --- -{%- comment %} - -{%- endcomment %} +Learn the first steps to building a Flutter app, from creating a project to understanding widgets and hot reload. + + title: What you'll accomplish @@ -20,19 +20,24 @@ items: icon: bolt +--- + +### What you'll build + In this first section of the Flutter tutorial, you'll build the core UI of an app called 'Birdle', a game similar to [Wordle, the popular New York Times game][]. +A screenshot that resembles the popular game Wordle. + By the end of this tutorial, you'll have learned the fundamentals of building Flutter UIs, and your app will look like the following screenshot (and it'll even mostly work 😀). -A screenshot that resembles the popular game Wordle. - [Wordle, the popular New York Times game]: https://www.nytimes.com/games/wordle/index.html -## Create a new Flutter project + +### Create a new Flutter project The first step to building Flutter apps is to create a new project. You create new apps with the [Flutter CLI tool][], @@ -49,7 +54,7 @@ This creates a new Flutter project using the minimal "empty" template. [Flutter CLI tool]: /reference/flutter-cli -## Examine the code +### Examine the code In your IDE, open the file at `lib/main.dart`. Starting from the top, you'll see this code. @@ -102,9 +107,9 @@ compose widgets from the SDK into larger, custom widgets that display a UI. At the moment, the widget tree is quite simple: -A screenshot that resembles the popular game Wordle. +A screenshot that resembles the popular game Wordle. -## Run your app +### Run your app 1. In your terminal, navigate to the root directory of your created Flutter app: @@ -121,9 +126,9 @@ At the moment, the widget tree is quite simple: The app will build and launch in a new instance of Chrome. -A screenshot that resembles the popular game Wordle. +A screenshot that resembles the popular game Wordle. -## Use hot reload +### Use hot reload **Stateful hot reload**, if you haven't heard of it, allows a running Flutter app to re-render updated business logic or UI code in @@ -141,7 +146,9 @@ Then, hot-reload your app by pressing `r` in the terminal where the app is running. The running app should instantly show your updated text. - +### Review + + title: What you accomplished subtitle: Here's a summary of what you built and learned in this lesson. completed: true @@ -202,3 +209,4 @@ items: correct: false explanation: "By default, you need to press `r` to trigger hot reload in the terminal." + diff --git a/src/content/learn/tutorial/devtools.md b/src/content/learn/tutorial/devtools.md index f59e5ff12a1..bb3999504c5 100644 --- a/src/content/learn/tutorial/devtools.md +++ b/src/content/learn/tutorial/devtools.md @@ -5,7 +5,9 @@ layout: tutorial sitemap: false --- -{%- comment %} TODO(ewindmill) embed video {%- endcomment %} +Learn to use the widget inspector and property editor to debug layout issues and experiment with properties in real-time. + + title: What you'll accomplish @@ -18,6 +20,10 @@ items: icon: tune +--- + +### Introduction + As your Flutter app grows in complexity, it becomes more important to understand how each of the widget properties affects the UI. The [Dart and Flutter DevTools][] provide you with @@ -45,14 +51,14 @@ The screenshots in this lesson are from VS Code. [VS Code]: /tools/vs-code [IntelliJ and Android Studio]: /tools/android-studio -## The widget inspector +### The widget inspector The widget inspector allows you to visualize and explore your widget tree. It helps you understand the layout of your UI and identifies which widgets are responsible for different parts of the screen. Running against the app you've built so far, the inspector looks like this: -A screenshot of the Flutter widget inspector tool. +A screenshot of the Flutter widget inspector tool. Consider the `GamePage` widget you created in this section: @@ -108,7 +114,7 @@ the `Row` widgets with `Tile` children. You can select any widget in the tree to see its properties and even jump to its source code in your IDE. -## Debugging layout issues +### Debugging layout issues The widget inspector is perhaps most useful for debugging layout issues. @@ -148,14 +154,14 @@ spot and resolve this issue. [`ListView`]: {{site.api}}/flutter/widgets/ListView-class.html [`ScrollView`]: {{site.api}}/flutter/widgets/ScrollView-class.html -## The property editor +### The property editor When you select a widget in the widget inspector, the property editor displays all the properties of that selected widget. This is a powerful tool for understanding why a widget looks the way it does and for experimenting with property value changes in real-time. -A screenshot of the Flutter property editor tool. +A screenshot of the Flutter property editor tool. Look at the `Tile` widget's `build` method from earlier: @@ -200,6 +206,8 @@ Then instantly see the update on your running app without needing to recompile or even hot reload. This allows for rapid iteration on UI design. +### Review + title: What you accomplished subtitle: Here's a summary of what you built and learned in this lesson. @@ -220,6 +228,14 @@ items: This happens when widgets like `Row`, `Column`, or `ListView` receive infinite constraints. Now you can recognize and fix these issues when they occur. + - title: Learned about common layout issues + icon: bug_report + details: >- + You learned about **unbounded constraints**, + one of the most common errors hit in Flutter development. + This happens when widgets like + `Row`, `Column`, or `ListView` receive infinite constraints. + Now you can recognize and fix these issues when they occur. - title: Experimented with properties in real-time icon: tune details: >- diff --git a/src/content/learn/tutorial/http-requests.md b/src/content/learn/tutorial/http-requests.md index fcc77d830a4..d71c6429a91 100644 --- a/src/content/learn/tutorial/http-requests.md +++ b/src/content/learn/tutorial/http-requests.md @@ -5,6 +5,10 @@ layout: tutorial sitemap: false --- +Learn the MVVM architecture pattern and how to build HTTP requests with async/await. + + + title: What you'll accomplish items: @@ -16,6 +20,10 @@ items: icon: data_object +--- + +### Introduction + The overarching pattern that this tutorial implements is called _Model-View-ViewModel_ or _MVVM_. MVVM is an [architectural pattern][] used in client apps that @@ -29,7 +37,7 @@ The core tenet of MVVM (and many other patterns) is *separation of concerns*. Managing state in separate classes (outside your UI widgets) makes your code more testable, reusable, and easier to maintain. -A diagram that shows the three layers of MVVM architecture: Model, ViewModel, and View. A single feature in your app contains each one of the MVVM components. @@ -38,7 +46,7 @@ you'll create `ArticleModel`, `ArticleViewModel`, and `ArticleView`. [architectural pattern]: /app-architecture/guide -## Define the Model +### Define the Model The Model is the source-of-truth for your app's data and is responsible for low-level tasks such as making HTTP requests, caching data, or @@ -53,7 +61,7 @@ class ArticleModel { } ``` -## Build the HTTP request +### Build the HTTP request Wikipedia provides a REST API that returns JSON data about articles. For this app, you'll use the endpoint that returns a random article summary. @@ -90,7 +98,7 @@ especially when dealing with special characters or query parameters. [`async` and `await`]: {{site.dart-site}}/language/async [`Future`]: {{site.api}}/flutter/dart-async/Future-class.html -## Handle network errors +### Handle network errors Always handle errors when making HTTP requests. A status code of **200** indicates success, while other codes indicate errors. @@ -115,7 +123,7 @@ class ArticleModel { } ``` -## Parse JSON from Wikipedia +### Parse JSON from Wikipedia The [Wikipedia API][] returns [JSON][] data that you decode into a `Summary` class @@ -147,6 +155,8 @@ check out the [Getting started with Dart][] tutorial. [JSON]: {{site.dart-site}}/tutorial/json [Getting started with Dart]: {{site.dart-site}}/tutorial +### Review + title: What you accomplished subtitle: Here's a summary of what you built and learned in this lesson. diff --git a/src/content/learn/tutorial/implicit-animations.md b/src/content/learn/tutorial/implicit-animations.md index 5a008850b4e..ec5852ff141 100644 --- a/src/content/learn/tutorial/implicit-animations.md +++ b/src/content/learn/tutorial/implicit-animations.md @@ -5,6 +5,12 @@ layout: tutorial sitemap: false --- +Flutter provides a rich set of animation APIs, and the simplest way to +start using them is with **implicit animations**. +"Implicit animations" refers to a group of widgets that +automatically animate changes to their properties without you +needing to manage any intermediate behavior. + title: What you'll accomplish items: @@ -16,12 +22,6 @@ items: icon: timeline -Flutter provides a rich set of animation APIs, and the simplest way to -start using them is with **implicit animations**. -"Implicit animations" refers to a group of widgets that -automatically animate changes to their properties without you -needing to manage any intermediate behavior. - In this lesson, you'll learn about one of the most common and versatile implicit animation widgets: [`AnimatedContainer`][]. With just two additional lines of code, the background color of each `Tile` @@ -29,7 +29,9 @@ animates to a new color in about half a second. [`AnimatedContainer`]: {{site.api}}/flutter/widgets/AnimatedContainer-class.html -## Convert `Container` to `AnimatedContainer` +--- + +### Convert `Container` to `AnimatedContainer` Currently, the `Tile.build` method returns a `Container` to display a letter. When the `hitType` changes, like from `HitType.none` to `HitType.hit`, @@ -126,14 +128,24 @@ Now, when the `hitType` changes and the `Tile` widget rebuilds the color of the tile smoothly animates from its old color to the new one over the specified duration. -## Adjust the animation curve +### Adjust the animation curve To add a bit of customization to an implicit animation, you can pass it a different [`Curve`][]. Different curves change the speed of the animation at different points throughout the animation. -{%- comment %} TODO(ewindmill) diagram {%- endcomment %} +For example, the default curve for Flutter animations is `Curves.linear`. This gif shows how the animation curve behaves: + + + +Compare that to `Curve.bounceIn`, another common curve: + + + + To change the `Curve` of this animation, update the code to the following: @@ -148,7 +160,7 @@ class Tile extends StatelessWidget { Widget build(BuildContext context) { return AnimatedContainer( duration: Duration(milliseconds: 500), - curve: Curves.decelerate, // NEW + curve: Curves.bounceIn, // NEW height: 60, width: 60, decoration: BoxDecoration( @@ -184,6 +196,8 @@ If you're curious, try it out in the [animations tutorial][]. [`Curve`]: {{site.api}}/flutter/animation/Curves-class.html [animations tutorial]: /ui/animations/tutorial +### Review + title: What you accomplished subtitle: Here's a summary of what you built and learned in this lesson. diff --git a/src/content/learn/tutorial/layout.md b/src/content/learn/tutorial/layout.md index 6dd40d3d949..1fe48ed4b65 100644 --- a/src/content/learn/tutorial/layout.md +++ b/src/content/learn/tutorial/layout.md @@ -5,7 +5,9 @@ layout: tutorial sitemap: false --- -{%- comment %} TODO(ewindmill) embed video {%- endcomment %} +Learn how to build layouts with common widgets like Scaffold, AppBar, Column, and Row. + + title: What you'll accomplish @@ -20,6 +22,10 @@ items: icon: grid_view +--- + +### Introduction + Given that Flutter is a UI toolkit, you'll spend a lot of time creating layouts with Flutter widgets. @@ -35,12 +41,12 @@ lay out widgets vertically or horizontally. [`Column`]: {{site.api}}/flutter/widgets/Column-class.html [`Row`]: {{site.api}}/flutter/widgets/Row-class.html -## `Scaffold` and `AppBar` +### `Scaffold` and `AppBar` Mobile applications often have a bar at the top called an "app bar" that can display a title, navigation controls, and/or actions. -A screenshot of a simple application with a bar across the top that has a title and settings button. +A screenshot of a simple application with a bar across the top that has a title and settings button. The simplest way to add an app bar to your app is by using two widgets: `Scaffold` and `AppBar`. @@ -80,16 +86,16 @@ class MainApp extends StatelessWidget { [`Align`]: {{site.api}}/flutter/widgets/Align-class.html -### An updated widget tree +#### An updated widget tree Considering your app's widget tree gets more important as your app grows. At this point, there's a "branch" in the widget tree for the first time, and it now looks like the following figure: -A screenshot that resembles the popular game Wordle. +A screenshot that resembles the popular game Wordle. -## Create a widget for the game page layout +### Create a widget for the game page layout Add the following code for a new widget, called `GamePage`, to your `main.dart` file. @@ -132,11 +138,11 @@ class MainApp extends StatelessWidget { ::: -## Arrange widgets with `Column` and `Row` +### Arrange widgets with `Column` and `Row` The `GamePage` layout contains the grid of tiles that display a user's guesses. -A screenshot that resembles the popular game Wordle. +A screenshot that resembles the popular game Wordle. There are a number of ways you can build this layout. The simplest is with the `Column` and `Row` widgets. @@ -224,12 +230,12 @@ one for each guess on the `Game` object. [collection for element]: /language/collections#for-element -### An updated widget tree +#### An updated widget tree The widget tree for this app has expanded significantly in this lesson. Now, it looks more like the following (abridged) figure: -A diagram showing a tree like structure with a node for each widget in the app. +A diagram showing a tree like structure with a node for each widget in the app. :::note Challenge @@ -270,10 +276,12 @@ class GamePage extends StatelessWidget { When you reload your app, you should see a 5x5 grid of white squares. -A screenshot that resembles the popular game Wordle. +A screenshot that resembles the popular game Wordle. [record]: {{site.dart-site}}/language/records +### Review + title: What you accomplished subtitle: Here's a summary of what you built and learned in this lesson. diff --git a/src/content/learn/tutorial/listenable-builder.md b/src/content/learn/tutorial/listenable-builder.md index 9e6698713be..7612ee51b44 100644 --- a/src/content/learn/tutorial/listenable-builder.md +++ b/src/content/learn/tutorial/listenable-builder.md @@ -5,6 +5,10 @@ layout: tutorial sitemap: false --- +Learn to use ListenableBuilder to automatically rebuild UI and handle all possible states with switch expressions. + + + title: What you'll accomplish items: @@ -16,6 +20,10 @@ items: icon: article +--- + +### Introduction + The view layer is your UI, and in Flutter, that refers to your app's widgets. As it pertains to this tutorial, the important part is @@ -27,7 +35,7 @@ provided `ChangeNotifier` calls `notifyListeners()`. [`ListenableBuilder`]: {{site.api}}/flutter/widgets/ListenableBuilder-class.html [`ChangeNotifier`]: {{site.api}}/flutter/foundation/ChangeNotifier-class.html -## Create the article view widget +### Create the article view widget Create the `ArticleView` widget that manages the overall page layout and state handling. @@ -51,7 +59,7 @@ class ArticleView extends StatelessWidget { } ``` -## Create the article view model +### Create the article view model Create the `ArticleViewModel` in this widget: @@ -75,7 +83,7 @@ class ArticleView extends StatelessWidget { } ``` -## Listen for state changes +### Listen for state changes Wrap your UI in a [`ListenableBuilder`][] to listen for state changes, and pass it a `ChangeNotifier` object. @@ -113,7 +121,7 @@ building different widgets based on the state. [`ListenableBuilder`]: {{site.api}}/flutter/widgets/ListenableBuilder-class.html -## Handle possible view model states +### Handle possible view model states Recall the `ArticleViewModel`, which has three properties that the UI is interested in: @@ -176,7 +184,7 @@ The business logic and rendering are completely separate from each other. [switch expressions]: {{site.dart-site}}/language/branches#switch-expressions -## Complete the UI +### Complete the UI The only thing remaining is to use the properties and methods provided by the view model to build the UI. @@ -202,7 +210,7 @@ class ArticlePage extends StatelessWidget { } ``` -## Add a scrollable layout +### Add a scrollable layout Replace the placeholder with a scrollable column layout: @@ -230,7 +238,7 @@ class ArticlePage extends StatelessWidget { } ``` -## Add article content and button +### Add article content and button Complete the layout with an article widget and navigation button: @@ -266,12 +274,12 @@ class ArticlePage extends StatelessWidget { } ``` -## Create the `ArticleWidget` +### Create the `ArticleWidget` The `ArticleWidget` handles the display of the actual article content with proper styling and conditional rendering. -### Set up the basic article structure +#### Set up the basic article structure Start with the widget that accepts a `summary` parameter: @@ -288,7 +296,7 @@ class ArticleWidget extends StatelessWidget { } ``` -### Add padding and column layout +#### Add padding and column layout Wrap the content in proper padding and layout: @@ -313,7 +321,7 @@ class ArticleWidget extends StatelessWidget { } ``` -### Add conditional image display +#### Add conditional image display Add the article image that only shows when available: @@ -342,7 +350,7 @@ class ArticleWidget extends StatelessWidget { } ``` -### Complete with styled text content +#### Complete with styled text content Replace the placeholder text with a properly styled title, description, and extract: @@ -396,7 +404,7 @@ This widget demonstrates a few important UI concepts: - **Overflow handling**: `TextOverflow.ellipsis` prevents text from breaking the layout. -## Update your app to include the article view +### Update your app to include the article view Connect everything together by updating your `MainApp` to include your completed `ArticleView`. @@ -419,7 +427,7 @@ class MainApp extends StatelessWidget { This change switches from the console-based test to the full UI experience with proper state management. -## Run the complete app +### Run the complete app Hot reload your app one final time. You should now see: @@ -433,6 +441,8 @@ click the **Next random article** button. The app shows a loading state, fetches new data, and updates the display automatically. +### Review + title: What you accomplished subtitle: Here's a summary of what you built and learned in this lesson. diff --git a/src/content/learn/tutorial/navigation.md b/src/content/learn/tutorial/navigation.md index 89636261b18..8f537c424f2 100644 --- a/src/content/learn/tutorial/navigation.md +++ b/src/content/learn/tutorial/navigation.md @@ -5,6 +5,10 @@ layout: tutorial sitemap: false --- +Learn to navigate between screens with Navigator.push and implement adaptive navigation patterns for different screen sizes. + + + title: What you'll accomplish items: @@ -16,6 +20,10 @@ items: icon: devices +--- + +### Introduction + Now that you understand slivers and scrolling, you can implement navigation between screens. In this lesson, @@ -52,7 +60,7 @@ class _AdaptiveLayoutState extends State { } ``` -## Add navigation to contact groups +### Add navigation to contact groups The `ContactGroupsPage` already uses a `_ContactGroupsView` and provides it with a callback. @@ -99,7 +107,7 @@ the following features: - Proper title handling. - Swipe-to-go-back gesture support. -## Create the sidebar component for large screens +### Create the sidebar component for large screens For large screens, you need a sidebar that doesn't navigate but instead updates the main content area. @@ -137,7 +145,7 @@ it calls `onListSelected` with the ID of the tapped list. It also passes the `selectedListId` to `_ContactGroupsView` so that the selected item can be highlighted. -## Create the detail view for large screens +### Create the detail view for large screens For the large screen layout, you need a detail view that doesn't show navigation controls. Just like the sidebar, @@ -167,7 +175,7 @@ The detail view reuses `_ContactListView` and sets the `automaticallyImplyLeading` parameter to `false` to hide the back button, as navigation is handled by the sidebar. -## Connect the sidebar to the adaptive layout +### Connect the sidebar to the adaptive layout Now, connect the sidebar to your adaptive layout. Update your `adaptive_layout.dart` file to import the necessary files and @@ -212,17 +220,17 @@ Widget _buildLargeScreenLayout() { This code creates the classic menu-detail layout where the sidebar controls the content of the detail area. -## Test the adaptive navigation behavior +### Test the adaptive navigation behavior Hot reload your app and test the navigation: -**Small screens (< 600px width):** +**Small screens (<600px width):** - Tap contact groups to navigate to contact details. - Use the back button or a swipe gesture to return. - This is a classic stack-based navigation flow. -**Large screens (> 600px width):** +**Large screens (>600px width):** - Click contact groups in the sidebar to update the detail view. - There is no navigation stack. The selection updates the content area. @@ -232,6 +240,9 @@ The app automatically chooses the appropriate navigation pattern based on screen size. This provides an optimal experience on both phones and tablets. +### Review + + title: What you accomplished subtitle: Here's a summary of what you built and learned in this lesson. diff --git a/src/content/learn/tutorial/set-up-state-project.md b/src/content/learn/tutorial/set-up-state-project.md index 24a9150ae57..4d68b12a4ca 100644 --- a/src/content/learn/tutorial/set-up-state-project.md +++ b/src/content/learn/tutorial/set-up-state-project.md @@ -5,6 +5,10 @@ layout: tutorial sitemap: false --- +Preview the Wikipedia reader app you'll build and set up the initial project with required packages. + + + title: What you'll accomplish items: @@ -16,11 +20,15 @@ items: icon: code +--- + +### Introduction + In the next few lessons, you'll learn how to work with data in a Flutter app. You'll build an app that fetches and displays article summaries from the [Wikipedia API][]. -A screenshot of the completed
 Wikipedia reader app showing an article with image, title,
 description, and extract text. @@ -53,7 +61,7 @@ free and accessible to everyone. [Wikipedia]: https://wikipedia.org/ [donating to Wikipedia]: https://donate.wikimedia.org/ -## Create a new Flutter project +### Create a new Flutter project Create a new Flutter project using the [Flutter CLI][]. In your preferred terminal, run the following command to @@ -65,7 +73,7 @@ $ flutter create wikipedia_reader --empty [Flutter CLI]: /reference/flutter-cli -## Add required dependencies +### Add required dependencies Your app needs two [packages][] to work with HTTP requests and Wikipedia data. Add them to your project: @@ -81,7 +89,7 @@ Wikipedia's API responses. [packages]: /packages-and-plugins/using-packages [`http` package]: {{site.pub}}/packages/http -## Examine the starter code +### Examine the starter code Open `lib/main.dart` and replace the existing code with this basic structure, which adds required imports that the app uses: @@ -122,7 +130,7 @@ a title bar and placeholder content. The imports at the top include everything you need for HTTP requests, JSON parsing, and Wikipedia data models. -## Run your app +### Run your app Test that everything works by running your app: @@ -133,6 +141,8 @@ $ flutter run -d chrome You should see a simple app with "Wikipedia Flutter" in the app bar and "Loading..." in the center of the screen. +### Review + title: What you accomplished subtitle: Here's a summary of what you built and learned in this lesson. diff --git a/src/content/learn/tutorial/slivers.md b/src/content/learn/tutorial/slivers.md index 8d9d78d8298..38c27d594c6 100644 --- a/src/content/learn/tutorial/slivers.md +++ b/src/content/learn/tutorial/slivers.md @@ -5,6 +5,17 @@ layout: tutorial sitemap: false --- +In this lesson, you'll learn about slivers, +which are special widgets that can take advantage of +Flutter's powerful and composable scrolling system. +Slivers enable you to create sophisticated scroll effects, +including collapsible headers, search integration, and custom scroll behaviors. +By the end of this section, you'll understand how to +use `CustomScrollView`, create navigation bars that collapse, +and organize content in scrollable sections. + + + title: What you'll accomplish items: @@ -18,16 +29,9 @@ items: icon: sort_by_alpha -In this lesson, you'll learn about slivers, -which are special widgets that can take advantage of -Flutter's powerful and composable scrolling system. -Slivers enable you to create sophisticated scroll effects, -including collapsible headers, search integration, and custom scroll behaviors. -By the end of this section, you'll understand how to -use `CustomScrollView`, create navigation bars that collapse, -and organize content in scrollable sections. +--- -## Slivers and widgets +### Slivers and widgets Slivers are scrollable areas that can be composed together in a `CustomScrollView` or other scroll views. @@ -53,7 +57,7 @@ This architectural separation allows Flutter to optimize scrolling performance while it maintains clear boundaries between different types of UI components. -## Add a basic sliver structure to contact groups +### Add a basic sliver structure to contact groups First, replace the placeholder content in your contact groups page. To avoid duplicating code between the phone layout and the tablet sidebar, @@ -154,7 +158,7 @@ This structure keeps the `ContactGroupsPage` clean and focused on its primary responsibility: navigation, which you'll learn about in the next section of this tutorial. -## Enhance the list with icons and visual elements +### Enhance the list with icons and visual elements Now, add icons and contact counts to make the list more informative. Add this `_buildTrailing` helper method to your `_ContactGroupsView` class: @@ -275,7 +279,7 @@ The updated code now shows icons that differentiate between the main "All iPhone" group and user-created groups, along with contact counts and navigation indicators. -## Create advanced scrolling for contacts +### Create advanced scrolling for contacts Now, work on the contacts page. Just like before, you'll create a private, reusable view to avoid code duplication. @@ -378,7 +382,7 @@ class ContactListsPage extends StatelessWidget { This basic implementation demonstrates how to use slivers with dynamic data in a reusable component. -## Add search integration with slivers +### Add search integration with slivers Now, enhance the contacts page with integrated search functionality UI. Update the `CustomScrollView` in `_ContactListView` to use the @@ -434,7 +438,7 @@ The `CupertinoSliverNavigationBar.search` constructor provides integrated search functionality. As you scroll down, the search field smoothly transitions into the collapsed navigation bar. -## Create alphabetized contact sections +### Create alphabetized contact sections Real-world contact apps organize contacts alphabetically. To do this, create sections for each letter. @@ -495,7 +499,7 @@ class ContactListSection extends StatelessWidget { This widget creates the familiar alphabetized sections that you see in the iOS Contacts app. -## Use `SliverList` for the alphabetized sections +### Use `SliverList` for the alphabetized sections Now, replace the placeholder content in `_ContactListView` with the alphabetized sections: @@ -538,6 +542,8 @@ In the next lesson, you'll learn about stack-based navigation and update the UI on small screens to navigate between the contacts list view and the contacts view. +### Review + title: What you accomplished subtitle: Here's a summary of what you built and learned in this lesson. diff --git a/src/content/learn/tutorial/stateful-widget.md b/src/content/learn/tutorial/stateful-widget.md index d8d5076475b..7166a20c00b 100644 --- a/src/content/learn/tutorial/stateful-widget.md +++ b/src/content/learn/tutorial/stateful-widget.md @@ -5,7 +5,9 @@ layout: tutorial sitemap: false --- -{%- comment %} TODO(ewindmill) embed video {%- endcomment %} +Learn when widgets need to be stateful and how to trigger UI updates with setState. + + title: What you'll accomplish @@ -18,6 +20,10 @@ items: icon: refresh +--- + +### Introduction + So far, your app displays a grid and an input field, but the grid doesn't yet update to reflect the user's guesses. When this app is complete, each tile in the next unfilled row should @@ -33,7 +39,7 @@ To handle this dynamic behavior, you need to convert `GamePage` from a [`StatefulWidget`]: {{site.api}}/flutter/widgets/StatefulWidget-class.html -## Why stateful widgets? +### Why stateful widgets? When a widget's appearance or data needs to change during its lifetime, you need a `StatefulWidget` and a companion `State` object. @@ -46,7 +52,7 @@ For example, the following widget tree imagines a simple app that uses a stateful widget with a counter that increases when the button is pressed. -A diagram of a widget tree with a stateful widget and state object. +A diagram of a widget tree with a stateful widget and state object. Here is the basic `StatefulWidget` structure (doesn't do anything yet): @@ -66,7 +72,7 @@ class _ExampleWidgetState extends State { } ``` -## Convert `GamePage` to a stateful widget +### Convert `GamePage` to a stateful widget To convert the `GamePage` (or any other) widget from a stateless widget to a stateful widget, do the following steps: @@ -132,7 +138,7 @@ class _GamePageState extends State { ["quick assists"]: /tools/android-studio#assists-quick-fixes -## Updating the UI with `setState` +### Updating the UI with `setState` Whenever you mutate a `State` object, you must call [`setState`][] to signal the framework to @@ -207,6 +213,8 @@ needs to repaint the screen, and the user wouldn't see any updates. [`setState`]: {{site.api}}/flutter/widgets/State/setState.html +### Review + title: What you accomplished subtitle: Here's a summary of what you built and learned in this lesson. diff --git a/src/content/learn/tutorial/user-input.md b/src/content/learn/tutorial/user-input.md index 48c0d41f7ca..b93e769f9fe 100644 --- a/src/content/learn/tutorial/user-input.md +++ b/src/content/learn/tutorial/user-input.md @@ -5,7 +5,9 @@ layout: tutorial sitemap: false --- -{%- comment %} TODO(ewindmill) embed video {%- endcomment %} +Learn to build text inputs, manage text with controllers, and handle user actions with buttons. + + title: What you'll accomplish @@ -20,6 +22,10 @@ items: icon: touch_app +--- + +### Introduction + The app will display the user's guesses in the `Tile` widgets, but it needs a way for the user to input those guesses. In this lesson, build that functionality with two interaction widgets: @@ -28,7 +34,7 @@ In this lesson, build that functionality with two interaction widgets: [`TextField`]: {{site.api}}/flutter/material/TextField-class.html [`IconButton`]: {{site.api}}/flutter/material/IconButton-class.html -## Implement callback functions +### Implement callback functions To allow users to type in their guesses, you'll create a dedicated widget named `GuessInput`. @@ -69,9 +75,9 @@ is called when a user enters a guess. First, you'll need to build the visual parts of this widget. This is what the widget will look like. -A screenshot of the Flutter property editor tool. +A screenshot of the Flutter property editor tool. -## The `TextField` widget +### The `TextField` widget Given that the text field and button are displayed side-by-side, create them as a `Row` widget. @@ -134,7 +140,7 @@ Thus far, `TextField` has the following configuration. [`Expanded`]: {{site.api}}/flutter/widgets/Expanded-class.html [unbounded width/height]: https://www.youtube.com/watch?v=jckqXR5CrPI -## Handle text with `TextEditingController` +### Handle text with `TextEditingController` Next, you need a way to manage the text that the user types into the input field. @@ -305,7 +311,7 @@ The preceding example does so. [`TextEditingController`]: {{site.api}}/flutter/widgets/TextEditingController-class.html [wildcard]: {{site.dart-site}}/language/variables#wildcard-variables -## Gain input focus +### Gain input focus Often, you want a specific input or widget to automatically gain focus without the user taking action. @@ -428,7 +434,7 @@ you can continue typing. [`FocusNode`]: {{site.api}}/flutter/widgets/FocusNode-class.html -## Use the input +### Use the input Finally, you need to handle the text that the user enters. Recall that the constructor for `GuessInput` requires a @@ -523,7 +529,7 @@ prove that it's wired up correctly. Submitting the guess requires using the functionality of a `StatefulWidget`, which you'll do in the next lesson. -## Buttons +### Buttons To improve the UX on mobile and reflect well-known UI practices, there should also be a button that can submit the guess. @@ -669,6 +675,9 @@ class GuessInput extends StatelessWidget { ::: +### Review + + title: What you accomplished subtitle: Here's a summary of what you built and learned in this lesson. diff --git a/src/content/learn/tutorial/widget-fundamentals.md b/src/content/learn/tutorial/widget-fundamentals.md index 2fea87a2ab9..15bcaf8173e 100644 --- a/src/content/learn/tutorial/widget-fundamentals.md +++ b/src/content/learn/tutorial/widget-fundamentals.md @@ -5,9 +5,10 @@ layout: tutorial sitemap: false --- -{%- comment %} - -{%- endcomment %} +Learn to create custom widgets and use the most common SDK widgets like +Container, Center, and Text. + + title: What you'll accomplish @@ -20,16 +21,9 @@ items: icon: palette -In this lesson, you'll create your own custom widget and -learn about some of the most common widgets included in the SDK. - -Custom widgets allow you to reuse UI components across your app, -organize complex UI code into manageable pieces, and -create cleaner, more maintainable code. -By the end of this lesson, you'll have created your own custom `Tile` widget. - +--- -## Before you start +### Before you start This app relies on a bit of game logic that isn't UI-related, and thus is outside the scope of this tutorial. @@ -57,7 +51,7 @@ instructions to import it into your project. [full-words]: https://github.com/ericwindmill/legal_wordle_words -## Anatomy of a stateless widget +### Anatomy of a stateless widget A `Widget` is a Dart class that extends one of the Flutter widget classes, in this case [`StatelessWidget`][]. @@ -78,7 +72,7 @@ class Tile extends StatelessWidget { [`StatelessWidget`]: {{site.api}}/flutter/widgets/StatelessWidget-class.html -### Constructor +#### Constructor The `Tile` class has a [constructor][] that defines what data needs to be passed into the widget to render the widget. @@ -94,7 +88,7 @@ Passing data into widget constructors is at the core of making widgets reusable. [constructor]: {{site.dart-site}}/language/constructors [enum value]: {{site.dart-site}}/language/enums -### Build method +#### Build method Finally, there's the all important `build` method, which must be defined on every widget, and will always return another widget. @@ -114,7 +108,7 @@ class Tile extends StatelessWidget { } ``` -## Use the custom widget +### Use the custom widget When the app is finished, there will be 25 instances of this widget on the screen. @@ -142,7 +136,7 @@ At the moment, your app will be blank, because the `Tile` widget returns an empty `Container`, which doesn't display anything by default. -## The `Container` widget +### The `Container` widget The `Tile` widget consists of three of the most common core widgets: `Container`, `Center`, and `Text`. @@ -180,7 +174,7 @@ class Tile extends StatelessWidget { [`SizedBox`]: {{site.api}}/flutter/widgets/SizedBox-class.html [`DecoratedBox`]: {{site.api}}/flutter/widgets/DecoratedBox-class.html -## BoxDecoration +### BoxDecoration Next, add a [`Border`][] to the box with the following code: @@ -221,7 +215,7 @@ gray if the guess is wrong in both respects. The following figure shows all three possibilities. -A screenshot of a green, yellow, and grey tile. +A screenshot of a green, yellow, and grey tile. To achieve this in UI, use a [switch expression][] to @@ -257,7 +251,7 @@ class Tile extends StatelessWidget { [`Border`]: {{site.api}}/flutter/widgets/Container-class.html [switch expression]: {{site.dart-site}}/language/branches#switch-expressions -## Child widgets +### Child widgets Finally, add the `Center` and `Text` widgets to the `Container.child` property. @@ -314,6 +308,9 @@ child: Tile('A', HitType.partial) Soon, this small box will be one of many widgets on the screen. In the next lesson, you'll start building the game grid itself. +### Review + + title: What you accomplished subtitle: Here's a summary of what you built and learned in this lesson. diff --git a/src/data/tutorial.yml b/src/data/tutorial.yml index 4349c9bbf48..d4175e0c342 100644 --- a/src/data/tutorial.yml +++ b/src/data/tutorial.yml @@ -19,7 +19,7 @@ units: url: /learn/tutorial/implicit-animations - title: State in Flutter apps chapters: - - title: Set up a new project + - title: The state management project url: /learn/tutorial/set-up-state-project - title: Make Http Requests url: /learn/tutorial/http-requests