diff --git a/.agents/skills/contribute-blog/SKILL.md b/.agents/skills/contribute-blog/SKILL.md new file mode 100644 index 00000000000..a1ea5c693ed --- /dev/null +++ b/.agents/skills/contribute-blog/SKILL.md @@ -0,0 +1,73 @@ +--- +name: contribute-blog +description: >- + Create, import, edit, or review posts for the + official Flutter blog at flutter.dev/blog, + including metadata, media, authors, and publication checks. +--- + +# Contribute to the Flutter blog + +## Editorial approach + +Preserve the author's voice, intent, and requested scope. +First person, contractions, anecdotes, humor, enthusiasm, +and future-facing announcements can all be appropriate. +Don't rewrite them merely to match documentation conventions. + +Apply `write-technical-docs` and `format-markdown` selectively for clarity, +accuracy, accessibility, links, code formatting, and sound structure. +Their documentation-specific preferences aren't blanket blog requirements: +semantic line breaks and an 80-character limit aren't required, +especially when importing or lightly editing an existing post. + +Fix or flag substantive issues with source fidelity, +technical accuracy, accessibility, metadata, or rendering. +Treat preference-only rewrites as suggestions +unless copyediting was requested. + +## Read relevant reference material + +- For a new post, import, metadata or media change, + structural edit, or full review, read + [Flutter blog post format](references/post-format.md). +- For a Google Doc import, also read + [Import from Google Docs](references/google-doc-import.md). +- To add or update an author or resolve an unknown author ID, read + [Manage blog authors](references/authors.md). + +A small prose-only correction might need no reference. +Consult `sites/www/content/blog/data.yaml`, +`sites/www/content/blog/authors.yaml`, comparable recent posts, +or implementation source when a convention is unclear. + +## Code and quality checks + +- If changing or writing Dart or Flutter samples, + apply `write-dart` and `write-flutter` as relevant. + Validate complete examples when practical and + label intentional omissions or pseudocode clearly. +- Before calling a post ready, + search the changed content for unresolved markers and placeholders, + such as `TODO`, `TBD`, or placeholder notes. + Report intentional markers or blockers instead of silently removing them. + +## Validate proportionally + +For a new post or a change to metadata or content, +run the build and link-reference checks: + +```bash +dart run dash_site --site=www build +dart run dash_site --site=www check-link-references +``` + +When layout, media, metadata, or content significantly changes, +consider previewing the site locally: + +```bash +dart run dash_site --site=www serve +``` + +For a prose-only edit that doesn't change links, +direct inspection of the content can be enough. diff --git a/.agents/skills/contribute-blog/references/authors.md b/.agents/skills/contribute-blog/references/authors.md new file mode 100644 index 00000000000..6bd6327c4fa --- /dev/null +++ b/.agents/skills/contribute-blog/references/authors.md @@ -0,0 +1,70 @@ +# Manage blog authors + +Use this reference when adding or updating an author, +or when a post's author ID doesn't resolve. + +Authors are defined in `sites/www/content/blog/authors.yaml`, and +their local profile images live in `sites/www/content/blog/author_images/`. + +The top-level key in `authors.yaml` is a stable content ID, +not necessarily the author's current social handle. + +## Add or resolve an author + +Before adding a record, +search `authors.yaml` by ID, display name, and profile URL. +Use verified or author-provided details. +Don't invent a name, profile, or image. +For a new ID, choose a recognizable lowercase handle or concise stable slug, +then insert the record alphabetically by ID. + +`name` is required while `image`, `imageUrl`, and `link` are optional: + +```yaml +author-id: + name: "Author Display Name" + image: "author-id.webp" + link: "https://github.com/author-id" +``` + +- `image` names a local image file in `author_images/`. +- `imageUrl` accepts an external URL to use as a fallback to `image`. + Prefer a local `image` over setting `imageUrl`. Don't set both. +- `link` is the destination linked from the byline. Prefer their GitHub profile. + +## Author images + +Name a local image after the exact author ID and +use its real lowercase `.jpg` or `.webp` extension, +such as `author-id.webp`. +Optimize it for a small square display and a centered circular crop. +Use an author-supplied or appropriately reusable image. + +## Update an author + +Keep the existing ID when an author's name, handle, link, or image changes. +If the ID must change, update every occurrence in post frontmatter. + +When replacing an image with a different filename or extension, +search for references to the old file before removing it. +Don't remove an apparently unused author without checking all posts. + +## Configure the authors of a post + +In a post's `index.md` frontmatter: + +For a single author, use a YAML scalar: + +```yaml +author: author-id +``` + +For multiple authors, use an ordered list: + +```yaml +author: + - first-author + - second-author +``` + +List order controls the displayed byline. diff --git a/.agents/skills/contribute-blog/references/google-doc-import.md b/.agents/skills/contribute-blog/references/google-doc-import.md new file mode 100644 index 00000000000..fc0fb55d218 --- /dev/null +++ b/.agents/skills/contribute-blog/references/google-doc-import.md @@ -0,0 +1,99 @@ +# Import from Google Docs + +Use this reference to convert an authored Google Doc into a Flutter blog post +without turning the import into an unsolicited rewrite. + +## Get the source + +First, try to download the source yourself. +Open the document in an authenticated browser and choose +**File > Download > Web Page (.html, zipped)**. +An authenticated Google Docs or Drive connector is also suitable +if it can produce the complete HTML export and original media. + +If you can't access the document, the download fails, +or the export is incomplete, +ask the user to download and provide the HTML zip. +Don't reconstruct content that the available source omits. + +Import the document body as exported. +Ignore comment threads and suggestion metadata, +don't reproduce or act on them. +Extract exports into a temporary directory and +don't commit the source archive or intermediate conversion files. + +## Convert content, not export debris + +Preserve the document's structure and meaning, +including headings, prose, lists, tables, emphasis, links, +code, images, and captions. +Convert ordinary content to clean Markdown, +using site components when they provide necessary behavior. + +Strip Google Docs export artifacts: + +- **Redirects**: + Replace Google redirect and tracking URLs with their direct destinations. +- **Styling spans & non-breaking spaces**: + Remove non-relevant markup and structure, such as + inline CSS, font declarations, classes, empty anchors, and wrapping spans. +- Replace ` ` with standard whitespace. +- **Code snippets**: + Convert exported code to fenced Markdown blocks + with appropriate language identifiers, such as `dart` or `bash`. + Google Docs might represent code as `

` tags with + ` ` indentation or as single-cell tables. +- **Titles**: + Don't replicate the document title as an `# H1` heading in the Markdown body. + The `title` in the post's frontmatter supplies the page H1. +- **Footnotes**: + Preserve footnote content, + but omit generated Google Docs anchor navigation links. +- **Videos**: Convert standalone YouTube links into `` components. + +Keep the author's person, tone, pacing, spelling variety, +heading style, and rhetorical choices unless editing was requested. +Correct conversion errors, +but surface optional prose changes separately. + +## Resolve authors + +Identify the ordered authors from an explicit byline or user context, +not from document ownership or editor metadata. + +To resolve their IDs and configure the post's frontmatter, +follow the steps in [Manage blog authors](authors.md). +If the source lacks enough information to identify or resolve an author, +ask the user for their GitHub or other profile URL. + +## Reconcile images + +Map each exported image to +its source position, caption, and purpose +before renaming or moving it: + +- Place images in the post's `images/` directory + with concise, descriptive names. +- Keep the highest-quality intended source. + Convert static raster images to WebP when quality and clarity are preserved. +- If the image is a JPEG, prefer a `.jpg` extension over `.jpeg`. +- Preserve intentional animation + and provide a static `socialImage` when needed. +- Follow the alt text and `` guidance in + [Flutter blog post format](post-format.md). +- If an image's purpose or placement is ambiguous, + ask for guidance rather than guessing. + +## Check fidelity + +Compare the converted post with the source section by section. +Confirm that no prose, list item, code block, table row, image, caption, +footnote, or intended link is missing or misplaced. +Check heading hierarchy, link destinations, image-caption pairings, +and meaningful emphasis. +Report any placeholder or editorial hold in the document body +that prevents the post from being ready. + +Finish with the format and accessibility checks in +[Flutter blog post format](post-format.md) +and the validation workflow in the [parent skill](../SKILL.md). diff --git a/.agents/skills/contribute-blog/references/post-format.md b/.agents/skills/contribute-blog/references/post-format.md new file mode 100644 index 00000000000..19b010852a1 --- /dev/null +++ b/.agents/skills/contribute-blog/references/post-format.md @@ -0,0 +1,168 @@ +# Flutter blog post format + +Use this reference for new posts, imports, metadata or media changes, +structural edits, and full reviews in `sites/www/content/blog/`. + +## Location and publication + +Each post uses this structure: + +```text +- sites/www/content/blog// + - index.md + - images/ +``` + +The directory name becomes the public URL path, like `/blog/`. +Don't rename a published post unless a URL change is requested +and redirects are handled. +Keep source documents, export archives, notes, and scratch files elsewhere. + +A future `publishDate` doesn't hide a post, +and there is no `draft` frontmatter field. + +## Post frontmatter + +A typical post begins with the following frontmatter: + +```yaml +--- +title: "A concise post title" +description: >- + A short, plain-text summary that stands on its own. +publishDate: YYYY-MM-DD +author: author-id +image: images/card-image.webp +category: deep-dive +layout: blog +--- +``` + +`title` (required) +: Supplies the page H1, card title, social title, and feed title. + Don't repeat it as an `# H1` heading in the Markdown body. + +`description` (required) +: Plain text used in the subtitle, cards, metadata, and feed. + Avoid Markdown formatting or links here. + +`publishDate` (required) +: The publication date in `YYYY-MM-DD` format. + Preserve it when editing an existing post unless a date change is requested. + +`author` (required) +: One or more author IDs from `sites/www/content/blog/authors.yaml`. + To set up authors, follow [Manage blog authors](authors.md). + +`category` (required) +: A valid category slug from `sites/www/content/blog/data.yaml`. + Current categories include `release`, `news`, `deep-dive`, and `case-study`. + +`layout` (required) +: Must be `blog`. + +`image` (optional) +: Post-relative path (for example, `images/hero.webp`) to the card and + default social preview image. + +`socialImage` (optional) +: Static image override (WebP or PNG under 5 MB) for social sharing metadata + when `image` is animated or exceeds 5 MB. + +## Card and social images + +`image` is optional in the data model but +recommended when suitable artwork exists. +It supplies the blog card image and social preview. +The body layout doesn't render `image` automatically. +If the hero image should appear in the post's body, +include it explicitly in the Markdown content with ``. + +Use post-local paths such as `images/hero.webp`. +Prefer optimized WebP for new static raster artwork when quality permits. +Don't upscale sources or commit unused variants. + +Set `socialImage` when `image` is animated (such as a GIF), +larger than 5 MB, or otherwise unsuitable for social previews: + +```yaml +image: images/animated-hero.gif +socialImage: images/social-cover.webp +``` + +Use a static WebP or PNG under 5 MB for `socialImage`. + +## Media in the body + +### Images + +Use `` for post-local figures, asset processing, or captions: + +```markdown + +``` + +Keep the entire component tag on one source line. + +The common `` attributes are: + +- `src` (required): + Post-local path to the image file (for example, `images/diagram.webp`). +- `alt` (required for new images): + Alternative text for accessibility. +- `caption` (optional): + Visible figure caption text. +- `figure` (optional): + Wraps the image and caption in a `

` element. + Use it when setting `caption`. + +Make the accessibility choice explicit: + +- Give informative images concise, purposeful alt text. +- Use `alt=""` for decorative images or images that only repeat nearby text. +- Explain a chart's key findings in surrounding prose or its caption. + Don't rely on pixels alone. +- Ensure animation isn't the only way to perceive important information. + +### Videos + +Embed YouTube videos using `` on a single line: + +```markdown + +``` + +## Body content and formatting + +### Structure + +- Start the body with an introduction, hero image, or `## H2` heading. + The layout renders the title, description, and author metadata automatically. +- Use semantic headings without skipping levels. + +### Markdown source formatting + +- Preserve valid source formatting and the author's stylistic choices. +- Use semantic line breaks when drafting new prose, + but don't reflow imported or existing text only to meet a line-length rule. +- Keep each Markdown link, inline code span, table row, + or component tag on a single line. + +### Links + +- Use descriptive link text. +- Verify all destination URLs. +- Use root-relative paths for internal blog links, + such as `/blog/`. +- Use absolute URLs for docs pages, + such as `https://docs.flutter.dev/...`. + +### Code, tables, and custom markup + +- Use fenced code blocks with accurate language identifiers. +- Verify commands, API names, and other technical details. +- Make intentional omissions in code samples clear. +- Don't use tables only for visual layout. + Give them a header row and sensible reading order. +- Only use raw HTML or site components when + Markdown can't express the required behavior. diff --git a/.agents/skills/publish-blog/SKILL.md b/.agents/skills/publish-blog/SKILL.md deleted file mode 100644 index b7ef277808f..00000000000 --- a/.agents/skills/publish-blog/SKILL.md +++ /dev/null @@ -1,64 +0,0 @@ ---- -name: publish-blog -description: Publish a new article to the Flutter blog from a Google Doc. ---- - -# Publish Blog Article - -## Overview - -Use this skill to add a new article to the Flutter blog -by extracting content from a Google Doc, -formatting it, -and placing it in the correct directory. - -## Workflow - -### 1. Get content and images from Google Doc - -- [ ] Ask the user to provide a link to the Google Doc. -- [ ] In your browser, open the Google Doc. -- [ ] Download the document as a Web Page (.html, zipped) via - **File > Download > Web Page** - (or ask the user to provide the zip if download fails). -- [ ] Extract the text content from the downloaded HTML file - and convert it to Markdown. -- [ ] Extract the images from the `images/` folder in the zip file. -- [ ] If the hero/cover image is an animated GIF, - ask the author for a high-quality static image (WebP or PNG) - for social media previews. - If none is available, extract a representative frame from the GIF - and convert it to WebP. - Keep the social image under 5 MB. -- [ ] Create a new folder for the blog post in `sites/www/content/blog/` - using a concise title. -- [ ] Save the Markdown content to `index.md` - and move the images to the `images/` directory in that folder. -- [ ] In the frontmatter, set `image:` to the image intended for blog cards. - If that image isn't suitable for social previews, such as when it's an - animated GIF or exceeds 5 MB, set `socialImage:` to a static WebP or PNG. - `socialImage` controls `twitter:image` and `og:image` and - falls back to `image` when omitted. -- [ ] In the Markdown body, - reference the intended hero image using ``. - An animated GIF or static image are both ok. -- [ ] Reference other images in the Markdown file using `` or - standard Markdown image tags as appropriate. - -### 2. Format the markdown - -- [ ] Apply semantic line breaks to any lines over 80 characters. -- [ ] Ensure there is a blank line after each heading. - -### 3. Add the author - -- [ ] Ask the user to provide the author's GitHub handle. - Search in `sites/www/content/blog/authors.yaml` for the handle. -- [ ] If the author is not found, add a new author entry. - Add the image to `sites/www/content/blog/authors_images/` - and any other metadata you can find from the user's GitHub profile. -- [ ] Update the frontmatter for the blog post with the author's handle. - -### 4. Review - -- [ ] Don't create a PR until the user has looked over the article. diff --git a/.agents/skills/write-technical-docs/SKILL.md b/.agents/skills/write-technical-docs/SKILL.md index c4291fd8ae7..f9c866086fe 100644 --- a/.agents/skills/write-technical-docs/SKILL.md +++ b/.agents/skills/write-technical-docs/SKILL.md @@ -81,11 +81,17 @@ Apply these rules to most developer-facing prose. only when it's part of the quoted content. For ordinary prose quotations, place commas and periods inside the closing quotation mark. - For examples, see [Punctuation](references/punctuation.md). + For examples, consult [Punctuation](references/punctuation.md). - Use the **serial comma**, also called the **Oxford comma**: "buttons, links, and menus". - Use **descriptive link text** that names the destination. Never link bare "click here", "here", "this", or "read more". +- Avoid using "see", "see the", or "for more information, see" to introduce + links or cross-references. + Prefer action-oriented or descriptive phrasing such as + "refer to", "consult", or "visit" + (for example, "To learn about X, visit Y" or + "For details, consult the X documentation"). - Write _and_, not `&`, except in code, a space-constrained label, or when matching a UI label. - Write **unambiguous dates**: @@ -107,7 +113,7 @@ Apply these rules to most developer-facing prose. - **Don't pre-announce** unreleased features, dates, or plans. Document only released behavior. - For additional guidance, see [Voice and tone](references/voice-and-tone.md). + For additional guidance, consult [Voice and tone](references/voice-and-tone.md). - Write for a **global audience**: avoid idioms, cultural references, humor that doesn't translate, and directional words that assume a layout. - Use **inclusive language**: @@ -116,7 +122,7 @@ Apply these rules to most developer-facing prose. _placeholder_ over _dummy_, and people-first or community-preferred phrasing. For additional guidance, - see [Inclusive and global writing](references/inclusive-and-global.md) + consult [Inclusive and global writing](references/inclusive-and-global.md) and [Word choice](references/word-choice.md). ## References diff --git a/.agents/skills/write-technical-docs/references/formatting-and-structure.md b/.agents/skills/write-technical-docs/references/formatting-and-structure.md index b0b709eb817..adc2ae6fa05 100644 --- a/.agents/skills/write-technical-docs/references/formatting-and-structure.md +++ b/.agents/skills/write-technical-docs/references/formatting-and-structure.md @@ -227,10 +227,15 @@ Use these rules to structure and format documentation. or "read more" as the link text. - Don't use a bare URL as link text in prose. Link a meaningful phrase. -- Introduce links naturally: +- Introduce links naturally without using "see": "For more information about quotas, - see [Quotas and limits][]". + consult [Quotas and limits][]", + or "To learn about quotas, visit [Quotas and limits][]". Use "about", not "on". +- Avoid using "see", "see the", or "for more information, see" + when directing readers to links or cross-references. + Prefer action-oriented or descriptive phrasing such as + "refer to", "consult", or "visit". - Don't say "the link below". Link the actual thing. - Write headings that produce stable, readable anchor links: diff --git a/.agents/skills/write-technical-docs/references/word-choice.md b/.agents/skills/write-technical-docs/references/word-choice.md index 70363f896a5..621d68ad5a7 100644 --- a/.agents/skills/write-technical-docs/references/word-choice.md +++ b/.agents/skills/write-technical-docs/references/word-choice.md @@ -3,7 +3,7 @@ Use this reference for common terms and naming rules in developer documentation. For UI verbs such as _click_, _tap_, and _select_, -see [UI elements and interaction](code-and-ui.md#ui-elements-and-interaction). +consult [UI elements and interaction](code-and-ui.md#ui-elements-and-interaction). For a strict terminology review or guidance about a term not covered here, consult the [strict-review word list](word-list.yaml). @@ -39,6 +39,10 @@ An explicit project convention takes precedence. - Instead of _above_ and _below_, refer to the named element or use _earlier_, _preceding_, _later_, or _following_ as appropriate. +- Avoid using _see_ or _see the_ to introduce links or references. + Prefer _refer to_, _consult_, or _visit_ + (for example, "To learn about X, visit Y" or + "For details, consult the X documentation"). - Replace _via_ with _through_, _with_, or _by using_. - Replace the verbs _leverage_ and _utilize_ with _use_. - Use _sign in_, not _log in_, unless the UI uses _log in_. diff --git a/.agents/skills/write-technical-docs/references/word-list.yaml b/.agents/skills/write-technical-docs/references/word-list.yaml index 58a37d80b97..7f946692e37 100644 --- a/.agents/skills/write-technical-docs/references/word-list.yaml +++ b/.agents/skills/write-technical-docs/references/word-list.yaml @@ -2008,9 +2008,12 @@ Pluralize as SDKs without an apostrophe. - term: "see" - status: ok + status: caution + prefer: ["refer to", "consult", "visit"] guidance: >- - OK as a general term and for links and cross-references. + Avoid using to introduce links and cross-references. + Prefer action-oriented or descriptive phrasing such as + refer to, consult, or visit (for example, "To learn about X, visit Y"). - term: "select" status: ok diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index be371cd1457..5676bd04765 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -36,7 +36,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 + uses: github/codeql-action/init@cdf488f595d80d6e07e03d4674febd5ab45fa938 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -47,7 +47,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 + uses: github/codeql-action/autobuild@cdf488f595d80d6e07e03d4674febd5ab45fa938 # ℹ️ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl @@ -61,4 +61,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 + uses: github/codeql-action/analyze@cdf488f595d80d6e07e03d4674febd5ab45fa938 diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 26e0b4bbe35..737b95fec7d 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -47,7 +47,7 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 with: persist-credentials: false - - uses: dart-lang/setup-dart@7654d458321ee25acccccfdb86cd48bd95768ff1 + - uses: dart-lang/setup-dart@6afc89df92d6eb3834022f73cd65adc8cdfcb92d with: sdk: beta - name: Fetch Dart dependencies @@ -64,7 +64,7 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 with: persist-credentials: false - - uses: dart-lang/setup-dart@7654d458321ee25acccccfdb86cd48bd95768ff1 + - uses: dart-lang/setup-dart@6afc89df92d6eb3834022f73cd65adc8cdfcb92d with: sdk: beta - name: Fetch Dart dependencies @@ -87,7 +87,7 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 with: persist-credentials: false - - uses: dart-lang/setup-dart@7654d458321ee25acccccfdb86cd48bd95768ff1 + - uses: dart-lang/setup-dart@6afc89df92d6eb3834022f73cd65adc8cdfcb92d with: sdk: beta - name: Fetch Dart dependencies diff --git a/.github/workflows/www.yml b/.github/workflows/www.yml index 6499041d05f..d1b27a2a5a4 100644 --- a/.github/workflows/www.yml +++ b/.github/workflows/www.yml @@ -43,7 +43,7 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 with: persist-credentials: false - - uses: dart-lang/setup-dart@7654d458321ee25acccccfdb86cd48bd95768ff1 + - uses: dart-lang/setup-dart@6afc89df92d6eb3834022f73cd65adc8cdfcb92d with: sdk: beta - name: Fetch Dart dependencies @@ -66,7 +66,7 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 with: persist-credentials: false - - uses: dart-lang/setup-dart@7654d458321ee25acccccfdb86cd48bd95768ff1 + - uses: dart-lang/setup-dart@6afc89df92d6eb3834022f73cd65adc8cdfcb92d with: sdk: beta - name: Fetch Dart dependencies diff --git a/AGENTS.md b/AGENTS.md index 478c011e62e..7ef7b2b50db 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -89,6 +89,12 @@ Writing should be consistent across the site and follow the In Markdown files, use [semantic line breaks](https://sembr.org/) and try to keep each line under 80 characters long. +#### Cross-references and links + +Avoid using "see", "see the", or "for more information, see" to introduce +links or references. Prefer action-oriented or descriptive phrasings such as +"refer to", "consult", or "visit" (for example, "To learn about X, visit Y"). + ## Coding guidelines All Dart code should follow [Effective Dart](https://dart.dev/effective-dart), diff --git a/examples/googleapis/pubspec.yaml b/examples/googleapis/pubspec.yaml index 5334a9fa1cb..86323ddb376 100644 --- a/examples/googleapis/pubspec.yaml +++ b/examples/googleapis/pubspec.yaml @@ -11,5 +11,5 @@ dependencies: flutter: sdk: flutter google_sign_in: ^7.2.0 - googleapis: ^16.0.0 + googleapis: ^17.0.0 http: ^1.6.0 diff --git a/examples/ui/navigation/pubspec.yaml b/examples/ui/navigation/pubspec.yaml index ed2f016a830..29c189d771a 100644 --- a/examples/ui/navigation/pubspec.yaml +++ b/examples/ui/navigation/pubspec.yaml @@ -10,7 +10,7 @@ dependencies: flutter: sdk: flutter - go_router: ^17.5.0 + go_router: ^18.0.0 flutter: uses-material-design: true diff --git a/packages/site_shared/lib/_sass/components/_button.scss b/packages/site_shared/lib/_sass/components/_button.scss index 79b5b15e8f5..2ce2f7322ea 100644 --- a/packages/site_shared/lib/_sass/components/_button.scss +++ b/packages/site_shared/lib/_sass/components/_button.scss @@ -39,6 +39,16 @@ button { padding: 0.4rem 0.9rem; text-decoration: none; cursor: pointer; + + &:disabled { + cursor: default; + opacity: 0.5; + } + } + + &.compact-button { + gap: 0.2rem; + padding: 0.25rem 0.65rem; } &.filled-button { @@ -52,11 +62,11 @@ button { font-size: 20px; } - &:hover { + &:not(:disabled):hover { @include mixins.interaction-style(8%); } - &:active { + &:not(:disabled):active { @include mixins.interaction-style(16%); } } @@ -64,11 +74,11 @@ button { &.text-button { color: var(--site-primary-color); - &:hover { + &:not(:disabled):hover { @include mixins.interaction-style(4%); } - &:active { + &:not(:disabled):active { @include mixins.interaction-style(8%); } } @@ -77,11 +87,11 @@ button { color: var(--site-primary-color); border: 1px solid var(--site-primary-color); - &:hover { + &:not(:disabled):hover { @include mixins.interaction-style(4%); } - &:active { + &:not(:disabled):active { @include mixins.interaction-style(8%); } } @@ -103,7 +113,7 @@ button { font-size: 1.75rem; } - &:hover { + &:not(:disabled):hover { color: var(--site-base-fgColor); } } diff --git a/packages/site_shared/lib/_sass/components/_tags.scss b/packages/site_shared/lib/_sass/components/_tags.scss new file mode 100644 index 00000000000..7c56bb17f8a --- /dev/null +++ b/packages/site_shared/lib/_sass/components/_tags.scss @@ -0,0 +1,88 @@ +.tag-label { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.25rem; + + border-radius: var(--site-radius); + background-color: var(--tag-bgColor); + color: var(--tag-fgColor); + font-family: var(--site-ui-fontFamily); + line-height: 1; + text-align: center; + text-wrap: nowrap; + user-select: none; + + &.tag-blue { + --tag-bgColor: var(--site-tag-blue-bgColor); + --tag-fgColor: var(--site-tag-blue-fgColor); + } + + &.tag-green { + --tag-bgColor: var(--site-tag-green-bgColor); + --tag-fgColor: var(--site-tag-green-fgColor); + } + + &.tag-teal { + --tag-bgColor: var(--site-tag-teal-bgColor); + --tag-fgColor: var(--site-tag-teal-fgColor); + } + + &.tag-purple { + --tag-bgColor: var(--site-tag-purple-bgColor); + --tag-fgColor: var(--site-tag-purple-fgColor); + } + + &.tag-magenta { + --tag-bgColor: var(--site-tag-magenta-bgColor); + --tag-fgColor: var(--site-tag-magenta-fgColor); + } + + &.tag-red { + --tag-bgColor: var(--site-tag-red-bgColor); + --tag-fgColor: var(--site-tag-red-fgColor); + } + + &.tag-orange { + --tag-bgColor: var(--site-tag-orange-bgColor); + --tag-fgColor: var(--site-tag-orange-fgColor); + } + + &.tag-amber { + --tag-bgColor: var(--site-tag-amber-bgColor); + --tag-fgColor: var(--site-tag-amber-fgColor); + } + + &.tag-grey { + --tag-bgColor: var(--site-tag-grey-bgColor); + --tag-fgColor: var(--site-tag-grey-fgColor); + } + + &.tag-subtle-grey { + --tag-bgColor: var(--site-tag-subtleGrey-bgColor); + --tag-fgColor: var(--site-tag-subtleGrey-fgColor); + + box-shadow: inset 0 0 0 1px var(--site-tag-subtleGrey-borderColor); + } + + &.tag-small { + padding: 0.3rem 0.5rem; + font-size: 0.8rem; + } + + &.tag-regular { + padding: 0.5rem 0.75rem; + font-size: 1rem; + } + + .material-symbols { + font-size: 1.25em; + } +} + +.tags { + display: flex; + flex-flow: row wrap; + align-items: center; + gap: 0.5rem; +} diff --git a/packages/site_shared/lib/components/common/button.dart b/packages/site_shared/lib/components/common/button.dart index 85feaebbd5d..2393c7f4706 100644 --- a/packages/site_shared/lib/components/common/button.dart +++ b/packages/site_shared/lib/components/common/button.dart @@ -18,6 +18,7 @@ class Button extends StatelessComponent { this.href, this.content, this.style = ButtonStyle.text, + this.size = ButtonSize.regular, this.id, this.attributes = const {}, this.classes, @@ -30,6 +31,7 @@ class Button extends StatelessComponent { final String? content; final String? title; final ButtonStyle style; + final ButtonSize size; final String? icon; final String? trailingIcon; final String? id; @@ -50,6 +52,7 @@ class Button extends StatelessComponent { final mergedClasses = [ style.cssClass, + if (size == ButtonSize.compact) 'compact-button', if ((icon != null || trailingIcon != null) && content == null) 'icon-button', ...?classes, @@ -83,6 +86,9 @@ class Button extends StatelessComponent { } } +/// The amount of space used within a [Button]. +enum ButtonSize { regular, compact } + enum ButtonStyle { filled, outlined, diff --git a/packages/site_shared/lib/components/common/tags.dart b/packages/site_shared/lib/components/common/tags.dart index a37641e2597..6484bbd4e46 100644 --- a/packages/site_shared/lib/components/common/tags.dart +++ b/packages/site_shared/lib/components/common/tags.dart @@ -5,17 +5,25 @@ import 'package:jaspr/dom.dart'; import 'package:jaspr/jaspr.dart'; +import '../../util.dart'; import 'material_icon.dart'; /// A display of multiple categorical or descriptive tags. class Tags extends StatelessComponent { - const Tags(this.tags); + /// Creates a collection containing [tags]. + const Tags(this.tags, {this.classes}); + /// The tags displayed in this collection. final List tags; + /// Additional CSS classes to apply to this tag container. + final List? classes; + @override - Component build(BuildContext context) => - div(classes: 'tags', [for (final tag in tags) tag]); + Component build(BuildContext context) => div( + classes: ['tags', ...?classes].toClasses, + [for (final tag in tags) tag], + ); } /// An individual tag to categorize an item, @@ -23,26 +31,95 @@ class Tags extends StatelessComponent { /// /// Generally displayed within a [Tags] component. class Tag extends StatelessComponent { - const Tag(this.content, {this.icon, this.title, this.label, this.color}); + /// Creates a tag displaying [content]. + const Tag( + this.content, { + this.icon, + this.title, + this.label, + this.color = .grey, + this.size = .regular, + this.classes, + }); + /// The text displayed in this tag. final String content; + + /// The ID of the optional Material Symbols icon displayed before [content]. final String? icon; + + /// The optional tooltip text for this tag. final String? title; + + /// The accessible label for this tag. + /// + /// If omitted, [title] is used as the accessible label. final String? label; - final String? color; + + /// The color treatment for this tag. + final TagColor color; + + /// The size of this tag's text and padding. + final TagSize size; + + /// Additional CSS classes to apply to this tag. + final List? classes; @override - Component build(BuildContext context) { - return div( - classes: 'tag-label', - attributes: { - 'title': ?title, - 'aria-label': ?(label ?? title), - }, - [ - if (icon case final iconId?) MaterialIcon(iconId), - span([.text(content)]), - ], - ); - } + Component build(BuildContext context) => div( + classes: [ + 'tag-label', + color._className, + size._className, + ...?classes, + ].toClasses, + attributes: { + 'title': ?title, + 'aria-label': ?(label ?? title), + }, + [ + if (icon case final iconId?) MaterialIcon(iconId), + span([.text(content)]), + ], + ); +} + +/// A supported color for a [Tag]. +enum TagColor { + blue, + green, + teal, + purple, + magenta, + red, + orange, + amber, + grey, + subtleGrey; + + /// The CSS class that applies this tag color. + String get _className => switch (this) { + TagColor.blue => 'tag-blue', + TagColor.green => 'tag-green', + TagColor.teal => 'tag-teal', + TagColor.purple => 'tag-purple', + TagColor.magenta => 'tag-magenta', + TagColor.red => 'tag-red', + TagColor.orange => 'tag-orange', + TagColor.amber => 'tag-amber', + TagColor.grey => 'tag-grey', + TagColor.subtleGrey => 'tag-subtle-grey', + }; +} + +/// A supported size for a [Tag]. +enum TagSize { + small, + regular; + + /// The CSS class that applies this tag size. + String get _className => switch (this) { + TagSize.small => 'tag-small', + TagSize.regular => 'tag-regular', + }; } diff --git a/packages/site_shared/lib/src/blog/models.dart b/packages/site_shared/lib/src/blog/models.dart index 3a55cb93ae9..69afa19d681 100644 --- a/packages/site_shared/lib/src/blog/models.dart +++ b/packages/site_shared/lib/src/blog/models.dart @@ -73,7 +73,7 @@ extension type Author(Map data) { String? resolveImageUrl(BuildContext context) { if (image case final localImage? when localImage.isNotEmpty) { - return context.resolveAsset('/blog/authors_images/$localImage'); + return context.resolveAsset('/blog/author_images/$localImage'); } if (imageUrl case final url? when url.isNotEmpty) { return url; diff --git a/sites/docs/firebase.json b/sites/docs/firebase.json index 4bcb70d2efc..1198ab9e8c4 100644 --- a/sites/docs/firebase.json +++ b/sites/docs/firebase.json @@ -35,12 +35,29 @@ { "source": "/ai-best-practices/:rest*", "destination": "/ai/best-practices/:rest*", "type": 301 }, { "source": "/ai-toolkit", "destination": "/ai/ai-toolkit", "type": 301 }, { "source": "/ai-toolkit/:rest*", "destination": "/ai/ai-toolkit/:rest*", "type": 301 }, - { "source": "/ai/flutter-ext-for-gemini", "destination": "/ai/antigravity-cli", "type": 301 }, - { "source": "/ai/gemini-cli-extension", "destination": "/ai/antigravity-cli", "type": 301 }, + { "source": "/ai/flutter-ext-for-gemini", "destination": "/ai/get-started", "type": 301 }, + { "source": "/ai/gemini-cli-extension", "destination": "/ai/get-started", "type": 301 }, { "source": "/ai/best-practices/tool-calls-aka-function-calls", "destination": "/ai/best-practices/tool-calls", "type": 301 }, - { "source": "/tools/antigravity", "destination": "/ai/antigravity", "type": 301 }, - { "source": "/ai/gemini-code-assist", "destination": "/ai/coding-assistants", "type": 301 }, + { "source": "/tools/antigravity", "destination": "/ai/get-started", "type": 301 }, + { "source": "/ai/gemini-code-assist", "destination": "/ai/get-started", "type": 301 }, { "source": "/ai/firebase-ai-logic", "destination": "https://firebase.google.com/docs/ai-logic/get-started?platform=flutter", "type": 301 }, + { "source": "/ai/create-with-ai", "destination": "/ai/get-started", "type": 301 }, + { "source": "/ai/create-with-ai/:rest*", "destination": "/ai/get-started", "type": 301 }, + { "source": "/ai/coding-assistants", "destination": "/ai/get-started", "type": 301 }, + { "source": "/ai/coding-assistants/:rest*", "destination": "/ai/get-started", "type": 301 }, + { "source": "/ai/antigravity", "destination": "/ai/get-started", "type": 301 }, + { "source": "/ai/antigravity/:rest*", "destination": "/ai/get-started", "type": 301 }, + { "source": "/ai/antigravity-cli", "destination": "/ai/get-started", "type": 301 }, + { "source": "/ai/antigravity-cli/:rest*", "destination": "/ai/get-started", "type": 301 }, + { "source": "/ai/agent-skills", "destination": "/ai/get-started", "type": 301 }, + { "source": "/ai/agent-skills/:rest*", "destination": "/ai/get-started", "type": 301 }, + { "source": "/ai/mcp-server", "destination": "/ai/get-started", "type": 301 }, + { "source": "/ai/mcp-server/:rest*", "destination": "/ai/get-started", "type": 301 }, + { "source": "/ai/ai-rules", "destination": "/ai/get-started", "type": 301 }, + { "source": "/ai/ai-rules/:rest*", "destination": "/ai/get-started", "type": 301 }, + { "source": "/ai/best-practices/developer-experience", "destination": "/ai/get-started", "type": 301 }, + { "source": "/ai/evals", "destination": "/ai/tools", "type": 301 }, + { "source": "/ai/evals/:rest*", "destination": "/ai/tools", "type": 301 }, { "source": "/android-release", "destination": "/deployment/android", "type": 301 }, { "source": "/animations", "destination": "/ui/animations", "type": 301 }, { "source": "/animations/:rest*", "destination": "/ui/animations/:rest*", "type": 301 }, @@ -211,7 +228,7 @@ { "source": "/release/breaking-changes/win_lifecycle_process_function", "destination": "/release/breaking-changes/win-lifecycle-process-function", "type": 301 }, { "source": "/release/archive", "destination": "/install/archive", "type": 301 }, { "source": "/release/upgrade", "destination": "/install/upgrade", "type": 301 }, - { "source": "/resources/ai-overview", "destination": "/ai/create-with-ai", "type": 301 }, + { "source": "/resources/ai-overview", "destination": "/ai/get-started", "type": 301 }, { "source": "/resources/books", "destination": "/reference/learning-resources", "type": 301 }, { "source": "/resources/bootstrap-into-dart", "destination": "https://dart.dev/learn", "type": 301 }, { "source": "/resources/compatibility", "destination": "/release/compatibility-policy", "type": 301 }, @@ -500,6 +517,7 @@ { "source": "/go/flutter-platform-views-windows", "destination": "https://docs.google.com/document/d/10nVF2f-QTQ5o2HMEZ1fUlBJzK4dEmhuPHTVFEgW-JRs/edit?usp=sharing&resourcekey=0-J-Lsp7xfA-Nr5EMr1av-Xg", "type": 301 }, { "source": "/go/flutter-plugin-languages", "destination": "https://docs.google.com/document/d/1Ok_mUPgmw8_l-ynLueEKtXm2Fs48lEVB0JqZd7M7uyA/edit", "type": 301 }, { "source": "/go/flutter-plugins-repo-migration", "destination": "https://docs.google.com/document/d/1VihAgx1e-X_H8VRc9WmUv_7qqsi81eeHrrhwhHgwtjQ/edit?usp=sharing", "type": 301 }, + { "source": "/go/flutter-pluggable-ci-design", "destination": "https://docs.google.com/document/d/1YuIN1aHzfU90NLXQ8adp7Nd5ke3AUhX5mS8jf2eLRzo/edit?tab=t.0", "type": 301 }, { "source": "/go/flutter-skia-ios-font-problem", "destination": "https://docs.google.com/document/d/1UkAhEKUEWNUjfKlqoBjJnx8zXrqErIqidPYjd9FBwsg", "type": 301 }, { "source": "/go/flutter-style-updates", "destination": "https://docs.google.com/document/d/1Ao6RZmI4F8VSCpN-89o0Y6bZXhYb1-qgVGV5tALxm48", "type": 301 }, { "source": "/go/flutter-support-multi-architecture", "destination": "https://docs.google.com/document/d/19tzWySgtgtTA99XQsjx5Pg0SFJeZKXyUlYavR0EXv8c/edit?usp=sharing", "type": 301 }, diff --git a/sites/docs/lib/_sass/_site.scss b/sites/docs/lib/_sass/_site.scss index 99092c6a676..b5078123f61 100644 --- a/sites/docs/lib/_sass/_site.scss +++ b/sites/docs/lib/_sass/_site.scss @@ -11,9 +11,11 @@ @use 'base/utils'; // Styles for individual components or content types, alphabetically ordered. +@use 'components/architecture-recommendations'; @use 'components/content'; @use 'components/expansion-list'; @use 'components/filter-search'; +@use 'components/filterable-index'; @use 'components/footer'; @use 'components/header'; @use 'components/icons'; @@ -22,7 +24,6 @@ @use 'components/os-selector'; @use 'components/pagenav'; @use 'components/platform-cards'; -@use 'components/pill'; @use 'components/sidebar'; @use 'components/side-menu'; @use 'components/trailing'; @@ -44,6 +45,7 @@ @use 'package:site_shared/_sass/components/stepper'; @use 'package:site_shared/_sass/components/summary-card'; @use 'package:site_shared/_sass/components/tabs'; +@use 'package:site_shared/_sass/components/tags'; @use 'package:site_shared/_sass/components/theming'; @use 'package:site_shared/_sass/components/tooltip'; diff --git a/sites/docs/lib/_sass/base/_root.scss b/sites/docs/lib/_sass/base/_root.scss index 4e302a796c3..deceb9c3150 100644 --- a/sites/docs/lib/_sass/base/_root.scss +++ b/sites/docs/lib/_sass/base/_root.scss @@ -94,6 +94,28 @@ body { --site-alert-warning-color: #9e6300; --site-alert-error-color: #cd3434; + --site-tag-blue-bgColor: #0468D7; + --site-tag-blue-fgColor: #fff; + --site-tag-green-bgColor: #188038; + --site-tag-green-fgColor: #fff; + --site-tag-grey-bgColor: var(--site-inset-borderColor); + --site-tag-grey-fgColor: var(--site-base-fgColor); + --site-tag-subtleGrey-bgColor: var(--site-raised-bgColor-translucent); + --site-tag-subtleGrey-borderColor: var(--site-inset-borderColor); + --site-tag-subtleGrey-fgColor: var(--site-base-fgColor); + --site-tag-purple-bgColor: #673AB7; + --site-tag-purple-fgColor: #fff; + --site-tag-teal-bgColor: #158477; + --site-tag-teal-fgColor: #fff; + --site-tag-magenta-bgColor: #9A2E6E; + --site-tag-magenta-fgColor: #fff; + --site-tag-red-bgColor: #B71C1C; + --site-tag-red-fgColor: #fff; + --site-tag-orange-bgColor: #C44F00; + --site-tag-orange-fgColor: #fff; + --site-tag-amber-bgColor: #A65A16; + --site-tag-amber-fgColor: #fff; + &:not(.dark-mode) .dark-mode-visible { display: none !important; } @@ -162,6 +184,28 @@ body { --site-alert-warning-color: #cea11f; --site-alert-error-color: #ff5d5d; + --site-tag-blue-bgColor: #0553B1; + --site-tag-blue-fgColor: #fff; + --site-tag-green-bgColor: #188038; + --site-tag-green-fgColor: #fff; + --site-tag-grey-bgColor: var(--site-inset-borderColor); + --site-tag-grey-fgColor: var(--site-base-fgColor); + --site-tag-subtleGrey-bgColor: var(--site-raised-bgColor-translucent); + --site-tag-subtleGrey-borderColor: var(--site-inset-borderColor); + --site-tag-subtleGrey-fgColor: var(--site-base-fgColor); + --site-tag-purple-bgColor: #673AB7; + --site-tag-purple-fgColor: #fff; + --site-tag-teal-bgColor: #106c62; + --site-tag-teal-fgColor: #fff; + --site-tag-magenta-bgColor: #84255D; + --site-tag-magenta-fgColor: #fff; + --site-tag-red-bgColor: #B71C1C; + --site-tag-red-fgColor: #fff; + --site-tag-orange-bgColor: #C44F00; + --site-tag-orange-fgColor: #fff; + --site-tag-amber-bgColor: #A65A16; + --site-tag-amber-fgColor: #fff; + .opal, .opal span { color: var(--opal-dark-color) !important; diff --git a/sites/docs/lib/_sass/components/_architecture-recommendations.scss b/sites/docs/lib/_sass/components/_architecture-recommendations.scss new file mode 100644 index 00000000000..7487390e896 --- /dev/null +++ b/sites/docs/lib/_sass/components/_architecture-recommendations.scss @@ -0,0 +1,5 @@ +.tag-label.recommendation-status { + display: flex; + margin: 0.5rem; + text-wrap: wrap; +} diff --git a/sites/docs/lib/_sass/components/_filterable-index.scss b/sites/docs/lib/_sass/components/_filterable-index.scss new file mode 100644 index 00000000000..99022c2e3cb --- /dev/null +++ b/sites/docs/lib/_sass/components/_filterable-index.scss @@ -0,0 +1,203 @@ +// The markup comes from the `FiltersSidebar` and `FilterSearchGroup` +// components in `filterable_index.dart`, so everything here is keyed off +// classes. Each page styles its own results list separately, keyed off the +// id of that list. + +.filterable-index { + $mobile-breakpoint: 839px; + $sidebar-width: 220px; + + display: flex; + flex-direction: row; + + .left-col { + margin-right: 1rem; + flex: 2; + } + + .right-col { + width: $sidebar-width; + } + + // The sidebar follows the page as it scrolls on wide screens and stays within + // the viewport. On narrow screens, it becomes an off-canvas drawer. + .filter-sidebar { + position: sticky; + top: calc(var(--site-header-height) + 1rem); + display: flex; + flex-direction: column; + max-height: calc(100vh - var(--site-header-height) - 2rem); + } + + .filter-group-wrapper { + // Scroll expanded filters independently so any sidebar footer stays visible. + min-height: 0; + border: 1px solid var(--site-inset-borderColor); + background-color: var(--site-inset-bgColor); + border-radius: var(--site-radius); + overflow-x: hidden; + overflow-y: auto; + overscroll-behavior: none; + } + + // Keep the title visible while the filters scroll. The close control is only + // shown while the sidebar is a drawer. + .filter-header { + position: sticky; + top: 0; + z-index: 1; + display: grid; + grid-template-columns: 2.25rem 1fr 2.25rem; + align-items: center; + background-color: var(--site-raised-bgColor); + border-bottom: 1px solid var(--site-inset-borderColor); + + .table-title { + grid-column: 2; + padding: .5rem 0; + text-align: center; + color: var(--site-base-fgColor-alt); + font-family: var(--site-ui-fontFamily); + font-weight: 600; + font-size: .925rem; + } + + .close-icon { + display: none; + grid-column: 3; + align-items: center; + justify-self: center; + cursor: pointer; + padding: 0.25rem; + border-radius: 4px; + background: none; + border: none; + color: var(--site-base-fgColor-alt); + transition: background-color 0.2s ease, color 0.2s ease; + + &:hover { + background-color: rgba(0, 0, 0, 0.1); + color: var(--site-primary-color); + } + + .material-symbols { + font-size: 20px; + } + } + } + + .filter-group { + .table-content { + padding: 1rem; + } + + ul { + padding-left: 0; + margin-bottom: .5rem; + + li { + list-style: none; + padding-left: 0; + padding-bottom: .25rem; + display: flex; + align-items: center; + + label { + padding-left: .35rem; + font-size: .9rem; + } + } + } + + h4 { + margin: 0 0 .5rem; + padding: 0; + } + + .filter-expansion-button { + margin-bottom: 1rem; + } + + .hidden { + display: none; + } + } + + .filter-search-group { + display: flex; + flex-direction: column; + + button.show-filters-button { + @media (min-width: $mobile-breakpoint + 1) { + display: none; + } + } + + .label-row { + display: flex; + justify-content: space-between; + font-size: .925rem; + margin-top: .25rem; + + label { + font-family: var(--site-ui-fontFamily); + color: var(--site-base-fgColor-lighter); + + margin: 0 .5rem 0 0; + text-align: end; + } + } + } + + // On narrow screens the sidebar becomes a drawer that slides in from the + // right. Opening and closing is driven entirely by the hidden + // `.filter-drawer-toggle` checkbox that precedes it. + @media (max-width: $mobile-breakpoint) { + flex-direction: column; + + .left-col { + margin-right: 0; + } + + // The drawer itself is fixed-positioned, so this column no longer needs to + // reserve width for it in the flex layout. + .right-col { + width: auto; + } + + .filter-header { + .close-icon { + display: flex; + } + } + + .filter-sidebar { + position: fixed; + top: var(--site-header-height); + bottom: 0; + right: -$sidebar-width; + width: $sidebar-width; + max-height: none; + display: flex; + flex-direction: column; + background-color: var(--site-inset-bgColor); + border-left: 1px solid var(--site-inset-borderColor); + transition: right 0.3s ease-in-out; + z-index: var(--site-z-top); + } + + .filter-drawer-toggle:checked + .filter-sidebar { + right: 0; + } + + // Fill the drawer rather than floating within it as a card, + // and scroll on its own so any footer stays in view. + .filter-group-wrapper { + flex: 1; + min-height: 0; + overflow-y: auto; + border: none; + border-radius: 0; + } + } +} diff --git a/sites/docs/lib/_sass/components/_pill.scss b/sites/docs/lib/_sass/components/_pill.scss deleted file mode 100644 index 0ab0453b731..00000000000 --- a/sites/docs/lib/_sass/components/_pill.scss +++ /dev/null @@ -1,55 +0,0 @@ -@use 'sass:color'; - -.rrec-pill { - border-radius: 8px; - margin: 0.5rem; - padding: 0.5rem; - text-align: center; - - // Grey by default - background: var(--site-inset-borderColor); - color: var(--site-base-fgColor-lighter); - - // Green - &.success { - background: color.scale(#f1fbf9, $lightness: -10%); - color: #155723; - } - - // Blue - &.info { - $info-bg: #e7f8ff; - background: color.scale($info-bg, $lightness: -10%); - color: color.scale($info-bg, $lightness: -60%); - } -} - -.pill-sm { - border-radius: 20px; - padding: 0.35rem 0.5rem; - text-align: center; - font-size: .8rem; - - - // Grey by default - background: var(--site-inset-borderColor); - color: var(--site-base-fgColor-lighter); - - // TODO(parlough): Consider if these colors can be shared and if - // compatible with dark mode. - - &.flutter-blue { - color: color.scale(#E7F8FF, $lightness: 10%); - background: #0468D7; - } - - &.teal { - color: color.scale(#B8EDE1, $lightness: 10%); - background: #158477; - } - - &.purple { - color: color.scale(#C6BAFA, $lightness: 10%); - background: #6200EE; - } -} diff --git a/sites/docs/lib/_sass/components/_platform-cards.scss b/sites/docs/lib/_sass/components/_platform-cards.scss index ba690e2c842..4bc24290bf1 100644 --- a/sites/docs/lib/_sass/components/_platform-cards.scss +++ b/sites/docs/lib/_sass/components/_platform-cards.scss @@ -34,23 +34,7 @@ } .platform-card-tags { - display: flex; - flex-flow: row wrap; - align-items: center; - gap: 0.5rem; - margin-top: 0.5rem; - - span { - font-size: 0.875rem; - line-height: 1; - - background-color: var(--site-raised-bgColor-translucent); - border-radius: var(--site-radius); - border: 1px solid var(--site-inset-borderColor); - - padding: 0.2rem 0.4rem; - } } .platform-card-details { diff --git a/sites/docs/lib/_sass/pages/_learning-resources-index.scss b/sites/docs/lib/_sass/pages/_learning-resources-index.scss index 0e6581adbd5..031c44eb3d9 100644 --- a/sites/docs/lib/_sass/pages/_learning-resources-index.scss +++ b/sites/docs/lib/_sass/pages/_learning-resources-index.scss @@ -1,235 +1,6 @@ @use 'package:site_shared/_sass/base/mixins'; -#resource-filter-group-wrapper { - border: 1px solid var(--site-inset-borderColor); - background-color: var(--site-inset-bgColor); - border-radius: var(--site-radius); - overflow: hidden; - position: sticky; - top: calc(var(--site-header-height) + 1rem); -} - -.filter-header { - display: none; - position: absolute; - top: 0; - right: 0; - padding: 0.75rem 1rem; - z-index: 10; -} - -.close-icon { - cursor: pointer; - padding: 0.25rem; - border-radius: 4px; - background: none; - border: none; - color: var(--site-base-fgColor-alt); - transition: all 0.2s ease; - position: absolute; - right: 1rem; - top: 75%; - transform: translateY(-50%); - - &:hover { - background-color: rgba(0, 0, 0, 0.1); - color: var(--site-primary-color); - } - - .material-symbols { - font-size: 20px; - } -} - -// Mobile screen customizations. -@media (max-width: 839px) { - .filter-header { - display: block; - } - - #resource-filter-group-wrapper { - position: fixed; - top: var(--site-header-height); - bottom: 0; - right: -220px; - width: 220px; - border-bottom: none; - height: 100%; - border-radius: 0; - transition: right 0.3s ease-in-out; - z-index: 1000; - } - - #open-filter-toggle:not(:checked)+#resource-filter-group-wrapper { - right: -220px; - } - - #open-filter-toggle:checked+#resource-filter-group-wrapper { - right: 0; - } -} - -//Desktop screens -@media (min-width: 840px) { - #resource-filter-group { - position: static !important; - right: auto !important; - } - - .filter-header { - display: none !important; - } -} - -#resource-index-content { - display: flex; - flex-direction: row; - - .left-col { - margin-right: 1rem; - flex: 2; - } - - .right-col { - width: 220px; - - @media (max-width: 840px) { - position: fixed; - top: var(--site-header-height); - bottom: 0; - right: -15rem; - box-shadow: 0 6px 18px 0 rgba(0, 0, 0, 0.2); - border-radius: 0.4rem; - width: 220px; - - @keyframes slidein { - 0% { - right: -10rem; - } - - 100% { - right: 0; - } - } - - &.show { - animation-duration: 500ms; - animation-delay: 200ms; - animation-name: slidein; - animation-iteration-count: 1; - animation-timing-function: ease; - animation-fill-mode: forwards; - } - } - } -} - -#resource-filter-group { - .table-title { - text-align: center; - color: var(--site-base-fgColor-alt); - background-color: var(--site-raised-bgColor); - font-family: var(--site-ui-fontFamily); - font-weight: 600; - font-size: .925rem; - padding: .5rem; - border-bottom: 1px solid var(--site-inset-borderColor); - } - - .table-content { - padding: 1rem; - } - - ul { - padding-left: 0; - margin-bottom: .5rem; - - li { - list-style: none; - padding-left: 0; - padding-bottom: .25rem; - display: flex; - align-items: center; - - label { - padding-left: .35rem; - font-size: .9rem; - } - } - } - - h4 { - margin: 0 0 .5rem; - padding: 0; - } - - button { - color: var(--site-primary-color); - margin: 0 0 2rem; - padding: .5rem 0 1rem 1rem; - - &:hover { - color: var(--site-onPrimary-color); - } - } - - @media (max-width: 840px) { - border-bottom: none; - height: 100%; - border-radius: 0; - - .table-title { - background-color: var(--site-raised-bgColor-translucent); - } - } - - .hidden { - display: none; - } -} - -#resource-search-group { - display: flex; - flex-direction: column; - - button.show-filters-button { - @media (min-width: 840px) { - display: none; - } - } - - .label-row { - display: flex; - justify-content: space-between; - font-size: .925rem; - - label { - font-family: var(--site-ui-fontFamily); - color: var(--site-base-fgColor-lighter); - - padding: .25rem 1rem 0 0; - margin: 0; - text-align: end; - } - - button { - padding: .25rem; - color: var(--site-primary-color); - display: flex; - align-items: center; - - &:hover { - color: var(--site-onPrimary-color-light); - } - - &:disabled, - &[disabled] { - color: var(--site-inset-bgColor-translucent); - cursor: default; - } - } - } -} +// Shared styles live in `_filterable-index.scss`. #all-resources-grid { margin-block-start: 1rem; diff --git a/sites/docs/lib/src/components/pages/architecture_recommendations.dart b/sites/docs/lib/src/components/pages/architecture_recommendations.dart index 7ea371b7176..39bbd04db7d 100644 --- a/sites/docs/lib/src/components/pages/architecture_recommendations.dart +++ b/sites/docs/lib/src/components/pages/architecture_recommendations.dart @@ -5,6 +5,7 @@ import 'package:jaspr/dom.dart'; import 'package:jaspr/jaspr.dart'; import 'package:jaspr_content/jaspr_content.dart'; +import 'package:site_shared/components/common/tags.dart'; import 'package:site_shared/markdown.dart'; class ArchitectureRecommendations extends CustomComponentBase { @@ -67,15 +68,21 @@ class ArchitectureRecommendations extends CustomComponentBase { td([ DashMarkdown(inline: true, content: rec.recommendation), switch (rec.confidence) { - 'strong' => const div(classes: 'rrec-pill success', [ - .text('Strongly recommend'), - ]), - 'recommend' => const div(classes: 'rrec-pill info', [ - .text('Recommend'), - ]), - _ => const div(classes: 'rrec-pill', [ - .text('Conditional'), - ]), + 'strong' => const Tag( + 'Strongly recommend', + color: TagColor.green, + classes: ['recommendation-status'], + ), + 'recommend' => const Tag( + 'Recommend', + color: TagColor.blue, + classes: ['recommendation-status'], + ), + _ => const Tag( + 'Conditional', + color: TagColor.grey, + classes: ['recommendation-status'], + ), }, ]), td([ diff --git a/sites/docs/lib/src/components/pages/filterable_index.dart b/sites/docs/lib/src/components/pages/filterable_index.dart new file mode 100644 index 00000000000..5a96dab8d91 --- /dev/null +++ b/sites/docs/lib/src/components/pages/filterable_index.dart @@ -0,0 +1,172 @@ +// 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. + +/// Reusable shell components for filterable index pages. +/// +/// The pages provide their own filter controls and results list, +/// but the surrounding layout, search field, and sidebar are the same, +/// and are styled by `_filterable-index.scss`. +library; + +import 'package:jaspr/dom.dart'; +import 'package:jaspr/jaspr.dart'; +import 'package:site_shared/components/common/button.dart'; +import 'package:site_shared/components/common/material_icon.dart'; +import 'package:site_shared/components/common/search.dart'; +import 'package:site_shared/components/utils/global_event_listener.dart'; +import 'package:universal_web/js_interop.dart'; +import 'package:universal_web/web.dart' as web; + +/// The right-hand column of a filterable index page. +/// +/// Renders [children] within a card titled 'Filter by', with [footer] pinned +/// below it. On narrow screens the card and footer become a drawer that slides +/// in from the right, toggled by the [FilterSearchGroup] search field. +class FiltersSidebar extends StatelessComponent { + const FiltersSidebar({ + required this.drawerToggleId, + required this.children, + this.footer = const [], + super.key, + }); + + /// The ID of the checkbox that toggles the drawer on narrow screens. + final String drawerToggleId; + + /// The filter controls, rendered within the filter card. + final List children; + + /// Optional page-specific content, rendered below the filter card. + final List footer; + + @override + Component build(BuildContext context) { + return div(classes: 'right-col', [ + input( + type: InputType.checkbox, + id: drawerToggleId, + classes: 'filter-drawer-toggle', + attributes: const {'hidden': 'true'}, + ), + div(classes: 'filter-sidebar', [ + div(classes: 'filter-group-wrapper', [ + div(classes: 'filter-group', [ + div(classes: 'filter-header', [ + const div(classes: 'table-title', [.text('Filter by')]), + label( + attributes: { + 'for': drawerToggleId, + 'aria-hidden': 'true', + }, + classes: 'close-icon', + const [MaterialIcon('close')], + ), + ]), + ...children, + ]), + ]), + ...footer, + ]), + ]); + } +} + +/// The search field of a filterable index page, +/// followed by the page-specific [children], such as a result count. +/// +/// On narrow screens, the field also contains the button +/// that opens the [FiltersSidebar] drawer. +class FilterSearchGroup extends StatelessComponent { + const FilterSearchGroup({ + required this.drawerToggleId, + required this.searchId, + required this.placeholder, + required this.label, + required this.value, + required this.onInput, + this.children = const [], + super.key, + }); + + /// The ID of the checkbox that toggles the [FiltersSidebar] drawer. + final String drawerToggleId; + + /// The ID of the search field, so the page can label it. + final String searchId; + + /// The placeholder text displayed in the search field. + final String placeholder; + + /// The accessibility label for the search field. + final String label; + + /// The current value of the search field. + final String value; + + /// Callback triggered when the search field input changes. + final void Function(String) onInput; + + /// Content rendered below the search field. + final List children; + + @override + Component build(BuildContext context) { + return div(classes: 'filter-search-group', [ + SearchBar( + placeholder: placeholder, + label: label, + value: value, + id: searchId, + onInput: onInput, + trailing: _DrawerToggleButton(drawerToggleId), + ), + ...children, + ]); + } +} + +/// The button that opens the filter drawer on narrow screens. +/// +/// Also closes the drawer again when anything outside of it is clicked. +class _DrawerToggleButton extends StatelessComponent { + const _DrawerToggleButton(this._drawerToggleId); + + /// The ID of the checkbox that this button toggles. + final String _drawerToggleId; + + /// The checkbox that controls the drawer, if present. + web.HTMLInputElement? get _toggle => + web.document.getElementById(_drawerToggleId) as web.HTMLInputElement?; + + @override + Component build(BuildContext context) { + return GlobalEventListener( + onClick: (event) { + final toggle = _toggle; + if (toggle == null || !toggle.checked) return; + + final target = event.target; + if (target == null || !target.isA()) return; + + final element = target as web.Element; + // Keep the drawer open for clicks anywhere within it, + // including in the sidebar footer. + if (element.closest('.filter-sidebar') == null && + element.closest('.show-filters-button') == null) { + toggle.checked = false; + } + }, + Button( + icon: 'filter_list', + classes: const ['show-filters-button'], + attributes: const {'aria-label': 'Show filters'}, + onClick: () { + if (_toggle case final toggle?) { + toggle.checked = !toggle.checked; + } + }, + ), + ); + } +} diff --git a/sites/docs/lib/src/components/pages/learning_resource_filters.dart b/sites/docs/lib/src/components/pages/learning_resource_filters.dart index dcd726c134d..a6b5badfa3c 100644 --- a/sites/docs/lib/src/components/pages/learning_resource_filters.dart +++ b/sites/docs/lib/src/components/pages/learning_resource_filters.dart @@ -7,38 +7,47 @@ import 'dart:math'; import 'package:jaspr/dom.dart'; import 'package:jaspr/jaspr.dart'; import 'package:site_shared/analytics.dart'; -import 'package:site_shared/components/common/material_icon.dart'; -import 'package:site_shared/components/common/search.dart'; -import 'package:site_shared/components/utils/global_event_listener.dart'; +import 'package:site_shared/components/common/button.dart'; import 'package:universal_web/js_interop.dart'; import 'package:universal_web/web.dart' as web; import '../../models/learning_resource_model.dart'; +import 'filterable_index.dart'; import 'learning_resource_filters_sidebar.dart'; +/// The search controls and result summary for the learning resources index. @client class LearningResourceFilters extends StatefulComponent { const LearningResourceFilters({super.key}); + /// The ID of the checkbox that toggles the filter drawer on narrow screens. + static const String drawerToggleId = 'learning-resource-filter-toggle'; + @override State createState() => _LearningResourceFiltersState(); } class _LearningResourceFiltersState extends State { - String searchQuery = ''; + /// The filters selected in the learning resource sidebar. + static LearningResourceFiltersNotifier get _filters => + LearningResourceFiltersSidebar.filters; + + /// The learning resources reconstructed from the rendered resource cards. + final List _resources = []; - FiltersNotifier get filters => LearningResourceFiltersSidebar.filters; + /// The current search query. + String _searchQuery = ''; - final List resources = []; - int filteredResourcesCount = 0; + /// The number of resources matching the active search and filters. + int _filteredResourcesCount = 0; @override void initState() { super.initState(); if (kIsWeb) { - filters.addListener(setFilters); + _filters.addListener(_setFilters); final resourceGrid = web.document.getElementById('all-resources-grid'); if (resourceGrid == null) { @@ -46,16 +55,17 @@ class _LearningResourceFiltersState extends State { } final resourceCards = resourceGrid.querySelectorAll('.card'); - recreateResources(resourceCards); - shuffleCards(resourceGrid); + _recreateResources(resourceCards); + _shuffleCards(resourceGrid); } } - void recreateResources(web.NodeList resourceCards) { + /// Populates [_resources] from [resourceCards] and registers click analytics. + void _recreateResources(web.NodeList resourceCards) { for (var i = 0; i < resourceCards.length; i++) { final element = resourceCards.item(i) as web.Element; final info = LearningResource.fromElement(element); - resources.add(info); + _resources.add(info); element.addEventListener( 'click', @@ -67,10 +77,11 @@ class _LearningResourceFiltersState extends State { }).toJS, ); } - filteredResourcesCount = resources.length; + _filteredResourcesCount = _resources.length; } - void shuffleCards(web.Element container) { + /// Randomizes the order of the resource cards in [container]. + void _shuffleCards(web.Element container) { final r = Random(); final elements = container.childNodes; for (var i = elements.length; i > 0; i--) { @@ -91,12 +102,12 @@ class _LearningResourceFiltersState extends State { /// searchQuery = '...'; /// }); /// ``` - void setFilters([void Function()? callback]) { + void _setFilters([void Function()? callback]) { setState(callback ?? () {}); - final resourcesToShow = filters.filterResources(resources, searchQuery); - filteredResourcesCount = resourcesToShow.length; - for (final info in resources) { + final resourcesToShow = _filters.filterResources(_resources, _searchQuery); + _filteredResourcesCount = resourcesToShow.length; + for (final info in _resources) { final element = web.document.getElementById(info.name) as web.HTMLElement?; if (element == null) { @@ -114,78 +125,51 @@ class _LearningResourceFiltersState extends State { @override void dispose() { if (kIsWeb) { - filters.removeListener(setFilters); + _filters.removeListener(_setFilters); } super.dispose(); } @override Component build(BuildContext context) { - return div(id: 'resource-search-group', classes: 'chip-filters-group', [ - SearchBar( - placeholder: 'Try "button" or "networking"...', - label: 'Search learning resources by name and category', - value: searchQuery, - id: 'resource-search', - onInput: (value) { - setFilters(() { - searchQuery = value; - }); - }, - trailing: GlobalEventListener( - onClick: (event) { - final target = event.target as web.Element?; - // If clicking outside the filters or toggle, close the filters. - if (target?.closest('#resource-filter-group-wrapper') == null && - target?.closest('.show-filters-button') == null) { - final toggle = web.document.getElementById( - 'open-filter-toggle', - ) as web.HTMLInputElement?; - toggle?.checked = false; - } - }, - button( - classes: 'icon-button show-filters-button', - onClick: () { - final toggle = web.document.getElementById( - 'open-filter-toggle', - ) as web.HTMLInputElement?; - toggle?.checked = !toggle.checked; - }, + return FilterSearchGroup( + drawerToggleId: LearningResourceFilters.drawerToggleId, + searchId: 'resource-search', + placeholder: 'Try "button" or "networking"...', + label: 'Search learning resources by name and category', + value: _searchQuery, + onInput: (value) { + _setFilters(() { + _searchQuery = value; + }); + }, + children: [ + div(classes: 'label-row', [ + label( + attributes: {'for': 'resource-search'}, [ - const MaterialIcon('filter_list'), + const .text('Showing '), + span([.text('$_filteredResourcesCount')]), + const .text(' / '), + span([.text('${_resources.length}')]), ], ), - ), - ), - div(classes: 'label-row', [ - label( - attributes: {'for': 'resource-search'}, - [ - const .text('Showing '), - span([.text('$filteredResourcesCount')]), - const .text(' / '), - span([.text('${resources.length}')]), - ], - ), - button( - attributes: { - if (searchQuery.isEmpty && - filters.selectedTags.isEmpty && - filters.selectedTypes.isEmpty) - 'disabled': 'true', - }, - onClick: () { - // No setState needed, since resetting filters will trigger it. - searchQuery = ''; - filters.reset(); - }, - [ - const MaterialIcon('close_small'), - const span([.text('Clear filters')]), - ], - ), - ]), - ]); + Button( + icon: 'close_small', + content: 'Clear filters', + size: ButtonSize.compact, + disabled: + _searchQuery.isEmpty && + _filters.selectedTags.isEmpty && + _filters.selectedTypes.isEmpty, + onClick: () { + // No setState needed, since resetting filters will trigger it. + _searchQuery = ''; + _filters.reset(); + }, + ), + ]), + ], + ); } } diff --git a/sites/docs/lib/src/components/pages/learning_resource_filters_sidebar.dart b/sites/docs/lib/src/components/pages/learning_resource_filters_sidebar.dart index cf512193009..d662418fed5 100644 --- a/sites/docs/lib/src/components/pages/learning_resource_filters_sidebar.dart +++ b/sites/docs/lib/src/components/pages/learning_resource_filters_sidebar.dart @@ -5,12 +5,14 @@ import 'package:jaspr/dom.dart'; import 'package:jaspr/jaspr.dart'; import 'package:site_shared/analytics.dart'; -import 'package:site_shared/components/common/material_icon.dart'; +import 'package:site_shared/components/common/button.dart'; import 'package:site_shared/util.dart'; import '../../models/learning_resource_model.dart'; +import 'filterable_index.dart'; import 'learning_resource_filters.dart'; +/// The subject and type filters for the learning resources index. @client class LearningResourceFiltersSidebar extends StatelessComponent { const LearningResourceFiltersSidebar({super.key}); @@ -19,103 +21,91 @@ class LearningResourceFiltersSidebar extends StatelessComponent { /// /// This is static so that [LearningResourceFilters] can access it, /// since both client components don't share a common ancestor. - static FiltersNotifier filters = FiltersNotifier(); + static final LearningResourceFiltersNotifier filters = + LearningResourceFiltersNotifier(); @override Component build(BuildContext context) { - return div(classes: 'right-col', [ - const input( - type: InputType.checkbox, - id: 'open-filter-toggle', - attributes: {'hidden': 'true'}, - ), - div(id: 'resource-filter-group-wrapper', [ - div(id: 'resource-filter-group', [ - const div(classes: 'filter-header', [ - label( - attributes: {'for': 'open-filter-toggle', 'aria-hidden': 'true'}, - classes: 'close-icon', - [MaterialIcon('close')], - ), - ]), - const div(classes: 'table-title', [.text('Filter by')]), - ListenableBuilder( - listenable: filters, - builder: (context) { - return div(classes: 'table-content', [ - const h4([.text('Subject')]), - ul(classes: filters.tagsExpanded ? '' : 'collapsed', [ - for (final (index, tag) in LearningResourceTag.values.indexed) - li( - classes: [ - if (!filters.tagsExpanded && index > 3) 'hidden', - ].toClasses, - [ - input( - type: InputType.checkbox, - attributes: { - 'role': 'checkbox', - 'name': 'filter-${tag.name}', - }, - id: 'filter-${tag.name}', - checked: filters.selectedTags.contains(tag), - onChange: (checked) { - filters.setTag(tag, checked as bool); - }, - ), - label( - attributes: {'for': 'filter-${tag.name}'}, - [.text(tag.label)], - ), - ], - ), - ]), - button(onClick: filters.toggleTagsExpanded, [ - span(classes: 'label', [ - .text(filters.tagsExpanded ? 'Less' : 'More'), - ]), - MaterialIcon( - filters.tagsExpanded ? 'expand_less' : 'expand_more', - ), - ]), - const h4([.text('Type')]), - ul([ - for (final type in LearningResourceType.values) - li([ + return FiltersSidebar( + drawerToggleId: LearningResourceFilters.drawerToggleId, + children: [ + ListenableBuilder( + listenable: filters, + builder: (context) { + return div(classes: 'table-content', [ + const h4([.text('Subject')]), + ul([ + for (final (index, tag) in LearningResourceTag.values.indexed) + li( + classes: [ + if (!filters.tagsExpanded && index > 3) 'hidden', + ].toClasses, + [ input( type: InputType.checkbox, - attributes: { - 'role': 'checkbox', - 'name': 'filter-${type.name}', - }, - id: 'filter-${type.name}', - checked: filters.selectedTypes.contains(type), + attributes: {'name': 'resource-filter-${tag.name}'}, + id: 'resource-filter-${tag.name}', + checked: filters.selectedTags.contains(tag), onChange: (checked) { - filters.setType(type, checked as bool); + filters.setTag(tag, checked as bool); }, ), label( - attributes: {'for': 'filter-${type.name}'}, - [.text(type.label)], + attributes: {'for': 'resource-filter-${tag.name}'}, + [.text(tag.label)], ), - ]), - ]), - ]); - }, - ), - ]), - ]), - ]); + ], + ), + ]), + Button( + content: filters.tagsExpanded ? 'Less' : 'More', + classes: const ['filter-expansion-button'], + size: ButtonSize.compact, + trailingIcon: filters.tagsExpanded + ? 'expand_less' + : 'expand_more', + onClick: filters.toggleTagsExpanded, + ), + const h4([.text('Type')]), + ul([ + for (final type in LearningResourceType.values) + li([ + input( + type: InputType.checkbox, + attributes: {'name': 'resource-filter-${type.name}'}, + id: 'resource-filter-${type.name}', + checked: filters.selectedTypes.contains(type), + onChange: (checked) { + filters.setType(type, isSelected: checked as bool); + }, + ), + label( + attributes: {'for': 'resource-filter-${type.name}'}, + [.text(type.label)], + ), + ]), + ]), + ]); + }, + ), + ], + ); } } -/// Notifier to manage the state of the filters. -class FiltersNotifier extends ChangeNotifier { - Set selectedTags = {}; - Set selectedTypes = {}; +/// Stores the selected learning resource filters and +/// notifies listeners when they change. +final class LearningResourceFiltersNotifier extends ChangeNotifier { + /// The currently selected subject tags. + final Set selectedTags = {}; + + /// The currently selected resource types. + final Set selectedTypes = {}; + /// Whether all subject tags are visible. bool tagsExpanded = false; + /// Updates whether [tag] is selected and notifies listeners. void setTag(LearningResourceTag tag, bool isSelected) { if (isSelected) { selectedTags.add(tag); @@ -133,7 +123,8 @@ class FiltersNotifier extends ChangeNotifier { notifyListeners(); } - void setType(LearningResourceType type, bool isSelected) { + /// Updates whether [type] is selected and notifies listeners. + void setType(LearningResourceType type, {required bool isSelected}) { if (isSelected) { selectedTypes.add(type); @@ -150,17 +141,20 @@ class FiltersNotifier extends ChangeNotifier { notifyListeners(); } + /// Toggles whether all subject tags are visible. void toggleTagsExpanded() { tagsExpanded = !tagsExpanded; notifyListeners(); } + /// Clears all selected tags and resource types. void reset() { selectedTags.clear(); selectedTypes.clear(); notifyListeners(); } + /// Returns the resources matching [searchQuery] and the selected filters. Set filterResources( List resources, String searchQuery, diff --git a/sites/docs/lib/src/components/pages/learning_resource_index.dart b/sites/docs/lib/src/components/pages/learning_resource_index.dart index 3141659ba0e..311ef620a49 100644 --- a/sites/docs/lib/src/components/pages/learning_resource_index.dart +++ b/sites/docs/lib/src/components/pages/learning_resource_index.dart @@ -5,7 +5,7 @@ import 'package:jaspr/dom.dart'; import 'package:jaspr/jaspr.dart'; import 'package:jaspr_content/jaspr_content.dart'; -import 'package:site_shared/util.dart'; +import 'package:site_shared/components/common/tags.dart'; import '../../models/learning_resource_model.dart'; import 'learning_resource_filters.dart'; @@ -30,8 +30,8 @@ final class LearningResourceIndex extends StatelessComponent { } } - return div(id: 'resource-index-content', [ - div(classes: 'left-col', id: 'resource-index-main-content', [ + return div(classes: 'filterable-index', [ + div(classes: 'left-col', [ const LearningResourceFilters(), section(classes: 'card-grid', id: 'all-resources-grid', [ for (final item in learningResources) _ResourceCard(item), @@ -65,21 +65,15 @@ final class _ResourceCard extends StatelessComponent { img(src: imageUrl, alt: ''), ]), div(classes: 'card-leading', [ - span( - classes: [ - 'pill-sm', - switch (resource.type) { - 'codelab' || 'workshop' => 'flutter-blue', - 'quickstart' || 'demo' => 'purple', - _ => 'teal', - }, - ].toClasses, - [ - .text( - resource.type.substring(0, 1).toUpperCase() + - resource.type.substring(1), - ), - ], + Tag( + resource.type.substring(0, 1).toUpperCase() + + resource.type.substring(1), + color: switch (resource.type) { + 'codelab' || 'workshop' => TagColor.blue, + 'quickstart' || 'demo' => TagColor.purple, + _ => TagColor.teal, + }, + size: TagSize.small, ), _iconForLabel(resource.link?.label ?? ''), ]), diff --git a/sites/docs/lib/src/components/pages/platforms_grid.dart b/sites/docs/lib/src/components/pages/platforms_grid.dart index d41f065bd38..191d8f1203c 100644 --- a/sites/docs/lib/src/components/pages/platforms_grid.dart +++ b/sites/docs/lib/src/components/pages/platforms_grid.dart @@ -7,6 +7,7 @@ import 'package:jaspr/jaspr.dart'; import 'package:jaspr_content/jaspr_content.dart'; import 'package:site_shared/components/common/button.dart'; import 'package:site_shared/components/common/material_icon.dart'; +import 'package:site_shared/components/common/tags.dart'; import 'package:site_shared/markdown.dart'; class PlatformsGrid extends CustomComponentBase { @@ -89,9 +90,17 @@ class PlatformCard extends CustomComponentBase { href: deployToLink, ), ]), - div(classes: 'platform-card-tags', [ - for (final a in arch) span([.text(a)]), - ]), + Tags( + [ + for (final architecture in arch) + Tag( + architecture, + color: .subtleGrey, + size: .small, + ), + ], + classes: const ['platform-card-tags'], + ), div(classes: 'platform-card-details', [ span([ diff --git a/sites/docs/src/content/ai/agent-skills.md b/sites/docs/src/content/ai/agent-skills.md deleted file mode 100644 index dd0dc4c9ea2..00000000000 --- a/sites/docs/src/content/ai/agent-skills.md +++ /dev/null @@ -1,85 +0,0 @@ ---- -title: Agent skills for Flutter and Dart -shortTitle: Agent skills -description: >- - Learn how to give AI agents new capabilities and expertise - using agent skills. ---- - -This guide covers how to enhance your AI agents and coding assistants -with domain-specific capabilities using agent skills. - -## Overview - -AI agents can write Flutter and Dart code, but they sometimes are unaware of -tools and best practices that professional developers use. - -[Agent skills](https://agentskills.io/) help solve this problem by providing a -standardized way to give your AI agent a set of task-oriented blueprints to -follow. By giving the agent actual domain expertise and repeatable workflows, -you drastically reduce mistakes and can enforce consistent patterns. - - -Skills use what we call "progressive disclosure," which is similar to deferred -loading in Flutter. Instead of loading every single instruction into the context -window up front, the agent only reads the metadata first. It pulls in the heavy, -detailed instructions only when it actually needs them for the task at hand. - -## Official repositories - -The Dart and Flutter teams maintain official repositories packed with skills -tailored specifically for our frameworks. - -* **[dart-lang/skills](https://github.com/dart-lang/skills)**: Provides skills - for Dart development. Use these to generate unit tests, resolve package - dependencies, and fix static analysis errors. -* **[flutter/agent-plugins](https://github.com/flutter/agent-plugins)**: - Provides skills for Flutter development. - These skills help the AI build responsive layouts, - set up declarative routing, and implement JSON serialization. - -## Install agent skills - -The recommended way to install skills for your project is by following the -[Get started with AI](/ai/get-started) guide, which provides step-by-step -instructions on how to install the official Flutter and Dart agent plugins for -Claude Code, Codex, Antigravity, Cursor, and other tools. These plugins act as a -complete package, bundling agent skills with the configuration for the Dart and -Flutter MCP server. - -### Universal agent installation - -By default, compatible AI agents discover agent skills within the -`.agents/skills` directory of your project workspace. - -To download and manage skills in that folder, you can use the `skills` CLI tool. -It's distributed through npm, so you need [Node.js](https://nodejs.org/) -installed to run it with `npx`. - -To install the official Flutter skills: - -```bash -npx skills add flutter/agent-plugins --skill '*' --agent universal --yes -``` - -And to install the official Dart skills: - -```bash -npx skills add dart-lang/skills --skill '*' --agent universal --yes -``` - -Running these commands automatically creates the `.agents/skills` -directory and downloads the requested skills into your project. - -## Manage and verify agent skills - -For more details on available skills, updating, and contributing, see the -[Dart skills repository](https://github.com/dart-lang/skills) and the -[Flutter agent-plugins repository](https://github.com/flutter/agent-plugins). - -:::tip -Once you've added skills to your project, try asking your AI agent to review -your installed skills. You can ask, "Which of my installed skills -can help me with [your current task]?" or "Summarize the capabilities of the -skills I have available." -::: diff --git a/sites/docs/src/content/ai/ai-rules.md b/sites/docs/src/content/ai/ai-rules.md deleted file mode 100644 index b99bd25de1c..00000000000 --- a/sites/docs/src/content/ai/ai-rules.md +++ /dev/null @@ -1,111 +0,0 @@ ---- -title: AI rules -shortTitle: AI rules -description: >- - Learn how to add AI rules to tools that accelerate your - development workflow. ---- - -This guide covers how you can leverage AI rules to -streamline your Flutter and Dart development. - -:::note Agent skills -While rules configure the default behavior for all tasks, -you can use [Agent skills](/ai/agent-skills) to give the AI specific tools -and instructions for discrete tasks. -::: - -## Overview - -AI-powered editors use rules files to provide context and -instructions to an underlying LLM. These files help you: - -* Customize AI behavior to your team's needs. -* Enforce project best practices for code style and - design. -* Provide critical project context to the AI. - -The Flutter project provides several versions of the rules file to accommodate -different tool limits: - -* [`rules.md`](https://raw.githubusercontent.com/flutter/flutter/refs/heads/main/docs/rules/rules.md): - The comprehensive master rule set. -* [`rules_10k.md`](https://raw.githubusercontent.com/flutter/flutter/refs/heads/main/docs/rules/rules_10k.md): - A condensed version (<10k chars) for tools with stricter context limits. -* [`rules_4k.md`](https://raw.githubusercontent.com/flutter/flutter/refs/heads/main/docs/rules/rules_4k.md): - A highly concise version (<4k chars) for limited contexts. -* [`rules_1k.md`](https://raw.githubusercontent.com/flutter/flutter/refs/heads/main/docs/rules/rules_1k.md): - An ultra-compact version (<1k chars) for very strict limits. - - - - Download the Flutter and Dart rules template - - -## Device and editor specific limits - -Different AI coding assistants and tools have varying limits for their "rules" -or "custom instructions" files. *Last updated: 2026-01-05.* - -| Tool / Product | Rules file / Feature | Limit (soft / hard) | Documentation | -|:---|:---|:---|:---| -| Antigravity (Google) | `.agent/rules/.md` | 12,000 chars (Hard) | [Configure rules][antigravity] | -| Claude Code | `CLAUDE.md` | No Hard Limit | [Claude Code Docs](https://code.claude.com/docs/en/memory) | -| Cursor | `AGENTS.md` | No Hard Limit | [Cursor Docs](https://cursor.com/docs/context/rules) | -| Gemini CLI | `GEMINI.md` | 1M+ Tokens (Context) | [Gemini CLI Docs](https://cloud.google.com/vertex-ai/generative-ai/docs/long-context) | -| GitHub Copilot | `.github/copilot-instructions.md` | ~4k chars | [GitHub Copilot Docs](https://docs.github.com/en/copilot/customizing-copilot/adding-custom-instructions-for-github-copilot) | -| JetBrains AI (Junie) | `.junie/guidelines.md` | No Hard Limit | [JetBrains AI Docs](https://www.jetbrains.com/help/junie/get-started-with-junie.html) | -| VS Code | `.instructions.md` | Unknown | [Configure instructions][vs-code] | - -{:.table .table-striped} - -:::note Support is evolving -Support for rules files is still evolving. -Please check the documentation for your specific development environment for -the most up-to-date naming conventions and instructions. -::: - -[copilot]: https://code.visualstudio.com/docs/copilot/customization/custom-instructions#_use-a-githubcopilotinstructionsmd-file -[claude]: https://www.anthropic.com/engineering/claude-code-best-practices#1-customize-your-setup -[cursor]: https://cursor.com/docs/context/rules -[firebase]: https://firebase.google.com/docs/studio/set-up-gemini#custom-instructions -[gemini-cli]: https://geminicli.com/docs/cli/gemini-md -[antigravity]: https://antigravity.google/docs/rules-workflows -[junie]: https://www.jetbrains.com/help/junie/customize-guidelines.html -[vs-code]: https://code.visualstudio.com/docs/copilot/customization/custom-instructions#_use-instructionsmd-files -[windsurf]: https://docs.windsurf.com/windsurf/cascade/memories#rules - -## Create rules for your editor - -You can adapt our Flutter and Dart rules template for your -specific environment. To do so, follow these steps: - -1. Download the Flutter and Dart rules template: - rules.md - -1. In an LLM like [Gemini][], attach the - `rules.md` file that you downloaded in - the last step. - -1. Provide a prompt to reformat the file for your desired - editor. - - Example prompt: - - ```text - Convert the attached rules.md file - into a guidelines.md file for Gemini CLI. Make sure - to use the styles required for a guidelines.md file. - ``` - -1. Review the LLM's output and make any necessary - adjustments. - -1. Follow your environment's instructions to add the new - rules file. This may involve adding to an existing file - or creating a new one. - -1. Verify that your AI assistant is using the new rules to - guide its responses. - -[Gemini]: https://gemini.google.com/ diff --git a/sites/docs/src/content/ai/antigravity-cli.md b/sites/docs/src/content/ai/antigravity-cli.md deleted file mode 100644 index a4c6fa31a71..00000000000 --- a/sites/docs/src/content/ai/antigravity-cli.md +++ /dev/null @@ -1,100 +0,0 @@ ---- -title: Antigravity CLI -shortTitle: CLI -description: Learn how to use the Antigravity CLI for Dart and Flutter. ---- - -## Introduction - -[Antigravity CLI][] (using the executable command `agy`) -is a terminal-based interface (TUI) -for the **Antigravity 2.0** agentic coding assistant. -It connects directly to your workspace -and leverages the **Dart and Flutter MCP server** -to help you build, modify, test, and release Flutter applications -from the command line. - -The Antigravity CLI replaces the legacy Gemini CLI. - -[Antigravity CLI]: https://antigravity.google/docs/cli - -## Installation - -Install the Antigravity CLI on your machine by running the -appropriate command for your platform: - - - - -```bash -curl -fsSL https://antigravity.google/install.sh | bash -``` - - - - -```powershell -irm https://antigravity.google/install.ps1 | iex -``` - - - - -```cmd -winget install Google.AntigravityCLI -``` - - - - -After installation, verify that the tool is available on your path by running: - -```console -$ agy --version -agy version 2.0.0 -``` - -## Migration from Gemini CLI - -If you previously used the Gemini CLI or Gemini CLI extension for Flutter, -migrating to the Antigravity CLI is straightforward. - -### Automatic migration - -The first time you run `agy` in your terminal, -the tool checks for existing Gemini configuration files -(such as `~/.gemini/config/mcp_config.json` -or legacy environment variables). -If found, the tool asks if you would like to -automatically migrate your settings, -preferences, and API configuration. - -### Manual migration - -If you want to migrate your plugins and history manually, -or if you skipped the automated setup, -run the import plugin tool command: - -```bash -agy plugin import gemini -``` - -This command parses your local Gemini configuration -and copies its configurations over to your active Antigravity profile. - -## Workspace configuration and rules - -Like the legacy Gemini CLI tool, -Antigravity CLI respects custom development guidelines -and configurations stored in your workspace directory: - -- **Local rules**: You can place rules files in your project directory - (such as `.agents/skills/` or `AGENTS.md`) - to instruct the agent on specific coding style guidelines - or architectural patterns. - Note that Antigravity CLI also supports backward compatibility - with the legacy `GEMINI.md` file; - however, we recommend renaming it to `AGENTS.md`. -- **Global configuration**: Antigravity CLI stores global settings - and configured MCP servers in `~/.antigravity/` - (e.g., `~/.antigravity/mcp_config.json`). diff --git a/sites/docs/src/content/ai/antigravity.md b/sites/docs/src/content/ai/antigravity.md deleted file mode 100644 index cb1935a616a..00000000000 --- a/sites/docs/src/content/ai/antigravity.md +++ /dev/null @@ -1,116 +0,0 @@ ---- -title: Google Antigravity -shortTitle: Antigravity -description: Learn about Google Antigravity agentic coding tools. ---- - -## Introduction - -Google Antigravity is a suite of agentic development tools -for building apps, including Flutter apps. -You can pair with Antigravity to solve coding tasks, -create new codebases, modify existing ones, -and answer questions. - -The Antigravity suite includes: - -* **Antigravity 2.0**: The core agentic assistant experience, - driven via a Terminal User Interface (TUI) - or Command-Line Interface (CLI). -* **Antigravity IDE**: The focused editor experience - featuring an integrated agent panel. - -This page describes the Antigravity IDE. -For details on using the command-line tool, -see the [Antigravity CLI](/ai/antigravity-cli) page. - - -To learn some of what Antigravity is capable of, -watch this talk from Google I/O 2026. - - - -## Installation and setup {: #setup} - -Install the latest version of Antigravity for your platform by visiting -the [Antigravity site](https://antigravity.google/download). - - 1.

Open Antigravity

- - When opening Antigravity for the first time, - a **How do you want to use Antigravity** screen displays and - provides some radio buttons and pulldown menus - for customizing your use of the tool. - - We recommend that you select **Review-driven development**. - This means that Antigravity asks you to approve each command - that it wants to run. - - You can change this setting at any time to give Antigravity - more or less control. Even if you select **Agent driven development**, - which allows Antigravity to directly run commands without approval, - you can specify certain commands that will _always_ ask - for your approval, such as the `rm` command to remove files. - - 1.

Install the Dart and Flutter extensions

- - 1. Open **Settings** (press Cmd/Ctrl + ,). - 1. Click the **Customizations** tab. - 1. In the **Build with Google Plugins** section, click **Customize**. - 1. Click **Download** next to the **Dart and Flutter** integration. - - 1.

Set up any MCP servers that you use

- - 1. Navigate to or open the **Agent** side panel. - - If it's closed, open it by either: - - * Pressing Cmd/Ctrl + L. - * Going to **View** - > **Open View...** - > **Agent**. - - In the upper right of the **Agent** panel, - click the **Additional options** (`...`) menu button. - - 1. Select **MCP Servers**. - - 1. In the upper right of the **Agent** panel, - click **Manage MCP Servers**. - - The **MCP Store** screen appears and you can search for Dart, - which is likely already in the list. - Click **Install**. - - After installing any servers that you want, - view them by clicking the **Manage MCP Servers** button - and click **View raw config** to access your JSON manifest. - - 1.

Get started developing

- - For these tips and more that show some of Antigravity's benefits, - watch the following 10-minute [Flutter + Antigravity video][ag-video]: - - - - For a walkthrough on creating a new Flutter app in Antigravity, - visit [Create a new Flutter app][]. - To learn more about the development features enabled by - the Dart and Flutter extensions, - check out [How to develop Flutter apps in VS Code][vs-code]. - -{:.steps} - -## Agentic Hot Reload {: #agentic-hot-reload} - -If you are using Antigravity in Agent mode, -the agent can automatically hot reload your running application -when you prompt it to modify your app. -This enables a hands-free, "prompt-to-reload" workflow -that reduces context switching and development latency. - -[ag-video]: {{site.yt.watch}}?v=YY2w2JEX2xk&t=1s -[Create a new Flutter app]: /reference/create-new-app#antigravity -[vs-code]: /tools/vs-code diff --git a/sites/docs/src/content/ai/best-practices/developer-experience.md b/sites/docs/src/content/ai/best-practices/developer-experience.md deleted file mode 100644 index 7cea121968f..00000000000 --- a/sites/docs/src/content/ai/best-practices/developer-experience.md +++ /dev/null @@ -1,157 +0,0 @@ ---- -title: Developer experience -description: > - Learn how to use spec-driven development and Gemini to plan, code, and - iterate on high-quality Flutter applications. -prev: - title: Mode of interaction - path: /ai/best-practices/mode-of-interaction ---- - - -Generative AI is not just useful for implementing features in your app; it's -also useful for generating the code to implement those features. - -Unfortunately, it's just as easy as prompting an AI coding agent to "build a -Flutter app that solves crossword puzzles." I'm sure that prompt would yield -something, but I doubt very much that it would give us the powerful AI-assisted, -user-validated combination the Crossword Companion provides. - -With better prompting, however, the sample app was implemented with Gemini 2.5 -Pro for the bulk of the functionality and Gemini 3 Pro Preview to add the final -touches. The process to get the best results from both models was the same: - -- Plan -- Code -- Validate -- Iterate - -### Plan - -The goal of the planning process is to kick off the coding process with enough -detail to let the agent know what you have in mind. The Crossword Companion -planning process was started with the following prompt: - -```plaintext -I'd like to create a file called requirements.md in the plans folder at the root of the project. here's a description of the project: - -The application will be an open-source sample hosted on GitHub in the flutter/demos directory. It aims to demonstrate the use of Flutter, Firebase AI Logic, and Gemini to produce an agentic workflow that can solve a small crossword puzzle (one with a size under 10x10)....lots more description of the app along with a sample puzzle screenshot... -Ask any questions you may have before you get started. -``` - -This prompt, with a little bit of Q&A, manual edits by a human, and some updates -during the coding process, yielded [the requirements file][requirements]. - -Before jumping into architectural design, the Gemini CLI was asked to initialize -the GEMINI.md rules file and then to update it with a list of architectural -principles: - -```plaintext -DRY (Don't Repeat Yourself) – eliminate duplicated logic by extracting shared utilities and modules. - -Separation of Concerns – each module should handle one distinct responsibility. - -Single Responsibility Principle (SRP) – every class/module/function/file should have exactly one reason to change. - -Clear Abstractions & Contracts – expose intent through small, stable interfaces and hide implementation details. - -Low Coupling, High Cohesion – keep modules self-contained, minimize cross-dependencies. - -Scalability & Statelessness – design components to scale horizontally and prefer stateless services when possible. - -Observability & Testability – build in logging, metrics, tracing, and ensure components can be unit/integration tested. - -KISS (Keep It Simple, Sir) - keep solutions as simple as possible. - -YAGNI (You're Not Gonna Need It) – avoid speculative complexity or over-engineering. -``` - -The GEMINI.md file is loaded into every new prompt you create with Gemini; it -provides the set of rules you want it to remember for any activity. Gemini was -running inside of an empty Flutter app project, so the `/init` command -documented how to build, test and run it, which was useful during coding. - -If you're building something more than a sample, I also recommend adding -something for test-driven development: - -```markdown -- **TDD (Test-Driven Development)** - write the tests first; the implementation - code isn't done until the tests pass. -``` - -This helps to build guardrails to ensure the coding agent is writing solid code -over time. - -With the requirements and rules in place, prompting for the design.md file was -next: - -```plaintext -great. i'd like to work on the design with you to be created in a design.md file to be stored in the plans folder. please use the @GEMINI.md and @requirements.md files as input. ask any questions you may have before you get started. -``` - -After inspecting and editing the generated app design, Gemini was prompted to -break it down into [tasks][tasks-spec]: - -```plaintext -please read the files in the @specs folder and create a corresponding tasks.md file in the same folder that lays out a set of tasks and subtasks representing the functionality of this app. lay out the top-level tasks as minimal new functionality that the user can see in the running app, step-by-step as each top-level task is completed. each top-level task should include sub-tasks for creating and running tests and updating the @README.md with a description of the current functionality of the app. ask any questions you may have before you get started. -``` - -All of this happens before any code is written. You don't have to split things -into separate files, but by carefully considering the requirements, the design -and the task breakdown, you're helping the agent to provide results that meet -your expectations. This is called "Spec-Driven Development" and it's currently -the best way we know of to upgrade your process from "vibe coding" to -"AI-assisted software development." - -Also, the sentence that says "ask any questions you may have before you get -started" is a great way for the agent to clarify anything that it doesn't -understand instead of just making up the answers as it goes. It's also useful to -help you to decide on details you might not otherwise have considered. - -### Code - -With the requirements, rules, design and tasks in place, kicking off the coding -part is easy: - -```plaintext -Read the @tasks.md file and implement the first milestone. -``` - -You can watch the coding agent at work, jumping in to correct it as it works, or -just let it go. Either way, when it's done, it's time to check its work. - -### Validate - -At this point, you have some code and (in the world outside of samples) some -tests. To validate, ask yourself some questions: - -- Does the analyzer show it to be free of errors? Of warnings? -- Does the app run? -- Does it have the features you asked for? Do they work? -- Do the tests pass? -- Does the code pass your review? - -The answers to these questions are the input for the next phase. - -### Iterate - -Gather the issues that need to be addressed and hand the ones that need fixing -back to the coding agent, iterating between it coding and your validation until -you get to a good place from a functional point of view. - -Now take another pass through validation from an architectural principles point -of view, spinning up a new agent to check the code. By clearing out the agent's -context, you remove the biases the original agent gathered choosing what code to -write in the first place. To ground it on just the code changes the agent has -just made, use a prompt like this: - -```plaintext -Use git diff to find the new code and check it against the architectural principles listed here: @GEMINI.md. Make recommendations for important improvements. -``` - -Doing this a few times keeps the code in good shape for AI agents and humans -alike. - - -[requirements]: {{site.repo.demos}}/blob/main/crossword_companion/specs/requirements.md -[tasks-spec]: {{site.repo.demos}}/blob/main/crossword_companion/specs/tasks.md diff --git a/sites/docs/src/content/ai/coding-assistants.md b/sites/docs/src/content/ai/coding-assistants.md deleted file mode 100644 index 6456cd145d2..00000000000 --- a/sites/docs/src/content/ai/coding-assistants.md +++ /dev/null @@ -1,65 +0,0 @@ ---- -title: AI coding assistants -description: > - Learn how to use AI-powered coding assistants like Antigravity - to accelerate your Flutter development. ---- - -AI tools are not only features in your app, -but can also be powerful assistants in your development workflow. - -To set up your preferred AI coding agent with official Flutter plugins, -skills, and rules, check out the [Get started with AI](/ai/get-started) guide. - -Tools like Antigravity can help you write code faster, -understand complex concepts, and reduce boilerplate. - -## Antigravity - -[Antigravity](https://antigravity.google/) is a suite of agentic development tools that includes: - -* **Antigravity 2.0**: The core agentic assistant experience (TUI/CLI-driven). -* **Antigravity IDE**: The focused editor experience featuring an integrated agent panel. - -Some of Antigravity's capabilities include: - -* **Agentic capabilities**: Unlike chat-based assistants, Antigravity can proactively edit files and run terminal commands to complete tasks. -* **Complex reasoning**: It can plan and execute multi-step workflows which makes it suitable for larger refactors or feature implementations. -* **Verification**: It can run tests and verify its own changes to ensure correctness. - -To get started with the editor experience, see the [Antigravity IDE](/ai/antigravity) page. -To get started with the command-line tool, see the [Antigravity CLI](/ai/antigravity-cli) page. - -## Gemini Code Assist - -[Gemini Code Assist](https://codeassist.google/) is an AI-powered collaborator -available for IDEs like Visual Studio Code, JetBrains IDEs, and Android Studio. -It has a deep understanding of your project's codebase and can help you with: - -* **Code completion and generation**: It suggests and generates entire blocks of - code based on the context of what you're writing. -* **In-editor chat**: You can ask questions about your code, Flutter concepts, - or best practices directly within your IDE. -* **Debugging and explanation**: If you encounter an error, you can ask Gemini - Code Assist to explain it and suggest a fix. - -## Gemini CLI (Legacy) - -The [Gemini CLI](https://geminicli.com/) is a command-line AI workflow tool. -For individual developers, it is superseded by the new [Antigravity CLI](/ai/antigravity-cli). -It continues to be supported for Gemini Enterprise users. - -For more details about Gemini CLI, visit the [Gemini CLI](https://geminicli.com/) website. - -## Claude Code - -[Claude Code](https://code.claude.com/) is an agentic coding assistant from -Anthropic that runs in your terminal. - -You can equip Claude Code with domain expertise and tools for Flutter and Dart -by installing the official Flutter plugin, which bundles official -[agent skills](/ai/agent-skills) and the -[Dart and Flutter MCP server](/ai/mcp-server). - -To install the plugin, follow the Claude Code instructions in -[Install agent skills](/ai/agent-skills#install-agent-skills). diff --git a/sites/docs/src/content/ai/create-with-ai.md b/sites/docs/src/content/ai/create-with-ai.md deleted file mode 100644 index 90eef4f9f3a..00000000000 --- a/sites/docs/src/content/ai/create-with-ai.md +++ /dev/null @@ -1,62 +0,0 @@ ---- -title: Create with AI -description: > - Learn how to use AI to build Flutter apps, and how to build AI experiences - into your apps. ---- - -Use artificial intelligence to accelerate your Flutter development workflow and build intelligent, AI-powered features into your applications. - -## Develop with AI - -Configure your development environment with official Flutter and Dart plugins, rules, and tools. - -
- - Set up your editor with official plugins, rules, and MCP configuration. - - - Learn about supported AI editors, IDEs, and CLI tools. - - - Enhance your AI assistant with domain-specific task blueprints. - - - Connect your AI to Dart and Flutter developer tools. - - - Configure project-wide guidelines and best practices for models. - -
- -## Build AI-powered apps - -Integrate generative AI features directly into your Flutter applications. - -
- - Use pre-built chat widgets and feature integrations. - - - Orchestrate interactive conversational UIs in your app. - - - Use Firebase to integrate Gemini and Vertex AI features. - - - Build and debug AI flows with an open-source framework. - - - Learn about prompting, tool calls, and interaction modes. - -
- -## AI evaluations - -Measure the reliability and performance of your AI integrations. - -
- - Learn how we use benchmarks and datasets to test and improve AI tooling. - -
diff --git a/sites/docs/src/content/ai/evals.md b/sites/docs/src/content/ai/evals.md deleted file mode 100644 index b1d6b8b2d17..00000000000 --- a/sites/docs/src/content/ai/evals.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -title: AI evaluations -description: > - Learn about Dart and Flutter's evaluation frameworks for - measuring AI tooling reliability. ---- - -:::experimental -Evaluation tooling and benchmarks are experimental and likely to change. -::: - -To explore the evaluation strategy, -view the open-source dataset and scoring rubrics, -or get involved with community benchmark datasets, -visit the [Flutter Evals repository](https://github.com/flutter/evals). - -Evaluating the capabilities and reliability of AI agents requires testing -approaches that model actual developer tasks. -Because LLMs are non-deterministic, -standard unit testing is insufficient for verifying agentic behaviors like -codebase navigation, plan execution, and code synthesis. - -To build developer confidence in AI tooling, -Dart and Flutter use an evaluation system ("evals") -to test critical user journeys (CUJs). -Evals measure both deterministic code correctness -(compilation, lints, automated tests) and qualitative performance -(reasoning, safety, and conciseness) using automated model judges -and expert human grading. diff --git a/sites/docs/src/content/ai/get-started.md b/sites/docs/src/content/ai/get-started.md index fc4a6d8bf97..c618114f1f2 100644 --- a/sites/docs/src/content/ai/get-started.md +++ b/sites/docs/src/content/ai/get-started.md @@ -2,40 +2,113 @@ title: Get started developing with AI shortTitle: Get started description: >- - Learn how to set up and use AI agent plugins for Flutter and Dart using - your preferred coding assistant. + Learn how to set up and use AI agent plugins for Flutter and Dart + using your preferred coding assistant. --- AI coding assistants can accelerate your Flutter development workflow by writing code, fixing errors, and building complete features. -To get the best experience with AI coding assistants, install the official -Flutter agent plugins, which bundle [agent skills](/ai/agent-skills) and -configuration for the [Dart and Flutter MCP server](/ai/mcp-server). +By default, general-purpose AI assistants might not have the latest context on +Flutter patterns, project diagnostics, or third-party packages. Installing the +official Flutter agent plugin equips your assistant with dedicated tools and +recipes tailored for Flutter and Dart development. + +## How Flutter AI plugins work + +An **agent plugin** bundles the tools and knowledge that an AI assistant needs +to understand and modify your Flutter codebase. + +When you install an official Flutter agent plugin, it connects your assistant +to two core capabilities: + +* **[Agent skills](/ai/tools#agent-skills)**: On-demand procedural guides from + the official Flutter and Dart repositories that teach the assistant how to + perform specific tasks, such as creating responsive layouts, managing state, + or writing widget tests. +* **[Dart and Flutter MCP server](/ai/tools#dart-and-flutter-mcp-server)**: + A Model Context Protocol (MCP) server that connects the assistant to the + Dart SDK, giving it real-time access to analyzer diagnostics, symbol + resolution, test runners, and runtime inspection. + +These tools work together automatically: the assistant uses MCP tools to query +live project state and static analysis, while using agent skills to guide its +coding strategies and best practices. + +In addition to core Flutter plugins, you can also equip your assistant with +skills that are shipped directly by third-party `pub.dev` +packages using the [skills](/ai/package-skills) package. + +To learn more about the underlying architecture and capabilities of each tool, +check out [How Flutter AI tools work](/ai/tools). ## Choose your AI coding agent -Select your agent below for instructions on how to install official plugins and -set up rules for Flutter development. +Select your agent below for instructions on how to install official plugins +and configure tools for Flutter development. -[Antigravity](https://antigravity.google/) is a suite of agentic development -tools built by Google that includes the Antigravity IDE and Antigravity CLI. +[Antigravity]( +https://antigravity.google/docs/build-with-google/#dart-and-flutter) +is an agentic development platform by Google that includes the Antigravity +IDE, IDE extensions (for VS Code and other editors), and the Antigravity CLI. -**Install the official plugin** +**Antigravity IDE and extensions** -Equip Antigravity with official Dart and Flutter tools -by installing the plugin from settings: +Equip the Antigravity IDE or Antigravity extension (such as in VS Code) +with official Dart and Flutter tools: -1. Open **Settings** in Antigravity by clicking the gear icon - or pressing Cmd/Ctrl + ,. +1. Open **Settings** in Antigravity by clicking the gear icon or pressing + Cmd/Ctrl + ,. 1. Click the **Customizations** tab. 1. In the **Build with Google Plugins** section, click **Customize**. 1. Click **Download** next to the **Dart and Flutter** integration. +To verify the installation, open the **Agent** panel +(Cmd/Ctrl + L) and ask the agent: + +```text +Summarize the Dart and Flutter tools and skills available in this project. +``` + +**Antigravity CLI** + +The Antigravity CLI runs as the `agy` command in your terminal. + +1. Configure the Dart and Flutter MCP server in your project's + `.agents/mcp_config.json` file (or globally in + `~/.gemini/config/mcp_config.json`): + + ```json + { + "mcpServers": { + "dart": { + "command": "dart", + "args": ["mcp-server"] + } + } + } + ``` + +1. Install official Flutter and Dart skills into your workspace: + + ```bash + # Install Flutter skills + npx skills add flutter/agent-plugins --skill '*' --agent universal --yes + + # Install Dart skills + npx skills add dart-lang/skills --skill '*' --agent universal --yes + ``` + +1. Start an interactive session in your project root: + + ```bash + agy + ``` + @@ -45,39 +118,62 @@ Anthropic that runs in your terminal. **Install the official plugin** -Equip Claude Code with domain expertise and tools for Flutter and Dart by -installing the official plugin from -[flutter/agent-plugins](https://github.com/flutter/agent-plugins): +Equip Claude Code with official Flutter and Dart skills and MCP configuration: 1. Add the marketplace for Claude Code plugins: - ```console - $ claude plugin marketplace add flutter/agent-plugins + ```bash + claude plugin marketplace add flutter/agent-plugins ``` 1. Install the Flutter and Dart plugin: - ```console - $ claude plugin install dart-flutter@dart-flutter + ```bash + claude plugin install dart-flutter@dart-flutter ``` -1. Verify the installation: +**Verify installation** - ```console - $ claude plugin marketplace list +Verify that the plugin is active in Claude Code: + +```bash +claude plugin marketplace list +``` + + + + + +[Cursor](https://cursor.com/) is an AI-powered code editor built on top of +VS Code. + +**Install the local plugin** + +You can install the official plugin bundle locally by copying it to your Cursor +plugins directory: + +1. Clone the repository: + + ```bash + git clone https://github.com/flutter/agent-plugins.git ``` -**Rules setup** +1. Copy the repository to your local Cursor plugins folder: -Claude Code plugins currently cannot bundle rules files automatically. -You can configure rules for your project by following [Rules for Flutter and Dart](/ai/ai-rules). + ```bash + mkdir -p ~/.cursor/plugins/local + cp -r agent-plugins ~/.cursor/plugins/local/dart-flutter + ``` + +1. Restart Cursor. The editor automatically discovers the bundled skills and + configures the Dart and Flutter MCP server. -[Codex](https://chatgpt.com/codex) is an agentic coding assistant -designed for terminal and IDE workflows. +[Codex](https://chatgpt.com/codex) is an agentic coding assistant designed for +terminal and IDE workflows. **Install the official plugin** @@ -85,79 +181,113 @@ Equip Codex with official Flutter and Dart skills and MCP configuration: 1. Add the Dart and Flutter marketplace for Codex plugins: - ```console - $ codex plugin marketplace add flutter/agent-plugins + ```bash + codex plugin marketplace add flutter/agent-plugins ``` 1. Install the Dart and Flutter plugin: - ```console - $ codex plugin add dart-flutter@dart-flutter + ```bash + codex plugin add dart-flutter@dart-flutter ``` -**Rules setup** +**Verify installation** -Codex plugins currently cannot bundle rules files automatically. -You can configure rules for your project by following [Rules for Flutter and Dart](/ai/ai-rules). +Verify that the plugin is active in Codex: - +```bash +codex plugin list +``` - + -[Cursor](https://cursor.com/) is an AI-first code editor built on VS Code. + -**Install the local plugin** +[GitHub Copilot](https://github.com/features/copilot) in VS Code supports +agentic coding, tool calling via MCP, and custom workspace instructions. -You can install the plugin locally by copying it to your Cursor plugins directory: +**Configure the MCP server** +1. Ensure the **Model Context Protocol (MCP)** extension is enabled in VS Code. +1. Create or open `.vscode/mcp.json` in your workspace and add the Dart MCP + server: -1. Clone the repository: - ```bash - git clone https://github.com/flutter/agent-plugins.git + ```json + { + "servers": { + "dart": { + "command": "dart", + "args": ["mcp-server"] + } + } + } ``` -1. Copy the repository directory to your local Cursor plugins folder: - ```bash - mkdir -p ~/.cursor/plugins/local - cp -r agent-plugins ~/.cursor/plugins/local/dart-flutter - ``` +**Install agent skills** + +In your Flutter project root, install official skills using the universal flag: -1. Restart Cursor. The editor automatically discovers and loads the skills - under `skills/` and configures the MCP server defined in `.mcp.json`. +```bash +# Install Flutter skills +npx skills add flutter/agent-plugins --skill '*' --agent universal --yes + +# Install Dart skills +npx skills add dart-lang/skills --skill '*' --agent universal --yes +``` - + -If you're using another compatible agentic assistant: +If you're using another coding assistant (such as Windsurf, Zed, or Cline) +that supports the Model Context Protocol (MCP) or Agent Skills specification, +configure the tools manually. -* **MCP Server**: To connect your agent to Dart and Flutter developer tools, - see the [Dart and Flutter MCP server](/ai/mcp-server) setup guide. -* **Agent Skills**: To manually install agent skills into your project's - `.agents/skills` directory using the `skills` CLI tool, see - [Install agent skills](/ai/agent-skills#install-agent-skills). -* **Rules**: To configure rules and project best practices for your assistant, - see [Rules for Flutter and Dart](/ai/ai-rules). +**Configure the MCP server** - +Most MCP-compatible clients use a standard JSON configuration format. Add the +following entry to your client's MCP configuration file: - +```json +{ + "mcpServers": { + "dart": { + "command": "dart", + "args": ["mcp-server"] + } + } +} +``` + +For client-specific setup guides, command-line arguments, and troubleshooting, +check out the [Dart and Flutter MCP server repository]( +https://github.com/dart-lang/ai/tree/main/pkgs/dart_mcp_server). -## The AI tooling stack +**Install agent skills** -To get the most out of AI, it helps to understand how the different pieces work together: +Use the `skills` CLI to download skills into your workspace's standard +`.agents/skills` directory: -| Component | What it is | How it helps | -| :--- | :--- | :--- | -| **Plugins** | Packages MCP and Skills together for your editor. | **Start here.** Recommended for quick setup. | -| **MCP Server** | Connects the AI to Flutter developer tools (hot reload, widget tree). | Provides the raw machinery for advanced tools. | -| **Agent Skills** | Step-by-step blueprints for specific tasks. | Provides the professional know-how to operate tools. | -| **AI Rules** | General guidelines and best practices for your project. | Enforces consistent coding standards. | +```bash +# Install Flutter skills +npx skills add flutter/agent-plugins --skill '*' --agent universal --yes -{:.table .table-striped} +# Install Dart skills +npx skills add dart-lang/skills --skill '*' --agent universal --yes +``` + + + + ## Next steps -* Learn more about [Agent skills](/ai/agent-skills) and how agents use them. -* Explore the [Dart and Flutter MCP server](/ai/mcp-server) integration. -* Check out [Rules for Flutter and Dart](/ai/ai-rules) to customize model behavior. +* To learn more about how skills and tools interact, check out + [How Flutter AI tools work](/ai/tools). +* To publish or consume skills from dependencies, refer to + [Package skills](/ai/package-skills). +* To give your assistant search access to official Flutter and Dart + documentation, connect to the [Developer Knowledge MCP server]( + https://developers.google.com/knowledge/mcp). +* To explore the official plugins, check out the + [flutter/agent-plugins](https://github.com/flutter/agent-plugins) repository. diff --git a/sites/docs/src/content/ai/mcp-server.md b/sites/docs/src/content/ai/mcp-server.md deleted file mode 100644 index d0982613e5f..00000000000 --- a/sites/docs/src/content/ai/mcp-server.md +++ /dev/null @@ -1,461 +0,0 @@ ---- -title: Dart and Flutter MCP server -shortTitle: MCP server -description: > - Learn about the Dart and Flutter MCP server tool that - exposes Dart and Flutter tools to compatible - AI-assistant clients and agents. ---- - -This guide discusses the Dart and Flutter MCP server. - -:::experimental -The Dart and Flutter MCP server is experimental and likely to evolve quickly. -The following instructions require Dart 3.9 or later. -::: - -## Overview - -The [Dart and Flutter MCP server][] -exposes Dart and Flutter development tool actions to -compatible AI-assistant clients. MCP (model context protocol) -is a protocol that enables communication between development tools -and AI assistants, allowing the assistants to understand the -context of the code and perform actions on behalf of the developer. - -The Dart and Flutter MCP server can work with any MCP client that -supports standard I/O (stdio) as the transport medium. -To access all the features of the Dart and Flutter MCP server, -an MCP client must support [Tools][] and [Resources][]. -For the best development experience with the Dart and Flutter MCP server, -an MCP client should also support [Roots][]. - -If you are using a client that claims it -supports roots but doesn't actually set them, -pass `--force-roots-fallback` flag to enable tools for managing the roots. - -The Dart and Flutter MCP server provides a growing list of tools that -grant AI assistants deep insights into your project. -Here is an overview of a few things it can do: - -* Analyze and fix errors in your project's code. -* Resolve symbols to elements to ensure their existence and - fetch documentation and signature information for them. -* Introspect and interact with your running application. -* Search the [pub.dev site]({{site.pub}}) for the best package for a use case. -* Manage package dependencies in your `pubspec.yaml` file. -* Run tests and analyze the results. -* Format code with the same formatter and config as - [`dart format`][] and the Dart analysis server. - -[Tools]: https://modelcontextprotocol.io/docs/concepts/tools -[Resources]: https://modelcontextprotocol.io/docs/concepts/resources -[Roots]: https://modelcontextprotocol.io/docs/concepts/roots -[Dart and Flutter MCP server]: https://github.com/dart-lang/ai/tree/main/pkgs/dart_mcp_server -[`dart format`]: {{site.dart-site}}/tools/dart-format - -## Set up your MCP client - -Run the server with the `dart mcp-server` command, -which must be configured in your preferred client. - -This section provides instructions for setting up the -Dart and Flutter MCP server with popular tools such as -Antigravity, Gemini CLI, Cursor, and GitHub Copilot. - -### Antigravity - -To configure Google [Antigravity][] to use the Dart and Flutter MCP server, -you can either install it from the list of available servers or -[connect it as a custom MCP server][antigravity-mcp]. - -1. Navigate to or open the **Agent** side panel. - - If it's closed, open it by either: - - - Pressing Cmd/Ctrl + L. - - Going to **View** - > **Open View...** - > **Agent**. - -1. In the upper right of the **Agent** panel, - click the **Additional options** (`...`) menu button. -1. Select **MCP Servers**. -1. In the upper right of the **Agent** panel, - click **Manage MCP Servers**. - -From here, you can choose to install the MCP server from -[the built-in MCP store](#antigravity-mcp-store-install) or by -[configuring it manually](#antigravity-mcp-manual-install). - -[Antigravity]: https://antigravity.google/ -[antigravity-mcp]: https://antigravity.google/docs/mcp#connecting-custom-mcp-servers - -#### Install from the MCP store {: #antigravity-mcp-store-install} - -1. In the list of available MCP servers, - find or search for **Dart** and click **Install**. - -#### Connect manually {: #antigravity-mcp-manual-install} - -1. In the upper right of the **Manage MCPs** editor view, - click **View raw config**. -1. Add the following `dart-mcp-server` entry to the `mcpServers` map: - - ```json title="mcp_config.json" highlightLines=3-10 - { - "mcpServers": { - "dart-mcp-server": { - "command": "dart", - "args": [ - "mcp-server" - ], - "env": {} - } - } - } - ``` - -#### Install extensions - -It is also recommended to install the Dart and Flutter extensions: - -1. Open the **Extensions** view by either: - - - Pressing Shift + - Cmd/Ctrl + - P. - - Going to **View** - > **Extensions**. - -1. In the **Search Extensions** input box, enter **Flutter**. -1. From the list of extensions, select **Flutter**. -1. In the **Extension: Flutter** view that opens, - click the **Install** button. - - This installs both the Dart and Flutter extensions. - -To learn more about the Dart and Flutter extensions, -check out [Develop Flutter apps in VS Code][]. - -[Develop Flutter apps in VS Code]: /tools/vs-code - -### Gemini CLI - -To configure the [Gemini CLI][] to use the Dart and Flutter MCP server, -add a Dart entry to the `mcpServers` section of the Gemini config. - -- To enable the server for all projects on your device, - edit the `~/.gemini/settings.json` file in your home directory. -- To enable the server for a specific project, - edit the `.gemini/settings.json` file in the project's root directory. - -```json title=".gemini/settings.json" -{ - "mcpServers": { - "dart": { - "command": "dart", - "args": [ - "mcp-server" - ] - } - } -} -``` - -For more information, check out the official Gemini CLI -documentation for [setting up MCP servers][]. - -[Gemini CLI]: https://geminicli.com/ -[setting up MCP servers]: https://geminicli.com/docs/tools/mcp-server/#how-to-set-up-your-mcp-server - -### Gemini Code Assist in VS Code - -[Gemini Code Assist][]'s [Agent mode][] integrates the -Gemini CLI to provide a powerful AI agent directly in your IDE. -If you haven't set up Gemini Code Assist or its agent mode yet, -follow its [Before you begin instructions][gca-setup] to get started. - -To configure Gemini Code Assist to use the Dart and Flutter MCP server, -follow the instructions to [configure the Gemini CLI][]. - -You can verify the MCP server has been configured -properly by typing `/mcp` in the chat window in Agent mode. - -For more information see the official Gemini Code Assist -documentation for [using agent mode][]. - -[gca-setup]: https://developers.google.com/gemini-code-assist/docs/use-agentic-chat-pair-programmer#before-you-begin -[Gemini Code Assist]: https://codeassist.google/ -[Agent mode]: https://developers.google.com/gemini-code-assist/docs/use-agentic-chat-pair-programmer -[configure the Gemini CLI]: #gemini-cli -[using agent mode]: https://developers.google.com/gemini-code-assist/docs/use-agentic-chat-pair-programmer#before-you-begin - -### GitHub Copilot in VS Code - -:::note -Support for the Dart and Flutter MCP server in VS Code requires -v3.116 or later of the [Dart Code extension][]. -::: - -By default, the Dart extension uses the -[VS Code MCP API][] to register the Dart and Flutter MCP server, as well -as a tool to provide the URI for the active Dart Tooling Daemon. - -Explicitly enable or disable the Dart and Flutter MCP server by -configuring the `dart.mcpServer` setting in your VS Code settings. - -To change this globally, update your user settings: - -1. In VS Code, click **View > Command Palette** and then - search for **Preferences: Open User Settings (JSON)**. - -1. Add the following setting: - - ```json - "dart.mcpServer": true - ``` - -If you'd like this setting to apply only to a specific workspace, -add the entry to your workspace settings: - -1. In VS Code, click **View > Command Palette** and then - search for **Preferences: Open Workspace Settings (JSON)**. - -1. Add the following setting: - - ```json - "dart.mcpServer": true - ``` - -For more information, see the official VS Code -documentation for [enabling MCP support][]. - -[Dart Code extension]: https://marketplace.visualstudio.com/items?itemName=Dart-Code.dart-code -[VS Code MCP API]: https://code.visualstudio.com/api/extension-guides/mcp -[enabling MCP support]: https://code.visualstudio.com/docs/copilot/chat/mcp-servers#_enable-mcp-support-in-vs-code - -### Cursor - -The easiest way to configure the Dart and Flutter MCP server with -Cursor is by clicking the **Add to Cursor** button: - -[![Add to Cursor](https://cursor.com/deeplink/mcp-install-dark.svg)](cursor://anysphere.cursor-deeplink/mcp/install?name=dart&config=eyJjb21tYW5kIjoiZGFydCBtY3Atc2VydmVyIn0%3D){:.light-mode-visible} -[![Add to Cursor](https://cursor.com/deeplink/mcp-install-light.svg)](cursor://anysphere.cursor-deeplink/mcp/install?name=dart&config=eyJjb21tYW5kIjoiZGFydCBtY3Atc2VydmVyIn0%3D){:.dark-mode-visible} - -Alternatively, you can configure the server manually: - -1. Go to **Cursor > Settings > Cursor Settings > Tools & Integrations**. -1. Click **Add Custom MCP** or **New MCP Server** - depending on whether you already have other MCP servers configured. -1. Edit the `.cursor/mcp.json` file in your local project - (configuration will only apply to this project) or - edit the global `~/.cursor/mcp.json` file in your home directory - (configuration will apply for all projects) to - configure the Dart and Flutter MCP server: - - ```json title=".cursor/mcp.json" - { - "mcpServers": { - "dart": { - "command": "dart", - "args": [ - "mcp-server" - ] - } - } - } - ``` - -For more information, see the official Cursor -documentation for [installing MCP servers][]. - -[installing MCP servers]: https://docs.cursor.com/context/model-context-protocol#installing-mcp-servers - -### OpenCode - -To configure [OpenCode][] to use the Dart and Flutter MCP server -for the current project, use the `opencode mcp add` CLI command: - -```console -$ opencode mcp add dart-mcp-server -- dart mcp-server -``` - -[OpenCode]: https://opencode.ai/ - -### Claude Code - -You can configure Claude Code to use the Dart and Flutter MCP server either -by installing the official plugin or by configuring the server manually. - -#### Install via plugin (recommended) - -The easiest way to set up the Dart and Flutter MCP server in Claude Code is to -install the official Flutter plugin, which bundles both the MCP server and -official [agent skills](/ai/agent-skills). - -To install the plugin, follow the Claude Code instructions in -[Install agent skills](/ai/agent-skills#install-agent-skills). - -#### Configure manually - -Alternatively, to configure Claude Code to use only the Dart and Flutter MCP -server for the current project, use the `claude mcp add` CLI command: - -```console -$ claude mcp add --transport stdio dart -- dart mcp-server -``` - -To learn more about configuring MCP servers in Claude Code, -check out their documentation on [Installing MCP servers][claude-install]. - -[claude-install]: https://code.claude.com/docs/en/mcp#installing-mcp-servers - -### Codex CLI - -To configure the Codex CLI to use the Dart and Flutter MCP server -for the current project, use the `codex mcp add` CLI command: - -```console -$ codex mcp add dart -- dart mcp-server --force-roots-fallback -``` - -To learn more about configuring MCP servers in the Codex CLI, -check out their documentation on [Connecting to MCP servers][codex-connect]. - -[codex-connect]: https://developers.openai.com/codex/mcp - -## Use your MCP client - -Once you've set up the Dart and Flutter MCP server with a client, -the Dart and Flutter MCP server enables the client to not only reason -about your project's context but also to take action with tools. - -The [Large Language Model (LLM)][LLM] decides which tools to use and when, -so you can focus on describing your goal in natural language. -Let's see this in action with a couple of examples using -GitHub Copilot's Agent mode in VS Code. - -[LLM]: https://developers.google.com/machine-learning/resources/intro-llms - -### Fix a runtime layout error in a Flutter app - -We've all been there: you build a beautiful UI, run the app, -and are greeted by the infamous yellow-and-black stripes of -a RenderFlex overflow error. -Instead of manually debugging the widget tree, you can now -ask your AI assistant for help with a prompt similar to the following: - -> Check for and fix static and runtime analysis issues. -> Check for and fix any layout issues. - -Behind the scenes, the AI agent uses the Dart and Flutter MCP server's tools to: - -* See the error: It uses a tool to get the current runtime errors - from the running application. -* Inspect the UI: It accesses the Flutter widget tree to understand - the layout that is causing the overflow. -* Apply a fix: Armed with this context, it applies a fix and - checks once more for any remaining errors. - -You can then keep or undo the code changes. - -### Add new functionality with package search - -Imagine you need to add a chart to your app. -Which package should you use? How do you add it and write the boilerplate? -The Dart and Flutter MCP server can streamline this entire process with -a prompt similar to the following: - -> Find a suitable package to add a line chart that -> maps the number of button presses over time. - -The AI agent now acts as a true assistant: - -* Find the right tool: It uses the `pub_dev_search` tool to - find popular and highly-rated charting libraries. -* Manage dependencies: After you confirm its choice, - such as [`package:fl_chart`][], - it uses a tool to add the package as a dependency. -* Generate the code: It generates the new widget code, - complete with boilerplate for a line chart that it places in the UI. - It even self-corrects syntax errors introduced during the process. - You can customize further from there. - -What used to be a multi-step process of research, -reading documentation, editing `pubspec.yaml`, and -writing the appropriate code in your app, -is now a single request. - -[`package:fl_chart`]: {{site.pub-pkg}}/fl_chart - -### Interact with a running app - -You can use the Dart and Flutter MCP server to drive a running -Flutter app from your AI assistant—take screenshots, tap buttons, -enter text, scroll, and hot reload. - -First, add the `flutter_driver` package: - -```console -$ flutter pub add "flutter_driver:{sdk: flutter}" -``` - -On mobile and desktop, gate `enableFlutterDriverExtension()` behind a -`--dart-define` flag in your app's `main()` so it stays out of -production builds: - -```dart -import 'package:flutter_driver/driver_extension.dart'; - -void main() { - if (const bool.fromEnvironment('ENABLE_FLUTTER_DRIVER')) { - enableFlutterDriverExtension(); - } - runApp(const MyApp()); -} -``` - -Launch your app with the flag turned on: - -```console -$ flutter run -d --dart-define=ENABLE_FLUTTER_DRIVER=true -``` - -:::warning -Enabling the Flutter Driver extension disables real keyboard input—typing -is dropped and the on-screen keyboard might not appear. To type manually, -use `enableFlutterDriverExtension(enableTextEntryEmulation: false)`, but -then the agent's `enterText` command stops working. -::: - -Then ask your assistant to connect: - -> Connect to my running Flutter app, take a screenshot, then tap "Sign In". - -The agent uses the `dtd` tool to discover the app and -`flutter_driver_command` to drive its UI. - -:::note -**Web**: the `flutter_driver` extension isn't supported on web builds, so -finder-based commands like screenshots and taps aren't available there. -Pair the Dart MCP server with a browser-driving MCP for those. Everything -that flows through DTD—widget tree, runtime errors, and hot reload—still -works in a normal `flutter run` web debug session. Prefer -`flutter run -d web-server` so the browser the agent drives is the one DTD -is connected to—with `-d chrome`, only the window Flutter spawned receives -hot reload patches. - -See the [setup guide][flutter-driver-guide] for the web-safe conditional -import, the `-d web-server` versus `-d chrome` modes, and common pitfalls. -::: - -[flutter-driver-guide]: https://github.com/dart-lang/ai/blob/main/pkgs/dart_mcp_server/README.md#connect-to-a-running-flutter-app - -## Provide feedback - -If you encounter any issues or have feedback about the -Dart and Flutter MCP server, file an issue on the -[`dart-lang/ai` issue tracker][ai-issues]. - -[ai-issues]: https://github.com/dart-lang/ai/issues diff --git a/sites/docs/src/content/ai/package-skills.md b/sites/docs/src/content/ai/package-skills.md new file mode 100644 index 00000000000..f0d0e9aa83f --- /dev/null +++ b/sites/docs/src/content/ai/package-skills.md @@ -0,0 +1,94 @@ +--- +title: Package skills +description: >- + Learn how to bundle and consume agent skills in Dart and Flutter packages. +--- + +Package skills allow library authors to distribute AI instructions alongside +their code on [pub.dev](https://pub.dev). When developers add your package to +their project, their AI coding assistant can discover and use your package's +official skills. + +## For app developers: Consuming package skills + +When you add a package that includes skills, your coding assistant can access +expert guidance tailored specifically to that library's APIs, patterns, and +code generation workflows. + +### Discover and install skills from dependencies + +To scan your project dependencies and install available package skills, run: + +```bash +dart run skills@ get +``` + +The `skills` CLI scans your dependencies for bundled `skills/` directories, +presents the skills discovered, and lets you choose which skills to install. +Subsequent runs are incremental—displaying only new, updated, or removed +skills. + +To automatically install all discovered skills without interactive prompts, use +the `--all` flag: + +```bash +dart run skills@ get --all +``` + +### Install external skills + +You can also install standalone skills from external Git repositories using the +`add` command: + +```bash +dart run skills@ add https://github.com/my-org/custom-ai-skills.git +``` + +--- + +## For package authors: Publishing skills + +Package authors can publish skills directly in their `pub.dev` package +releases to teach AI assistants how to use their libraries correctly. + +### Add skills to your package repository + +1. Create a `skills/` directory in the root of your package repository. +2. Inside `skills/`, create a subdirectory prefixed with your package name (for + example, `skills/my-package-network/`). +3. Add a `SKILL.md` file containing YAML frontmatter (`name`, `description`) + and step-by-step markdown instructions. +4. Verify that the `skills/` directory is not excluded in your `.pubignore` + file. +5. Publish your package to `pub.dev`. + +### Example `SKILL.md` structure + +```markdown +--- +name: my_package-error-handling +description: >- + Use when making network requests with my_package to ensure safe patterns. +--- + +# Error handling guidelines + +## Best practices +* Always wrap network operations in a try/catch block. +* Catch `NetworkException` specifically to handle retries and diagnostics. +* Provide fallback UI state in the event of a timeout. + +## Example +1. Call `MyClient.fetchData()`. +2. Catch `NetworkException` and log `e.context`. +3. Return a fallback UI model. +``` + +--- + +## Next steps + +* To explore the underlying specification, check out + [agentskills.io](https://agentskills.io/). +* To file issues or give feedback on Dart skills tooling, visit the + [dart-lang/ai repository](https://github.com/dart-lang/ai). diff --git a/sites/docs/src/content/ai/tools.md b/sites/docs/src/content/ai/tools.md new file mode 100644 index 00000000000..333a9b4d5d7 --- /dev/null +++ b/sites/docs/src/content/ai/tools.md @@ -0,0 +1,129 @@ +--- +title: How Flutter AI tools work +shortTitle: How AI tools work +description: >- + Learn how agent skills, the Dart and Flutter MCP server, and package skills + interact to assist your development workflow. +--- + +Flutter and Dart provide an integrated suite of tools designed to help AI +coding assistants understand your codebase, follow best practices, and execute +development tasks accurately. + +## Tooling ecosystem overview + +The Flutter AI tooling ecosystem consists of four complementary components: + +1. **Agent skills**: Task-oriented blueprints that teach assistants how to + perform specific workflows (such as creating responsive layouts or writing + tests). +2. **Dart and Flutter MCP server**: A Model Context Protocol (MCP) server + exposing real-time SDK diagnostics, symbol resolution, and runtime + introspection to assistants. +3. **Developer Knowledge MCP server**: A cloud-hosted documentation search + server giving assistants direct access to official Flutter and Dart docs. +4. **Package skills**: Skills published directly inside third-party `pub.dev` + packages, giving assistants instant expertise on specific libraries. + +--- + +## Agent skills + +[Agent skills](https://agentskills.io/) provide a standardized way to equip AI +assistants with procedural domain expertise. + +### Progressive disclosure + +Skills use **progressive disclosure** to conserve model context: + +* **Discovery phase**: The assistant initially reads only skill metadata (name + and short description) from the `.agents/skills` directory. +* **Execution phase**: When the assistant determines a skill is relevant to + your prompt, it loads the complete instructions (`SKILL.md`) and supporting + reference scripts. + +### Official skills repositories + +* [**flutter/agent-plugins**](https://github.com/flutter/agent-plugins): + Provides skills for Flutter development, including widget construction, + declarative navigation, responsive design, and state management. +* [**dart-lang/skills**](https://github.com/dart-lang/skills): Provides skills + for Dart development, including unit test authoring, dependency resolution, + and static analysis remediation. + +--- + +## Dart and Flutter MCP server + +The [Dart and Flutter MCP server]( +https://github.com/dart-lang/ai/tree/main/pkgs/dart_mcp_server) +implements the open Model Context Protocol (MCP), connecting AI assistants to +live Dart SDK and Flutter tooling over standard I/O (stdio). + +Key capabilities provided by the MCP server include: + +* **Static analysis & diagnostics**: Inspect and fix analyzer errors and + warnings across your project. +* **Symbol resolution**: Resolve symbols to their definitions, signatures, and + documentation. +* **Runtime introspection**: Query the state of a running Flutter application + during active debugging sessions. +* **Package management**: Search [pub.dev](https://pub.dev) and manage + dependencies in `pubspec.yaml`. +* **Testing & formatting**: Execute unit and widget tests and format code using + `dart format` rules. + +--- + +## Developer Knowledge MCP server + +While the local Dart and Flutter MCP server focuses on local codebase analysis +and runtime debugging, you can also equip your assistant with search access to +official online documentation. + +The [Developer Knowledge MCP server]( +https://developers.google.com/knowledge/mcp) +connects AI assistants to Google's developer documentation corpus, including +[docs.flutter.dev](https://docs.flutter.dev), +[dart.dev](https://dart.dev), and official API references. This allows +assistants to retrieve up-to-date guides, migration notes, and API references +directly during conversation. + +--- + +## Package skills + +In addition to core Flutter skills, library authors can bundle official skills +directly inside their packages on [pub.dev](https://pub.dev). + +When you add a dependency to your project, you can discover and install its +skills using the `skills` CLI: + +```bash +dart run skills@ get +``` + +To learn how to install or publish package skills, check out +[Package skills](/ai/package-skills). + +--- + +## When to use what + +Use the following mental model to understand how different AI mechanisms +interact: + +| Mechanism | Scope and behavior | Best suited for | +| :--- | :--- | :--- | +| **Agent skill** | On demand | Multi-step recipes, patterns | +| **Dart MCP server** | Local SDK tools | Diagnostics, tests, runtime | +| **Developer Knowledge MCP** | Online search | Live docs, API references | +| **Package skill** | From dependencies | Library APIs, code generation | + +{:.table .table-striped} + +## Next steps + +* Follow the [Get started with AI](/ai/get-started) guide to install official + plugins in your editor. +* Learn how to consume and publish [Package skills](/ai/package-skills). diff --git a/sites/docs/src/content/deployment/android.md b/sites/docs/src/content/deployment/android.md index 1f939067d3f..477a940bf81 100644 --- a/sites/docs/src/content/deployment/android.md +++ b/sites/docs/src/content/deployment/android.md @@ -37,7 +37,7 @@ substitute `[project]` with your app's directory. ## Add a launcher icon When a new Flutter app is created, it has a default launcher icon. -To customize this icon, you might want to check out the +To learn about customizing this icon, check out the [flutter_launcher_icons][] package. Alternatively, you can do it manually using the following steps: @@ -191,8 +191,8 @@ If not, create one using one of the following methods: Create a file named `[project]/android/key.properties` that contains a reference to your keystore. -Don't include the angle brackets (`< >`). -They indicate that the text serves as a placeholder for your values. +Don't include the angle brackets (`< >`), +which indicate that the text serves as a placeholder for your values: ```properties storePassword= @@ -222,7 +222,7 @@ To configure Gradle, edit the `/android/app/build.gradle.kts` file. 1. Define and load the keystore properties file before the `android` property block. -1. Set the `keystoreProperties` object to load the `key.properties` file. +1. Set the `keystoreProperties` object to load the `key.properties` file: @@ -272,7 +272,7 @@ To configure Gradle, edit the `/android/app/build.gradle.kts` file. 1. Add the signing configuration before the `buildTypes` property block - inside the `android` property block. + inside the `android` property block: @@ -283,10 +283,10 @@ To configure Gradle, edit the `/android/app/build.gradle.kts` file. + signingConfigs { + create("release") { -+ keyAlias = keystoreProperties["keyAlias"] as String -+ keyPassword = keystoreProperties["keyPassword"] as String -+ storeFile = keystoreProperties["storeFile"]?.let { file(it) } -+ storePassword = keystoreProperties["storePassword"] as String ++ keyAlias = keystoreProperties.getProperty("keyAlias") ++ keyPassword = keystoreProperties.getProperty("keyPassword") ++ storeFile = keystoreProperties.getProperty("storeFile")?.let { file(it) } ++ storePassword = keystoreProperties.getProperty("storePassword") + } + } buildTypes { @@ -340,7 +340,7 @@ You might need to run `flutter clean` after changing the Gradle file. This prevents cached builds from affecting the signing process. ::: -To learn more about signing your app, check out +To learn about signing your app, check out [Sign your app][] on the Android developer docs. [Sign your app]: {{site.android-dev}}/studio/publish/app-signing.html#generate-key @@ -365,8 +365,8 @@ attacks that make use of quantum computing. Note that you must create a new classical key; you cannot reuse the older one. -For more information, check out the -[Android documentation on PQC APK signing][android-doc] +For details, consult the +[Android documentation on PQC APK signing][android-doc]. [android-doc]: {{site.android-dev}}/about/versions/17/features#pqc-apk-signing @@ -383,7 +383,7 @@ the compile time of an Android application. The `--[no-]shrink` flag has no effect. Code shrinking is always enabled in release builds. -To learn more, check out [Shrink, obfuscate, and optimize your app][]. +For details, visit [Shrink, obfuscate, and optimize your app][]. ::: [R8]: {{site.android-dev}}/studio/build/shrink-code @@ -423,7 +423,7 @@ The Flutter tool enables multidex support and retries the build: The output of a successful build after adding multidex. -:::note +:::version-note Multidex support is natively included when targeting Android SDK 21 or later. ::: @@ -438,15 +438,15 @@ io/flutter/util/PathUtils.class ``` Also, include any other classes used in app startup. -For more detailed guidance on adding multidex support manually, -check out the official [Android documentation][multidex-docs]. +To learn about adding multidex support manually, +consult the official [Android documentation][multidex-docs]. [multidex-keep]: {{site.android-dev}}/studio/build/multidex#keep [multidex-docs]: {{site.android-dev}}/studio/build/multidex ## Review the app manifest -Review the default [App Manifest][manifest] file. +Review the default [App Manifest][manifest] file: ```xml title="[project]/android/app/src/main/AndroidManifest.xml" @@ -476,8 +476,8 @@ Verify the following values: To verify the Android build configuration, review the `android` block in the default -[Gradle build script][gradlebuild]. -The default Gradle build script is found at `[project]/android/app/build.gradle.kts`. +[Gradle build script][gradlebuild] +found at `[project]/android/app/build.gradle.kts`: ```kotlin title="[project]/android/app/build.gradle.kts" android { @@ -494,7 +494,7 @@ android { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId = "com.example.[project]" // You can update the following values to match your application needs. - // For more information, see: https://flutter.dev/to/review-gradle-config. + // For details, see https://flutter.dev/to/review-gradle-config. minSdk = flutter.minSdkVersion targetSdk = flutter.targetSdkVersion versionCode = flutter.versionCode @@ -541,9 +541,12 @@ For example: The Flutter tooling sets default values for the Android SDK versions: -* **`compileSdk`**: The version of the Android SDK used to compile the app. -* **`minSdk`**: The minimum Android version that the app supports. -* **`targetSdk`**: The Android version the app is designed and tested to run on. +* **`compileSdk`**: The version of the Android SDK used to + compile the app. +* **`minSdk`**: The minimum Android version that the + app supports. +* **`targetSdk`**: The Android version the app is designed and + tested to run on. These default values (`flutter.compileSdkVersion`, etc.) are managed by Flutter to ensure compatibility with the framework and plugins. @@ -572,7 +575,8 @@ publishing to the Play Store. :::note The Google Play Store prefers the app bundle format. -To learn more, check out [About Android App Bundles][bundle]. +To learn about Android App Bundles, visit +[About Android App Bundles][bundle]. ::: [bundle]: {{site.android-dev}}/guide/app-bundle @@ -684,8 +688,8 @@ From the command line: ## Publish to the Google Play Store -For detailed instructions on publishing your app to the Google Play Store, -check out the [Google Play launch][play] documentation. +To learn about publishing your app to the Google Play Store, +consult the [Google Play launch][play] documentation. ## Update the app's version number @@ -706,8 +710,8 @@ Both the version and the build number can be overridden in Flutter's build by specifying `--build-name` and `--build-number`, respectively. In Android, `build-name` is used as `versionName` while -`build-number` used as `versionCode`. For more information, -check out [Version your app][] in the Android documentation. +`build-number` used as `versionCode`. For details, +refer to [Version your app][] in the Android documentation. When you rebuild the app for Android, any updates in the version number from the pubspec file will @@ -805,9 +809,9 @@ Run `apkanalyzer manifest print ` and look for a `` tag with `android:name="flutterEmbedding"`. The value can be `1` or `2`. -Example: -`apkanalyzer manifest print some-flutter-app.apk | grep flutterEmbedding -C 2` -returns the following style string. +This command, +`apkanalyzer manifest print some-flutter-app.apk | grep flutterEmbedding -C 2`, +returns the following output: ``` - View the many videos on the Flutter YouTube channel. - + Learn how to build with and integrate powerful AI tools. diff --git a/sites/docs/src/content/learn/pathway/index.md b/sites/docs/src/content/learn/pathway/index.md index 7469d90789e..21687640024 100644 --- a/sites/docs/src/content/learn/pathway/index.md +++ b/sites/docs/src/content/learn/pathway/index.md @@ -64,6 +64,12 @@ take full advantage of the framework. +## Ready to start? + +Get started by installing Flutter and setting up your development environment: + +[Start with Step 1: Quick Install →](/learn/pathway/quick-install) + Dash with question marks around her; looking curious. diff --git a/sites/docs/src/content/llms.txt b/sites/docs/src/content/llms.txt index a0ab33e79cc..00b4ff2bf6f 100644 --- a/sites/docs/src/content/llms.txt +++ b/sites/docs/src/content/llms.txt @@ -98,8 +98,8 @@ This file provides a curated list of resources to help Large Language Models und ### AI-assisted development -- [Create with AI](https://docs.flutter.dev/ai/create-with-ai): An overview of how to use AI to supercharge your Flutter development. -- [The Dart and Flutter MCP Server](https://docs.flutter.dev/ai/mcp-server): Technical details on the server that enables AI features in the IDE. +- [Get started with AI](https://docs.flutter.dev/ai/get-started): How to set up and use AI agent plugins for Flutter and Dart. +- [How Flutter AI tools work](https://docs.flutter.dev/ai/tools): Technical details on the skills, MCP server, and tools for AI coding assistants. - [Supercharge your Dart & Flutter development experience with the Dart and Flutter MCP server](https://blog.flutter.dev/supercharge-your-dart-flutter-development-experience-with-the-dart-mcp-server-2edcc8107b49): A blog post explaining the benefits of the MCP server. - [Gemini in Android Studio now speaks fluent Flutter](https://blog.flutter.dev/gemini-in-android-studio-now-speaks-fluent-flutter-915dfec98274): A blog post about Gemini's Flutter capabilities in Android Studio. diff --git a/sites/docs/src/content/packages-and-plugins/developing-packages.md b/sites/docs/src/content/packages-and-plugins/developing-packages.md index 01751451f4c..93af492a2d9 100644 --- a/sites/docs/src/content/packages-and-plugins/developing-packages.md +++ b/sites/docs/src/content/packages-and-plugins/developing-packages.md @@ -1064,4 +1064,21 @@ file, like any other Dart package. [test your plugin]: #testing-your-plugin [unit tests]: /testing/overview#unit-tests [`url_launcher`]: {{site.pub}}/packages/url_launcher +## Bundle AI package skills + +You can help AI coding assistants use your package effectively by bundling +custom instructions and recipes directly inside your repository. + +To distribute skills with your package: + +1. Create a `skills/` directory in the root of your package repo. +2. Inside `skills/`, create a subdirectory named after your package (for + example, `skills//`). +3. Add a `SKILL.md` file with guidelines and idiomatic code examples. +4. Publish your package to `pub.dev`. + +When developers add your package as a dependency, their assistants can +automatically discover your instructions. To learn more about authoring +guidelines and best practices, check out [Package skills](/ai/package-skills). + [Writing a good plugin]: {{site.flutter-blog}}/writing-a-good-flutter-plugin-1a561b986c9c diff --git a/sites/docs/src/content/packages-and-plugins/swift-package-manager/for-app-developers.md b/sites/docs/src/content/packages-and-plugins/swift-package-manager/for-app-developers.md index 3d7a5077feb..dcbcaf9a115 100644 --- a/sites/docs/src/content/packages-and-plugins/swift-package-manager/for-app-developers.md +++ b/sites/docs/src/content/packages-and-plugins/swift-package-manager/for-app-developers.md @@ -39,8 +39,91 @@ yet support Swift Package Manager. If you previously disabled SwiftPM, you might need to enable it with `flutter config --enable-swift-package-manager`. -If automatic migration works for you, that's it! -You are done with this page. +If automatic migration works for you and all of your dependencies +support Swift Package Manager, +you can [remove CocoaPods integration][removeCocoaPods]. + +## How to remove CocoaPods integration + +**Flutter falls back to CocoaPods if any of your project dependencies +do not support Swift Package Manager.** +Ensure that all plugins in your project support Swift Package Manager +before removing CocoaPods. + +:::note +If you have manually edited your `Podfile` to add CocoaPods +dependencies or logic other than Flutter plugins, +you must manually remove those changes. +::: + + + + +To remove CocoaPods from your project, use the following steps. + +1. To de-integrate CocoaPods from your Xcode projects, run: + + ```sh + cd ios + pod deintegrate + cd .. + ``` + +2. Delete CocoaPods-related files and directories: + + Delete `ios/Podfile`, `ios/Podfile.lock`, + the `ios/Pods/` directory, and + the `ios/.symlinks/` directory (if present). + +3. Check your configuration files: + + In `ios/Flutter/Debug.xcconfig` + and `ios/Flutter/Release.xcconfig`, + remove any `#include` lines referencing `Pods/Target Support Files` + or CocoaPods `.xcconfig` files if they are still present. + +4. Clean and build your project to verify the migration: + + ```sh + flutter clean + flutter pub get + flutter run + ``` + + + + +To remove CocoaPods from your project, use the following steps. + +1. To de-integrate CocoaPods from your Xcode projects, run: + + ```sh + cd macos + pod deintegrate + cd .. + ``` + +2. Delete CocoaPods-related files and directories: + + Delete `macos/Podfile`, the `macos/Pods/` directory, + and the `macos/.symlinks/` directory (if present). + +3. Check your configuration files: + + In `macos/Flutter/Flutter-Debug.xcconfig` and `macos/Flutter/Flutter-Release.xcconfig` + remove any `#include` lines referencing `Pods/Target Support Files` + or CocoaPods `.xcconfig` files if they are still present. + +4. Clean and build your project to verify the migration: + + ```sh + flutter clean + flutter pub get + flutter run + ``` + + + ## How to add Swift Package Manager integration manually @@ -223,4 +306,5 @@ This turns off Swift Package Manager for the current user. If a project is incompatible with Swift Package Manager, all contributors need to run this command. +[removeCocoaPods]: #how-to-remove-cocoapods-integration [removeSPM]: #how-to-remove-swift-package-manager-integration diff --git a/sites/docs/src/content/packages-and-plugins/using-packages.md b/sites/docs/src/content/packages-and-plugins/using-packages.md index 33e957c21ca..622defeee25 100644 --- a/sites/docs/src/content/packages-and-plugins/using-packages.md +++ b/sites/docs/src/content/packages-and-plugins/using-packages.md @@ -501,3 +501,17 @@ To use this plugin: before adding the plugin). Click **Show Flutter homepage**. You should see the default browser open on the device, displaying the homepage for flutter.dev. +## Install AI skills from packages + +Many `pub.dev` packages bundle official **package skills** that teach AI coding +assistants how to use their APIs and follow recommended patterns. + +If your project dependencies include skills, you can discover and install them +into your local workspace with a single command: + +```bash +dart run skills@ get +``` + +To learn more about how skills help coding assistants understand your +dependencies, check out [Package skills](/ai/package-skills). diff --git a/sites/docs/src/content/platform-integration/android/local-network-permission.md b/sites/docs/src/content/platform-integration/android/local-network-permission.md new file mode 100644 index 00000000000..d3d8af929e5 --- /dev/null +++ b/sites/docs/src/content/platform-integration/android/local-network-permission.md @@ -0,0 +1,154 @@ +--- +title: Request local network permissions on Android +shortTitle: Local network permission +description: >- + Learn how to declare and request Android local area network permissions + before opening Dart sockets in Flutter. +--- + +Starting in Android 17 (API level 37), Android blocks local network access +by default. Apps targeting Android 17 or higher that discover, scan, +or connect to devices on the local area network must declare and request +the `ACCESS_LOCAL_NETWORK` runtime permission. + +:::note +Although enforced for apps targeting Android 17 (API level 37) and higher, +you can opt in to test this behavior on Android 16 (API level 36). +::: + +## Understand the impact on Dart sockets + +The standard `dart:io` library provides low-level socket networking, +including [`Socket`][socket-class], [`RawSocket`][raw-socket-class], +and [`ServerSocket`][server-socket-class]. +These socket classes operate at the system level +and do not interact with Android's application framework UI. + +Because Dart sockets cannot display an Android permission prompt, +any attempt to connect to a local IP address without the required permission +fails and throws a `SocketException`. +To prevent connection failures, +request the runtime permission in Flutter +before you initialize or connect your Dart socket. + +## Declare the permission in Android + +Before you can request the permission at runtime, +declare it in your Android manifest. + +1. Open `android/app/src/main/AndroidManifest.xml`. +1. Add the `ACCESS_LOCAL_NETWORK` permission inside the `` tag: + +```xml + + + + +``` + +## Add the permission_handler package + +To request Android runtime permissions from Dart, +use the [`permission_handler`][] plugin from [pub.dev][]. + +Add `permission_handler` to your Flutter project dependencies: + +```bash +flutter pub add permission_handler +``` + +## Request permission before connecting a socket + +Call `Permission.accessLocalNetwork.request()` +before opening a Dart socket connection. + +The following example demonstrates how to check the platform, +request local network permission, +and open a socket connection: + +```dart +import 'dart:io'; +import 'package:flutter/material.dart'; +import 'package:permission_handler/permission_handler.dart'; + +Future connectToLocalDevice({ + required String host, + required int port, +}) async { + // Check and request runtime permission on Android. + if (Platform.isAndroid) { + final status = await Permission.accessLocalNetwork.request(); + + if (status.isPermanentlyDenied) { + // The user opted not to grant permission and checked "Don't ask again". + // In a real app, show an explanation dialog before calling openAppSettings(). + debugPrint('Permission permanently denied. Please enable it in settings.'); + return; + } + + if (!status.isGranted) { + // Handle the case where the user denied permission. + debugPrint('Local network permission was denied.'); + return; + } + } + + // Connect the Dart socket after the permission is granted. + try { + final socket = await Socket.connect( + host, + port, + timeout: const Duration(seconds: 5), + ); + + debugPrint( + 'Connected to ${socket.remoteAddress.address}:${socket.remotePort}', + ); + + // Use the socket... + + await socket.close(); + } on SocketException catch (e) { + debugPrint('Failed to connect: $e'); + } +} +``` + +## Handle permission status + +The `Permission.accessLocalNetwork.request()` method +returns a `PermissionStatus` enum that indicates the outcome: + +* `PermissionStatus.granted`: The user granted local network access. + You can proceed to connect your Dart sockets. +* `PermissionStatus.denied`: The user denied permission for this request. + Prompt the user with an explanation before requesting permission again. +* `PermissionStatus.permanentlyDenied`: The user denied permission permanently. + Call `openAppSettings()` to help the user enable permission + in the system settings. + +:::tip +You only need to obtain permission once during an app session, +but checking the status before establishing a socket connection +ensures reliable network calls. +::: + +## More information + +To learn more about local network permissions and socket programming, +consult the following resources: + +* Android developer guide on [Local network permission][android-local-network] +* [`permission_handler`][] package on pub.dev +* Dart [`Socket` class documentation][socket-class] +* [Issue #184859 on GitHub][flutter-issue-184859] + +[android-local-network]: https://developer.android.com/privacy-and-security/local-network-permission#android-17-enforcement +[`permission_handler`]: {{site.pub-pkg}}/permission_handler +[pub.dev]: {{site.pub}} +[socket-class]: {{site.dart.api}}/dart-io/Socket-class.html +[raw-socket-class]: {{site.dart.api}}/dart-io/RawSocket-class.html +[server-socket-class]: {{site.dart.api}}/dart-io/ServerSocket-class.html +[flutter-issue-184859]: {{site.repo.flutter}}/issues/184859 diff --git a/sites/docs/src/content/platform-integration/index.md b/sites/docs/src/content/platform-integration/index.md index 712d0384a68..aee1e3d8b97 100644 --- a/sites/docs/src/content/platform-integration/index.md +++ b/sites/docs/src/content/platform-integration/index.md @@ -165,6 +165,14 @@ Learn how to add custom integrations with Android to your Flutter app.

Learn how to launch a Jetpack Compose activity from your app.

+ +
+ Request local network permissions +
+
+

Learn how to handle Android local network permissions in Flutter.

+
+
### Integrate with iOS {:#ios} diff --git a/sites/docs/src/content/reference/create-new-app.md b/sites/docs/src/content/reference/create-new-app.md index c99a8ace287..703c09c06b2 100644 --- a/sites/docs/src/content/reference/create-new-app.md +++ b/sites/docs/src/content/reference/create-new-app.md @@ -304,7 +304,7 @@ check out the [IntelliJ for Flutter reference][ij-more]. To create a Flutter app with Antigravity, you first need to install and set up Antigravity as described on the -[Antigravity page](/ai/antigravity). +[Get started with AI](/ai/get-started). Then follow these steps: 1.

Open Antigravity and create a Workspace

diff --git a/sites/docs/src/content/release/breaking-changes/index.md b/sites/docs/src/content/release/breaking-changes/index.md index ec95d837bcd..0ddde760623 100644 --- a/sites/docs/src/content/release/breaking-changes/index.md +++ b/sites/docs/src/content/release/breaking-changes/index.md @@ -37,8 +37,12 @@ They're sorted by release and listed in alphabetical order: ### Not yet released to stable * [Added enabled property and made onChanged optional for DropdownButton][] +* [Migrate to standalone `material_ui` and `cupertino_ui` packages][] +* [Restrict command-line flags for prebuilt Android release binaries][] [Added enabled property and made onChanged optional for DropdownButton]: /release/breaking-changes/dropdownbutton-enabled-property +[Migrate to standalone `material_ui` and `cupertino_ui` packages]: /release/breaking-changes/material-ui-and-cupertino-ui +[Restrict command-line flags for prebuilt Android release binaries]: /release/breaking-changes/restrict-command-line-flags-prebuilt-android-release-binaries.md ### Released in Flutter 3.47 diff --git a/sites/docs/src/content/release/breaking-changes/material-ui-and-cupertino-ui.md b/sites/docs/src/content/release/breaking-changes/material-ui-and-cupertino-ui.md new file mode 100644 index 00000000000..2e66b1f7cee --- /dev/null +++ b/sites/docs/src/content/release/breaking-changes/material-ui-and-cupertino-ui.md @@ -0,0 +1,244 @@ +--- +title: Migrate to standalone material_ui and cupertino_ui packages +description: >- + The Material and Cupertino design libraries are now available as + standalone packages, decoupling design systems from the Flutter SDK. +--- + +{% render "docs/breaking-changes.md" %} + +## Summary + +The Material and Cupertino design libraries are now available as +standalone packages: [`package:material_ui`][] and [`package:cupertino_ui`][]. +In Flutter 3.47, developers can opt in to these packages ahead of +the formal deprecation of the in-framework design libraries. + +## Background + +Historically, Flutter's Material (`package:flutter/material.dart`) +and Cupertino (`package:flutter/cupertino.dart`) widget libraries +were bundled directly inside the core Flutter SDK. +While this made it easy to get started with pixel-perfect widgets, +bundling design systems inside the core framework presented several challenges: + +* Widget updates, bug fixes, and new components were tied to + quarterly Flutter SDK releases. +* Contributions and iteration on design libraries moved slower + than standalone Dart packages. +* Applications couldn't update design libraries independently of + the engine and framework. + +To resolve these challenges, the design systems are now decoupled +from the core framework. +Starting in Flutter 3.47, version 1.0 of the standalone +`material_ui` and `cupertino_ui` packages is published on `pub.dev`. + +Decoupling the design systems provides several benefits: + +* **Independent release cycles**: + The packages can ship bug fixes, improvements, and new components + on their own release schedules (planned for weekly releases), + without requiring a full Flutter SDK upgrade. +* **Faster contributions**: + Community members can contribute directly to the libraries in the + [`flutter/packages`][] repository. +* **Style-neutral core**: + Decoupling lays the groundwork for a style-neutral Flutter core widget + catalog, making it easier to build and maintain custom design systems. + +Contributions to `package:flutter/material.dart` and +`package:flutter/cupertino.dart` inside the core SDK were frozen +starting in Flutter 3.44. +The initial 1.0.0 versions of `package:material_ui` and `package:cupertino_ui` +match the frozen framework code to provide a seamless transition. +Future releases of the packages will use semantic versioning for major, minor, +and patch updates. +The in-framework design libraries are scheduled for formal deprecation +in an upcoming stable release. + +## Migration guide + +To migrate your project from the in-framework design libraries to the +standalone packages, you can use the automated `dart fix` tool +or perform the migration manually. + +### Automated migration with dart fix + +The quickest way to migrate is using the `migrate_design_widgets` fix rule +with `dart fix`: + +```console +$ dart fix --apply --code=migrate_design_widgets +``` + +This command automatically updates your imports from +`package:flutter/material.dart` and `package:flutter/cupertino.dart` +to the corresponding standalone package imports. + +:::note +If `dart fix` doesn't automatically add the new packages to your +`pubspec.yaml` dependencies, manually add them by running +`flutter pub add material_ui` or `flutter pub add cupertino_ui` as needed. +::: + +Run `dart fix` once more to ensure all other automated migrations are applied, +and lint warnings addressed, such as import sorting. + +### Manual migration + +To migrate manually, follow these steps: + +1. Add the standalone packages to your `pubspec.yaml` dependencies: + + ```console + $ flutter pub add material_ui + $ flutter pub add cupertino_ui + ``` + +1. Update the imports in your Dart files to reference the new packages. + + Code before migration: + + ```dart + import 'package:flutter/cupertino.dart'; + import 'package:flutter/material.dart'; + ``` + + Code after migration: + + ```dart + import 'package:cupertino_ui/cupertino_ui.dart'; + import 'package:material_ui/material_ui.dart'; + ``` + +### Compatibility bridge + +During the ecosystem transition, your application might depend on +third-party packages that still import the legacy in-framework libraries +(`package:flutter/material.dart` or `package:flutter/cupertino.dart`). + +To allow your application to migrate to the standalone packages immediately +without waiting for all dependencies to update, `material_ui` and +`cupertino_ui` provide compatibility bridge utilities. +Wrap your app using `MaterialUiCompatibilityBridge` inside +`MaterialApp.builder`: + +```dart +import 'package:material_ui/material_ui.dart'; + +void main() { + runApp(const MyApp()); +} + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + theme: ThemeData( + colorScheme: ColorScheme.fromSeed( + seedColor: const Color(0xFF6750A4), + ), + ), + builder: (BuildContext context, Widget? child) { + return MaterialUiCompatibilityBridge(child: child!); + }, + home: const HomeScreen(), + ); + } +} +``` + +#### Bridge capabilities and limitations + +`MaterialUiCompatibilityBridge` injects theme and localization data +downward into the widget tree. +This allows unmigrated child widgets and dependencies that read design state +from context +(such as `Theme.of(context)` or `MaterialLocalizations.of(context)`) +to continue functioning properly. + +However, the compatibility bridge cannot resolve type mismatches +when a dependency exposes, accepts, or returns in-framework SDK types +in its public API signatures (such as passing a `FloatingActionButtonLocation`, +`ColorScheme`, or `TextTheme` as a parameter or callback return value). +Because Dart enforces static typing across distinct package imports, +values from `package:flutter/material.dart` cannot be assigned to parameters +expecting types from `package:material_ui`. +Dependencies with API-signature coupling must be migrated to the standalone +package before your application can pass modern types to them. + +### Decoupled localizations + +As part of this transition, `flutter_localizations` is unbundled +for design components. +Localization delegates and translated strings for Material and Cupertino +widgets now reside directly in `package:material_ui` and +`package:cupertino_ui`. + +Code before migration: + +```dart +import 'package:flutter/material.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; + +// ... +MaterialApp( + localizationsDelegates: const >[ + GlobalCupertinoLocalizations.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + ], + // ... +); +``` + +Code after migration: + +```dart +import 'package:material_ui/material_ui.dart'; + +// ... +MaterialApp( + localizationsDelegates: GlobalMaterialLocalizations.delegates, + // ... +); +``` + +Setting `localizationsDelegates` to `GlobalMaterialLocalizations.delegates` +automatically includes the Cupertino and Widgets delegates as well, +simplifying localization setup. + +### Guidance for package authors + +If you maintain an open source package or plugin in the Flutter ecosystem, +migrating to `package:material_ui` or `package:cupertino_ui` introduces +a breaking change for consumers who haven't updated their dependencies. +While the symbols and class names remain the same, +they are technically referenced from an entirely new library. +Treat this migration as a major version bump for your package. + +## References + +Package documentation: + +* [`package:material_ui`][] +* [`package:cupertino_ui`][] + +Relevant issues: + +* [Issue 191448][] + +Relevant blog posts and announcements: + +* [What's new in Flutter 3.47][] +* [Flutter's Material and Cupertino code freeze][] + +[`flutter/packages`]: {{site.repo.packages}} +[Issue 191448]: {{site.repo.flutter}}/issues/191448 +[`package:cupertino_ui`]: {{site.pub-pkg}}/cupertino_ui +[`package:material_ui`]: {{site.pub-pkg}}/material_ui +[Flutter's Material and Cupertino code freeze]: {{site.main-url}}/blog/flutters-material-and-cupertino-code-freeze +[What's new in Flutter 3.47]: {{site.main-url}}/blog/whats-new-in-flutter-3-47 diff --git a/sites/docs/src/content/release/breaking-changes/restrict-command-line-flags-prebuilt-android-release-binaries.md b/sites/docs/src/content/release/breaking-changes/restrict-command-line-flags-prebuilt-android-release-binaries.md new file mode 100644 index 00000000000..a4ad39e61ce --- /dev/null +++ b/sites/docs/src/content/release/breaking-changes/restrict-command-line-flags-prebuilt-android-release-binaries.md @@ -0,0 +1,162 @@ +--- +title: Restrict command-line flags for prebuilt Android release binaries +description: >- + Passing configuration flags to prebuilt Android release binaries with + `--use-application-binary` is no longer supported. +--- + +{% render "docs/breaking-changes.md" %} + +## Summary + +Previously, the Flutter CLI could pass engine configuration flags +(such as `--dart-flags`) to a prebuilt Android release binary +(`--use-application-binary --release`) at launch time +using Android `Intent` extras, +which the embedding accepted in all build modes. + +To protect production applications +against `Intent`-based spoofing vulnerabilities, +Flutter Android release builds now ignore `Intent` extras +and read engine configuration strictly +from the compiled `AndroidManifest.xml`. +Because prebuilt binaries cannot have their manifests +dynamically modified after compilation, +the Flutter CLI now produces a fatal error +if you pass engine configuration flags to a prebuilt release binary +(preventing flags from being silently ignored). + +Standard release builds +(where the CLI compiles the app and injects flags into the manifest) +and all debug and profile workflows continue to work without changes. + +## Context + +The Flutter CLI allows passing flags +(such as `--dart-flags` or tracing options) +to configure the Flutter engine when running or driving an application. +Historically, the Flutter Android embedding accepted these flags at runtime +through [`Intent`][] extras. + +However, runtime `Intent` extras on Android +can be spoofed or intercepted by other applications on a user's device. +To harden production applications, +Flutter Android release builds now read configuration strictly +from a cryptographically signed `AndroidManifest.xml` +and ignore runtime `Intent` flags. + +For release builds of standard Gradle-based projects, +the Flutter CLI automatically injects command-line flags +into `AndroidManifest.xml` during compilation. +When you use a prebuilt release binary with `--use-application-binary`, +the CLI cannot modify the compiled manifest, +and the binary ignores runtime `Intent` flags. +To prevent tests or scripts from running +with unnoticed configuration failures, +the CLI now reports a fatal error. + +Debug and profile builds intentionally maintain runtime flag support +to preserve testing velocity and dynamic benchmarking workflows. + +[`Intent`]: https://developer.android.com/reference/android/content/Intent + +## Description of change + +The Flutter CLI enforces the following behavior +when running Flutter apps on Android: + +| Build mode | Using `--use-application-binary` | CLI behavior | Notes | +| :--- | :--- | :--- | :--- | +| **Debug / Profile** | Yes | Passes flags to binary through `adb` | No rebuild required; flags apply at runtime. | +| **Debug / Profile** | No | Builds and passes flags through `adb` | Standard development workflow. | +| **Release** | Yes | **Fatal error** if configuration flags are provided | Prebuilt release binaries cannot be dynamically configured. | +| **Release** | No | Injects flags into `AndroidManifest.xml` during compilation | Standard release build workflow. | + +## Migration guide + +:::note +You are **not affected** and do not need to take action if: +- You build and run standard release apps (`flutter run --release`, + `flutter build apk --release`, `flutter build appbundle`). +- You run tests and benchmarks in **debug** or **profile** mode. +- You use `--use-application-binary` + without passing engine configuration flags. +::: + +If your CI/CD pipelines, automated scripts, or build systems +pass flags to prebuilt release binaries, +use one of the following migration paths: + +### Switch testing and benchmarking to profile mode + +If your automated test pipelines use `--use-application-binary` +with `--release` to dynamically test different engine configurations: + +1. Switch your test target to **profile mode** (`--profile`). + Profile mode mirrors release performance characteristics + while retaining support for dynamic runtime flag configuration + without recompilation. + +### Build release binaries with flags directly + +If you must run tests against a release binary: + +1. Run `flutter build` or `flutter run` with your configuration flags + without `--use-application-binary`. + The CLI automatically embeds the flags into the compiled manifest. +1. Alternatively, compile separate release binaries + for each required test configuration. + +### Configure non-Gradle or hermetic build systems + +If you build Flutter Android applications using hermetic build systems +(such as Bazel) that separate compilation from execution: + +1. Statically declare any necessary engine flags in `AndroidManifest.xml` + before compiling the release APK. +1. Use **profile mode** for test targets + that require dynamic configuration at launch time. + +### Declare engine flags in `AndroidManifest.xml` + +To configure engine flags statically in release builds, +add `` elements under the `` tag in +your `android/app/src/main/AndroidManifest.xml` file: + +```xml title="AndroidManifest.xml" highlightLines=6-12 + + + + + + + + +``` + +## Timeline + +Landed in version: TBD
+In stable release: TBD + +## References + +Relevant issues: + +* [Issue 180686][] + +Relevant pull requests: + +* [PR 190870][] + +[Issue 180686]: https://github.com/flutter/flutter/issues/180686 +[PR 190870]: https://github.com/flutter/flutter/pull/190870 diff --git a/sites/docs/src/content/resources/faq.md b/sites/docs/src/content/resources/faq.md index caa1f263e34..4b66150dc2f 100644 --- a/sites/docs/src/content/resources/faq.md +++ b/sites/docs/src/content/resources/faq.md @@ -170,7 +170,7 @@ of the many editors that support [editing Dart][]. [Android Studio]: {{site.android-dev}}/studio [Android Studio/IntelliJ]: /tools/android-studio -[Antigravity]: /ai/antigravity +[Antigravity]: /ai/get-started [editing Dart]: {{site.dart-site}}/tools [editor configuration]: /tools/editors [IntelliJ IDEA]: https://www.jetbrains.com/idea/ diff --git a/sites/docs/src/content/tools/hot-reload.md b/sites/docs/src/content/tools/hot-reload.md index f99cf55c9c7..a56e01ff179 100644 --- a/sites/docs/src/content/tools/hot-reload.md +++ b/sites/docs/src/content/tools/hot-reload.md @@ -18,10 +18,10 @@ A demo of hot reload in DartPad ## How to perform a hot reload -If you are using an AI coding assistant like [Google Antigravity](/ai/antigravity), -you can use Agent mode to automatically hot reload your running application -as soon as you prompt the agent to apply changes. -For details, see [Agentic Hot Reload](/ai/antigravity#agentic-hot-reload). +If you are using an AI coding assistant like Google Antigravity, +the assistant can automatically hot reload your running application after +making changes, or when asked to "Reload the app". +To learn more, check out [Get started with AI](/ai/get-started). To hot reload a Flutter app manually: diff --git a/sites/docs/src/data/sidenav/default.yml b/sites/docs/src/data/sidenav/default.yml index df3b6a28a8f..f3fc4564fe1 100644 --- a/sites/docs/src/data/sidenav/default.yml +++ b/sites/docs/src/data/sidenav/default.yml @@ -394,6 +394,8 @@ permalink: /platform-integration/android/chromeos - title: Protect your app's sensitive content permalink: /platform-integration/android/sensitive-content + - title: Request local network permissions + permalink: /platform-integration/android/local-network-permission - title: iOS permalink: /platform-integration/ios children: @@ -472,6 +474,8 @@ permalink: /packages-and-plugins/dependency-management - title: Develop packages & plugins permalink: /packages-and-plugins/developing-packages + - title: Package skills + permalink: /ai/package-skills - title: Swift Package Manager permalink: /packages-and-plugins/swift-package-manager children: @@ -639,35 +643,16 @@ - divider - header: AI -- title: Overview - permalink: /ai/create-with-ai - icon: psychology - - title: Develop with AI permalink: /ai/get-started icon: smart_toy children: - title: Get started permalink: /ai/get-started - - title: AI coding assistants - permalink: /ai/coding-assistants - children: - - title: Overview - permalink: /ai/coding-assistants - - title: Antigravity - permalink: /ai/antigravity - - title: Antigravity CLI - permalink: /ai/antigravity-cli - - title: Agent skills - permalink: /ai/agent-skills - - title: Dart and Flutter MCP server - permalink: /ai/mcp-server - - title: AI rules - permalink: /ai/ai-rules - - title: Developer experience - permalink: /ai/best-practices/developer-experience - - title: "AI evaluations (experimental)" - permalink: /ai/evals + - title: How AI tools work + permalink: /ai/tools + - title: Package skills + permalink: /ai/package-skills - title: Build AI-powered apps permalink: /ai/genui diff --git a/sites/docs/src/data/tutorial.yml b/sites/docs/src/data/tutorial.yml index 5d025e238f9..c543ba90bf9 100644 --- a/sites/docs/src/data/tutorial.yml +++ b/sites/docs/src/data/tutorial.yml @@ -7,6 +7,8 @@ units: url: /learn/pathway/quick-install - title: Introduction to Flutter UI chapters: + - title: Tutorial introduction + url: /learn/pathway/tutorial - title: Create a Flutter app url: /learn/pathway/tutorial/create-an-app - title: Widget fundamentals diff --git a/sites/www/content/blog/a2ui-client-side-functions/images/A2UI.gif b/sites/www/content/blog/a2ui-client-side-functions/images/A2UI.gif new file mode 100644 index 00000000000..b860fcf7fc3 Binary files /dev/null and b/sites/www/content/blog/a2ui-client-side-functions/images/A2UI.gif differ diff --git a/sites/www/content/blog/a2ui-client-side-functions/images/A2UI_Still.png b/sites/www/content/blog/a2ui-client-side-functions/images/A2UI_Still.png new file mode 100644 index 00000000000..cec4679b125 Binary files /dev/null and b/sites/www/content/blog/a2ui-client-side-functions/images/A2UI_Still.png differ diff --git a/sites/www/content/blog/a2ui-client-side-functions/index.md b/sites/www/content/blog/a2ui-client-side-functions/index.md new file mode 100644 index 00000000000..d4958e27102 --- /dev/null +++ b/sites/www/content/blog/a2ui-client-side-functions/index.md @@ -0,0 +1,250 @@ +--- +title: "Quick, reliable calculations with A2UI's Client-Side Functions" +description: >- + Learn how client-side functions allow an agent to delegate local operations + directly to Dart code running on a user's device. +publishDate: 2026-08-28 +author: redbrogdon +image: images/A2UI.gif +socialImage: images/A2UI_Still.png +category: deep-dive +layout: blog +--- + + + + +Generative UI (GenUI) is transforming how I approach UI +development in Flutter. Instead of hardcoding fixed screens +for every scenario, GenUI enables an AI agent to dynamically +generate and adjust user interface components at runtime. +Using Agent-to-User Interface (A2UI) JSON messages and the +[`genui`](https://pub.dev/packages/genui) package, a Flutter app +can render dynamic AI-driven cards and surfaces on the fly. + +However, as I've started building real-world agentic apps, +I've found myself looking for ways to reduce latency and decrease +the number of opportunities my models have to make mistakes. +When building with Large Language Models (LLMs), it's tempting +to rely on the model for _everything_. But asking an LLM to take on +tasks it wasn't necessarily designed for (such as arithmetic) +can introduce latency and create more opportunity for errors. + +That's where A2UI's **client-side** functions come in. + +In this post, I'll walk through how client-side functions +allow an agent to delegate local operations directly to Dart +code running on a user's device, reducing the need for round-trips +and delivering more predictable results in your Flutter GenUI apps. + +--- + +## What client-side functions solve + +Sending raw prompts back and forth to an LLM for pure math is inefficient, +adding round-trip latency and consuming extra tokens. Instead, +client-side functions let the agent compute values like ingredient costs, +tax totals, or unit conversions locally on the device. + +Client-side functions solve this by creating a clean division of labor: + +1. **The Flutter Client** declares available client-side functions + in the GenUI catalog, informing the agent of which local operations + can be invoked, what parameters they expect, and what format they return. +1. **The LLM Agent** decides _when_ and _where_ a component should + be displayed and emits an A2UI expression calling the client function + with the necessary arguments (such as an item ID and quantity). +1. **The Flutter Client** evaluates the expression locally and executes + the math synchronously in Dart code, rendering a clean, + formatted result on screen right away. + +By offloading calculations to the client device, your application avoids +unnecessary network overhead, achieves consistent formatting, +and lowers overall token usage. + +```dart +sequenceDiagram + participant LLM as Gemini Agent + participant Client as Flutter App (genui) + participant Function as CalculateCost (Dart) + LLM->>Client: A2UI Payload with calculateCost(black_beans, 3) + Client->>Function: executeSync(args) + Function->>Client: "$2.97" + Client->>Client: Render Text widget ($2.97) +``` + +Let's look at how I implemented this pattern in a sample application +called **Commis**, an intelligent assistant built for commercial kitchens and catering teams. + +--- + +## Anatomy of the `CalculateCostFunction` class + +When preparing for an upcoming catering event, a chef might ask +questions about ingredients used in the menu. To calculate and +display ingredient costs reliably without waiting for server round-trips, +I created a client-side function called `CalculateCostFunction`. + +In the `genui` package, synchronous client-side functions extend +`SynchronousClientFunction`. Here is the complete implementation +from `client_functions.dart`: + +```dart +/// A client-side function that calculates the cost for an ingredient +/// directly on the device using local Dart logic. +class CalculateCostFunction extends SynchronousClientFunction { + const CalculateCostFunction(); + + // 1. The identifier referenced by the LLM in A2UI payloads. + @override + String get name => 'calculateCost'; + +// 2. Clear description provided to the LLM so it knows when + // and why to use the function. + @override + String get description => + 'Calculates the cost for a certain quantity of an ingredient. ' + 'Returns a formatted dollar string (for example, \$4.50).'; + + // 3. The expected return type for the binding. + @override + ClientFunctionReturnType get returnType => ClientFunctionReturnType.string; + + // 4. JSON Schema defining required input arguments. + @override + Schema get argumentSchema => S.object( + properties: { + 'ingredient_id': S.string(description: 'The ID of the ingredient.'), + 'quantity': S.number(description: 'The quantity of the ingredient.'), + }, + required: ['ingredient_id', 'quantity'], + ); + + // 5. Synchronous Dart execution logic on the client + @override + Object? executeSync(JsonMap args, ExecutionContext context) { + final ingredientId = args['ingredient_id'].toString(); + final quantity = num.tryParse(args['quantity'].toString())?.toDouble(); + + if (quantity == null || quantity < 1) { + return '\$0.00'; + } + + // Call the local cost service to fetch price and format as currency + final cost = CostService().fetchPrice(ingredientId, quantity); + return '\$${cost.toStringAsFixed(2)}'; + } +} +``` + +Let's break down the key parts of this class: + +1. `name`: The unique identifier (`calculateCost`) that the AI agent + uses when generating function calls inside A2UI payloads. +1. `description`: A concise explanation sent to the LLM so it knows + _when_ and _why_ to invoke this function and what output format to expect. +1. `returnType`: Specifies the data type returned by the function + (in this case, a string). +1. `argumentSchema`: Built using + [`json_schema_builder`](https://pub.dev/packages/json_schema_builder), + this schema informs the LLM exactly which parameters are required + (`ingredient_id` and `quantity`). +1. `executeSync`: The core Dart method executed on the user's device + when the UI renders. It parses the incoming arguments, + calls my local `CostService`, and returns the formatted dollar string. + +If you just noticed that this pattern looks very similar to the one used +for catalog entries for UI components, you're spot on! Both provide +metadata for the agent to use when reasoning, paired with Dart logic +that does something useful: either create widgets or, in this case, +calculate a value. + +--- + +## Catalog registration and System prompt integration + +To make the agent aware of `calculateCost`, I register it in my app's +GenUI `Catalog`. + +When instantiating `Catalog`, I pass `CalculateCostFunction()` into the +`functions` list alongside my UI components: + +```dart +// lib/ui/catalog/catalog.dart + +final commisCatalog = Catalog( + [ + cateringJobItem, + recipeLineCatalogItem, + ingredientLineCatalogItem, + navigationCardCatalogItem, + simpleCardCatalogItem, + ], + functions: [ + CalculateCostFunction(), // Here it is! + ], + catalogId: 'commis_catalog', +); +``` + +When initializing the conversation session, +`genui`'s `PromptBuilder` inspects the catalog and automatically +extracts all client function declarations, incorporating their names, +descriptions, and schemas into the system prompt provided to Gemini. + +With that in place, when a chef asks about recipe pricing, Gemini doesn't +try to guess or compute the dollar total. Instead, it emits an A2UI message +containing a call to calculateCost (in this case, for the price of three +cans of beans): + +```json +{ + "id": "cost_val", + "component": "Text", + "text": { + "call": "calculateCost", + "args": { + "ingredient_id": "black_beans", + "quantity": 3 + }, + "returnType": "string" + }, + "variant": "h2" +} +``` + +Notice how the value for the `call` property in the A2UI message matches +the function name (`calculateCost`) registered in the catalog. When the +`SurfaceController` receives this message, it evaluates `calculateCost` +locally on the device using `executeSync` and renders the accurate +dollar string (such as `$2.97`) on screen right away. + +--- + +## Fast developer iteration with Flutter Hot Reload + +Because client-side functions aren't locked behind a backend microservice +or cloud function deployment, working with them still feels like regular +old Dart. + +If I want to update currency formatting (for example, adding bulk discount +logic or switching from `$4.50` to USD `4.50`), I can simply edit +`executeSync` in Dart, save the file, and watch hot reload update +my app with the new result. + +--- + +## Summary and next steps + +Ready to try GenUI and client-side functions in your own apps? + +* Check out the official + [Intro to GenUI Codelab](https://codelabs.developers.google.com/codelabs/genui-intro#0) + to learn the basics of Generative UI. +* Explore the [`genui` package on pub.dev](https://pub.dev/packages/genui) + for API details and catalog definitions. +* Browse the + [**flutter/demos** repository on GitHub](https://github.com/flutter/demos) + to inspect the complete source code for Commis and other Dart GenUI samples. + +Happy building! diff --git a/sites/www/content/blog/authors_images/1uLWpn1jOuAz8_lEu1M9Srw.webp b/sites/www/content/blog/author_images/abd99.webp similarity index 100% rename from sites/www/content/blog/authors_images/1uLWpn1jOuAz8_lEu1M9Srw.webp rename to sites/www/content/blog/author_images/abd99.webp diff --git a/sites/www/content/blog/authors_images/1beM676ZukZSi32yoyuhqxw.jpeg b/sites/www/content/blog/author_images/abdallahshaban557.jpg similarity index 100% rename from sites/www/content/blog/authors_images/1beM676ZukZSi32yoyuhqxw.jpeg rename to sites/www/content/blog/author_images/abdallahshaban557.jpg diff --git a/sites/www/content/blog/authors_images/2AZjKy9ApAXsgYd9ElZSvlg.jpeg b/sites/www/content/blog/author_images/aguinis.jpg similarity index 100% rename from sites/www/content/blog/authors_images/2AZjKy9ApAXsgYd9ElZSvlg.jpeg rename to sites/www/content/blog/author_images/aguinis.jpg diff --git a/sites/www/content/blog/authors_images/1BgqwW36VvDjTOlmJk5VQ-A.jpeg b/sites/www/content/blog/author_images/amir_h.jpg similarity index 100% rename from sites/www/content/blog/authors_images/1BgqwW36VvDjTOlmJk5VQ-A.jpeg rename to sites/www/content/blog/author_images/amir_h.jpg diff --git a/sites/www/content/blog/authors_images/19QBbBTP_oiuZGZf0I49AKA.webp b/sites/www/content/blog/author_images/anderdobo.webp similarity index 100% rename from sites/www/content/blog/authors_images/19QBbBTP_oiuZGZf0I49AKA.webp rename to sites/www/content/blog/author_images/anderdobo.webp diff --git a/sites/www/content/blog/authors_images/0MWQEEWaslbr-PNFW.webp b/sites/www/content/blog/author_images/andremobilelabonte.webp similarity index 100% rename from sites/www/content/blog/authors_images/0MWQEEWaslbr-PNFW.webp rename to sites/www/content/blog/author_images/andremobilelabonte.webp diff --git a/sites/www/content/blog/authors_images/0Rcs4RmjLKoM8oYfZ.webp b/sites/www/content/blog/author_images/anjannarain.webp similarity index 100% rename from sites/www/content/blog/authors_images/0Rcs4RmjLKoM8oYfZ.webp rename to sites/www/content/blog/author_images/anjannarain.webp diff --git a/sites/www/content/blog/authors_images/1JXjnwMAb89ePR-FoV4-5Hg.jpeg b/sites/www/content/blog/author_images/antfitch.jpg similarity index 100% rename from sites/www/content/blog/authors_images/1JXjnwMAb89ePR-FoV4-5Hg.jpeg rename to sites/www/content/blog/author_images/antfitch.jpg diff --git a/sites/www/content/blog/authors_images/0tNv4NL4miSZG0eeS.jpg b/sites/www/content/blog/author_images/arbullard.jpg similarity index 100% rename from sites/www/content/blog/authors_images/0tNv4NL4miSZG0eeS.jpg rename to sites/www/content/blog/author_images/arbullard.jpg diff --git a/sites/www/content/blog/authors_images/2Iw2blcHT-LRD9lr8NTx8HQ.jpeg b/sites/www/content/blog/author_images/ayushbherwani1998.jpg similarity index 100% rename from sites/www/content/blog/authors_images/2Iw2blcHT-LRD9lr8NTx8HQ.jpeg rename to sites/www/content/blog/author_images/ayushbherwani1998.jpg diff --git a/sites/www/content/blog/authors_images/0Jzj4ct7CmcvOiR3g.webp b/sites/www/content/blog/author_images/bdero.webp similarity index 100% rename from sites/www/content/blog/authors_images/0Jzj4ct7CmcvOiR3g.webp rename to sites/www/content/blog/author_images/bdero.webp diff --git a/sites/www/content/blog/authors_images/1d1hf9rmd3uJL8yb1sPMIDA.webp b/sites/www/content/blog/author_images/bkonyi.webp similarity index 100% rename from sites/www/content/blog/authors_images/1d1hf9rmd3uJL8yb1sPMIDA.webp rename to sites/www/content/blog/author_images/bkonyi.webp diff --git a/sites/www/content/blog/authors_images/1nFmOI6gpDMxUgSzNIZ19Kg.webp b/sites/www/content/blog/author_images/brandonbadger.webp similarity index 100% rename from sites/www/content/blog/authors_images/1nFmOI6gpDMxUgSzNIZ19Kg.webp rename to sites/www/content/blog/author_images/brandonbadger.webp diff --git a/sites/www/content/blog/authors_images/0xM4OOQfKuCiHd5rI.webp b/sites/www/content/blog/author_images/chinmaygarde.webp similarity index 100% rename from sites/www/content/blog/authors_images/0xM4OOQfKuCiHd5rI.webp rename to sites/www/content/blog/author_images/chinmaygarde.webp diff --git a/sites/www/content/blog/authors_images/1fcJn7cFYnwInCzoug_PoDA.jpeg b/sites/www/content/blog/author_images/clocksmith.jpg similarity index 100% rename from sites/www/content/blog/authors_images/1fcJn7cFYnwInCzoug_PoDA.jpeg rename to sites/www/content/blog/author_images/clocksmith.jpg diff --git a/sites/www/content/blog/authors_images/1xu-sgiT2AZrrzBVu1oYnoQ.jpeg b/sites/www/content/blog/author_images/craiglabenz.jpg similarity index 100% rename from sites/www/content/blog/authors_images/1xu-sgiT2AZrrzBVu1oYnoQ.jpeg rename to sites/www/content/blog/author_images/craiglabenz.jpg diff --git a/sites/www/content/blog/authors_images/06LK838dGSbGJRtat.webp b/sites/www/content/blog/author_images/csells.webp similarity index 100% rename from sites/www/content/blog/authors_images/06LK838dGSbGJRtat.webp rename to sites/www/content/blog/author_images/csells.webp diff --git a/sites/www/content/blog/authors_images/1Wio_YseYYApcaC6FSU52aQ.webp b/sites/www/content/blog/author_images/devoncarew.webp similarity index 100% rename from sites/www/content/blog/authors_images/1Wio_YseYYApcaC6FSU52aQ.webp rename to sites/www/content/blog/author_images/devoncarew.webp diff --git a/sites/www/content/blog/authors_images/0R7VUzyb9_HQm9afP.jpg b/sites/www/content/blog/author_images/domesticmouse.jpg similarity index 100% rename from sites/www/content/blog/authors_images/0R7VUzyb9_HQm9afP.jpg rename to sites/www/content/blog/author_images/domesticmouse.jpg diff --git a/sites/www/content/blog/authors_images/1f-mpgT2KL1I-4NugTuv2Rg.jpeg b/sites/www/content/blog/author_images/efortuna.jpg similarity index 100% rename from sites/www/content/blog/authors_images/1f-mpgT2KL1I-4NugTuv2Rg.jpeg rename to sites/www/content/blog/author_images/efortuna.jpg diff --git a/sites/www/content/blog/authors_images/1_vDVbsLisergcnpUiXcPRQ.webp b/sites/www/content/blog/author_images/ericwindmill.webp similarity index 100% rename from sites/www/content/blog/authors_images/1_vDVbsLisergcnpUiXcPRQ.webp rename to sites/www/content/blog/author_images/ericwindmill.webp diff --git a/sites/www/content/blog/authors_images/1X-6ifCWQoKYCZ-uLGdPa4w.webp b/sites/www/content/blog/author_images/esouthren.webp similarity index 100% rename from sites/www/content/blog/authors_images/1X-6ifCWQoKYCZ-uLGdPa4w.webp rename to sites/www/content/blog/author_images/esouthren.webp diff --git a/sites/www/content/blog/authors_images/1tTlm5zfgYWqtvLbIzUByLQ.webp b/sites/www/content/blog/author_images/ezra-sandzer-bell.webp similarity index 100% rename from sites/www/content/blog/authors_images/1tTlm5zfgYWqtvLbIzUByLQ.webp rename to sites/www/content/blog/author_images/ezra-sandzer-bell.webp diff --git a/sites/www/content/blog/authors_images/1dZx7Dxmr4VXmjc5U8yDf3w.jpeg b/sites/www/content/blog/author_images/filiph.jpg similarity index 100% rename from sites/www/content/blog/authors_images/1dZx7Dxmr4VXmjc5U8yDf3w.jpeg rename to sites/www/content/blog/author_images/filiph.jpg diff --git a/sites/www/content/blog/authors_images/2wcnmZEJVlZsf2nNvDOziRA.jpeg b/sites/www/content/blog/author_images/fitzface.jpg similarity index 100% rename from sites/www/content/blog/authors_images/2wcnmZEJVlZsf2nNvDOziRA.jpeg rename to sites/www/content/blog/author_images/fitzface.jpg diff --git a/sites/www/content/blog/authors_images/0ALqtMy61v8ANAVWO.webp b/sites/www/content/blog/author_images/gaaclarke.webp similarity index 100% rename from sites/www/content/blog/authors_images/0ALqtMy61v8ANAVWO.webp rename to sites/www/content/blog/author_images/gaaclarke.webp diff --git a/sites/www/content/blog/authors_images/1QrcTxiCCIAj38rUnnPTryg.jpeg b/sites/www/content/blog/author_images/gskinner.jpg similarity index 100% rename from sites/www/content/blog/authors_images/1QrcTxiCCIAj38rUnnPTryg.jpeg rename to sites/www/content/blog/author_images/gskinner.jpg diff --git a/sites/www/content/blog/authors_images/0gNIf0pPqSsP1oOcM.jpg b/sites/www/content/blog/author_images/haddadniaj.jpg similarity index 100% rename from sites/www/content/blog/authors_images/0gNIf0pPqSsP1oOcM.jpg rename to sites/www/content/blog/author_images/haddadniaj.jpg diff --git a/sites/www/content/blog/authors_images/09ombi9JukiF7Z1-d.webp b/sites/www/content/blog/author_images/hansmuller.webp similarity index 100% rename from sites/www/content/blog/authors_images/09ombi9JukiF7Z1-d.webp rename to sites/www/content/blog/author_images/hansmuller.webp diff --git a/sites/www/content/blog/authors_images/0ob8GIJbyVIHe-aQu.jpg b/sites/www/content/blog/author_images/harryterkelsen.jpg similarity index 100% rename from sites/www/content/blog/authors_images/0ob8GIJbyVIHe-aQu.jpg rename to sites/www/content/blog/author_images/harryterkelsen.jpg diff --git a/sites/www/content/blog/authors_images/1EAeJ6vonuQM5daZCF9CfeQ.jpeg b/sites/www/content/blog/author_images/hellobrianjames.jpg similarity index 100% rename from sites/www/content/blog/authors_images/1EAeJ6vonuQM5daZCF9CfeQ.jpeg rename to sites/www/content/blog/author_images/hellobrianjames.jpg diff --git a/sites/www/content/blog/authors_images/1HEfsynQuQpVrGR-qdGeSvg.jpeg b/sites/www/content/blog/author_images/itsjustkevin.jpg similarity index 100% rename from sites/www/content/blog/authors_images/1HEfsynQuQpVrGR-qdGeSvg.jpeg rename to sites/www/content/blog/author_images/itsjustkevin.jpg diff --git a/sites/www/content/blog/authors_images/0SQ_vs0LMPjY6ejrP.webp b/sites/www/content/blog/author_images/ivan.webp similarity index 100% rename from sites/www/content/blog/authors_images/0SQ_vs0LMPjY6ejrP.webp rename to sites/www/content/blog/author_images/ivan.webp diff --git a/sites/www/content/blog/authors_images/109ekgauj46kjGlHoi94FwA.jpeg b/sites/www/content/blog/author_images/jayoung-lee.jpg similarity index 100% rename from sites/www/content/blog/authors_images/109ekgauj46kjGlHoi94FwA.jpeg rename to sites/www/content/blog/author_images/jayoung-lee.jpg diff --git a/sites/www/content/blog/authors_images/0Qp4aTK0f2iGxt-UF.webp b/sites/www/content/blog/author_images/jensjohansen.webp similarity index 100% rename from sites/www/content/blog/authors_images/0Qp4aTK0f2iGxt-UF.webp rename to sites/www/content/blog/author_images/jensjohansen.webp diff --git a/sites/www/content/blog/authors_images/1yJ9UamxmYZ_VwVDeM4mDzA.jpeg b/sites/www/content/blog/author_images/jjmutter.jpg similarity index 100% rename from sites/www/content/blog/authors_images/1yJ9UamxmYZ_VwVDeM4mDzA.jpeg rename to sites/www/content/blog/author_images/jjmutter.jpg diff --git a/sites/www/content/blog/authors_images/1XqSzX8F7rKZnrHwIVIr71A.jpeg b/sites/www/content/blog/author_images/johnpryan.jpg similarity index 100% rename from sites/www/content/blog/authors_images/1XqSzX8F7rKZnrHwIVIr71A.jpeg rename to sites/www/content/blog/author_images/johnpryan.jpg diff --git a/sites/www/content/blog/authors_images/0hPHb0U1EVsdSI-ZK.jpg b/sites/www/content/blog/author_images/josealba.jpg similarity index 100% rename from sites/www/content/blog/authors_images/0hPHb0U1EVsdSI-ZK.jpg rename to sites/www/content/blog/author_images/josealba.jpg diff --git a/sites/www/content/blog/authors_images/16L6xLJHqC5vlGBrTFHc6xg.jpeg b/sites/www/content/blog/author_images/justinmc.jpg similarity index 100% rename from sites/www/content/blog/authors_images/16L6xLJHqC5vlGBrTFHc6xg.jpeg rename to sites/www/content/blog/author_images/justinmc.jpg diff --git a/sites/www/content/blog/authors_images/0VdlGNyd_YGGPFkrc.jpg b/sites/www/content/blog/author_images/jwren.jpg similarity index 100% rename from sites/www/content/blog/authors_images/0VdlGNyd_YGGPFkrc.jpg rename to sites/www/content/blog/author_images/jwren.jpg diff --git a/sites/www/content/blog/authors_images/06siHzzkrp68l6H-V.jpg b/sites/www/content/blog/author_images/kenzieschmoll.jpg similarity index 100% rename from sites/www/content/blog/authors_images/06siHzzkrp68l6H-V.jpg rename to sites/www/content/blog/author_images/kenzieschmoll.jpg diff --git a/sites/www/content/blog/authors_images/1iUVmcyWC2Pj7ERaoSF-Onw.jpeg b/sites/www/content/blog/author_images/kevmoo.jpg similarity index 100% rename from sites/www/content/blog/authors_images/1iUVmcyWC2Pj7ERaoSF-Onw.jpeg rename to sites/www/content/blog/author_images/kevmoo.jpg diff --git a/sites/www/content/blog/authors_images/2stkoPD1JQ5tooR-s76YSmw.jpeg b/sites/www/content/blog/author_images/kf6gpe.jpg similarity index 100% rename from sites/www/content/blog/authors_images/2stkoPD1JQ5tooR-s76YSmw.jpeg rename to sites/www/content/blog/author_images/kf6gpe.jpg diff --git a/sites/www/content/blog/authors_images/1Nmt27LItpgVqSBTY8WIPwg.jpeg b/sites/www/content/blog/author_images/khanhnwin.jpg similarity index 100% rename from sites/www/content/blog/authors_images/1Nmt27LItpgVqSBTY8WIPwg.jpeg rename to sites/www/content/blog/author_images/khanhnwin.jpg diff --git a/sites/www/content/blog/authors_images/0IxpUINuAJhraX2My.jpg b/sites/www/content/blog/author_images/kkboateng.jpg similarity index 100% rename from sites/www/content/blog/authors_images/0IxpUINuAJhraX2My.jpg rename to sites/www/content/blog/author_images/kkboateng.jpg diff --git a/sites/www/content/blog/authors_images/1vRMW7uCsV7VsqqC6dIghNA.jpeg b/sites/www/content/blog/author_images/ktjlee.jpg similarity index 100% rename from sites/www/content/blog/authors_images/1vRMW7uCsV7VsqqC6dIghNA.jpeg rename to sites/www/content/blog/author_images/ktjlee.jpg diff --git a/sites/www/content/blog/authors_images/0z8A7a6gvtNuF-TxZ.jpg b/sites/www/content/blog/author_images/lamek.jpg similarity index 100% rename from sites/www/content/blog/authors_images/0z8A7a6gvtNuF-TxZ.jpg rename to sites/www/content/blog/author_images/lamek.jpg diff --git a/sites/www/content/blog/authors_images/12P-T3BTzLSSPrvomuK_oRQ.webp b/sites/www/content/blog/author_images/leighajarett.webp similarity index 100% rename from sites/www/content/blog/authors_images/12P-T3BTzLSSPrvomuK_oRQ.webp rename to sites/www/content/blog/author_images/leighajarett.webp diff --git a/sites/www/content/blog/authors_images/1T1lv65-OBsGrBSs6mvgvqg.jpeg b/sites/www/content/blog/author_images/liyuqian.jpg similarity index 100% rename from sites/www/content/blog/authors_images/1T1lv65-OBsGrBSs6mvgvqg.jpeg rename to sites/www/content/blog/author_images/liyuqian.jpg diff --git a/sites/www/content/blog/author_images/louisehsu.jpg b/sites/www/content/blog/author_images/louisehsu.jpg new file mode 100644 index 00000000000..1598b8b0963 Binary files /dev/null and b/sites/www/content/blog/author_images/louisehsu.jpg differ diff --git a/sites/www/content/blog/authors_images/1osRrOD-rpUZlVPFwpn2_GQ.webp b/sites/www/content/blog/author_images/lukeaf.webp similarity index 100% rename from sites/www/content/blog/authors_images/1osRrOD-rpUZlVPFwpn2_GQ.webp rename to sites/www/content/blog/author_images/lukeaf.webp diff --git a/sites/www/content/blog/authors_images/1N3hr8E92Mseta1S7yCX2Mg.webp b/sites/www/content/blog/author_images/magder.webp similarity index 100% rename from sites/www/content/blog/authors_images/1N3hr8E92Mseta1S7yCX2Mg.webp rename to sites/www/content/blog/author_images/magder.webp diff --git a/sites/www/content/blog/authors_images/1TDttyOKULZENb7QG-0Wl3w.jpeg b/sites/www/content/blog/author_images/mariam_hasnany.jpg similarity index 100% rename from sites/www/content/blog/authors_images/1TDttyOKULZENb7QG-0Wl3w.jpeg rename to sites/www/content/blog/author_images/mariam_hasnany.jpg diff --git a/sites/www/content/blog/authors_images/19Ub8P3iIx6Ji3hUUrSCZlg.webp b/sites/www/content/blog/author_images/maryx.webp similarity index 100% rename from sites/www/content/blog/authors_images/19Ub8P3iIx6Ji3hUUrSCZlg.webp rename to sites/www/content/blog/author_images/maryx.webp diff --git a/sites/www/content/blog/authors_images/2j6fhR3Uq9ariRWaoB1MGPg.webp b/sites/www/content/blog/author_images/matthew-carroll.webp similarity index 100% rename from sites/www/content/blog/authors_images/2j6fhR3Uq9ariRWaoB1MGPg.webp rename to sites/www/content/blog/author_images/matthew-carroll.webp diff --git a/sites/www/content/blog/author_images/mattkae.webp b/sites/www/content/blog/author_images/mattkae.webp new file mode 100644 index 00000000000..1c1c417f28b Binary files /dev/null and b/sites/www/content/blog/author_images/mattkae.webp differ diff --git a/sites/www/content/blog/authors_images/1snI23_LOekhWjI-XVYtHmQ.jpeg b/sites/www/content/blog/author_images/mehmetf.jpg similarity index 100% rename from sites/www/content/blog/authors_images/1snI23_LOekhWjI-XVYtHmQ.jpeg rename to sites/www/content/blog/author_images/mehmetf.jpg diff --git a/sites/www/content/blog/authors_images/1NC_dwnZV11nnMuK4D0mXYA.jpeg b/sites/www/content/blog/author_images/melyndahoover.jpg similarity index 100% rename from sites/www/content/blog/authors_images/1NC_dwnZV11nnMuK4D0mXYA.jpeg rename to sites/www/content/blog/author_images/melyndahoover.jpg diff --git a/sites/www/content/blog/authors_images/1WKhF9u7jo7OkE26I5PjGiw.webp b/sites/www/content/blog/author_images/mhclin113.webp similarity index 100% rename from sites/www/content/blog/authors_images/1WKhF9u7jo7OkE26I5PjGiw.webp rename to sites/www/content/blog/author_images/mhclin113.webp diff --git a/sites/www/content/blog/authors_images/0Y_CFLc1qadgr3tPK.jpg b/sites/www/content/blog/author_images/mit-mit.jpg similarity index 100% rename from sites/www/content/blog/authors_images/0Y_CFLc1qadgr3tPK.jpg rename to sites/www/content/blog/author_images/mit-mit.jpg diff --git a/sites/www/content/blog/authors_images/0cgL8XWJBpDSy2mHs.jpg b/sites/www/content/blog/author_images/mjohnsullivan.jpg similarity index 100% rename from sites/www/content/blog/authors_images/0cgL8XWJBpDSy2mHs.jpg rename to sites/www/content/blog/author_images/mjohnsullivan.jpg diff --git a/sites/www/content/blog/authors_images/1HcMDVJ2lxSQDClP1IUCGzg.jpeg b/sites/www/content/blog/author_images/mravn.jpg similarity index 100% rename from sites/www/content/blog/authors_images/1HcMDVJ2lxSQDClP1IUCGzg.jpeg rename to sites/www/content/blog/author_images/mravn.jpg diff --git a/sites/www/content/blog/authors_images/2G__H_oe3299HvlPy7YkpKg.webp b/sites/www/content/blog/author_images/nikkitagandhi.webp similarity index 100% rename from sites/www/content/blog/authors_images/2G__H_oe3299HvlPy7YkpKg.webp rename to sites/www/content/blog/author_images/nikkitagandhi.webp diff --git a/sites/www/content/blog/authors_images/1Yh3FMfH210BvJATo22X4Aw.jpeg b/sites/www/content/blog/author_images/parlough.jpg similarity index 100% rename from sites/www/content/blog/authors_images/1Yh3FMfH210BvJATo22X4Aw.jpeg rename to sites/www/content/blog/author_images/parlough.jpg diff --git a/sites/www/content/blog/authors_images/2d-UpSVU0saEV2On5JBvYaQ.jpeg b/sites/www/content/blog/author_images/perclasson.jpg similarity index 100% rename from sites/www/content/blog/authors_images/2d-UpSVU0saEV2On5JBvYaQ.jpeg rename to sites/www/content/blog/author_images/perclasson.jpg diff --git a/sites/www/content/blog/authors_images/00yA7OjxKWthRwjK2.webp b/sites/www/content/blog/author_images/piinks.webp similarity index 100% rename from sites/www/content/blog/authors_images/00yA7OjxKWthRwjK2.webp rename to sites/www/content/blog/author_images/piinks.webp diff --git a/sites/www/content/blog/authors_images/0UfXdwppirgDYRNaj.webp b/sites/www/content/blog/author_images/redbrogdon.webp similarity index 100% rename from sites/www/content/blog/authors_images/0UfXdwppirgDYRNaj.webp rename to sites/www/content/blog/author_images/redbrogdon.webp diff --git a/sites/www/content/blog/authors_images/1w7Y-bThACd0V1YJ6N7QePg.webp b/sites/www/content/blog/author_images/rich_hall.webp similarity index 100% rename from sites/www/content/blog/authors_images/1w7Y-bThACd0V1YJ6N7QePg.webp rename to sites/www/content/blog/author_images/rich_hall.webp diff --git a/sites/www/content/blog/authors_images/rodydavis.jpg b/sites/www/content/blog/author_images/rodydavis.jpg similarity index 100% rename from sites/www/content/blog/authors_images/rodydavis.jpg rename to sites/www/content/blog/author_images/rodydavis.jpg diff --git a/sites/www/content/blog/authors_images/0m23OLfjD-pystWVb.jpg b/sites/www/content/blog/author_images/scheglov.jpg similarity index 100% rename from sites/www/content/blog/authors_images/0m23OLfjD-pystWVb.jpg rename to sites/www/content/blog/author_images/scheglov.jpg diff --git a/sites/www/content/blog/authors_images/10pSgo-h67AMv_gytSDEiJg.webp b/sites/www/content/blog/author_images/sethladd.webp similarity index 100% rename from sites/www/content/blog/authors_images/10pSgo-h67AMv_gytSDEiJg.webp rename to sites/www/content/blog/author_images/sethladd.webp diff --git a/sites/www/content/blog/authors_images/0LHz-9FgHd3doKgCM.jpg b/sites/www/content/blog/author_images/sfshaza.jpg similarity index 100% rename from sites/www/content/blog/authors_images/0LHz-9FgHd3doKgCM.jpg rename to sites/www/content/blog/author_images/sfshaza.jpg diff --git a/sites/www/content/blog/authors_images/0vHty7_58DD6eW53D.webp b/sites/www/content/blog/author_images/shamiramarshall.webp similarity index 100% rename from sites/www/content/blog/authors_images/0vHty7_58DD6eW53D.webp rename to sites/www/content/blog/author_images/shamiramarshall.webp diff --git a/sites/www/content/blog/authors_images/0Qgrum-sTozr6lXCm.jpg b/sites/www/content/blog/author_images/swavkulinski.jpg similarity index 100% rename from sites/www/content/blog/authors_images/0Qgrum-sTozr6lXCm.jpg rename to sites/www/content/blog/author_images/swavkulinski.jpg diff --git a/sites/www/content/blog/authors_images/0jbYlQ-K9Yh-HVUBp.jpg b/sites/www/content/blog/author_images/taodong.jpg similarity index 100% rename from sites/www/content/blog/authors_images/0jbYlQ-K9Yh-HVUBp.jpg rename to sites/www/content/blog/author_images/taodong.jpg diff --git a/sites/www/content/blog/authors_images/0cNAKYVA08TeS6TVB.webp b/sites/www/content/blog/author_images/theaflowers.webp similarity index 100% rename from sites/www/content/blog/authors_images/0cNAKYVA08TeS6TVB.webp rename to sites/www/content/blog/author_images/theaflowers.webp diff --git a/sites/www/content/blog/authors_images/0xV6b-152TcEMSjUr.webp b/sites/www/content/blog/author_images/tianguang.webp similarity index 100% rename from sites/www/content/blog/authors_images/0xV6b-152TcEMSjUr.webp rename to sites/www/content/blog/author_images/tianguang.webp diff --git a/sites/www/content/blog/authors_images/1krC2R3xE1FCZTdIbXQDT_A.jpeg b/sites/www/content/blog/author_images/timothyhoang.jpg similarity index 100% rename from sites/www/content/blog/authors_images/1krC2R3xE1FCZTdIbXQDT_A.jpeg rename to sites/www/content/blog/author_images/timothyhoang.jpg diff --git a/sites/www/content/blog/authors_images/14HRhyWLOwtZEN7MQ8zLuHA.jpeg b/sites/www/content/blog/author_images/timsneath.jpg similarity index 100% rename from sites/www/content/blog/authors_images/14HRhyWLOwtZEN7MQ8zLuHA.jpeg rename to sites/www/content/blog/author_images/timsneath.jpg diff --git a/sites/www/content/blog/authors_images/15Yq8VeVDQqM51HKi50phdg.jpeg b/sites/www/content/blog/author_images/tomayac.jpg similarity index 100% rename from sites/www/content/blog/authors_images/15Yq8VeVDQqM51HKi50phdg.jpeg rename to sites/www/content/blog/author_images/tomayac.jpg diff --git a/sites/www/content/blog/authors_images/0EUSBYlBzvqL9j_Xr.webp b/sites/www/content/blog/author_images/tvolkert.webp similarity index 100% rename from sites/www/content/blog/authors_images/0EUSBYlBzvqL9j_Xr.webp rename to sites/www/content/blog/author_images/tvolkert.webp diff --git a/sites/www/content/blog/authors_images/0lw25USWxDjT-i0kV.jpg b/sites/www/content/blog/author_images/twerske.jpg similarity index 100% rename from sites/www/content/blog/authors_images/0lw25USWxDjT-i0kV.jpg rename to sites/www/content/blog/author_images/twerske.jpg diff --git a/sites/www/content/blog/authors_images/1XUX_mTFnhm4BKyT9q9sOUQ.webp b/sites/www/content/blog/author_images/verygoodopensource.webp similarity index 100% rename from sites/www/content/blog/authors_images/1XUX_mTFnhm4BKyT9q9sOUQ.webp rename to sites/www/content/blog/author_images/verygoodopensource.webp diff --git a/sites/www/content/blog/authors_images/0c8OAb9nT35XgY56T.jpg b/sites/www/content/blog/author_images/wmleler.jpg similarity index 100% rename from sites/www/content/blog/authors_images/0c8OAb9nT35XgY56T.jpg rename to sites/www/content/blog/author_images/wmleler.jpg diff --git a/sites/www/content/blog/authors_images/1h_lwkzhaAtW5WTkM750_Xw.jpeg b/sites/www/content/blog/author_images/xster.jpg similarity index 100% rename from sites/www/content/blog/authors_images/1h_lwkzhaAtW5WTkM750_Xw.jpeg rename to sites/www/content/blog/author_images/xster.jpg diff --git a/sites/www/content/blog/authors_images/0PSrFnoWOMMa0YZUM.jpg b/sites/www/content/blog/author_images/yanchenum.jpg similarity index 100% rename from sites/www/content/blog/authors_images/0PSrFnoWOMMa0YZUM.jpg rename to sites/www/content/blog/author_images/yanchenum.jpg diff --git a/sites/www/content/blog/authors_images/08pUBYXbTsN324CAB.jpg b/sites/www/content/blog/author_images/yegorj.jpg similarity index 100% rename from sites/www/content/blog/authors_images/08pUBYXbTsN324CAB.jpg rename to sites/www/content/blog/author_images/yegorj.jpg diff --git a/sites/www/content/blog/authors_images/2Cl8MkXie_Fr91CEmLGzQtw.jpeg b/sites/www/content/blog/author_images/zoeyfan.jpg similarity index 100% rename from sites/www/content/blog/authors_images/2Cl8MkXie_Fr91CEmLGzQtw.jpeg rename to sites/www/content/blog/author_images/zoeyfan.jpg diff --git a/sites/www/content/blog/authors.yaml b/sites/www/content/blog/authors.yaml index 886bdf4c29d..51a3b0bce93 100644 --- a/sites/www/content/blog/authors.yaml +++ b/sites/www/content/blog/authors.yaml @@ -4,279 +4,283 @@ # # Authors should have: # - name: The author's display name (required). -# - image: The author's image file, located in /content/blog/authors_images/ (optional). +# - image: The author's image file, located in /content/blog/author_images/ (optional). # - imageUrl: A url to an image for the author, as an alternative to a local image (optional). # - link: A link to their GitHub, social profile, or personal website (optional). abd99: name: "Abdullah Deshmukh" - image: "1uLWpn1jOuAz8_lEu1M9Srw.webp" + image: "abd99.webp" link: "https://github.com/abd99" abdallahshaban557: name: "Abdallah W Shaban" - image: "1beM676ZukZSi32yoyuhqxw.jpeg" + image: "abdallahshaban557.jpg" link: "https://github.com/abdallahshaban557" aguinis: name: "Martin Aguinis" - image: "2AZjKy9ApAXsgYd9ElZSvlg.jpeg" + image: "aguinis.jpg" link: "https://github.com/maguinis" amir_h: name: "Amir Hardon" - image: "1BgqwW36VvDjTOlmJk5VQ-A.jpeg" + image: "amir_h.jpg" link: "https://twitter.com/AmirHardon" anderdobo: name: "Ander Dobo" - image: "19QBbBTP_oiuZGZf0I49AKA.webp" + image: "anderdobo.webp" link: "https://github.com/anderdobo" andremobilelabonte: name: "André Labonté" - image: "0MWQEEWaslbr-PNFW.webp" + image: "andremobilelabonte.webp" link: "https://github.com/andrelabonte" anjannarain: name: "Anjan Narain" - image: "0Rcs4RmjLKoM8oYfZ.webp" + image: "anjannarain.webp" link: null antfitch: name: "Amanda Fitch" - image: "1JXjnwMAb89ePR-FoV4-5Hg.jpeg" + image: "antfitch.jpg" link: "https://github.com/antfitch" arbullard: name: "Anthony Bullard" - image: "0tNv4NL4miSZG0eeS.jpg" + image: "arbullard.jpg" link: "https://github.com/gamebox" ayushbherwani1998: name: "Ayush Bherwani" - image: "2Iw2blcHT-LRD9lr8NTx8HQ.jpeg" + image: "ayushbherwani1998.jpg" link: "https://github.com/AyushBherwani1998" bdero: name: "Brandon DeRosier" - image: "0Jzj4ct7CmcvOiR3g.webp" + image: "bdero.webp" link: "https://github.com/bdero" bkonyi: name: "Ben Konyi" - image: "1d1hf9rmd3uJL8yb1sPMIDA.webp" + image: "bkonyi.webp" link: "https://github.com/bkonyi" brandonbadger: name: "Brandon Badger" - image: "1nFmOI6gpDMxUgSzNIZ19Kg.webp" + image: "brandonbadger.webp" link: null chinmaygarde: name: "Chinmay Garde" - image: "0xM4OOQfKuCiHd5rI.webp" + image: "chinmaygarde.webp" link: "https://github.com/chinmaygarde" clocksmith: name: "Anthony Robledo" - image: "1fcJn7cFYnwInCzoug_PoDA.jpeg" + image: "clocksmith.jpg" link: "https://github.com/clocksmith" craiglabenz: name: "Craig Labenz" - image: "1PpHwzypsYF2ce75cfkt0_w.webp" + image: "craiglabenz.jpg" link: "https://github.com/craiglabenz" csells: name: "Chris Sells" - image: "06LK838dGSbGJRtat.webp" + image: "csells.webp" link: "https://github.com/csells" devoncarew: name: "Devon Carew" - image: "1Wio_YseYYApcaC6FSU52aQ.webp" + image: "devoncarew.webp" link: "https://github.com/devoncarew" domesticmouse: name: "Brett Morgan" - image: "0R7VUzyb9_HQm9afP.jpg" + image: "domesticmouse.jpg" link: "https://github.com/domesticmouse" efortuna: name: "Emily Fortuna" - image: "1f-mpgT2KL1I-4NugTuv2Rg.jpeg" + image: "efortuna.jpg" link: "https://github.com/efortuna" ericwindmill: name: "Eric Windmill" - image: "1_vDVbsLisergcnpUiXcPRQ.webp" + image: "ericwindmill.webp" link: "https://github.com/ericwindmill" esouthren: name: "Eilidh Southren" - image: "1X-6ifCWQoKYCZ-uLGdPa4w.webp" + image: "esouthren.webp" link: "https://github.com/esouthren" ezra-sandzer-bell: name: "Ezra Sandzer-Bell" - image: "1tTlm5zfgYWqtvLbIzUByLQ.webp" + image: "ezra-sandzer-bell.webp" link: "https://twitter.com/EzraSandzer" filiph: name: "Filip Hracek" - image: "1dZx7Dxmr4VXmjc5U8yDf3w.jpeg" + image: "filiph.jpg" link: "https://github.com/filiph" fitzface: name: "Andrew Fitz Gibbon" - image: "2wcnmZEJVlZsf2nNvDOziRA.jpeg" + image: "fitzface.jpg" link: null gaaclarke: name: "Aaron Clarke" - image: "0ALqtMy61v8ANAVWO.webp" + image: "gaaclarke.webp" link: "https://github.com/gaaclarke" gskinner: name: "Grant Skinner" - image: "1QrcTxiCCIAj38rUnnPTryg.jpeg" + image: "gskinner.jpg" link: "https://github.com/gskinner" guidez: name: "Pierre-Louis Guidez" link: "https://github.com/guidezpl" haddadniaj: name: "Justin Haddadnia" - image: "0gNIf0pPqSsP1oOcM.jpg" + image: "haddadniaj.jpg" link: "https://github.com/Haddadnia" hansmuller: name: "Hans Muller" - image: "09ombi9JukiF7Z1-d.webp" + image: "hansmuller.webp" link: "https://github.com/HansMuller" harryterkelsen: name: "Harry Terkelsen" - image: "0ob8GIJbyVIHe-aQu.jpg" + image: "harryterkelsen.jpg" link: "https://github.com/harryterkelsen" hellobrianjames: name: "Brian James" - image: "1EAeJ6vonuQM5daZCF9CfeQ.jpeg" + image: "hellobrianjames.jpg" link: "https://github.com/malloc-error" itsjustkevin: name: "Kevin Chisholm" - image: "1HEfsynQuQpVrGR-qdGeSvg.jpeg" + image: "itsjustkevin.jpg" link: "https://github.com/itsjustkevin" ivan: name: "Ivan Leider" - image: "0SQ_vs0LMPjY6ejrP.webp" + image: "ivan.webp" link: "https://github.com/ileider" jayoung-lee: name: "JaYoung Lee" - image: "109ekgauj46kjGlHoi94FwA.jpeg" + image: "jayoung-lee.jpg" link: "https://github.com/jayoung-lee" jensjohansen: name: "Jens Johansen" - image: "0Qp4aTK0f2iGxt-UF.webp" + image: "jensjohansen.webp" link: "https://github.com/jensjohansen" jjmutter: name: "John Mutter" - image: "1yJ9UamxmYZ_VwVDeM4mDzA.jpeg" + image: "jjmutter.jpg" link: null johnpryan: name: "John Ryan" - image: "1XqSzX8F7rKZnrHwIVIr71A.jpeg" + image: "johnpryan.jpg" link: "https://github.com/johnpryan" josealba: name: "Jose Alba" - image: "0hPHb0U1EVsdSI-ZK.jpg" + image: "josealba.jpg" link: "https://github.com/JoseAlba" justinmc: name: "Justin McCandless" - image: "16L6xLJHqC5vlGBrTFHc6xg.jpeg" + image: "justinmc.jpg" link: "https://github.com/justinmc" jwren: name: "Jaime Wren" - image: "0VdlGNyd_YGGPFkrc.jpg" + image: "jwren.jpg" link: "https://github.com/jwren" kenzieschmoll: name: "Kenzie Davisson" - image: "06siHzzkrp68l6H-V.jpg" + image: "kenzieschmoll.jpg" link: "https://github.com/kenzieschmoll" kevmoo: name: "Kevin Moore" - image: "1iUVmcyWC2Pj7ERaoSF-Onw.jpeg" + image: "kevmoo.jpg" link: "https://github.com/kevmoo" kf6gpe: name: "Ray Rischpater, KF6GPE" - image: "2stkoPD1JQ5tooR-s76YSmw.jpeg" + image: "kf6gpe.jpg" link: "https://github.com/kf6gpe" khanhnwin: name: "Khanh Nguyen" - image: "1Nmt27LItpgVqSBTY8WIPwg.jpeg" + image: "khanhnwin.jpg" link: "https://github.com/khanhnwin" kkboateng: name: "Kelvin Boateng" - image: "0IxpUINuAJhraX2My.jpg" + image: "kkboateng.jpg" link: "https://github.com/kkboateng" ktjlee: name: "Katie Lee" - image: "1vRMW7uCsV7VsqqC6dIghNA.jpeg" + image: "ktjlee.jpg" link: null lamek: name: "Kevin Lamenzo" - image: "0z8A7a6gvtNuF-TxZ.jpg" + image: "lamek.jpg" link: "https://github.com/lamek" leighajarett: name: "Leigha Jarett" - image: "12P-T3BTzLSSPrvomuK_oRQ.webp" + image: "leighajarett.webp" link: "https://github.com/leighajarett" liyuqian: name: "Yuqian Li" - image: "1T1lv65-OBsGrBSs6mvgvqg.jpeg" + image: "liyuqian.jpg" link: "https://github.com/liyuqian" +louisehsu: + name: "Louise Hsu" + image: "louisehsu.jpg" + link: "https://github.com/LouiseHsu" lukeaf: name: "Luke Freeman" - image: "1osRrOD-rpUZlVPFwpn2_GQ.webp" + image: "lukeaf.webp" link: "https://twitter.com/lukeaf" magder: name: "Jenn Magder" - image: "1N3hr8E92Mseta1S7yCX2Mg.webp" + image: "magder.webp" link: "https://github.com/magder" mariam_hasnany: name: "Mariam Hasnany" - image: "1TDttyOKULZENb7QG-0Wl3w.jpeg" + image: "mariam_hasnany.jpg" link: "https://github.com/mariamhas" maryx: name: "Mary" - image: "19Ub8P3iIx6Ji3hUUrSCZlg.webp" + image: "maryx.webp" link: "https://github.com/maryx" matthew-carroll: name: "Matt Carroll" - image: "2j6fhR3Uq9ariRWaoB1MGPg.webp" + image: "matthew-carroll.webp" link: "https://github.com/matthew-carroll" mattkae: name: "Matthew Kosarek" - image: "mattkae.png" + image: "mattkae.webp" link: "https://github.com/mattkae" mehmetf: name: "Mehmet Fidanboylu" - image: "1snI23_LOekhWjI-XVYtHmQ.jpeg" + image: "mehmetf.jpg" link: "https://github.com/mehmetf" melyndahoover: name: "Mindy Hoover" - image: "1NC_dwnZV11nnMuK4D0mXYA.jpeg" + image: "melyndahoover.jpg" link: "https://github.com/melyndahoover" mhclin113: name: "Cheng Lin" - image: "1WKhF9u7jo7OkE26I5PjGiw.webp" + image: "mhclin113.webp" link: "https://x.com/chenglinlim" mit-mit: name: "Michael Thomsen" - image: "0Y_CFLc1qadgr3tPK.jpg" + image: "mit-mit.jpg" link: "https://github.com/mit-mit" mjohnsullivan: name: "Matt Sullivan" - image: "0cgL8XWJBpDSy2mHs.jpg" + image: "mjohnsullivan.jpg" link: "https://github.com/mjohnsullivan" mravn: name: "Mikkel Ravn" - image: "1HcMDVJ2lxSQDClP1IUCGzg.jpeg" + image: "mravn.jpg" link: "https://github.com/mravn" nikkitagandhi: name: "Nikita Gandhi" - image: "2G__H_oe3299HvlPy7YkpKg.webp" + image: "nikkitagandhi.webp" link: "https://twitter.com/Nikkitagandhi" parlough: name: "Parker Lougheed" - image: "1Yh3FMfH210BvJATo22X4Aw.jpeg" + image: "parlough.jpg" link: "https://github.com/parlough" perclasson: name: "Per Classon" - image: "2d-UpSVU0saEV2On5JBvYaQ.jpeg" + image: "perclasson.jpg" link: "https://github.com/perclasson" piinks: name: "Kate Lovett" - image: "00yA7OjxKWthRwjK2.webp" + image: "piinks.webp" link: "https://github.com/Piinks" redbrogdon: name: "Andrew Brogdon" - image: "0UfXdwppirgDYRNaj.webp" + image: "redbrogdon.webp" link: "https://github.com/redbrogdon" rich_hall: name: "Rich Hall" - image: "1w7Y-bThACd0V1YJ6N7QePg.webp" + image: "rich_hall.webp" link: "https://twitter.com/rich_hall_" rodydavis: name: "Rody Davis" @@ -284,77 +288,77 @@ rodydavis: link: "https://github.com/rodydavis" scheglov: name: "Konstantin Scheglov" - image: "0m23OLfjD-pystWVb.jpg" + image: "scheglov.jpg" link: "https://github.com/scheglov" sethladd: name: "Seth Ladd" - image: "10pSgo-h67AMv_gytSDEiJg.webp" + image: "sethladd.webp" link: "https://github.com/sethladd" sfshaza: name: "Shams Zakhour" - image: "0LHz-9FgHd3doKgCM.jpg" + image: "sfshaza.jpg" link: "https://github.com/sfshaza2" shamiramarshall: name: "Shamira Marshall" - image: "0vHty7_58DD6eW53D.webp" + image: "shamiramarshall.webp" link: "https://github.com/MiraMarshall" swavkulinski: name: "Swav Kulinski" - image: "0Qgrum-sTozr6lXCm.jpg" + image: "swavkulinski.jpg" link: "https://github.com/swavkulinski" taodong: name: "Tao Dong" - image: "0jbYlQ-K9Yh-HVUBp.jpg" + image: "taodong.jpg" link: "https://github.com/InMatrix" theaflowers: name: "Alethea K. Flowers" - image: "0cNAKYVA08TeS6TVB.webp" + image: "theaflowers.webp" link: "https://github.com/theacodes" tianguang: name: "Tianguang Zhang" - image: "0xV6b-152TcEMSjUr.webp" + image: "tianguang.webp" link: null timothyhoang: name: "Timothy Hoang" - image: "1krC2R3xE1FCZTdIbXQDT_A.jpeg" + image: "timothyhoang.jpg" link: null timsneath: name: "Tim Sneath" - image: "14HRhyWLOwtZEN7MQ8zLuHA.jpeg" + image: "timsneath.jpg" link: "https://github.com/timsneath" tomayac: name: "Thomas Steiner" - image: "15Yq8VeVDQqM51HKi50phdg.jpeg" + image: "tomayac.jpg" link: "https://github.com/tomayac" tvolkert: name: "Todd Volkert" - image: "0EUSBYlBzvqL9j_Xr.webp" + image: "tvolkert.webp" link: "https://github.com/tvolkert" twerske: name: "Emma Twersky" - image: "0lw25USWxDjT-i0kV.jpg" + image: "twerske.jpg" link: "https://github.com/twerske" verygoodopensource: name: "Very Good Ventures" - image: "1XUX_mTFnhm4BKyT9q9sOUQ.webp" + image: "verygoodopensource.webp" link: "https://github.com/VeryGoodOpenSource" -wmleler1: +wmleler: name: "Wm Leler" - image: "0c8OAb9nT35XgY56T.jpg" + image: "wmleler.jpg" link: "https://github.com/wmleler" xster: name: "xster" - image: "1h_lwkzhaAtW5WTkM750_Xw.jpeg" + image: "xster.jpg" link: "https://github.com/xster" yanchenum: name: "Yan Chen" - image: "0PSrFnoWOMMa0YZUM.jpg" + image: "yanchenum.jpg" link: "https://twitter.com/yanchenum" yegorj: name: "Yegor Jbanov" - image: "08pUBYXbTsN324CAB.jpg" + image: "yegorj.jpg" link: "https://github.com/yjbanov" zoeyfan: name: "Zoey Fan" - image: "2Cl8MkXie_Fr91CEmLGzQtw.jpeg" + image: "zoeyfan.jpg" link: "https://github.com/zoeyfan" diff --git a/sites/www/content/blog/authors_images/0cxUEIlx7f0R9rFrO.jpg b/sites/www/content/blog/authors_images/0cxUEIlx7f0R9rFrO.jpg deleted file mode 100644 index 3b887981cae..00000000000 Binary files a/sites/www/content/blog/authors_images/0cxUEIlx7f0R9rFrO.jpg and /dev/null differ diff --git a/sites/www/content/blog/authors_images/1PpHwzypsYF2ce75cfkt0_w.webp b/sites/www/content/blog/authors_images/1PpHwzypsYF2ce75cfkt0_w.webp deleted file mode 100644 index 78fc46ea628..00000000000 Binary files a/sites/www/content/blog/authors_images/1PpHwzypsYF2ce75cfkt0_w.webp and /dev/null differ diff --git a/sites/www/content/blog/authors_images/mattkae.png b/sites/www/content/blog/authors_images/mattkae.png deleted file mode 100644 index 1b5d117f733..00000000000 Binary files a/sites/www/content/blog/authors_images/mattkae.png and /dev/null differ diff --git a/sites/www/content/blog/desktop-windowing-apis/index.md b/sites/www/content/blog/desktop-windowing-apis/index.md index 816456641de..6a13e055751 100644 --- a/sites/www/content/blog/desktop-windowing-apis/index.md +++ b/sites/www/content/blog/desktop-windowing-apis/index.md @@ -63,6 +63,14 @@ Our design centers around five window types: regular, dialog, tooltip, popup, and satellite. Each window type has a specific, cross-platform behavior and purpose. +**Regular windows** are just that–an average window on a desktop. +They have a toolbar and are resizable, maximizable, minimizable, +and fullscreenable. They typically contain the main content of +an application. Application developers often have at least one +per application, but it is not uncommon to have more than one. +For example, a web browser application can open multiple regular +windows for the user to browse independently between them. + **Popup** windows provide functionality such as dropdown diff --git a/sites/www/content/blog/how-flutter-stays-ahead-of-ios-releases/images/banner.jpg b/sites/www/content/blog/how-flutter-stays-ahead-of-ios-releases/images/banner.jpg new file mode 100644 index 00000000000..4545a67abff Binary files /dev/null and b/sites/www/content/blog/how-flutter-stays-ahead-of-ios-releases/images/banner.jpg differ diff --git a/sites/www/content/blog/how-flutter-stays-ahead-of-ios-releases/index.md b/sites/www/content/blog/how-flutter-stays-ahead-of-ios-releases/index.md new file mode 100644 index 00000000000..1e4dd61a824 --- /dev/null +++ b/sites/www/content/blog/how-flutter-stays-ahead-of-ios-releases/index.md @@ -0,0 +1,240 @@ +--- +title: "How Flutter stays ahead of iOS releases" +description: >- + Learn how the Flutter team navigates WWDC, beta releases, + and proactive engineering to deliver Day 0 iOS support. +publishDate: 2026-08-25 +author: + - craiglabenz + - louisehsu +category: deep-dive +layout: blog +--- + + + +As a Flutter developer, your relationship with each of Flutter's six primary +supported platforms can vary (to say nothing of the extended universe platforms, +like webOS). For the platform running the phone in your pocket, +you might tune in to release events and get excited about each announcement. +For other platforms, +you might only cross your fingers and hope that annual changes won't disrupt +your ability to run `flutter build [platform]` and release your app. +Luckily, no matter who you are, +you benefit from a band of platform-focused developers on the Flutter team +whose job is to make sense of each announcement and deprecation so you don't have +to. + +Each platform-specific sub-team's workflow is tailored to how that platform +evolves and, in Apple's case, that tends to orbit a single week in June +when the eyes of the tech world turn to Cupertino, California. + +## Apple's flagship event, WWDC + +Every June, Apple hosts an event called WWDC to preview how their software will +evolve in the coming year. +This is typically separate from hardware announcements, +which trickle out during the rest of the year. +At WWDC, Apple's many operating systems (iOS, macOS, watchOS, and more) +are the main characters. + +If you've ever tuned in to WWDC, +then you know its general post-COVID format: first, the keynote, +then, the "Platform State of the Union", +and lastly, a massive release of topical videos. +In 2026, WWDC included over 145 technical sessions helping Apple's community +of developers understand what to expect—an incredible resource, to be sure, +but also far too much content to manually sift through. + +You already know where this is going. +We don't even have to write the following sentence for you to know it's true, +but we will anyway: + +_In 2026, the Flutter team began using AI to categorize each technical session._ + +Beginning this year, +a small piece of Dart code first extracted transcripts from each video. +Then, Gemini ranked and categorized each session based on the following rubric: + +1. Importance to Flutter contributors (inside and outside of Google) +1. Area of impact +1. Recommended actions + +Instead of having to watch 145 (admittedly very high quality) technical +sessions, by sundown on the day the videos were released, +we had a Gemini-produced triage document with early signal on every session. +For example, our system recognized that we could largely ignore a SwiftData +session because Flutter handles persistence differently. +Separately, it flagged a "Modernize your UIKit app" talk as critical [1] +for the Flutter Engine team because it described mandatory changes +to the `UIScene` [2] lifecycle API. + +> [1] To see everything important for Flutter, +we do physically pop some popcorn and watch each session that Gemini flags +in this way. +> [2] More on the `UIScene` API later! + +By sun-up the next morning, +Flutter's iOS team was officially in its crunch period. +Three months separate WWDC in June from the eventual stable releases in +September, so it's a race against the clock filled with sprints and bug bashes +to ensure timely compliance. +It's a lot of work, but the Flutter team is committed to always delivering Day 0 +support for every major iOS release. To follow our progress on supporting iOS +27, [check out its project on GitHub][github-project]. + +That system of Dart code and Gemini workflows processes all of WWDC, +which accounts for the vast majority of changes from Apple. +However, "vast majority" and "entirety" are different things; +and sometimes, a critical breaking change comes from something as innocuous +as a minor patch's release notes. + +## The beta releases heard 'round the Flutter team + +The year was 2024 and the iOS 18.2 developer beta had just arrived +with a slew of updates around, amongst other things, +gesture and pointer event handling. +Days later, we received an alarming bug report: +after clicking on a widget above a webview, +no ensuing gestures or clicks to the underlying webview would trigger `onClick` +events. + +This wasn't just bad. No, this was a Code Red, five alarm fire—because ads +depend on webviews, and depriving developers of even a single dollar of revenue +is absolutely a non-starter. + +The root cause of the bug was originally opaque, +but in time-honored, tech-debugging tradition, +a member of the team thought to try _turning it off and back on again_, +so to speak. +We discovered that swapping out one of the gesture recognizers +in the webview stack on every click solved the problem. +No one was terribly happy with the solution, +but it was a solution, and an invisible one to end-users at that. + +Fast forward to August 2025 and the iOS 26 beta, +and seemingly unrelated changes clashed with our gesture recognizer toggling +trick which caused a more serious regression where Flutter's touch +and gesture blocking system completely failed. +With no other options, we reverted the previous workaround… +_and were immediately reacquainted with the unresponsive webview bug_. + +Investigations into the anomaly from Flutter's end proved fruitless, +so we had to simplify things. +The team created a test project in pure Swift. +With no Flutter, no Dart, just a standard UIKit parent view, +a native gesture recognizer, and a standalone webview, +we saw the same faulty behavior. + +This was a huge win! +With a clean reproduction, +we sent our bug report and a recommended fix to Apple, +who wasted little time identifying and patching the issue. +By iOS 26.4, webview clicks were back to behaving how you would expect, +whether or not the project used Flutter. + +### Multiple fixes from multiple directions + +Complementing this happy ending was the fact that, at the same time, +the Flutter team was busy +[migrating all of our Dart code off of a separate "UI thread" and back on to the main "platform thread"][platform-thread-migration] +(as Flutter calls them). +This meant that Dart code could synchronously talk to platform code, +like Swift and Objective-C for iOS, +which opened the door to other modernizations, +such as introducing a synchronous system for hit testing. +These refactors further improved gesture behavior on iOS +by removing microtask delays and other sources of chaos. + +Combined, these changes delivered major stability improvements +for both Flutter apps running on iOS _and_ native iOS apps. + +## Sometimes we're even proactive, too + +WWDC announcements and bug reports are definitionally reactive maneuvers, +but sometimes the Flutter team is actually ahead of the game. +In the spring of 2025, Apple released iOS 18.4, +which produced the following warning in Flutter apps: + +> CLIENT OF UIKIT REQUIRES UPDATE: This process does not adopt UIScene +> lifecycle. This will become an assert in a future version. + +"A future version" is ambiguous, +but the Flutter team wasn't about to procrastinate with your apps, +so we dove in ahead of schedule. +Unfortunately, the implied change would prove to be a big one. + +To understand why, +one must consider how much the mobile landscape has changed +since Flutter's inception in 2014. +Once upon a time, Flutter was iOS and Android only, +filled those screens completely, and enjoyed stable window sizes. +Now, in 2026, Flutter runs on other platforms, can have multiple windows, +is embedded into non-Flutter apps, +and can have its geometry folded in half at a moment's notice. +These changes complicated once simple questions like "How big is the window?", +"Is the app backgrounded?", and also complicated the adoption of `UIScene`! + +Then, in 2025's WWDC, +an individual technical session included the following all-important sentence: + +> In the release following iOS 26, any `UIKit` app built with the latest SDK +> will be required to use the `UIScene` life cycle, otherwise it will not +> launch. + +_Thank goodness our intake and our (then manual) triage process flagged that +session!_ + +We spent Q3 of 2025 designing and implementing, +and were ultimately already beta testing our `UIScene` implementation by Q4. +An experimental flag unlocked it for adventurous Flutter developers, +and we owe a huge thank you to everyone who kicked the tires +because you collectively helped us find critical edge cases, +timing issues, and other quirks. + +By January of 2026, +we shifted our focus to helping the ecosystem update iOS plugins, +as their adoption was also required for app health. +After migrating every plugin we directly maintain, +we began filing issues against community plugins. +Thankfully, plugin authors from around the world were highly responsive +and we enjoyed a tremendous amount of buy-in and team spirit in getting +everything updated! + +### Shipping the feature + +Full support for Apple's `UIScene` API landed in Flutter's 3.41 stable release +in February of 2026. +And if you've never heard of `UIScene` and basically don't know what we're +talking about—good! +Our definition of success was that most Flutter developers would run +`flutter upgrade`, resume writing Dart code, and never have to think about this. +(Some advanced users, like those with add-to-app scenarios, +did have a short-but-manual migration guide to follow.) + +This proactive work paid _huge_ dividends, +because Apple turned that warning into an assert in the iOS 27 beta +just as they said they would. +But, thanks to our early efforts, +Flutter was ready for iOS 27 months before it was even announced. + +## We sweat in the desert; you get sweet desserts + +The Flutter team's goal remains the same as ever: +to figure out the annoying parts of app development +so you can focus on the fun parts: building features, making users happy, +and shipping great releases. +Whether it's iOS, Android, or any of the other many platforms where Flutter +apps run, we want your build targets to remain just another implementation +detail en route to creating great experiences that delight your users. + +For more information on Flutter, +check out [the video version of this blog post][video-version], +our [docs][], [YouTube channel][], or find us on socials. +Until then, we can't wait to see what you build! + +[docs]: https://docs.flutter.dev +[platform-thread-migration]: https://www.youtube.com/watch?v=miW7vCmQwnw +[video-version]: https://www.youtube.com/watch?v=vFYxYjepaK8 +[YouTube channel]: https://youtube.com/flutterdev +[github-project]: https://github.com/orgs/flutter/projects/212/views/1?sliceBy%5Bvalue%5D=27 diff --git a/sites/www/content/blog/perspective-on-flutter/index.md b/sites/www/content/blog/perspective-on-flutter/index.md index 6f218151c51..4d7adf7660b 100644 --- a/sites/www/content/blog/perspective-on-flutter/index.md +++ b/sites/www/content/blog/perspective-on-flutter/index.md @@ -3,7 +3,7 @@ title: "Perspective on Flutter" description: >- Fun with 3D and the Transform widget publishDate: 2018-06-18 -author: wmleler1 +author: wmleler image: images/1WdpLQEC_L1PYDO1KgxxfMQ.webp category: case-study layout: blog diff --git a/sites/www/content/blog/pitching-flutter-to-your-company-or-client/index.md b/sites/www/content/blog/pitching-flutter-to-your-company-or-client/index.md index 77f84da112e..dd35498a40a 100644 --- a/sites/www/content/blog/pitching-flutter-to-your-company-or-client/index.md +++ b/sites/www/content/blog/pitching-flutter-to-your-company-or-client/index.md @@ -3,7 +3,7 @@ title: "Pitching Flutter to your company or client" description: >- The business case for Flutter publishDate: 2018-11-01 -author: wmleler1 +author: wmleler image: images/1OOmIZ2plXASJUl49v6TlDA.gif category: deep-dive layout: blog diff --git a/sites/www/content/blog/whats-new-in-flutter-3-47/index.md b/sites/www/content/blog/whats-new-in-flutter-3-47/index.md index 1d6825e0a81..548e088aa1a 100644 --- a/sites/www/content/blog/whats-new-in-flutter-3-47/index.md +++ b/sites/www/content/blog/whats-new-in-flutter-3-47/index.md @@ -558,4 +558,4 @@ with this new and improved version of Flutter! [spm-blog-post]: /blog/saying-goodbye-to-cocoapods-swift-package-manager-is-soon-the-default-in-flutter [uiscene-guide]: https://docs.flutter.dev/release/breaking-changes/uiscene-lifecycle-ios [impeller-doc]: https://docs.flutter.dev/perf/impeller -[opengles-breaking-change]: https://docs.flutter.dev/release/breaking-changes/opengles-render-to-texture +[opengles-breaking-change]: https://docs.flutter.dev/release/breaking-changes/opengles-render-to-texture-top-down diff --git a/sites/www/content/showcase/images/third_party/case_studies/maxint/flutter-showcase-1-6000x2834.webp b/sites/www/content/showcase/images/third_party/case_studies/maxint/flutter-showcase-1-6000x2834.webp new file mode 100644 index 00000000000..31e53c58f16 Binary files /dev/null and b/sites/www/content/showcase/images/third_party/case_studies/maxint/flutter-showcase-1-6000x2834.webp differ diff --git a/sites/www/content/showcase/images/third_party/case_studies/maxint/flutter-showcase-2-6000x2834.webp b/sites/www/content/showcase/images/third_party/case_studies/maxint/flutter-showcase-2-6000x2834.webp new file mode 100644 index 00000000000..dcd09ab19b9 Binary files /dev/null and b/sites/www/content/showcase/images/third_party/case_studies/maxint/flutter-showcase-2-6000x2834.webp differ diff --git a/sites/www/content/showcase/images/third_party/case_studies/maxint/flutter-showcase-3-6000x2834.webp b/sites/www/content/showcase/images/third_party/case_studies/maxint/flutter-showcase-3-6000x2834.webp new file mode 100644 index 00000000000..1a0be7fa3fa Binary files /dev/null and b/sites/www/content/showcase/images/third_party/case_studies/maxint/flutter-showcase-3-6000x2834.webp differ diff --git a/sites/www/content/showcase/images/third_party/case_studies/maxint/flutter-showcase-4-6000x2834.webp b/sites/www/content/showcase/images/third_party/case_studies/maxint/flutter-showcase-4-6000x2834.webp new file mode 100644 index 00000000000..ea81074d077 Binary files /dev/null and b/sites/www/content/showcase/images/third_party/case_studies/maxint/flutter-showcase-4-6000x2834.webp differ diff --git a/sites/www/content/showcase/images/third_party/case_studies/maxint/maxint_logo.webp b/sites/www/content/showcase/images/third_party/case_studies/maxint/maxint_logo.webp new file mode 100644 index 00000000000..3df01f82f3c Binary files /dev/null and b/sites/www/content/showcase/images/third_party/case_studies/maxint/maxint_logo.webp differ diff --git a/sites/www/content/showcase/maxint.md b/sites/www/content/showcase/maxint.md new file mode 100644 index 00000000000..d13f68a75c8 --- /dev/null +++ b/sites/www/content/showcase/maxint.md @@ -0,0 +1,168 @@ +--- +title: Maxint +description: + Learn how Maxint deployed to six platforms simultaneously with Flutter, saving + 65% in codebase overhead. +headline: Maxint +summary: Building an autonomous financial OS across six platforms with Flutter +appName: Maxint +companyName: Maxint +logo: images/third_party/case_studies/maxint/maxint_logo.webp +card: images/third_party/case_studies/maxint/maxint_logo.webp +locations: + - North America +platforms: + - Mobile + - Desktop + - Web +industries: + - Banking & Finance +successMetrics: + - perc: 65 + desc: code avoided vs native multi-platform + - text: "3-4x" + desc: fewer front-end engineers needed + - text: "6" + desc: platforms launched from day one +tags: + - mobile + - desktop + - web + - ios + - android + - macos + - windows + - linux + - ai +publishDate: 2026-08-21 +--- + +[Maxint](https://maxint.com) is an autonomous financial operating system. Built +with Flutter, it unifies enterprise accounting, multi-platform monetization, and +AI-powered robo-advising into a single, precision-engineered workspace designed +to provide the signal minus the noise. + +As a lean startup, Maxint needed to rapidly deploy an enterprise-grade financial +operating system across iOS, Android, macOS, Windows, Linux, and Web +simultaneously. By launching on every major app store from day one, the team +leveraged organic search discovery across multiple platform ecosystems to drive +consistent user growth without spending a dollar on paid marketing. + +Maxint running across desktop, tablet, and mobile platforms + +**Trial by fire: From hackathon to production in three months** + +Rather than spending weeks evaluating frameworks in committee, the team tested +Flutter during the TechCrunch Disrupt hackathon. Faced with tight time +constraints after evaluating options like React Native, Ionic, Xamarin, and +native platforms, they chose Flutter to build a fully functional proof of +concept for Maxint in less than 24 hours. + +The success of this hackathon validated Flutter's developer velocity, responsive +charting, and local state management across devices. Following the hackathon +success, a lean team spent three months architecting the production-ready +financial OS, deploying native applications across all six target platforms +simultaneously. This was a timeline that likely would have required over a year +using siloed native teams. + +**Why Flutter? Single codebase portability and rendering performance** + +Portability across desktop and mobile served as Maxint's foundational growth +engine. By maintaining a single Dart codebase, complex mission-critical +logic—such as their double-entry ledger, local encryption, and financial +reporting engine—only had to be written and audited once, guaranteeing complete +feature parity and data accuracy across all user devices. + +To meet the high UI performance standards of financial software, Maxint relies +on Flutter's Impeller rendering engine on iOS and Android. By pre-compiling +shaders ahead-of-time (AOT) and using modern native graphics APIs like Metal and +Vulkan, Impeller renders complex, data-dense financial charts at 120 frames per +second without blocking the UI thread. Additionally, Flutter's hot reload +feature accelerated iteration, allowing the team to refine dense ledger layouts +and state logic without restarting the app. + +Maxint enterprise accounting interface rendering at 120fps + +**Building Finsight: Bidirectional conversational AI with Flutter** + +Maxint built Finsight, a fluid voice-driven financial assistant, directly within +the Flutter ecosystem by orchestrating +[`speech_to_text`](https://pub.dev/packages/speech_to_text) and +[`flutter_tts`](https://pub.dev/packages/flutter_tts). The application uses +continuous listening and partial transcription to stream user queries to +language models while the user speaks, minimizing perceived latency. + +When generating spoken feedback, Finsight attaches context tags (such as urgent +alerts or routine portfolio summaries) to dynamically adjust parameters like +`setSpeechRate` and `setPitch` in `flutter_tts`. Using +[Riverpod](https://riverpod.dev/) for state management, the on-screen visualizer +pulses in synchronization with audio callbacks, maintaining a unified +interactive state across mobile, desktop, and web platforms. + +Maxint complex market analysis powered by local LLMs + +**Overcoming multi-platform friction: Building Orca** + +Synchronizing user entitlements across the Apple App Store, Google Play, +Microsoft Store, and Stripe presented a major technical hurdle due to isolated +store silos. To solve multi-platform subscription management, Maxint developed +an internal unified billing infrastructure and released it to the developer +community as [Orca](https://orca.maxint.com/). + +Building for desktop also required investing in desktop-native UX paradigms, +including information-dense layouts, robust keyboard shortcuts, right-click +context menus, and window state management, while keeping double-entry ledger +math completely isolated from UI rendering. + +**Key results and business impact** + +Building with Flutter transformed Maxint's resource efficiency and release +cadence: + +- **65% less code:** Avoided writing and maintaining at least 65% more code + compared to managing separate native codebases (Swift, Kotlin, C\#, C++, and + TypeScript). +- **3x to 4x team efficiency:** Operates with 3x to 4x fewer front-end engineers + than needed for native multi-platform parity. +- **Day-one six-platform cadence:** Pushes simultaneous, feature-complete + updates to iOS, Android, Web, macOS, Windows, and Linux on a weekly basis. +- **Ecosystem contribution:** Created and launched Orca to streamline + cross-platform billing for Flutter developers. + +
+
+ +**What's next for Maxint and Orca** + +Maxint's roadmap includes expanding Orca's SDK capabilities across web and +desktop platforms for unified paywalls and revenue analytics. For the core +platform, Maxint is building upon its current on-device receipt parsing, powered +by Google’s ML Kit, by doubling down on privacy-first AI intelligence. + +Unified cross-platform monetization architecture powered by Orca + +The team is integrating small-footprint local LLMs (including custom +135M-parameter transformer models) to execute highly advanced document parsing, +cashflow forecasting, and transaction categorization entirely on the user's +hardware. The team is also expanding their tax engine to support +multi-jurisdiction calculations, local currency reconciliation, and native +electronic tax filing with the IRS. + +
+