From 82f6166609a74af5199ec286fcc123332b3fc193 Mon Sep 17 00:00:00 2001 From: Kilian Schulte Date: Tue, 21 Oct 2025 10:18:24 +0200 Subject: [PATCH 1/6] feat: migrate resource learning index to jaspr --- site/lib/jaspr_options.dart | 54 ++- site/lib/src/analytics/analytics_web.dart | 4 + .../src/components/client/archive_table.dart | 4 +- .../client/learning_resource_filters.dart | 246 +++++++++++++ .../learning_resource_filters_sidebar.dart | 221 +++++++++++ .../pages/learning_resource_index.dart | 281 ++------------ .../web/assets/js/learning-resources-index.js | 344 ------------------ src/content/reference/learning-resources.md | 1 - 8 files changed, 544 insertions(+), 611 deletions(-) create mode 100644 site/lib/src/components/client/learning_resource_filters.dart create mode 100644 site/lib/src/components/client/learning_resource_filters_sidebar.dart delete mode 100644 site/web/assets/js/learning-resources-index.js diff --git a/site/lib/jaspr_options.dart b/site/lib/jaspr_options.dart index bb2e8c266f6..4e4a38b7f14 100644 --- a/site/lib/jaspr_options.dart +++ b/site/lib/jaspr_options.dart @@ -13,21 +13,25 @@ import 'package:docs_flutter_dev_site/src/components/client/dartpad_injector.dar as prefix2; import 'package:docs_flutter_dev_site/src/components/client/download_latest_button.dart' as prefix3; -import 'package:docs_flutter_dev_site/src/components/client/on_this_page_button.dart' +import 'package:docs_flutter_dev_site/src/components/client/learning_resource_filters.dart' as prefix4; -import 'package:docs_flutter_dev_site/src/components/header/menu_toggle.dart' +import 'package:docs_flutter_dev_site/src/components/client/learning_resource_filters_sidebar.dart' as prefix5; -import 'package:docs_flutter_dev_site/src/components/header/site_switcher.dart' +import 'package:docs_flutter_dev_site/src/components/client/on_this_page_button.dart' as prefix6; -import 'package:docs_flutter_dev_site/src/components/header/theme_switcher.dart' +import 'package:docs_flutter_dev_site/src/components/header/menu_toggle.dart' as prefix7; -import 'package:docs_flutter_dev_site/src/components/cookie_notice.dart' +import 'package:docs_flutter_dev_site/src/components/header/site_switcher.dart' as prefix8; -import 'package:docs_flutter_dev_site/src/components/copy_button.dart' +import 'package:docs_flutter_dev_site/src/components/header/theme_switcher.dart' as prefix9; -import 'package:docs_flutter_dev_site/src/components/feedback.dart' as prefix10; -import 'package:docs_flutter_dev_site/src/components/os_selector.dart' +import 'package:docs_flutter_dev_site/src/components/cookie_notice.dart' + as prefix10; +import 'package:docs_flutter_dev_site/src/components/copy_button.dart' as prefix11; +import 'package:docs_flutter_dev_site/src/components/feedback.dart' as prefix12; +import 'package:docs_flutter_dev_site/src/components/os_selector.dart' + as prefix13; /// Default [JasprOptions] for use with your jaspr project. /// @@ -66,37 +70,47 @@ JasprOptions get defaultJasprOptions => JasprOptions( params: _prefix3DownloadLatestButton, ), - prefix4.OnThisPageButton: ClientTarget( + prefix4.LearningResourceFilters: + ClientTarget( + 'src/components/client/learning_resource_filters', + ), + + prefix5.LearningResourceFiltersSidebar: + ClientTarget( + 'src/components/client/learning_resource_filters_sidebar', + ), + + prefix6.OnThisPageButton: ClientTarget( 'src/components/client/on_this_page_button', ), - prefix8.CookieNotice: ClientTarget( + prefix10.CookieNotice: ClientTarget( 'src/components/cookie_notice', ), - prefix9.CopyButton: ClientTarget( + prefix11.CopyButton: ClientTarget( 'src/components/copy_button', - params: _prefix9CopyButton, + params: _prefix11CopyButton, ), - prefix10.FeedbackComponent: ClientTarget( + prefix12.FeedbackComponent: ClientTarget( 'src/components/feedback', - params: _prefix10FeedbackComponent, + params: _prefix12FeedbackComponent, ), - prefix5.MenuToggle: ClientTarget( + prefix7.MenuToggle: ClientTarget( 'src/components/header/menu_toggle', ), - prefix6.SiteSwitcher: ClientTarget( + prefix8.SiteSwitcher: ClientTarget( 'src/components/header/site_switcher', ), - prefix7.ThemeSwitcher: ClientTarget( + prefix9.ThemeSwitcher: ClientTarget( 'src/components/header/theme_switcher', ), - prefix11.OsSelector: ClientTarget( + prefix13.OsSelector: ClientTarget( 'src/components/os_selector', ), }, @@ -116,11 +130,11 @@ Map _prefix2DartPadInjector(prefix2.DartPadInjector c) => { Map _prefix3DownloadLatestButton( prefix3.DownloadLatestButton c, ) => {'os': c.os, 'arch': c.arch}; -Map _prefix9CopyButton(prefix9.CopyButton c) => { +Map _prefix11CopyButton(prefix11.CopyButton c) => { 'toCopy': c.toCopy, 'buttonText': c.buttonText, 'classes': c.classes, 'title': c.title, }; -Map _prefix10FeedbackComponent(prefix10.FeedbackComponent c) => +Map _prefix12FeedbackComponent(prefix12.FeedbackComponent c) => {'issueUrl': c.issueUrl}; diff --git a/site/lib/src/analytics/analytics_web.dart b/site/lib/src/analytics/analytics_web.dart index dfa0cb5cb7e..568e56e3c9c 100644 --- a/site/lib/src/analytics/analytics_web.dart +++ b/site/lib/src/analytics/analytics_web.dart @@ -6,6 +6,7 @@ import 'package:meta/meta.dart'; import 'package:universal_web/js_interop.dart'; import 'package:universal_web/web.dart' as web; +import '../util.dart'; import 'analytics.dart'; /// Web implementation of [Analytics]. @@ -15,6 +16,9 @@ import 'analytics.dart'; final class AnalyticsImplementation extends Analytics { @override void sendEvent(String eventName, Map parameters) { + if (!productionBuild) { + return; + } final dataLayer = web.window['dataLayer']; if (dataLayer.isA()) { (dataLayer as JSArray).toDart.add( diff --git a/site/lib/src/components/client/archive_table.dart b/site/lib/src/components/client/archive_table.dart index eaffdd8fa41..9a3293430df 100644 --- a/site/lib/src/components/client/archive_table.dart +++ b/site/lib/src/components/client/archive_table.dart @@ -144,8 +144,8 @@ class _ArchiveTableState extends State { final archiveExtension = os == 'linux' ? 'tar.xz' : 'zip'; return a( href: - '${FlutterRelease.baseReleasesUrl}$channel/$os/' - 'flutter_${os}_${release.version}-$channel.$archiveExtension.intoto.jsonl', + '${FlutterRelease.baseReleasesUrl}$channel/$os/flutter_${os}_' + '${release.version}-$channel.$archiveExtension.intoto.jsonl', target: Target.blank, [text('Attestation bundle')], ); diff --git a/site/lib/src/components/client/learning_resource_filters.dart b/site/lib/src/components/client/learning_resource_filters.dart new file mode 100644 index 00000000000..9dfd37ddbcc --- /dev/null +++ b/site/lib/src/components/client/learning_resource_filters.dart @@ -0,0 +1,246 @@ +// Copyright 2025 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:math'; + +import 'package:jaspr/jaspr.dart'; +import 'package:universal_web/js_interop.dart'; +import 'package:universal_web/web.dart' as web; + +import '../../analytics/analytics.dart'; +import '../../models/learning_resource_model.dart'; +import '../util/global_event_listener.dart'; +import 'learning_resource_filters_sidebar.dart'; + +class ResourceInfo { + ResourceInfo({ + required this.name, + required this.type, + required this.tags, + required this.description, + }); + + factory ResourceInfo.fromElement(web.Element element) { + final dataType = element.getAttribute('data-type') ?? ''; + final dataTags = element.getAttribute('data-tags') ?? ''; + final dataDescription = element.getAttribute('data-description') ?? ''; + + return ResourceInfo( + name: element.id, + type: LearningResourceType.values.firstWhere( + (type) => type.id == dataType, + orElse: () => LearningResourceType.sample, + ), + tags: dataTags + .split(',') + .map((t) => t.trim().toLowerCase()) + .map((tagId) { + return LearningResourceTag.values + .where((tag) => tag.id == tagId) + .firstOrNull; + }) + .nonNulls + .toList(), + description: dataDescription, + ); + } + + final String name; + final LearningResourceType type; + final List tags; + final String description; +} + +@client +class LearningResourceFilters extends StatefulComponent { + const LearningResourceFilters({super.key}); + + @override + State createState() => + _LearningResourceFiltersState(); +} + +class _LearningResourceFiltersState extends State { + String searchQuery = ''; + + FiltersNotifier get filters => LearningResourceFiltersSidebar.filters; + + final List resourceInfos = []; + int filteredResourcesCount = 0; + + @override + void initState() { + super.initState(); + + if (kIsWeb) { + filters.addListener(setFilters); + + final resourceGrid = web.document.getElementById('all-resources-grid'); + if (resourceGrid == null) { + return; + } + + final resourceCards = resourceGrid.querySelectorAll('.card'); + loadResourceInfos(resourceCards); + shuffleCards(resourceGrid); + } + } + + void loadResourceInfos(web.NodeList resourceCards) { + for (var i = 0; i < resourceCards.length; i++) { + final element = resourceCards.item(i) as web.Element; + final info = ResourceInfo.fromElement(element); + resourceInfos.add(info); + + element.addEventListener( + 'click', + ((web.Event event) { + analytics.sendEvent('learning_resource_index_click', { + 'learning_resource_type': info.type.id, + 'learning_resource_title': info.name, + }); + }).toJS, + ); + } + } + + void shuffleCards(web.Element container) { + final r = Random(); + final elements = container.childNodes; + for (var i = elements.length; i > 0; i--) { + final card = elements.item(r.nextInt(i)); + container.appendChild(card!); + } + } + + /// Update the filter state and re-evaluate which resources to show. + /// + /// Use like the `setState` method by passing a callback that updates + /// the relevant state variables. + /// + /// Example: + /// + /// ```dart + /// setFilters(() { + /// searchQuery = '...'; + /// }); + /// ``` + void setFilters([void Function()? callback]) { + setState(callback ?? () {}); + + final resourcesToShow = filters.filterResources(resourceInfos, searchQuery); + filteredResourcesCount = resourcesToShow.length; + for (final info in resourceInfos) { + final element = + web.document.getElementById(info.name) as web.HTMLElement?; + if (element == null) { + continue; + } + + if (resourcesToShow.contains(info)) { + element.classList.remove('hidden'); + } else { + element.classList.add('hidden'); + } + } + } + + @override + void dispose() { + if (kIsWeb) { + filters.removeListener(setFilters); + } + super.dispose(); + } + + @override + Component build(BuildContext context) { + return div(id: 'resource-search-group', classes: 'chip-filters-group', [ + div(classes: 'top-row', [ + div(classes: 'search-wrapper', id: 'resource-search', [ + span( + classes: 'material-symbols leading-icon', + attributes: {'aria-hidden': 'true', 'translate': 'no'}, + [text('search')], + ), + input( + type: InputType.search, + attributes: { + 'placeholder': 'Try "button" or "networking"...', + 'aria-label': 'Search learning resources by name and category', + }, + value: searchQuery, + onInput: (value) { + setFilters(() { + searchQuery = value as String; + }); + }, + ), + ]), + 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; + }, + [ + span( + classes: 'material-symbols', + attributes: {'aria-hidden': 'true', 'translate': 'no'}, + [text('filter_list')], + ), + ], + ), + ), + ]), + div(classes: 'label-row', [ + label( + attributes: {'for': 'resource-search'}, + [ + text('Showing '), + span([text('$filteredResourcesCount')]), + text(' / '), + span([text('${resourceInfos.length}')]), + ], + ), + button( + attributes: { + if (searchQuery.isEmpty && + filters.selectedTags.isEmpty && + filters.selectedTypes.isEmpty) + 'disabled': 'true', + }, + onClick: () { + setState(() { + searchQuery = ''; + }); + filters.reset(); + }, + [ + span( + classes: 'material-symbols', + attributes: {'aria-hidden': 'true', 'translate': 'no'}, + [text('close_small')], + ), + span([text('Clear filters')]), + ], + ), + ]), + ]); + } +} diff --git a/site/lib/src/components/client/learning_resource_filters_sidebar.dart b/site/lib/src/components/client/learning_resource_filters_sidebar.dart new file mode 100644 index 00000000000..1b604635a40 --- /dev/null +++ b/site/lib/src/components/client/learning_resource_filters_sidebar.dart @@ -0,0 +1,221 @@ +// Copyright 2025 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:jaspr/jaspr.dart'; +import 'package:universal_web/web.dart'; +import 'package:universal_web/web.dart' as web; + +import '../../analytics/analytics.dart'; +import '../../models/learning_resource_model.dart'; +import '../../util.dart'; +import '../material_icon.dart'; +import 'learning_resource_filters.dart'; + +@client +class LearningResourceFiltersSidebar extends StatelessComponent { + const LearningResourceFiltersSidebar({super.key}); + + /// The filter state for the resources list. + /// + /// This is static so that [LearningResourceFilters] can access it, + /// since both client components don't share a common ancestor. + static FiltersNotifier filters = FiltersNotifier(); + + @override + Component build(BuildContext context) { + return div(classes: 'right-col', [ + input( + type: InputType.checkbox, + id: 'open-filter-toggle', + attributes: {'hidden': 'true'}, + ), + div(id: 'resource-filter-group-wrapper', [ + div(id: 'resource-filter-group', [ + div(classes: 'filter-header', [ + label( + attributes: {'for': 'open-filter-toggle', 'aria-hidden': 'true'}, + classes: 'close-icon', + [const MaterialIcon('close')], + ), + ]), + div(classes: 'table-title', [text('Filter by')]), + ListenableBuilder( + listenable: filters, + builder: (context) { + return div(classes: 'table-content', [ + 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.id}', + }, + id: 'filter-${tag.id}', + onChange: (checked) { + filters.setTag(tag, checked as bool); + }, + ), + label( + attributes: {'for': 'filter-${tag.id}'}, + [text(tag.formattedName)], + ), + ], + ), + ]), + button(onClick: filters.toggleTagsExpanded, [ + span(classes: 'label', [ + text(filters.tagsExpanded ? 'Less' : 'More'), + ]), + span( + classes: 'material-symbols', + attributes: {'aria-hidden': 'true', 'translate': 'no'}, + [ + text( + filters.tagsExpanded ? 'expand_less' : 'expand_more', + ), + ], + ), + ]), + h4([text('Type')]), + ul([ + for (final type in LearningResourceType.values) + li([ + input( + type: InputType.checkbox, + attributes: { + 'role': 'checkbox', + 'name': 'filter-${type.id}', + }, + id: 'filter-${type.id}', + onChange: (checked) { + filters.setType(type, checked as bool); + }, + ), + label( + attributes: {'for': 'filter-${type.id}'}, + [text(type.formattedName)], + ), + ]), + ]), + ]); + }, + ), + ]), + ]), + ]); + } +} + +/// Notifier to manage the state of the filters. +class FiltersNotifier extends ChangeNotifier { + Set selectedTags = {}; + Set selectedTypes = {}; + + bool tagsExpanded = false; + + void setTag(LearningResourceTag tag, bool isSelected) { + if (isSelected) { + selectedTags.add(tag); + + analytics.sendEvent( + 'learning_resource_index_filter_selected', + { + 'learning_resource_filter_name': tag.id, + 'learning_resource_filter_type': 'tags', + }, + ); + } else { + selectedTags.remove(tag); + } + notifyListeners(); + } + + void setType(LearningResourceType type, bool isSelected) { + if (isSelected) { + selectedTypes.add(type); + + analytics.sendEvent( + 'learning_resource_index_filter_selected', + { + 'learning_resource_filter_name': type.id, + 'learning_resource_filter_type': 'type', + }, + ); + } else { + selectedTypes.remove(type); + } + notifyListeners(); + } + + void toggleTagsExpanded() { + tagsExpanded = !tagsExpanded; + notifyListeners(); + } + + void reset() { + selectedTags.clear(); + selectedTypes.clear(); + notifyListeners(); + + for (final tag in LearningResourceTag.values) { + final element = + web.document.getElementById('filter-${tag.id}') as HTMLInputElement?; + element?.checked = false; + } + for (final type in LearningResourceType.values) { + final element = + web.document.getElementById('filter-${type.id}') as HTMLInputElement?; + element?.checked = false; + } + } + + Set filterResources( + List resourceInfos, + String searchQuery, + ) { + if (searchQuery.isEmpty && selectedTags.isEmpty && selectedTypes.isEmpty) { + // No filters applied, return all resources. + return resourceInfos.toSet(); + } + + final resourcesToShow = {}; + searchQuery = searchQuery.trim().toLowerCase(); + + for (final info in resourceInfos) { + final matchesTags = + selectedTags.isEmpty || + info.tags.any((tag) => selectedTags.contains(tag)); + if (!matchesTags) { + continue; + } + + final matchesTypes = + selectedTypes.isEmpty || selectedTypes.contains(info.type); + if (!matchesTypes) { + continue; + } + + final matchesSearchQuery = + searchQuery.isEmpty || + info.name.toLowerCase().contains(searchQuery) || + info.tags.any((t) => t.id.contains(searchQuery)) || + info.type.id.contains(searchQuery) || + info.description.toLowerCase().contains(searchQuery); + if (!matchesSearchQuery) { + continue; + } + + resourcesToShow.add(info); + } + + return resourcesToShow; + } +} diff --git a/site/lib/src/components/pages/learning_resource_index.dart b/site/lib/src/components/pages/learning_resource_index.dart index 55df04b57aa..cfb0f00060c 100644 --- a/site/lib/src/components/pages/learning_resource_index.dart +++ b/site/lib/src/components/pages/learning_resource_index.dart @@ -6,7 +6,8 @@ import 'package:jaspr/jaspr.dart'; import '../../models/learning_resource_model.dart'; import '../../util.dart'; -import '../material_icon.dart'; +import '../client/learning_resource_filters.dart'; +import '../client/learning_resource_filters_sidebar.dart'; final class LearningResourceIndex extends StatelessComponent { LearningResourceIndex(this.resources); @@ -14,214 +15,17 @@ final class LearningResourceIndex extends StatelessComponent { final List resources; @override - Component build(BuildContext _) => div( - id: 'resource-index-content', - [ - _buildMainContent(), - _buildFilterSidebar(), - ], - ); - - Component _buildMainContent() => div( - classes: 'left-col', - id: 'resource-index-main-content', - [ - div( - id: 'resource-search-group', - classes: 'chip-filters-group', - [ - div( - classes: 'top-row', - [ - div( - classes: 'search-wrapper', - id: 'resource-search', - [ - span( - classes: 'material-symbols leading-icon', - attributes: { - 'aria-hidden': 'true', - 'translate': 'no', - }, - [text('search')], - ), - input( - type: InputType.search, - attributes: { - 'placeholder': 'Try "button" or "networking"...', - 'aria-label': - 'Search learning resources by name and category', - }, - ), - ], - ), - button( - classes: 'icon-button show-filters-button', - [ - span( - classes: 'material-symbols', - attributes: { - 'aria-hidden': 'true', - 'translate': 'no', - }, - [text('filter_list')], - ), - ], - ), - ], - ), - div( - classes: 'label-row', - [ - label( - attributes: {'for': 'resource-search'}, - [ - text('Showing '), - span(id: 'displayed-resource-card-count', [text('0')]), - text(' / '), - span(id: 'total-resource-card-count', [text('0')]), - ], - ), - button( - id: 'clear-resource-index-filters', - attributes: {'disabled': 'true'}, - [ - span( - classes: 'material-symbols', - attributes: { - 'aria-hidden': 'true', - 'translate': 'no', - }, - [text('close_small')], - ), - span([text('Clear filters')]), - ], - ), - ], - ), - ], - ), - section( - classes: 'card-grid', - id: 'all-resources-grid', - [ + Component build(BuildContext _) { + return div(id: 'resource-index-content', [ + div(classes: 'left-col', id: 'resource-index-main-content', [ + const LearningResourceFilters(), + section(classes: 'card-grid', id: 'all-resources-grid', [ for (final item in resources) _ResourceCard(item), - ], - ), - ], - ); - - Component _buildFilterSidebar() => div( - classes: 'right-col', - [ - input( - type: InputType.checkbox, - id: 'open-filter-toggle', - attributes: {'hidden': 'true'}, - ), - div( - id: 'resource-filter-group-wrapper', - [ - div( - id: 'resource-filter-group', - [ - div( - classes: 'filter-header', - [ - label( - attributes: { - 'for': 'open-filter-toggle', - 'aria-hidden': 'true', - }, - classes: 'close-icon', - [ - const MaterialIcon('close'), - ], - ), - ], - ), - div( - classes: 'table-title', - [text('Filter by')], - ), - div( - classes: 'table-content', - [ - h4([text('Subject')]), - ul( - id: 'filters-tags', - classes: 'collapsed', - [ - for (final tag in LearningResourceTag.values) - li( - classes: 'hidden', - [ - input( - type: InputType.checkbox, - attributes: { - 'role': 'checkbox', - 'name': 'filter-${tag.id}', - 'data-category': 'tags', - }, - id: 'filter-${tag.id}', - ), - label( - attributes: {'for': 'filter-${tag.id}'}, - [text(tag.formattedName)], - ), - ], - ), - ], - ), - button( - id: 'filters-tags-show-button', - [ - span( - classes: 'label', - [text('More')], - ), - span( - classes: 'material-symbols', - attributes: { - 'aria-hidden': 'true', - 'translate': 'no', - }, - [text('expand_more')], - ), - ], - ), - h4([text('Type')]), - ul( - id: 'filters-type', - [ - for (final type in LearningResourceType.values) - li( - [ - input( - type: InputType.checkbox, - attributes: { - 'role': 'checkbox', - 'data-category': 'type', - 'name': 'filter-${type.id}', - }, - id: 'filter-${type.id}', - ), - label( - attributes: {'for': 'filter-${type.id}'}, - [text(type.formattedName)], - ), - ], - ), - ], - ), - ], - ), - ], - ), - ], - ), - ], - ); + ]), + ]), + const LearningResourceFiltersSidebar(), + ]); + } } final class _ResourceCard extends StatelessComponent { @@ -243,43 +47,32 @@ final class _ResourceCard extends StatelessComponent { }, [ if (resource.imageUrl case final imageUrl?) - div( - classes: 'card-image-holder-material-3', - [ - img(src: imageUrl, alt: ''), - ], + div(classes: 'card-image-holder-material-3', [ + img(src: imageUrl, alt: ''), + ]), + div(classes: 'card-leading', [ + span( + classes: [ + 'pill-sm', + switch (resource.type) { + LearningResourceType.recipe => 'teal', + LearningResourceType.sample => 'purple', + LearningResourceType.tutorial => 'flutter-blue', + LearningResourceType.workshop => 'flutter-blue', + }, + ].toClasses, + [text(resource.type.formattedName)], ), - div( - classes: 'card-leading', - [ - span( - classes: [ - 'pill-sm', - switch (resource.type) { - LearningResourceType.recipe => 'teal', - LearningResourceType.sample => 'purple', - LearningResourceType.tutorial => 'flutter-blue', - LearningResourceType.workshop => 'flutter-blue', - }, - ].toClasses, - [text(resource.type.formattedName)], - ), - _iconForSource(resource.link.source), - ], - ), - div( - classes: 'card-header', - [ - span( - classes: 'card-title', - [text(resource.name)], - ), - ], - ), - div( - classes: 'card-content', - [text(resource.description)], - ), + _iconForSource(resource.link.source), + ]), + div(classes: 'card-header', [ + span(classes: 'card-title', [ + text(resource.name), + ]), + ]), + div(classes: 'card-content', [ + text(resource.description), + ]), ], ); } diff --git a/site/web/assets/js/learning-resources-index.js b/site/web/assets/js/learning-resources-index.js deleted file mode 100644 index 127b8c8eaf1..00000000000 --- a/site/web/assets/js/learning-resources-index.js +++ /dev/null @@ -1,344 +0,0 @@ -const filters = { - // These properties track the active filters and/or search term - type: new Set(), - tags: new Set(), - searchTerm: '', - // The keys correspond to a checkbox in the filter sidebar (created from - // 'type' in src/_data/learning-resources-index/filters.yml) - // The items inside the value array correspond to the 'type' property that - // can exist in the metadata (on items in the files in src/_data/learning-resources-index) - resourceTypeMapping: { - 'tutorial': ['codelab', 'tutorial'], - 'sample code': ['quickstart', 'demo', 'sample', 'sample code'], - 'workshop': ['workshop', 'video'], - 'recipe': ['recipe', 'how to', 'cookbook'] - }, - resourceTagMapping: { - 'ai': ['ai', 'gemini', 'llm'], - 'animation': ['animations', 'animate', 'animation'], - 'architecture': ['state-management', 'architecture', 'provider', 'bloc', 'stream'], - 'cupertino': ['cupertino', 'ios', 'macos'], - 'dart': ['dart', 'cli'], - 'design': ['design', 'widgets'], - 'desktop': ['windows', 'macos', 'linux'], - 'firebase': ['firebase', 'firestore', 'cloud'], - 'good for beginners': ['beginner', 'beginners'], - 'google apis': ['google', 'gemini', 'maps', 'firebase', 'cloud'], - 'ios': ['cupertino', 'ios'], - 'layout': ['layout', 'lists', 'scrolling', 'widgets'], - 'material': ['material', 'android'], - 'routing and navigation': ['routing', 'route', 'navigation', 'navigator'], - 'state management': ['state-management', 'architecture', 'provider', 'bloc', 'stream'], - 'testing': ['testing', 'tests', "test", 'perf', 'performance'], - 'web': ['web', 'wasm'], - 'widgets': ['widgets', 'layout'], - }, - - // Checks for existing filters, but not search terms - hasFilters: function () { - return this.type.size > 0 || - this.tags.size > 0; - }, - // Takes a Set, and returns a filtered Set - filter: function (resources) { - const resourcesToShow = new Set(); - let filteredResources = []; - if (this.hasFilters()) { - for (const resource of resources) { - const tags = resource.tags.join(' ').toLowerCase(); - const selectedFilterTags = Array.from(filters.tags); - const matchesTags = selectedFilterTags.some(t => { - return tags.includes(t) - }); - - const type = resource.type.toLowerCase(); - const selectedTypes = Array.from(filters.type); - const matchesTypes = selectedTypes.some(t => { - return t === type - }); - - if (matchesTags || matchesTypes) { - filteredResources.push(resource); - } - } - } else { - filteredResources = resources; - } - - for (const resource of filteredResources) { - const tags = resource.tags.join('').toLowerCase(); - const type = resource.type.toLowerCase(); - const description = resource.description.toLowerCase(); - const name = resource.name.toLowerCase(); - - if (name.includes(this.searchTerm) || - tags.toLowerCase().includes(this.searchTerm) || - type.includes(this.searchTerm) || - description.includes(this.searchTerm) - ) { - resourcesToShow.add(resource.name); - } - } - - return resourcesToShow; - }, - clear: function () { - this.type.clear(); - this.tags.clear(); - this.searchTerm = ''; - } -} - -function _setupResourceFilters() { - // index the resource metadata - const resourceGrid = document.getElementById('all-resources-grid'); - if (!resourceGrid) return; - const resourceCards = resourceGrid.querySelectorAll('.card'); - const resourcesInfo = _setupResourceInfo(resourceCards); - - // sets up the resource count element that says "Showing x / y" below the search bar. - const allResourcesCount = document.getElementById('total-resource-card-count'); - allResourcesCount.textContent = (resourcesInfo.length).toString(); - - // set up search bar interaction - const searchSection = document.getElementById('resource-search-group'); - const searchInput = searchSection.querySelector('.search-wrapper input'); - searchInput.addEventListener('input', _ => { - filters.searchTerm = searchInput.value.toLowerCase(); - filterResources(); - }); - - // set up checkbox interaction handling - const filterSection = document.getElementById('resource-filter-group'); - const allCheckboxes = filterSection.querySelectorAll('input'); - allCheckboxes.forEach(checkbox => { - _setupFilterChange(checkbox, filterResources); - }); - - // Clear filters button - const clearFiltersButton = document.getElementById("clear-resource-index-filters"); - clearFiltersButton.addEventListener('click', _ => { - filters.clear(); - searchInput.value = ''; - filterResources(); - allCheckboxes.forEach(box => { - box.checked = false; - }) - }); - - function toggleClearFiltersButton() { - clearFiltersButton.disabled = !(filters.hasFilters() || filters.searchTerm > 0); - } - - function filterResources() { - toggleClearFiltersButton(); - const resourcesToShow = filters.filter(resourcesInfo); - resourceCards.forEach(card => { - const resourceName = card.id; - if (resourcesToShow.has(resourceName)) { - card.classList.remove('hidden'); - } else { - card.classList.add('hidden'); - } - }); - const resourcesCount = document.getElementById('displayed-resource-card-count'); - resourcesCount.textContent = resourcesToShow.size.toString(); - } - - filterResources(); -} - - -function _setUpCollapsibleFilterLists() { - const filterSidebar = document.getElementById('resource-filter-group'); - const filterGroups = filterSidebar.querySelectorAll('ul'); - const toggleButtons = filterSidebar.querySelectorAll('button'); - - toggleButtons.forEach(button => { - const id = button.id; - const correspondingUlId = "#" + id.split('-').slice(0, 2).join('-'); - const ul = filterSidebar.querySelector(correspondingUlId); - - const liCount = Array.from(ul.querySelectorAll('li')).length; - if (liCount <= 2) { - button.classList.add('hidden'); - return; - } - - button.addEventListener('click', _ => { - const nodeList = ul.querySelectorAll('li'); - const liElements = Array.from(nodeList); - const isCollapsed = ul.classList.contains('collapsed'); - const icon = button.querySelector('.material-symbols'); - const label = button.querySelector('.label'); - if (isCollapsed) { - liElements.forEach(li => li.classList.remove('hidden')); - label.textContent = 'Less'; - icon.textContent = 'expand_less'; - ul.classList.remove('collapsed'); - } else { - liElements.slice(2).forEach(li => li.classList.add('hidden')); - icon.textContent = 'expand_more'; - label.textContent = 'More'; - ul.classList.add('collapsed'); - } - }); - }); - - // Show the first few items to start. - filterGroups.forEach(ul => { - const allFiltersForGroup = ul.querySelectorAll('li'); - const initialAmountToShow = 4; - for (let filterIndex = 0; filterIndex < initialAmountToShow && filterIndex < allFiltersForGroup.length; filterIndex += 1) { - allFiltersForGroup[filterIndex].classList.remove('hidden'); - } - }); -} - -function _setupResourceInfo(resourceCards) { - const resourcesInfo = []; - resourceCards.forEach(card => { - const resourceName = card.id; - if (!resourceName) return; - const tags = card.dataset.tags.split(', ').map(t => t.toLowerCase()); - resourcesInfo.push({ - name: resourceName, - type: card.dataset.type, - tags: tags, - description: card.dataset.description, - }); - - card.addEventListener('click', async (_) => { - window.dataLayer?.push({ - 'event': 'learning_resource_index_click', - 'learning_resource_type': card.dataset.type, - 'learning_resource_title': resourceName, - }); - }); - }); - return resourcesInfo; -} - -function _setupFilterChange(checkbox, filterResources) { - const id = checkbox.id; - const filter = id.split('-')[1].toLowerCase(); - // category refers to the filter types: tags, type - const category = checkbox.dataset.category.toLowerCase(); - - checkbox.addEventListener('change', _ => { - if (checkbox.checked) { - window.dataLayer?.push({ - 'event': 'learning_resource_index_filter_selected', - 'learning_resource_filter_name': filter, - 'learning_resource_filter_type': category, - }); - switch (category) { - case 'tags': - const tagGroup = filters.resourceTagMapping[filter]; - tagGroup.forEach(tag => filters[category].add(tag)) - break; - case 'type': - const typeGroup = filters.resourceTypeMapping[filter]; - typeGroup.forEach(type => filters[category].add(type)) - break; - } - } else { - switch (category) { - case 'tags': - const tagGroup = filters.resourceTagMapping[filter]; - tagGroup.forEach(tag => filters[category].delete(tag)) - break; - case 'type': - const typeGroup = filters.resourceTypeMapping[filter]; - typeGroup.forEach(type => filters[category].delete(type)) - break; - } - } - - filterResources(); - }); -} - -// This button is only displayed on smaller screens -function _setupDropdownMenu() { - const pageContent = document.getElementById('resource-index-content'); - if (!pageContent) return; - - const filtersButton = pageContent.querySelector('.show-filters-button'); - const filtersEl = document.getElementById('resource-filter-group-wrapper') || pageContent.querySelector('.right-col'); - const openToggleCheckbox = document.getElementById('open-filter-toggle'); - - if (!filtersButton || !filtersEl) return; - - function _closeMenu() { - if (openToggleCheckbox) { - openToggleCheckbox.checked = false; - } else { - filtersEl.classList.remove('show'); - } - filtersButton.ariaExpanded = 'false'; - } - - function _openMenu() { - if (openToggleCheckbox) { - openToggleCheckbox.checked = true; - } else { - filtersEl.classList.add('show'); - } - filtersButton.ariaExpanded = 'true'; - } - - function _isMenuOpen() { - if (openToggleCheckbox) { - return openToggleCheckbox.checked; - } else { - return filtersEl.classList.contains('show'); - } - } - - filtersButton.addEventListener('click', (_) => { - if (_isMenuOpen()) { - _closeMenu(); - } else { - _openMenu(); - } - }); - - document.addEventListener('keydown', (event) => { - if (event.key === 'Escape') { - _closeMenu(); - } - }); - - // Close the dropdown if anywhere not in the filters menu is. - const content = document.getElementById('all-resources-grid'); - if (content) { - content.addEventListener('click', () => { - if (_isMenuOpen()) { - _closeMenu(); - } - }); - } -} - -function shuffleElements(container) { - const elements = container.children; - for (let i = elements.length; i >= 0; i--) { - container.appendChild(elements[Math.random() * i | 0]); - } -} - -window.addEventListener('load', (_) => { - const resourceGrid = document.getElementById('all-resources-grid'); - shuffleElements(resourceGrid); -}) - - -document.onreadystatechange = () => { - if (document.readyState === "interactive" || - document.readyState === "complete") { - _setupResourceFilters(); - _setUpCollapsibleFilterLists(); - _setupDropdownMenu(); - } -} diff --git a/src/content/reference/learning-resources.md b/src/content/reference/learning-resources.md index 8589bce072e..f3826a95b9a 100644 --- a/src/content/reference/learning-resources.md +++ b/src/content/reference/learning-resources.md @@ -5,7 +5,6 @@ shortTitle: Learning resources showBreadcrumbs: false bodyClass: wide-site-content showToc: false -js: [ { url: '/assets/js/learning-resources-index.js', defer: true } ] --- :::secondary From 9927e4bf85ea6d5004595862f9c4a005bd65ebec Mon Sep 17 00:00:00 2001 From: Kilian Schulte Date: Tue, 21 Oct 2025 13:47:08 +0200 Subject: [PATCH 2/6] apply review --- .../lib/src/components/client/learning_resource_filters.dart | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/site/lib/src/components/client/learning_resource_filters.dart b/site/lib/src/components/client/learning_resource_filters.dart index 9dfd37ddbcc..a9387e91a27 100644 --- a/site/lib/src/components/client/learning_resource_filters.dart +++ b/site/lib/src/components/client/learning_resource_filters.dart @@ -226,9 +226,8 @@ class _LearningResourceFiltersState extends State { 'disabled': 'true', }, onClick: () { - setState(() { - searchQuery = ''; - }); + // No setState needed, since resetting filters will trigger it. + searchQuery = ''; filters.reset(); }, [ From bd4556e415c6b8966cd5c542c08ecf18c89d4bc2 Mon Sep 17 00:00:00 2001 From: Kilian Schulte Date: Tue, 21 Oct 2025 20:08:34 +0200 Subject: [PATCH 3/6] refactor: reintroduce resource yaml files and fix tag filtering --- site/lib/main.dart | 3 +- .../client/learning_resource_filters.dart | 52 +- .../learning_resource_filters_sidebar.dart | 56 +- .../pages/learning_resource_index.dart | 97 +- site/lib/src/data/learning_resources.dart | 1844 ----------------- site/lib/src/loaders/data_processor.dart | 32 + .../src/models/learning_resource_model.dart | 132 +- site/pubspec.yaml | 4 +- .../learning-resources-index/codelabs.yml | 438 ++++ .../learning-resources-index/cookbook.yml | 731 +++++++ src/data/learning-resources-index/demos.yml | 55 + .../quickstarts_dart.yml | 78 + .../quickstarts_flutter.yml | 257 +++ tool/dash_site/lib/src/utils.dart | 2 +- 14 files changed, 1756 insertions(+), 2025 deletions(-) delete mode 100644 site/lib/src/data/learning_resources.dart create mode 100644 src/data/learning-resources-index/codelabs.yml create mode 100644 src/data/learning-resources-index/cookbook.yml create mode 100644 src/data/learning-resources-index/demos.yml create mode 100644 src/data/learning-resources-index/quickstarts_dart.yml create mode 100644 src/data/learning-resources-index/quickstarts_flutter.yml diff --git a/site/lib/main.dart b/site/lib/main.dart index 42dcd851269..7a0dd09486e 100644 --- a/site/lib/main.dart +++ b/site/lib/main.dart @@ -15,7 +15,6 @@ import 'src/components/expansion_list.dart'; import 'src/components/os_selector.dart'; import 'src/components/pages/learning_resource_index.dart'; import 'src/components/tabs.dart'; -import 'src/data/learning_resources.dart'; import 'src/extensions/registry.dart'; import 'src/layouts/catalog_page_layout.dart'; import 'src/layouts/doc_layout.dart'; @@ -127,7 +126,7 @@ List get _embeddableComponents => [ ), CustomComponent( pattern: RegExp('LearningResourceIndex', caseSensitive: false), - builder: (_, _, _) => LearningResourceIndex(allLearningResources), + builder: (_, _, _) => LearningResourceIndex(), ), CustomComponent( pattern: RegExp('ArchiveTable'), diff --git a/site/lib/src/components/client/learning_resource_filters.dart b/site/lib/src/components/client/learning_resource_filters.dart index a9387e91a27..79bfa5e8eb1 100644 --- a/site/lib/src/components/client/learning_resource_filters.dart +++ b/site/lib/src/components/client/learning_resource_filters.dart @@ -13,44 +13,6 @@ import '../../models/learning_resource_model.dart'; import '../util/global_event_listener.dart'; import 'learning_resource_filters_sidebar.dart'; -class ResourceInfo { - ResourceInfo({ - required this.name, - required this.type, - required this.tags, - required this.description, - }); - - factory ResourceInfo.fromElement(web.Element element) { - final dataType = element.getAttribute('data-type') ?? ''; - final dataTags = element.getAttribute('data-tags') ?? ''; - final dataDescription = element.getAttribute('data-description') ?? ''; - - return ResourceInfo( - name: element.id, - type: LearningResourceType.values.firstWhere( - (type) => type.id == dataType, - orElse: () => LearningResourceType.sample, - ), - tags: dataTags - .split(',') - .map((t) => t.trim().toLowerCase()) - .map((tagId) { - return LearningResourceTag.values - .where((tag) => tag.id == tagId) - .firstOrNull; - }) - .nonNulls - .toList(), - description: dataDescription, - ); - } - - final String name; - final LearningResourceType type; - final List tags; - final String description; -} @client class LearningResourceFilters extends StatefulComponent { @@ -66,7 +28,7 @@ class _LearningResourceFiltersState extends State { FiltersNotifier get filters => LearningResourceFiltersSidebar.filters; - final List resourceInfos = []; + final List resources = []; int filteredResourcesCount = 0; @override @@ -90,14 +52,14 @@ class _LearningResourceFiltersState extends State { void loadResourceInfos(web.NodeList resourceCards) { for (var i = 0; i < resourceCards.length; i++) { final element = resourceCards.item(i) as web.Element; - final info = ResourceInfo.fromElement(element); - resourceInfos.add(info); + final info = LearningResource.fromElement(element); + resources.add(info); element.addEventListener( 'click', ((web.Event event) { analytics.sendEvent('learning_resource_index_click', { - 'learning_resource_type': info.type.id, + 'learning_resource_type': info.type, 'learning_resource_title': info.name, }); }).toJS, @@ -129,9 +91,9 @@ class _LearningResourceFiltersState extends State { void setFilters([void Function()? callback]) { setState(callback ?? () {}); - final resourcesToShow = filters.filterResources(resourceInfos, searchQuery); + final resourcesToShow = filters.filterResources(resources, searchQuery); filteredResourcesCount = resourcesToShow.length; - for (final info in resourceInfos) { + for (final info in resources) { final element = web.document.getElementById(info.name) as web.HTMLElement?; if (element == null) { @@ -215,7 +177,7 @@ class _LearningResourceFiltersState extends State { text('Showing '), span([text('$filteredResourcesCount')]), text(' / '), - span([text('${resourceInfos.length}')]), + span([text('${resources.length}')]), ], ), button( diff --git a/site/lib/src/components/client/learning_resource_filters_sidebar.dart b/site/lib/src/components/client/learning_resource_filters_sidebar.dart index 1b604635a40..494d81b3431 100644 --- a/site/lib/src/components/client/learning_resource_filters_sidebar.dart +++ b/site/lib/src/components/client/learning_resource_filters_sidebar.dart @@ -3,8 +3,6 @@ // found in the LICENSE file. import 'package:jaspr/jaspr.dart'; -import 'package:universal_web/web.dart'; -import 'package:universal_web/web.dart' as web; import '../../analytics/analytics.dart'; import '../../models/learning_resource_model.dart'; @@ -56,16 +54,17 @@ class LearningResourceFiltersSidebar extends StatelessComponent { type: InputType.checkbox, attributes: { 'role': 'checkbox', - 'name': 'filter-${tag.id}', + 'name': 'filter-${tag.name}', }, - id: 'filter-${tag.id}', + id: 'filter-${tag.name}', + checked: filters.selectedTags.contains(tag), onChange: (checked) { filters.setTag(tag, checked as bool); }, ), label( - attributes: {'for': 'filter-${tag.id}'}, - [text(tag.formattedName)], + attributes: {'for': 'filter-${tag.name}'}, + [text(tag.label)], ), ], ), @@ -92,16 +91,17 @@ class LearningResourceFiltersSidebar extends StatelessComponent { type: InputType.checkbox, attributes: { 'role': 'checkbox', - 'name': 'filter-${type.id}', + 'name': 'filter-${type.name}', }, - id: 'filter-${type.id}', + id: 'filter-${type.name}', + checked: filters.selectedTypes.contains(type), onChange: (checked) { filters.setType(type, checked as bool); }, ), label( - attributes: {'for': 'filter-${type.id}'}, - [text(type.formattedName)], + attributes: {'for': 'filter-${type.name}'}, + [text(type.label)], ), ]), ]), @@ -128,7 +128,7 @@ class FiltersNotifier extends ChangeNotifier { analytics.sendEvent( 'learning_resource_index_filter_selected', { - 'learning_resource_filter_name': tag.id, + 'learning_resource_filter_name': tag.label.toLowerCase(), 'learning_resource_filter_type': 'tags', }, ); @@ -145,7 +145,7 @@ class FiltersNotifier extends ChangeNotifier { analytics.sendEvent( 'learning_resource_index_filter_selected', { - 'learning_resource_filter_name': type.id, + 'learning_resource_filter_name': type.label.toLowerCase(), 'learning_resource_filter_type': 'type', }, ); @@ -164,41 +164,33 @@ class FiltersNotifier extends ChangeNotifier { selectedTags.clear(); selectedTypes.clear(); notifyListeners(); - - for (final tag in LearningResourceTag.values) { - final element = - web.document.getElementById('filter-${tag.id}') as HTMLInputElement?; - element?.checked = false; - } - for (final type in LearningResourceType.values) { - final element = - web.document.getElementById('filter-${type.id}') as HTMLInputElement?; - element?.checked = false; - } } - Set filterResources( - List resourceInfos, + Set filterResources( + List resources, String searchQuery, ) { if (searchQuery.isEmpty && selectedTags.isEmpty && selectedTypes.isEmpty) { // No filters applied, return all resources. - return resourceInfos.toSet(); + return resources.toSet(); } - final resourcesToShow = {}; + final resourcesToShow = {}; searchQuery = searchQuery.trim().toLowerCase(); - for (final info in resourceInfos) { + final filterTags = selectedTags.expand((e) => e.tags).toSet(); + final filterTypes = selectedTypes.expand((e) => e.tags).toSet(); + + for (final info in resources) { final matchesTags = selectedTags.isEmpty || - info.tags.any((tag) => selectedTags.contains(tag)); + info.tags.any(filterTags.contains); if (!matchesTags) { continue; } final matchesTypes = - selectedTypes.isEmpty || selectedTypes.contains(info.type); + selectedTypes.isEmpty || filterTypes.contains(info.type); if (!matchesTypes) { continue; } @@ -206,8 +198,8 @@ class FiltersNotifier extends ChangeNotifier { final matchesSearchQuery = searchQuery.isEmpty || info.name.toLowerCase().contains(searchQuery) || - info.tags.any((t) => t.id.contains(searchQuery)) || - info.type.id.contains(searchQuery) || + info.tags.any((t) => t.contains(searchQuery)) || + info.type.contains(searchQuery) || info.description.toLowerCase().contains(searchQuery); if (!matchesSearchQuery) { continue; diff --git a/site/lib/src/components/pages/learning_resource_index.dart b/site/lib/src/components/pages/learning_resource_index.dart index cfb0f00060c..71db5d42b11 100644 --- a/site/lib/src/components/pages/learning_resource_index.dart +++ b/site/lib/src/components/pages/learning_resource_index.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'package:jaspr/jaspr.dart'; +import 'package:jaspr_content/jaspr_content.dart'; import '../../models/learning_resource_model.dart'; import '../../util.dart'; @@ -10,12 +11,13 @@ import '../client/learning_resource_filters.dart'; import '../client/learning_resource_filters_sidebar.dart'; final class LearningResourceIndex extends StatelessComponent { - LearningResourceIndex(this.resources); - - final List resources; + LearningResourceIndex({super.key}); @override - Component build(BuildContext _) { + Component build(BuildContext context) { + final resources = + context.page.data['learningResources'] as List? ?? []; + return div(id: 'resource-index-content', [ div(classes: 'left-col', id: 'resource-index-main-content', [ const LearningResourceFilters(), @@ -38,11 +40,11 @@ final class _ResourceCard extends StatelessComponent { return a( id: resource.name, classes: 'card outlined-card', - href: resource.link.url, + href: resource.link?.url ?? '#', target: Target.blank, attributes: { - 'data-type': resource.type.id, - 'data-tags': resource.tags.map((tag) => tag.id).join(', '), + 'data-type': resource.type, + 'data-tags': resource.tags.join(', '), 'data-description': resource.description, }, [ @@ -55,15 +57,19 @@ final class _ResourceCard extends StatelessComponent { classes: [ 'pill-sm', switch (resource.type) { - LearningResourceType.recipe => 'teal', - LearningResourceType.sample => 'purple', - LearningResourceType.tutorial => 'flutter-blue', - LearningResourceType.workshop => 'flutter-blue', + 'codelab' || 'workshop' => 'flutter-blue', + 'quickstart' || 'demo' => 'purple', + _ => 'teal', }, ].toClasses, - [text(resource.type.formattedName)], + [ + text( + resource.type.substring(0, 1).toUpperCase() + + resource.type.substring(1), + ), + ], ), - _iconForSource(resource.link.source), + _iconForLabel(resource.link?.label ?? ''), ]), div(classes: 'card-header', [ span(classes: 'card-title', [ @@ -77,68 +83,53 @@ final class _ResourceCard extends StatelessComponent { ); } - Component _iconForSource(LearningResourceSource source) => switch (source) { - LearningResourceSource.gitHub => svg( + Component _iconForLabel(String label) => switch (label) { + 'Flutter Github' => svg( classes: 'monochrome-icon', width: 24.px, height: 24.px, [ const Component.element( tag: 'use', - attributes: { - 'href': '/assets/images/social/github.svg#github', - }, + attributes: {'href': '/assets/images/social/github.svg#github'}, ), ], ), - LearningResourceSource.dartDocs => img( + 'Dart Github' || 'Dart docs' => img( src: '/assets/images/branding/dart/logo.svg', width: 24, alt: 'Dart logo', ), - LearningResourceSource.flutterDocs => img( - src: '/assets/images/branding/flutter/icon/1080.png', - alt: 'Flutter logo', - width: 24, - ), - LearningResourceSource.googleCodelab => svg( - width: 24.px, - height: 24.px, - [ - const Component.element( - tag: 'use', - attributes: { - 'href': - '/assets/images/social/google-developers.svg#google-developers', - }, - ), - ], - ), - LearningResourceSource.youTube => svg( + 'Google Codelab' => svg(width: 24.px, height: 24.px, [ + const Component.element( + tag: 'use', + attributes: { + 'href': + '/assets/images/social/google-developers.svg#google-developers', + }, + ), + ]), + 'YouTube' => svg( attributes: {'style': 'color: red'}, width: 24.px, height: 24.px, [ const Component.element( tag: 'use', - attributes: { - 'href': '/assets/images/social/youtube.svg#youtube', - }, + attributes: {'href': '/assets/images/social/youtube.svg#youtube'}, ), ], ), - LearningResourceSource.medium => svg( - classes: 'monochrome-icon', - width: 24.px, - height: 24.px, - [ - const Component.element( - tag: 'use', - attributes: { - 'href': '/assets/images/social/medium.svg#medium', - }, - ), - ], + 'Medium' => svg(classes: 'monochrome-icon', width: 24.px, height: 24.px, [ + const Component.element( + tag: 'use', + attributes: {'href': '/assets/images/social/medium.svg#medium'}, + ), + ]), + 'Flutter docs' || _ => img( + src: '/assets/images/branding/flutter/icon/1080.png', + alt: 'Flutter logo', + width: 24, ), }; } diff --git a/site/lib/src/data/learning_resources.dart b/site/lib/src/data/learning_resources.dart deleted file mode 100644 index 6fe2235a30a..00000000000 --- a/site/lib/src/data/learning_resources.dart +++ /dev/null @@ -1,1844 +0,0 @@ -// Copyright 2025 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import '../models/learning_resource_model.dart'; - -final List allLearningResources = [ - ..._codelabs, - ..._cookbookRecipes, - ..._demos, - ..._videos, - ..._quickStartsForDart, - ..._quickStartsForFlutter, -]; - -final List _videos = [ - LearningResource( - name: 'Your first Flutter app workshop', - description: - 'An instructor-led version of our very popular ' - '\'Write your first Flutter app\' codelab.', - type: LearningResourceType.workshop, - tags: [ - LearningResourceTag.goodForBeginners, - ], - link: ( - url: 'https://www.youtube.com/watch?v=8sAyPDLorek', - source: LearningResourceSource.youTube, - ), - ), -]; - -final List _codelabs = [ - LearningResource( - name: 'Your first Flutter app', - description: - 'Create a simple random-name generator app. ' - 'This app is responsive and runs on mobile, desktop, and web.', - type: LearningResourceType.tutorial, - tags: [ - LearningResourceTag.goodForBeginners, - ], - link: ( - url: - 'https://codelabs.developers.google.com/codelabs/flutter-codelab-first', - source: LearningResourceSource.googleCodelab, - ), - ), - LearningResource( - name: 'Records and Patterns in Dart', - description: - 'Discover Dart 3\'s new records and patterns features. ' - 'Learn how you can use them in a Flutter app to help you ' - 'write more readable and maintainable Dart code.', - type: LearningResourceType.tutorial, - tags: [ - LearningResourceTag.dart, - ], - link: ( - url: - 'https://codelabs.developers.google.com/codelabs/dart-patterns-records', - source: LearningResourceSource.googleCodelab, - ), - ), - LearningResource( - name: 'Scrolling experiences in Flutter', - description: - 'Start with an app that performs ' - 'simple, straightforward scrolling and enhance it to ' - 'create fancy and custom scrolling effects by using slivers.', - type: LearningResourceType.tutorial, - tags: [], - link: ( - url: 'https://www.youtube.com/watch?v=YY-_yrZdjGc', - source: LearningResourceSource.youTube, - ), - ), - LearningResource( - name: 'Take your Flutter app from boring to beautiful', - description: - 'Learn how to use some of the features in Material 3 to ' - 'make your app beautiful and responsive.', - type: LearningResourceType.tutorial, - tags: [ - LearningResourceTag.design, - LearningResourceTag.material, - ], - link: ( - url: - 'https://codelabs.developers.google.com/codelabs/flutter-boring-to-beautiful', - source: LearningResourceSource.googleCodelab, - ), - ), - LearningResource( - name: 'Building next generation UIs in Flutter', - description: - 'Learn how to build a Flutter app that uses the ' - 'power of `flutter_animate`, fragment shaders, and particle fields.', - type: LearningResourceType.tutorial, - tags: [ - LearningResourceTag.animation, - LearningResourceTag.design, - ], - link: ( - url: - 'https://codelabs.developers.google.com/codelabs/flutter-next-gen-uis', - source: LearningResourceSource.googleCodelab, - ), - ), - LearningResource( - name: 'Adaptive Apps in Flutter', - description: - 'Learn how to build a Flutter app that adapts to ' - 'the platform that it\'s running on, be that ' - 'Android, iOS, the web, Windows, macOS, or Linux.', - type: LearningResourceType.tutorial, - tags: [ - LearningResourceTag.desktop, - LearningResourceTag.ios, - LearningResourceTag.web, - ], - link: ( - url: - 'https://codelabs.developers.google.com/codelabs/flutter-adaptive-app', - source: LearningResourceSource.googleCodelab, - ), - ), - LearningResource( - name: 'Animations in Flutter', - description: - 'Learn how to build animated effects in Flutter. ' - 'You\'ll learn how to build implicit and explicit animations, ' - 'and customize navigation transition animations using ' - 'the animations package and predictive back on Android.', - type: LearningResourceType.tutorial, - tags: [ - LearningResourceTag.animation, - ], - link: ( - url: 'https://codelabs.developers.google.com/advanced-flutter-animations', - source: LearningResourceSource.googleCodelab, - ), - ), - LearningResource( - name: 'Building Beautiful Transitions with Material Motion for Flutter', - description: - 'Learn how to use the Material animations package to ' - 'add pre-built transitions to a Material app called Reply.', - type: LearningResourceType.tutorial, - tags: [ - LearningResourceTag.animation, - LearningResourceTag.material, - ], - link: ( - url: - 'https://codelabs.developers.google.com/codelabs/material-motion-flutter', - source: LearningResourceSource.googleCodelab, - ), - ), - LearningResource( - name: 'How to debug layout issues with the Flutter Inspector', - description: - 'Step-by-step instructions on how to debug common layout problems ' - 'using the Flutter Inspector and Layout Explorer.', - type: LearningResourceType.tutorial, - tags: [], - link: ( - url: - 'https://blog.flutter.dev/how-to-debug-layout-issues-with-the-flutter-inspector-87460a7b9db', - source: LearningResourceSource.medium, - ), - ), - LearningResource( - name: 'Implicit animations', - description: - 'Use DartPad (no downloads required!) to learn how ' - 'to use implicit animations to add motion and ' - 'create visual effects for the widgets in your UI.', - type: LearningResourceType.tutorial, - tags: [ - LearningResourceTag.animation, - ], - link: ( - url: '/codelabs/implicit-animations', - source: LearningResourceSource.flutterDocs, - ), - ), - LearningResource( - name: 'MDC-101 - Material Components (MDC) Basics', - description: - 'Learn the basics of using Material Components by ' - 'building a simple app with core components.', - type: LearningResourceType.tutorial, - tags: [ - LearningResourceTag.material, - LearningResourceTag.design, - ], - link: ( - url: 'https://codelabs.developers.google.com/codelabs/mdc-101-flutter', - source: LearningResourceSource.googleCodelab, - ), - ), - LearningResource( - name: 'MDC-102 - Material Structure and Layout', - description: - 'Learn how to use Material for structure and layout in Flutter. ' - 'Continue building the e-commerce app, introduced in MDC-101, ' - 'by adding navigation, structure, and data.', - type: LearningResourceType.tutorial, - tags: [ - LearningResourceTag.material, - LearningResourceTag.design, - ], - link: ( - url: 'https://codelabs.developers.google.com/codelabs/mdc-102-flutter', - source: LearningResourceSource.googleCodelab, - ), - ), - LearningResource( - name: 'MDC-103 - Material Theming with Color, Shape, Elevation, and Type', - description: - 'Discover how Material Components for Flutter make it easy to ' - 'differentiate your product, and express your brand through design.', - type: LearningResourceType.tutorial, - tags: [ - LearningResourceTag.material, - LearningResourceTag.design, - ], - link: ( - url: 'https://codelabs.developers.google.com/codelabs/mdc-103-flutter', - source: LearningResourceSource.googleCodelab, - ), - ), - LearningResource( - name: 'MDC-104 - Material Advanced Components', - description: - 'Improve your design and learn to use our advanced backdrop menu.', - type: LearningResourceType.tutorial, - tags: [ - LearningResourceTag.material, - LearningResourceTag.design, - ], - link: ( - url: 'https://codelabs.developers.google.com/codelabs/mdc-104-flutter', - source: LearningResourceSource.googleCodelab, - ), - ), - LearningResource( - name: 'Adding AdMob Ads to a Flutter app', - description: - 'Learn how to add an AdMob banner, an interstitial ad, and ' - 'a rewarded ad to an app called Awesome Drawing Quiz, ' - 'a game that lets players guess the name of the drawing.', - type: LearningResourceType.tutorial, - tags: [], - link: ( - url: - 'https://codelabs.developers.google.com/codelabs/admob-ads-in-flutter', - source: LearningResourceSource.googleCodelab, - ), - ), - LearningResource( - name: 'Adding an AdMob banner and native inline ads to a Flutter app', - description: - 'Learn how to implement inline banner and native ads to a ' - 'travel booking app that lists possible flight destinations.', - type: LearningResourceType.tutorial, - tags: [], - link: ( - url: - 'https://codelabs.developers.google.com/codelabs/admob-inline-ads-in-flutter', - source: LearningResourceSource.googleCodelab, - ), - ), - LearningResource( - name: 'Adding in-app purchases to your Flutter app', - description: - 'Extend a simple gaming app that uses the Dash mascot as ' - 'currency to offer three types of in-app purchases: ' - 'consumable, non-consumable, and subscription.', - type: LearningResourceType.tutorial, - tags: [], - link: ( - url: - 'https://codelabs.developers.google.com/codelabs/flutter-in-app-purchases', - source: LearningResourceSource.googleCodelab, - ), - ), - LearningResource( - name: 'Add a user authentication flow using FirebaseUI', - description: - 'Learn how to add Firebase authentication to a Flutter app with ' - 'only a few lines of code.', - type: LearningResourceType.tutorial, - tags: [ - LearningResourceTag.firebase, - ], - link: ( - url: 'https://firebase.google.com/codelabs/firebase-auth-in-flutter-apps', - source: LearningResourceSource.googleCodelab, - ), - ), - LearningResource( - name: 'Get to know Firebase for Flutter', - description: - 'Build an event RSVP and guestbook chat app on ' - 'both Android and iOS using Flutter, authenticating users with ' - 'Firebase Authentication, and sync data using Cloud Firestore.', - type: LearningResourceType.tutorial, - tags: [ - LearningResourceTag.firebase, - LearningResourceTag.ios, - ], - link: ( - url: 'https://firebase.google.com/codelabs/firebase-get-to-know-flutter', - source: LearningResourceSource.googleCodelab, - ), - ), - LearningResource( - name: 'Notifications with Firebase Cloud Messaging', - description: - 'Learn how to develop a multi-platform app with ' - 'Flutter and Firebase Cloud Messaging, integrating FCM to ' - 'send and receive messages on Android, iOS, and web.', - type: LearningResourceType.tutorial, - tags: [ - LearningResourceTag.firebase, - LearningResourceTag.web, - LearningResourceTag.ios, - ], - link: ( - url: 'https://firebase.google.com/codelabs/firebase-fcm-flutter', - source: LearningResourceSource.googleCodelab, - ), - ), - LearningResource( - name: 'Add sound and music to your Flutter game with SoLoud', - description: - 'The SoLoud package, a free and portable engine, ' - 'delivers the low-latency and high-performance sound ' - 'that\'s essential for many games.', - type: LearningResourceType.tutorial, - tags: [], - link: ( - url: - 'https://codelabs.developers.google.com/codelabs/flutter-codelab-soloud', - source: LearningResourceSource.googleCodelab, - ), - ), - LearningResource( - name: 'Build a 2D physics game with Flutter and Flame', - description: - 'This codelab guides you through crafting game mechanics in a ' - 'Flutter and Flame game using a 2D physics simulation called Forge2D.', - type: LearningResourceType.tutorial, - tags: [], - link: ( - url: - 'https://codelabs.developers.google.com/codelabs/flutter-flame-forge2d', - source: LearningResourceSource.googleCodelab, - ), - ), - LearningResource( - name: 'Build a word puzzle with Flutter', - description: - 'This codelab focuses on building word puzzle games, and ' - 'dives into using Flutter\'s background processing to ' - 'generate expansive crossword-style grids of interlocking words.', - type: LearningResourceType.tutorial, - tags: [], - link: ( - url: - 'https://codelabs.developers.google.com/codelabs/flutter-word-puzzle', - source: LearningResourceSource.googleCodelab, - ), - ), - LearningResource( - name: 'Introduction to Flame with Flutter', - description: - 'Build a Breakout clone using the Flame 2D game engine and ' - 'embed it in a Flutter wrapper.', - type: LearningResourceType.tutorial, - tags: [ - LearningResourceTag.goodForBeginners, - ], - link: ( - url: - 'https://codelabs.developers.google.com/codelabs/flutter-flame-brick-breaker', - source: LearningResourceSource.googleCodelab, - ), - ), - LearningResource( - name: 'Adding Google Maps to a Flutter app', - description: - 'Display a Google map in an app, retrieve data from a web service, ' - 'and display the data as markers on the map.', - type: LearningResourceType.tutorial, - tags: [ - LearningResourceTag.googleApis, - ], - link: ( - url: - 'https://codelabs.developers.google.com/codelabs/google-maps-in-flutter', - source: LearningResourceSource.googleCodelab, - ), - ), - LearningResource( - name: 'Adding WebView to your Flutter app', - description: - 'With the WebView Flutter plugin you can add a WebView widget to ' - 'your Android or iOS Flutter app.', - type: LearningResourceType.tutorial, - tags: [ - LearningResourceTag.ios, - ], - link: ( - url: 'https://codelabs.developers.google.com/codelabs/flutter-webview', - source: LearningResourceSource.googleCodelab, - ), - ), - LearningResource( - name: 'Using FFI in a Flutter plugin', - description: - 'Learn how to use Dart\'s FFI (foreign function interface) library, ' - 'ffigen, allowing you to leverage existing native libraries that ' - 'provide a C interface.', - type: LearningResourceType.tutorial, - tags: [], - link: ( - url: 'https://codelabs.developers.google.com/codelabs/flutter-ffigen', - source: LearningResourceSource.googleCodelab, - ), - ), - LearningResource( - name: 'How to test a Flutter app', - description: - 'Start with a simple app that manages state with the Provider package. ' - 'Unit test the provider package. ' - 'Write widget tests for two of the widgets. ' - 'Use Flutter Driver to create an integration test.', - type: LearningResourceType.tutorial, - tags: [ - LearningResourceTag.testing, - ], - link: ( - url: - 'https://codelabs.developers.google.com/codelabs/flutter-app-testing/', - source: LearningResourceSource.googleCodelab, - ), - ), - LearningResource( - name: 'Adding a Home Screen widget to your Flutter app', - description: - 'Learn how to add a Home Screen widget to your Flutter app on iOS. ' - 'This applies to your home screen, lock screen, or the today view.', - type: LearningResourceType.tutorial, - tags: [ - LearningResourceTag.ios, - ], - link: ( - url: 'https://codelabs.developers.google.com/flutter-home-screen-widgets', - source: LearningResourceSource.googleCodelab, - ), - ), - LearningResource( - name: 'Write a Flutter desktop application', - description: - 'Build a Flutter desktop app (Windows, Linux, or macOS) that ' - 'accesses GitHub APIs, and create and use plugins to ' - 'interact with native APIs and desktop applications.', - type: LearningResourceType.tutorial, - tags: [ - LearningResourceTag.desktop, - ], - link: ( - url: - 'https://codelabs.developers.google.com/codelabs/flutter-github-client', - source: LearningResourceSource.googleCodelab, - ), - ), -]; - -final List _cookbookRecipes = [ - LearningResource( - name: 'Animate a page route transition', - description: - 'Transition between routes by animating the new route ' - 'into view from the bottom of the screen.', - type: LearningResourceType.recipe, - tags: [ - LearningResourceTag.animation, - ], - link: ( - url: '/cookbook/animation/page-route-animation', - source: LearningResourceSource.flutterDocs, - ), - ), - LearningResource( - name: 'Animate a widget using a physics simulation', - description: - 'Learn how to move a widget from a dragged point back to ' - 'the center using a spring simulation.', - type: LearningResourceType.recipe, - tags: [ - LearningResourceTag.animation, - ], - link: ( - url: '/cookbook/animation/physics-simulation', - source: LearningResourceSource.flutterDocs, - ), - ), - LearningResource( - name: 'Animate the properties of a container', - description: - 'Use AnimatedContainer to animate the ' - 'size, background color, and border radius of a Container.', - type: LearningResourceType.recipe, - tags: [ - LearningResourceTag.animation, - ], - link: ( - url: '/cookbook/animation/animated-container', - source: LearningResourceSource.flutterDocs, - ), - ), - LearningResource( - name: 'Fade a widget in and out', - description: - 'The AnimatedOpacity widget makes it easy to ' - 'perform opacity animations.', - type: LearningResourceType.recipe, - tags: [ - LearningResourceTag.animation, - ], - link: ( - url: '/cookbook/animation/opacity-animation', - source: LearningResourceSource.flutterDocs, - ), - ), - LearningResource( - name: 'Add a drawer to a screen', - description: - 'Use the Drawer widget in combination with a Scaffold to ' - 'create a layout with a Material Design drawer.', - type: LearningResourceType.recipe, - tags: [ - LearningResourceTag.widgets, - LearningResourceTag.material, - LearningResourceTag.layout, - ], - link: ( - url: '/cookbook/design/drawer', - source: LearningResourceSource.flutterDocs, - ), - ), - LearningResource( - name: 'Display a snackbar', - description: 'Use the Snackbar widget to display messages to your users.', - type: LearningResourceType.recipe, - tags: [], - link: ( - url: '/cookbook/design/snackbars', - source: LearningResourceSource.flutterDocs, - ), - ), - LearningResource( - name: 'Export fonts from a package', - description: 'Use a font across multiple apps.', - type: LearningResourceType.recipe, - tags: [], - link: ( - url: '/cookbook/design/package-fonts', - source: LearningResourceSource.flutterDocs, - ), - ), - LearningResource( - name: 'Update the UI based on orientation', - description: - 'Build a list that displays two columns in portrait mode and ' - 'three columns in landscape mode.', - type: LearningResourceType.recipe, - tags: [], - link: ( - url: '/cookbook/design/orientation', - source: LearningResourceSource.flutterDocs, - ), - ), - LearningResource( - name: 'Use a custom font', - description: 'Apply fonts to your entire app or individual widgets.', - type: LearningResourceType.recipe, - tags: [], - link: ( - url: '/cookbook/design/fonts', - source: LearningResourceSource.flutterDocs, - ), - ), - LearningResource( - name: 'Use themes to share colors and font styles', - description: 'To share styles across your app, use Themes.', - type: LearningResourceType.recipe, - tags: [], - link: ( - url: '/cookbook/design/themes', - source: LearningResourceSource.flutterDocs, - ), - ), - LearningResource( - name: 'Work with tabs', - description: - 'Working with tabs is a common pattern in mobile apps that ' - 'follow the Material Design or Cupertino guidelines.', - type: LearningResourceType.recipe, - tags: [], - link: ( - url: '/cookbook/design/tabs', - source: LearningResourceSource.flutterDocs, - ), - ), - LearningResource( - name: 'Create a download button', - description: - 'Build a download button that transitions through ' - 'multiple visual states, based on the status of an app download.', - type: LearningResourceType.recipe, - tags: [ - LearningResourceTag.design, - LearningResourceTag.animation, - ], - link: ( - url: '/cookbook/effects/download-button', - source: LearningResourceSource.flutterDocs, - ), - ), - LearningResource( - name: 'Create a nested navigation flow', - description: - 'Create top level routes, and routes nested below specific widgets.', - type: LearningResourceType.recipe, - tags: [ - LearningResourceTag.routingAndNavigation, - ], - link: ( - url: '/cookbook/effects/nested-nav', - source: LearningResourceSource.flutterDocs, - ), - ), - LearningResource( - name: 'Create a scrolling parallax effect', - description: - 'Create the parallax effect by building a ' - 'list of cards with images that \'move\'.', - type: LearningResourceType.recipe, - tags: [ - LearningResourceTag.design, - ], - link: ( - url: '/cookbook/effects/parallax-scrolling', - source: LearningResourceSource.flutterDocs, - ), - ), - LearningResource( - name: 'Create a shimmer loading effect', - description: - 'Communicate that data is loading with a ' - 'chrome color shimmer on the screen.', - type: LearningResourceType.recipe, - tags: [ - LearningResourceTag.animation, - ], - link: ( - url: '/cookbook/effects/shimmer-loading', - source: LearningResourceSource.flutterDocs, - ), - ), - LearningResource( - name: 'Create a staggered menu animation', - description: - 'Build a drawer menu with animated content that ' - 'is staggered and has a button that pops in at the bottom', - type: LearningResourceType.recipe, - tags: [ - LearningResourceTag.animation, - LearningResourceTag.design, - ], - link: ( - url: '/cookbook/effects/staggered-menu-animation', - source: LearningResourceSource.flutterDocs, - ), - ), - LearningResource( - name: 'Create a typing indicator', - description: - 'Build a speech bubble typing indicator that ' - 'animates in and out of view.', - type: LearningResourceType.recipe, - tags: [], - link: ( - url: '/cookbook/effects/typing-indicator', - source: LearningResourceSource.flutterDocs, - ), - ), - LearningResource( - name: 'Create an expandable FAB', - description: - 'Create a floating action button that spawns other action buttons.', - type: LearningResourceType.recipe, - tags: [ - LearningResourceTag.design, - ], - link: ( - url: '/cookbook/effects/expandable-fab', - source: LearningResourceSource.flutterDocs, - ), - ), - LearningResource( - name: 'Drag a UI element', - description: - 'Build a drag-and-drop interaction when the user long presses.', - type: LearningResourceType.recipe, - tags: [ - LearningResourceTag.design, - ], - link: ( - url: '/cookbook/effects/drag-a-widget', - source: LearningResourceSource.flutterDocs, - ), - ), - LearningResource( - name: 'Build a form with validation', - description: 'Learn how to add validation to a form.', - type: LearningResourceType.recipe, - tags: [], - link: ( - url: '/cookbook/forms/validation', - source: LearningResourceSource.flutterDocs, - ), - ), - LearningResource( - name: 'Create and style a text field', - description: 'In this recipe, explore how to create and style text fields.', - type: LearningResourceType.recipe, - tags: [], - link: ( - url: '/cookbook/forms/text-input', - source: LearningResourceSource.flutterDocs, - ), - ), - LearningResource( - name: 'Focus and text fields', - description: 'Shift focus to a text field programmatically.', - type: LearningResourceType.recipe, - tags: [], - link: ( - url: '/cookbook/forms/focus', - source: LearningResourceSource.flutterDocs, - ), - ), - LearningResource( - name: 'Handle changes to a text field', - description: 'Listen for changes to a TextField using a callback.', - type: LearningResourceType.recipe, - tags: [], - link: ( - url: '/cookbook/forms/text-field-changes', - source: LearningResourceSource.flutterDocs, - ), - ), - LearningResource( - name: 'Retrieve the value of a text field', - description: - 'Learn how to retrieve the text a user has entered into a text field.', - type: LearningResourceType.recipe, - tags: [], - link: ( - url: '/cookbook/forms/retrieve-input', - source: LearningResourceSource.flutterDocs, - ), - ), - LearningResource( - name: 'Add achievements and leaderboards to your game', - description: - 'Use the games_services package to ' - 'add leaderboard functionality to your mobile game.', - type: LearningResourceType.recipe, - tags: [], - link: ( - url: '/cookbook/games/achievements-leaderboard', - source: LearningResourceSource.flutterDocs, - ), - ), - LearningResource( - name: 'Add multiplayer support to your Flutter game', - description: - 'Use the cloud_firestore package to ' - 'implement multiplayer capabilities in your game.', - type: LearningResourceType.recipe, - tags: [ - LearningResourceTag.firebase, - ], - link: ( - url: '/cookbook/games/firestore-multiplayer', - source: LearningResourceSource.flutterDocs, - ), - ), - LearningResource( - name: 'Add ads to your Flutter game', - description: - 'Use the google_mobile_ads package to ' - 'add a banner ad to your app or game.', - type: LearningResourceType.recipe, - tags: [], - link: ( - url: '/cookbook/plugins/google-mobile-ads', - source: LearningResourceSource.flutterDocs, - ), - ), - LearningResource( - name: 'Add Material touch ripples', - description: 'Use the Inkwell widget to display a ripple animation.', - type: LearningResourceType.recipe, - tags: [ - LearningResourceTag.material, - ], - link: ( - url: '/cookbook/gestures/ripples', - source: LearningResourceSource.flutterDocs, - ), - ), - LearningResource( - name: 'Handle taps', - description: - 'Use the GestureDetector widget to respond to ' - 'fundamental actions, such as tapping and dragging.', - type: LearningResourceType.recipe, - tags: [], - link: ( - url: '/cookbook/gestures/handling-taps', - source: LearningResourceSource.flutterDocs, - ), - ), - LearningResource( - name: 'Implement swipe to dismiss', - description: 'Learn how to use the Dismissible widget.', - type: LearningResourceType.recipe, - tags: [], - link: ( - url: '/cookbook/gestures/dismissible', - source: LearningResourceSource.flutterDocs, - ), - ), - LearningResource( - name: 'Display images from the internet', - description: - 'To work with images from a URL, use the Image.network() constructor.', - type: LearningResourceType.recipe, - tags: [ - LearningResourceTag.goodForBeginners, - ], - link: ( - url: '/cookbook/images/network-image', - source: LearningResourceSource.flutterDocs, - ), - ), - LearningResource( - name: 'Fade in images with a placeholder', - description: - 'Use the FadeInImage widget to ' - 'show a visual placeholder before an image loads.', - type: LearningResourceType.recipe, - tags: [], - link: ( - url: '/cookbook/images/fading-in-images', - source: LearningResourceSource.flutterDocs, - ), - ), - LearningResource( - name: 'Grid lists', - description: 'Learn to use a GridView widget.', - type: LearningResourceType.recipe, - tags: [ - LearningResourceTag.layout, - ], - link: ( - url: '/cookbook/lists/grid-lists', - source: LearningResourceSource.flutterDocs, - ), - ), - LearningResource( - name: 'Horizontal lists', - description: 'Learn to display items horizontally in a scrollable list.', - type: LearningResourceType.recipe, - tags: [ - LearningResourceTag.layout, - ], - link: ( - url: '/cookbook/lists/horizontal-list', - source: LearningResourceSource.flutterDocs, - ), - ), - LearningResource( - name: 'Lists with different types of items', - description: 'Create a list with headers followed by a few list items.', - type: LearningResourceType.recipe, - tags: [ - LearningResourceTag.layout, - ], - link: ( - url: '/cookbook/lists/mixed-list', - source: LearningResourceSource.flutterDocs, - ), - ), - LearningResource( - name: 'Lists and floating app bars', - description: 'Place a floating app bar or navigation bar above a list.', - type: LearningResourceType.recipe, - tags: [ - LearningResourceTag.layout, - ], - link: ( - url: '/cookbook/lists/floating-app-bar', - source: LearningResourceSource.flutterDocs, - ), - ), - LearningResource( - name: 'Basic lists', - description: 'Learn to display items with the ListView widget.', - type: LearningResourceType.recipe, - tags: [ - LearningResourceTag.layout, - ], - link: ( - url: '/cookbook/lists/basic-list', - source: LearningResourceSource.flutterDocs, - ), - ), - LearningResource( - name: 'Long lists', - description: - 'Work with longer lists with the Listview.builder constructor.', - type: LearningResourceType.recipe, - tags: [ - LearningResourceTag.layout, - ], - link: ( - url: '/cookbook/lists/long-lists', - source: LearningResourceSource.flutterDocs, - ), - ), - LearningResource( - name: 'Lists with spaced items', - description: 'Create a list with padding between items.', - type: LearningResourceType.recipe, - tags: [ - LearningResourceTag.layout, - ], - link: ( - url: '/cookbook/lists/spaced-items', - source: LearningResourceSource.flutterDocs, - ), - ), - LearningResource( - name: 'Animate a widget across screens', - description: - 'Use the Hero widget to animate a widget from one screen to the next.', - type: LearningResourceType.recipe, - tags: [ - LearningResourceTag.routingAndNavigation, - LearningResourceTag.animation, - ], - link: ( - url: '/cookbook/navigation/hero-animations', - source: LearningResourceSource.flutterDocs, - ), - ), - LearningResource( - name: 'Navigate to a new screen and back', - description: 'This recipe uses the Navigator to navigate to a new route.', - type: LearningResourceType.recipe, - tags: [ - LearningResourceTag.routingAndNavigation, - ], - link: ( - url: '/cookbook/navigation/navigation-basics', - source: LearningResourceSource.flutterDocs, - ), - ), - LearningResource( - name: 'Named routes', - description: 'Create named routes and navigate to them.', - type: LearningResourceType.recipe, - tags: [ - LearningResourceTag.routingAndNavigation, - ], - link: ( - url: '/cookbook/navigation/named-routes', - source: LearningResourceSource.flutterDocs, - ), - ), - LearningResource( - name: 'Arguments and named routes', - description: - 'Pass arguments to a named route and read the arguments on that route.', - type: LearningResourceType.recipe, - tags: [ - LearningResourceTag.routingAndNavigation, - ], - link: ( - url: '/cookbook/navigation/navigate-with-arguments', - source: LearningResourceSource.flutterDocs, - ), - ), - LearningResource( - name: 'Android app links', - description: 'Set up deep linking on Android', - type: LearningResourceType.recipe, - tags: [ - LearningResourceTag.routingAndNavigation, - ], - link: ( - url: '/cookbook/navigation/set-up-app-links', - source: LearningResourceSource.flutterDocs, - ), - ), - LearningResource( - name: 'iOS universal links', - description: 'Set up universal links for iOS', - type: LearningResourceType.recipe, - tags: [ - LearningResourceTag.routingAndNavigation, - LearningResourceTag.ios, - ], - link: ( - url: '/cookbook/navigation/set-up-universal-links', - source: LearningResourceSource.flutterDocs, - ), - ), - LearningResource( - name: 'Return data from a screen', - description: - 'Return data from one screen to another with the Navigator.pop method.', - type: LearningResourceType.recipe, - tags: [ - LearningResourceTag.routingAndNavigation, - ], - link: ( - url: '/cookbook/navigation/returning-data', - source: LearningResourceSource.flutterDocs, - ), - ), - LearningResource( - name: 'Send data to a new screen', - description: 'Send data from one screen to new one.', - type: LearningResourceType.recipe, - tags: [ - LearningResourceTag.routingAndNavigation, - ], - link: ( - url: '/cookbook/navigation/passing-data', - source: LearningResourceSource.flutterDocs, - ), - ), - LearningResource( - name: 'Fetch data from the internet', - description: 'Learn to use HTTP in your app.', - type: LearningResourceType.recipe, - tags: [], - link: ( - url: '/cookbook/networking/fetch-data', - source: LearningResourceSource.flutterDocs, - ), - ), - LearningResource( - name: 'Make authenticated requests', - description: 'Authorization headers in HTTP', - type: LearningResourceType.recipe, - tags: [], - link: ( - url: '/cookbook/networking/authenticated-requests', - source: LearningResourceSource.flutterDocs, - ), - ), - LearningResource( - name: 'Send data to the internet', - description: 'Send HTTP POST requests in your app.', - type: LearningResourceType.recipe, - tags: [], - link: ( - url: '/cookbook/networking/send-data', - source: LearningResourceSource.flutterDocs, - ), - ), - LearningResource( - name: 'Update data over the internet', - description: 'Send an HTTP put request.', - type: LearningResourceType.recipe, - tags: [], - link: ( - url: '/cookbook/networking/update-data', - source: LearningResourceSource.flutterDocs, - ), - ), - LearningResource( - name: 'Delete data on the internet', - description: 'Send an HTTP delete request.', - type: LearningResourceType.recipe, - tags: [], - link: ( - url: '/cookbook/networking/delete-data', - source: LearningResourceSource.flutterDocs, - ), - ), - LearningResource( - name: 'WebSockets', - description: 'Connect to and communicate with a websocket.', - type: LearningResourceType.recipe, - tags: [], - link: ( - url: '/cookbook/networking/web-sockets', - source: LearningResourceSource.flutterDocs, - ), - ), - LearningResource( - name: 'Parse JSON in the background', - description: 'Learn to use Dart\'s Isolate objects', - type: LearningResourceType.recipe, - tags: [], - link: ( - url: '/cookbook/networking/background-parsing', - source: LearningResourceSource.flutterDocs, - ), - ), - LearningResource( - name: 'Persist data with SQLite', - description: 'Use the sqflite package.', - type: LearningResourceType.recipe, - tags: [], - link: ( - url: '/cookbook/persistence/sqlite', - source: LearningResourceSource.flutterDocs, - ), - ), - LearningResource( - name: 'Read and write files', - description: - 'Use the dart:io library and path_provider plugin to ' - 'save files to disk.', - type: LearningResourceType.recipe, - tags: [], - link: ( - url: '/cookbook/persistence/reading-writing-files', - source: LearningResourceSource.flutterDocs, - ), - ), - LearningResource( - name: 'Store key-value data on disk', - description: 'Persist data with shared_preferences', - type: LearningResourceType.recipe, - tags: [], - link: ( - url: '/cookbook/persistence/key-value', - source: LearningResourceSource.flutterDocs, - ), - ), - LearningResource( - name: 'Play and pause a video', - description: - 'Play videos stored on the file system, as an asset, ' - 'or from the internet.', - type: LearningResourceType.recipe, - tags: [], - link: ( - url: '/cookbook/plugins/play-video', - source: LearningResourceSource.flutterDocs, - ), - ), - LearningResource( - name: 'Use the camera', - description: 'Learn to use a devices camera.', - type: LearningResourceType.recipe, - tags: [], - link: ( - url: '/cookbook/plugins/picture-using-camera', - source: LearningResourceSource.flutterDocs, - ), - ), - LearningResource( - name: 'Report errors to a service', - description: 'Report errors to Sentry crash reporting.', - type: LearningResourceType.recipe, - tags: [ - LearningResourceTag.testing, - ], - link: ( - url: '/cookbook/maintenance/error-reporting', - source: LearningResourceSource.flutterDocs, - ), - ), - LearningResource( - name: 'Performance profiling', - description: 'Write a test that records a performance timeline.', - type: LearningResourceType.recipe, - tags: [ - LearningResourceTag.testing, - ], - link: ( - url: '/cookbook/testing/integration/profiling', - source: LearningResourceSource.flutterDocs, - ), - ), - LearningResource( - name: 'Write unit tests', - description: 'An introduction to writing unit tests.', - type: LearningResourceType.recipe, - tags: [ - LearningResourceTag.testing, - ], - link: ( - url: '/cookbook/testing/unit/introduction', - source: LearningResourceSource.flutterDocs, - ), - ), - LearningResource( - name: 'Write widget tests', - description: 'An introduction to writing widget tests.', - type: LearningResourceType.recipe, - tags: [ - LearningResourceTag.testing, - ], - link: ( - url: '/cookbook/testing/widget/introduction', - source: LearningResourceSource.flutterDocs, - ), - ), - LearningResource( - name: 'Mock dependencies in tests', - description: 'The basics of mocking with the Mockito package.', - type: LearningResourceType.recipe, - tags: [ - LearningResourceTag.testing, - ], - link: ( - url: '/cookbook/testing/unit/mocking', - source: LearningResourceSource.flutterDocs, - ), - ), - LearningResource( - name: 'Find widgets in tests', - description: - 'This recipe looks at the \'find\' constant ' - 'provided by the flutter_test package.', - type: LearningResourceType.recipe, - tags: [ - LearningResourceTag.testing, - ], - link: ( - url: '/cookbook/testing/widget/finders', - source: LearningResourceSource.flutterDocs, - ), - ), - LearningResource( - name: 'Handle scrolling', - description: 'Learn how to scroll in widget tests.', - type: LearningResourceType.recipe, - tags: [ - LearningResourceTag.testing, - ], - link: ( - url: '/cookbook/testing/widget/scrolling', - source: LearningResourceSource.flutterDocs, - ), - ), - LearningResource( - name: 'App orientation', - description: 'Learn how to check app orientation in widget tests.', - type: LearningResourceType.recipe, - tags: [ - LearningResourceTag.testing, - ], - link: ( - url: '/cookbook/testing/widget/orientation', - source: LearningResourceSource.flutterDocs, - ), - ), - LearningResource( - name: 'Tap, drag, and enter text', - description: 'Interact with widgets in widget tests.', - type: LearningResourceType.recipe, - tags: [ - LearningResourceTag.testing, - ], - link: ( - url: '/cookbook/testing/widget/tap-drag', - source: LearningResourceSource.flutterDocs, - ), - ), - LearningResource( - name: 'Persistent storage architecture - SQL', - description: 'Save complex application data to a user\'s device with SQL.', - type: LearningResourceType.recipe, - tags: [ - LearningResourceTag.architecture, - ], - link: ( - url: '/app-architecture/design-patterns/sql', - source: LearningResourceSource.flutterDocs, - ), - ), - LearningResource( - name: 'Error handling with Result objects', - description: 'Improve error handling across classes with Result objects.', - type: LearningResourceType.recipe, - tags: [ - LearningResourceTag.testing, - LearningResourceTag.architecture, - ], - link: ( - url: '/app-architecture/design-patterns/result', - source: LearningResourceSource.flutterDocs, - ), - ), - LearningResource( - name: 'Optimistic state', - description: - 'Improve the perception of responsiveness of an application by ' - 'implementing optimistic state.', - type: LearningResourceType.recipe, - tags: [ - LearningResourceTag.architecture, - ], - link: ( - url: '/app-architecture/design-patterns/optimistic-state', - source: LearningResourceSource.flutterDocs, - ), - ), - LearningResource( - name: 'Offline First', - description: - 'Implement offline-first support for one feature in an application.', - type: LearningResourceType.recipe, - tags: [ - LearningResourceTag.architecture, - ], - link: ( - url: '/app-architecture/design-patterns/offline-first', - source: LearningResourceSource.flutterDocs, - ), - ), - LearningResource( - name: 'Persistent storage architecture - Key-value data', - description: - 'Save application data to a user\'s on-device key-value store.', - type: LearningResourceType.recipe, - tags: [ - LearningResourceTag.architecture, - ], - link: ( - url: '/app-architecture/design-patterns/key-value-data', - source: LearningResourceSource.flutterDocs, - ), - ), - LearningResource( - name: 'Command pattern', - description: 'Simplify view model logic by implementing a Command class.', - type: LearningResourceType.recipe, - tags: [ - LearningResourceTag.architecture, - ], - link: ( - url: '/app-architecture/design-patterns/command', - source: LearningResourceSource.flutterDocs, - ), - ), -]; - -final List _demos = [ - LearningResource( - name: 'Add-to-app', - description: 'Recommended approaches for adding Flutter to existing apps.', - type: LearningResourceType.sample, - tags: [ - LearningResourceTag.ios, - ], - link: ( - url: 'https://github.com/flutter/samples/tree/main/add_to_app', - source: LearningResourceSource.gitHub, - ), - ), - LearningResource( - name: 'Android splash screen', - description: - 'A Flutter sample app that exemplifies how to ' - 'implement an animated splash screen for Android devices.', - type: LearningResourceType.sample, - tags: [], - link: ( - url: 'https://github.com/flutter/samples/tree/main/android_splash_screen', - source: LearningResourceSource.gitHub, - ), - ), - LearningResource( - name: 'iOS app clip', - description: - 'A sample project demonstrating integration with iOS App Clip.', - type: LearningResourceType.sample, - tags: [ - LearningResourceTag.ios, - ], - link: ( - url: 'https://github.com/flutter/samples/tree/main/ios_app_clip', - source: LearningResourceSource.gitHub, - ), - ), - LearningResource( - name: 'Swift platform view', - description: - 'A Flutter sample app that combines a iOS-native ' - 'UIViewController with a full-screen Flutter view.', - type: LearningResourceType.sample, - tags: [ - LearningResourceTag.ios, - ], - link: ( - url: 'https://github.com/flutter/samples/tree/main/platform_view_swift', - source: LearningResourceSource.gitHub, - ), - ), - LearningResource( - name: 'Simplistic editor', - description: - 'This sample text editor showcases the use of TextEditingDeltas and ' - 'a DeltaTextInputClient to expand and contract styled ranges of text.', - type: LearningResourceType.sample, - tags: [], - link: ( - url: 'https://github.com/flutter/samples/tree/main/simplistic_editor', - source: LearningResourceSource.gitHub, - ), - ), -]; - -final List _quickStartsForDart = [ - LearningResource( - name: 'Command-line app', - description: - 'A command line app that parses command-line options and ' - 'fetches data from GitHub.', - type: LearningResourceType.sample, - tags: [ - LearningResourceTag.dart, - ], - link: ( - url: 'https://github.com/dart-lang/samples/tree/main/command_line', - source: LearningResourceSource.gitHub, - ), - ), - LearningResource( - name: 'Extension methods', - description: 'Demonstrates Dart\'s extensions method syntax.', - type: LearningResourceType.sample, - tags: [ - LearningResourceTag.dart, - ], - link: ( - url: 'https://github.com/dart-lang/samples/tree/main/extension_methods', - source: LearningResourceSource.gitHub, - ), - ), - LearningResource( - name: 'FFI', - description: - 'A series of simple examples demonstrating how to ' - 'call C libraries from Dart.', - type: LearningResourceType.sample, - tags: [ - LearningResourceTag.dart, - ], - link: ( - url: 'https://github.com/dart-lang/samples/tree/main/ffi', - source: LearningResourceSource.gitHub, - ), - ), - LearningResource( - name: 'Isolates (in a CLI)', - description: - 'Command line applications that demonstrate how to ' - 'work with Concurrency in Dart using isolates.', - type: LearningResourceType.sample, - tags: [ - LearningResourceTag.dart, - ], - link: ( - url: 'https://github.com/dart-lang/samples/tree/main/ffi', - source: LearningResourceSource.gitHub, - ), - ), - LearningResource( - name: 'Native Dart app', - description: - 'A command line application that can be compiled to ' - 'native code using `dart compile exe`.', - type: LearningResourceType.sample, - tags: [ - LearningResourceTag.dart, - ], - link: ( - url: 'https://github.com/dart-lang/samples/tree/main/native_app', - source: LearningResourceSource.gitHub, - ), - ), - LearningResource( - name: 'Server side Dart', - description: 'Examples of running Dart on the server.', - type: LearningResourceType.sample, - tags: [ - LearningResourceTag.dart, - ], - link: ( - url: 'https://github.com/dart-lang/samples/tree/main/server', - source: LearningResourceSource.gitHub, - ), - ), - LearningResource( - name: 'Package constraint solver', - description: - 'Demonstrates best-practices for publishing packages on pub.dev.', - type: LearningResourceType.sample, - tags: [ - LearningResourceTag.dart, - ], - link: ( - url: 'https://github.com/dart-lang/samples/tree/main/server', - source: LearningResourceSource.gitHub, - ), - ), -]; - -final List _quickStartsForFlutter = [ - LearningResource( - name: 'Asset transformation', - description: - 'Demonstrates how to transform images\' color scales and formats.', - type: LearningResourceType.sample, - tags: [ - LearningResourceTag.design, - ], - link: ( - url: 'https://github.com/flutter/samples/tree/main/asset_transformation', - source: LearningResourceSource.gitHub, - ), - ), - LearningResource( - name: 'Background isolate channels', - description: 'Demonstrates how to use long-lived isolates.', - type: LearningResourceType.sample, - tags: [], - link: ( - url: - 'https://github.com/flutter/samples/tree/main/background_isolate_channels', - source: LearningResourceSource.gitHub, - ), - ), - LearningResource( - name: 'Code sharing', - description: - 'Demonstrates how to share business logic between ' - 'a Flutter client and Dart server using `package:shelf`.', - type: LearningResourceType.sample, - tags: [ - LearningResourceTag.dart, - ], - link: ( - url: 'https://github.com/flutter/samples/tree/main/code_sharing', - source: LearningResourceSource.gitHub, - ), - ), - LearningResource( - name: 'Context menus', - description: - 'This sample shows how to create and customize ' - 'cross-platform context menus, such as the ' - 'text selection toolbar on mobile or the right click menu on desktop.', - type: LearningResourceType.sample, - tags: [ - LearningResourceTag.desktop, - ], - link: ( - url: 'https://github.com/flutter/samples/tree/main/context_menus', - source: LearningResourceSource.gitHub, - ), - ), - LearningResource( - name: 'Desktop UI', - description: - 'Demonstrates desktop features in both ' - 'Material and FluentUI design systems.', - type: LearningResourceType.sample, - tags: [ - LearningResourceTag.material, - LearningResourceTag.desktop, - ], - link: ( - url: 'https://github.com/flutter/samples/tree/main/desktop_photo_search', - source: LearningResourceSource.gitHub, - ), - ), - LearningResource( - name: 'AI generated dynamic theme', - description: - 'Demonstrates how to call on-device Flutter APIs ' - 'based on output from the Gemini API.', - type: LearningResourceType.sample, - tags: [ - LearningResourceTag.ai, - LearningResourceTag.googleApis, - ], - link: ( - url: 'https://github.com/flutter/samples/tree/main/dynamic_theme', - source: LearningResourceSource.gitHub, - ), - ), - LearningResource( - name: 'Form app', - description: - 'A sample demonstrating different types of forms and best practices.', - type: LearningResourceType.sample, - tags: [ - LearningResourceTag.layout, - ], - link: ( - url: 'https://github.com/flutter/samples/tree/main/form_app', - source: LearningResourceSource.gitHub, - ), - ), - LearningResource( - name: 'AI todo list', - description: - 'A developer sample written in Flutter demonstrating how to ' - 'interact with a to-do list in natural language using the Gemini API.', - type: LearningResourceType.sample, - tags: [ - LearningResourceTag.ai, - LearningResourceTag.googleApis, - ], - link: ( - url: 'https://github.com/flutter/samples/tree/main/gemini_tasks', - source: LearningResourceSource.gitHub, - ), - ), - LearningResource( - name: 'Google Maps plugin', - description: 'Demonstrates the Google Maps for Flutter plugin.', - type: LearningResourceType.sample, - tags: [ - LearningResourceTag.googleApis, - ], - link: ( - url: 'https://github.com/flutter/samples/tree/main/google_maps', - source: LearningResourceSource.gitHub, - ), - ), - LearningResource( - name: 'Infinite list', - description: - 'A Flutter sample app that shows an implementation of ' - 'the \'infinite list\' UX pattern.', - type: LearningResourceType.sample, - tags: [ - LearningResourceTag.layout, - ], - link: ( - url: 'https://github.com/flutter/samples/tree/main/infinite_list', - source: LearningResourceSource.gitHub, - ), - ), - LearningResource( - name: 'Isolates', - description: - 'A sample application that demonstrates ' - 'best practices when using isolates.', - type: LearningResourceType.sample, - tags: [], - link: ( - url: 'https://github.com/flutter/samples/tree/main/isolate_example', - source: LearningResourceSource.gitHub, - ), - ), - LearningResource( - name: 'Navigation and routing', - description: - 'A sample that shows how to use `go_router` API to ' - 'handle common navigation scenarios.', - type: LearningResourceType.sample, - tags: [ - LearningResourceTag.routingAndNavigation, - ], - link: ( - url: - 'https://github.com/flutter/samples/tree/main/navigation_and_routing', - source: LearningResourceSource.gitHub, - ), - ), - LearningResource( - name: 'Google Maps Flutter plugin', - description: - 'A sample place tracking app that uses the google_apps_flutter plugin.', - type: LearningResourceType.sample, - tags: [ - LearningResourceTag.googleApis, - ], - link: ( - url: 'https://github.com/flutter/samples/tree/main/place_tracker', - source: LearningResourceSource.gitHub, - ), - ), - LearningResource( - name: 'Platform adaptive design', - description: - 'This sample project shows a Flutter app that ' - 'maximizes application code reuse while adhering to ' - 'different design patterns on Android and iOS.', - type: LearningResourceType.sample, - tags: [ - LearningResourceTag.design, - ], - link: ( - url: 'https://github.com/flutter/samples/tree/main/platform_design', - source: LearningResourceSource.gitHub, - ), - ), - LearningResource( - name: 'Counter app with Provider', - description: - 'The starter Flutter application, but ' - 'using the Provider package to manage state.', - type: LearningResourceType.sample, - tags: [ - LearningResourceTag.stateManagement, - LearningResourceTag.architecture, - ], - link: ( - url: 'https://github.com/flutter/samples/tree/main/provider_counter', - source: LearningResourceSource.gitHub, - ), - ), - LearningResource( - name: 'Shopping app with Provider', - description: - 'A Flutter sample app that shows a ' - 'state management approach using the Provider package.', - type: LearningResourceType.sample, - tags: [ - LearningResourceTag.stateManagement, - LearningResourceTag.architecture, - ], - link: ( - url: 'https://github.com/flutter/samples/tree/main/provider_shopper', - source: LearningResourceSource.gitHub, - ), - ), - LearningResource( - name: 'Simple shaders', - description: 'A simple Flutter fragment shaders project.', - type: LearningResourceType.sample, - tags: [], - link: ( - url: 'https://github.com/flutter/samples/tree/main/simple_shader', - source: LearningResourceSource.gitHub, - ), - ), - LearningResource( - name: 'Desktop calculator', - description: - 'A calculator sample to demonstrate a ' - 'simple start for a desktop Flutter app.', - type: LearningResourceType.sample, - tags: [ - LearningResourceTag.desktop, - ], - link: ( - url: 'https://github.com/flutter/samples/tree/main/simplistic_calculator', - source: LearningResourceSource.gitHub, - ), - ), - LearningResource( - name: 'Testing app', - description: - 'A sample app that shows different types of testing in Flutter.', - type: LearningResourceType.sample, - tags: [ - LearningResourceTag.testing, - ], - link: ( - url: 'https://github.com/flutter/samples/tree/main/testing_app', - source: LearningResourceSource.gitHub, - ), - ), - LearningResource( - name: 'Web element embedding', - description: - 'Modifies the index.html of a Flutter app so ' - 'it is launched in a custom hostElement.', - type: LearningResourceType.sample, - tags: [ - LearningResourceTag.web, - ], - link: ( - url: - 'https://github.com/flutter/samples/tree/main/web_embedding/element_embedding_demo', - source: LearningResourceSource.gitHub, - ), - ), - LearningResource( - name: 'ng-flutter', - description: - 'A simple Angular app (and component) that ' - 'replicates the element embedding example, but in an Angular app.', - type: LearningResourceType.sample, - tags: [ - LearningResourceTag.web, - LearningResourceTag.googleApis, - ], - link: ( - url: - 'https://github.com/flutter/samples/tree/main/web_embedding/ng-flutter', - source: LearningResourceSource.gitHub, - ), - ), - LearningResource( - name: 'Platform channels', - description: - 'A sample Flutter app which demonstrates how to ' - 'use `MethodChannel`, `EventChannel`, ' - '`BasicMessageChannel` and `MessageCodec`.', - type: LearningResourceType.sample, - tags: [ - LearningResourceTag.ios, - ], - link: ( - url: 'https://github.com/flutter/samples/tree/main/platform_channels', - source: LearningResourceSource.gitHub, - ), - ), -]; diff --git a/site/lib/src/loaders/data_processor.dart b/site/lib/src/loaders/data_processor.dart index 37fac337173..831dce08de1 100644 --- a/site/lib/src/loaders/data_processor.dart +++ b/site/lib/src/loaders/data_processor.dart @@ -9,6 +9,7 @@ import 'package:jaspr_content/jaspr_content.dart'; import 'package:path/path.dart' as path; import '../data/devtools_releases.dart'; +import '../models/learning_resource_model.dart'; /// A shared data loader to add data to each loaded page. final class DataProcessor implements DataLoader { @@ -16,6 +17,7 @@ final class DataProcessor implements DataLoader { Future loadData(Page page) async { _loadDevToolsReleases(page); _loadLastModified(page); + _processLearningResources(page); } static void _loadDevToolsReleases(Page page) { @@ -49,6 +51,36 @@ final class DataProcessor implements DataLoader { }, ); } + + static List? _cachedLearningResources; + + static void _processLearningResources(Page page) { + if (_cachedLearningResources != null) { + page.apply( + data: {'learningResources': _cachedLearningResources!}, + ); + return; + } + + final resourceGroups = + page.data['learning-resources-index'] as Map?; + if (resourceGroups == null) return; + + final learningResources = _cachedLearningResources = []; + for (final group in resourceGroups.values) { + for (final resource in group as List) { + learningResources.add( + LearningResource.fromMap(resource as Map), + ); + } + } + + page.apply( + data: { + 'learningResources': learningResources, + }, + ); + } } /// Determines the last modified date for a given path diff --git a/site/lib/src/models/learning_resource_model.dart b/site/lib/src/models/learning_resource_model.dart index 263c5decd74..c3c4e221a8f 100644 --- a/site/lib/src/models/learning_resource_model.dart +++ b/site/lib/src/models/learning_resource_model.dart @@ -2,67 +2,107 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -final class LearningResource { - final String name; - final String description; - final LearningResourceType type; - final List tags; - final ({String url, LearningResourceSource source}) link; - final String? imageUrl; +import 'package:universal_web/web.dart' as web; +final class LearningResource { LearningResource({ required this.name, required this.description, required this.type, required this.tags, - required this.link, + this.link, this.imageUrl, }); + + /// Creates a [LearningResource] from a Map, used on the server + /// when parsing the yaml data files. + factory LearningResource.fromMap(Map map) { + return LearningResource( + name: map['name'] as String, + description: map['description'] as String, + type: map['type'] as String, + tags: (map['tags'] as List?)?.cast() ?? [], + link: ( + label: (map['link'] as Map)['label'] as String, + url: (map['link'] as Map)['url'] as String, + ), + imageUrl: map['imageUrl'] as String?, + ); + } + + /// Creates a [LearningResource] from a DOM Element, used on the client + /// for recreating and filtering existing resources. + factory LearningResource.fromElement(web.Element element) { + final dataType = element.getAttribute('data-type') ?? ''; + final dataTags = element.getAttribute('data-tags') ?? ''; + final dataDescription = element.getAttribute('data-description') ?? ''; + + return LearningResource( + name: element.id, + type: dataType, + tags: dataTags.split(',').map((t) => t.trim().toLowerCase()).toList(), + description: dataDescription, + ); + } + + final String name; + final String description; + final String type; + final List tags; + final ({String url, String label})? link; + final String? imageUrl; } enum LearningResourceType { - recipe('cookbook', 'Cookbook recipe'), - sample('demo', 'Demo'), - tutorial('codelab', 'Codelab'), - workshop('workshop', 'Workshop'); + tutorial('Tutorial', ['codelab', 'tutorial']), + sampleCode('Sample code', ['quickstart', 'demo', 'sample', 'sample code']), + workshop('Workshop', ['workshop', 'video']), + recipe('Recipe', ['recipe', 'how to', 'cookbook']); - const LearningResourceType(this.id, this.formattedName); + const LearningResourceType(this.label, this.tags); - final String id; - final String formattedName; + final String label; + final List tags; } enum LearningResourceTag { - ai('ai', 'AI'), - animation('animation', 'Animation'), - architecture('architecture', 'Architecture'), - cupertino('cupertino', 'Cupertino'), - dart('dart', 'Dart'), - design('design', 'Design'), - desktop('desktop', 'Desktop'), - firebase('firebase', 'Firebase'), - goodForBeginners('good-for-beginners', 'Good for beginners'), - googleApis('google-apis', 'Google APIs'), - ios('ios', 'iOS'), - layout('layout', 'Layout'), - material('material', 'Material'), - routingAndNavigation('routing-and-navigation', 'Routing and navigation'), - stateManagement('state-management', 'State management'), - testing('testing', 'Testing'), - web('web', 'Web'), - widgets('widgets', 'Widgets'); + ai('AI', ['ai', 'gemini', 'llm']), + animation('Animation', ['animations', 'animate', 'animation']), + architecture('Architecture', [ + 'state-management', + 'architecture', + 'provider', + 'bloc', + 'stream', + ]), + cupertino('Cupertino', ['cupertino', 'ios', 'macos']), + design('Design', ['design', 'widgets']), + desktop('Desktop', ['windows', 'macos', 'linux']), + firebase('Firebase', ['firebase', 'firestore', 'cloud']), + goodForBeginners('Good for beginners', ['beginner', 'beginners']), + googleApis('Google APIs', ['google', 'gemini', 'maps', 'firebase', 'cloud']), + ios('iOS', ['cupertino', 'ios']), + layout('Layout', ['layout', 'lists', 'scrolling', 'widgets']), + material('Material', ['material', 'android']), + routingAndNavigation('Routing and navigation', [ + 'routing', + 'route', + 'navigation', + 'navigator', + ]), + stateManagement('State management', [ + 'state-management', + 'architecture', + 'provider', + 'bloc', + 'stream', + ]), + testing('Testing', ['testing', 'tests', 'test', 'perf', 'performance']), + web('Web', ['web', 'wasm']), + widgets('Widgets', ['widgets', 'layout']); - const LearningResourceTag(this.id, this.formattedName); + const LearningResourceTag(this.label, this.tags); - final String id; - final String formattedName; -} - -enum LearningResourceSource { - dartDocs, - flutterDocs, - gitHub, - youTube, - googleCodelab, - medium, -} + final String label; + final List tags; +} \ No newline at end of file diff --git a/site/pubspec.yaml b/site/pubspec.yaml index b8b10b12570..f3f03bcef8e 100644 --- a/site/pubspec.yaml +++ b/site/pubspec.yaml @@ -12,7 +12,7 @@ dependencies: crypto: ^3.0.6 html: ^0.15.6 http: ^1.5.0 - jaspr: ^0.21.5 + jaspr: ^0.21.6 jaspr_content: ^0.4.2 # Used as our template engine. liquify: ^1.3.0 @@ -32,7 +32,7 @@ dev_dependencies: ref: 88aa84df953e67b7595b1e214b717f26d81ed538 build_runner: ^2.9.0 build_web_compilers: ^4.3.0 - jaspr_builder: ^0.21.5 + jaspr_builder: ^0.21.6 sass: ^1.93.2 sass_builder: ^2.4.0 diff --git a/src/data/learning-resources-index/codelabs.yml b/src/data/learning-resources-index/codelabs.yml new file mode 100644 index 00000000000..4fa7114a50b --- /dev/null +++ b/src/data/learning-resources-index/codelabs.yml @@ -0,0 +1,438 @@ +# Good for beginners +- name: Your first Flutter app workshop + description: | + An instructor-led version of our very popular + 'Write your first Flutter app' codelab. + tags: + - beginner + - intro + type: workshop + link: + label: YouTube + url: https://www.youtube.com/watch?v=8sAyPDLorek + +- name: Your first Flutter app + description: | + Create a simple random-name generator app. + This app is responsive and runs on mobile, desktop, and web. + tags: + - beginner + - intro + type: codelab + link: + label: Google Codelab + url: https://codelabs.developers.google.com/codelabs/flutter-codelab-first + +# Next steps + +- name: Records and Patterns in Dart + description: | + Discover Dart 3's new records and patterns features. + Learn how you can use them in a Flutter app to help you + write more readable and maintainable Dart code. + tags: + - dart + - records + - pattern matching + sdk: dart + type: codelab + link: + label: Google Codelab + url: https://codelabs.developers.google.com/codelabs/dart-patterns-records + +- name: Scrolling experiences in Flutter + description: | + Start with an app that performs simple, straightforward scrolling + and enhance it to create fancy and custom scrolling effects + by using slivers. + tags: + - scrolling + - scroll + type: codelab + link: + label: YouTube + url: https://www.youtube.com/watch?v=YY-_yrZdjGc + +#Design + +- name: Take your Flutter app from boring to beautiful + description: | + Learn how to use some of the features in Material 3 + to make your app beautiful and responsive. + tags: + - design + - material + - responsive + - adaptive + type: codelab + link: + label: Google Codelab + url: https://codelabs.developers.google.com/codelabs/flutter-boring-to-beautiful + +- name: Building next generation UIs in Flutter + description: | + Learn how to build a Flutter app that uses the power of `flutter_animate`, + fragment shaders, and particle fields. + tags: + - animation + - flutter animate + - ui + - shaders + + type: codelab + link: + label: Google Codelab + url: https://codelabs.developers.google.com/codelabs/flutter-next-gen-uis + +- name: Adaptive Apps in Flutter + description: | + Learn how to build a Flutter app that adapts to the + platform that it's running on, be that Android, iOS, + the web, Windows, macOS, or Linux. + tags: + - adaptive + - linux + - macos + - desktop + - windows + - android + - ios + - web + + type: codelab + link: + label: Google Codelab + url: https://codelabs.developers.google.com/codelabs/flutter-adaptive-app + +- name: Animations in Flutter + description: | + Learn how to build animated effects in Flutter. You'll learn how to build + implicit and explicit animations, and customize + navigation transition animations the animations package and predictive back + on Android. + tags: + - animations + type: codelab + link: + label: Google Codelab + url: https://codelabs.developers.google.com/advanced-flutter-animations + +- name: Building Beautiful Transitions with Material Motion for Flutter + description: | + Learn how to use the Material animations package to + add pre-built transitions to a Material app called Reply. + tags: + - animations + - material + type: codelab + link: + label: Google Codelab + url: https://codelabs.developers.google.com/codelabs/material-motion-flutter + +- name: How to debug layout issues with the Flutter Inspector + description: | + Step-by-step instructions on how to debug + common layout problems using the Flutter + Inspector and Layout Explorer. + tags: + - debug + - tooling + - developer tools + type: codelab + link: + label: Medium + url: https://blog.flutter.dev/how-to-debug-layout-issues-with-the-flutter-inspector-87460a7b9db + +- name: Implicit animations + description: | + Use DartPad (no downloads required!) to learn how to use + implicit animations to add motion and create + visual effects for the widgets in your UI. + tags: + - animations + type: codelab + link: + label: Flutter docs + url: /codelabs/implicit-animations + +- name: MDC-101 - Material Components (MDC) Basics + description: | + Learn the basics of using Material Components by building + a simple app with core components. + tags: + - material + - design + type: codelab + link: + label: Google Codelab + url: https://codelabs.developers.google.com/codelabs/mdc-101-flutter + +- name: MDC-102 - Material Structure and Layout + description: | + Learn how to use Material for structure and layout in Flutter. + Continue building the e-commerce app, introduced in MDC-101, + by adding navigation, structure, and data. + tags: + - material + - design + type: codelab + link: + label: Google Codelab + url: https://codelabs.developers.google.com/codelabs/mdc-102-flutter + +- name: MDC-103 - Material Theming with Color, Shape, Elevation, and Type + description: | + Discover how Material Components for Flutter make it + easy to differentiate your product, and express your + brand through design. + tags: + - material + - design + type: codelab + link: + label: Google Codelab + url: https://codelabs.developers.google.com/codelabs/mdc-103-flutter + +- name: MDC-104 - Material Advanced Components + description: | + Improve your design and learn to use our advanced + component backdrop menu. + tags: + - material + - design + type: codelab + link: + label: Google Codelab + url: https://codelabs.developers.google.com/codelabs/mdc-104-flutter + +#Flutter with + +## Monetizing Flutter +- name: Adding AdMob Ads to a Flutter app + description: | + Learn how to add an AdMob banner, an interstitial ad, + and a rewarded ad to an app called Awesome Drawing Quiz, + a game that lets players guess the name of the drawing. + tags: + - ads + - admob + - monetization + + type: codelab + link: + label: Google Codelab + url: https://codelabs.developers.google.com/codelabs/admob-ads-in-flutter + +- name: Adding an AdMob banner and native inline ads to a Flutter app + description: | + Learn how to implement inline banner and native ads + to a travel booking app that lists possible + flight destinations. + tags: + - ads + - admob + - monetization + type: codelab + link: + label: Google Codelab + url: https://codelabs.developers.google.com/codelabs/admob-inline-ads-in-flutter + +- name: Adding in-app purchases to your Flutter app + description: | + Extend a simple gaming app that uses the Dash mascot as + currency to offer three types of in-app purchases: + consumable, non-consumable, and subscription. + tags: + - in app purchases + - monetization + type: codelab + link: + label: Google Codelab + url: https://codelabs.developers.google.com/codelabs/flutter-in-app-purchases + +## Firebase + +- name: Add a user authentication flow using FirebaseUI + description: | + Learn how to add Firebase authentication to a Flutter app + with only a few lines of code. + tags: + - firebase + - authentication + - firebase UI + type: codelab + link: + label: Google Codelab + url: https://firebase.google.com/codelabs/firebase-auth-in-flutter-apps + +- name: Get to know Firebase for Flutter + description: | + Build an event RSVP and guestbook chat app on both Android + and iOS using Flutter, authenticating users with Firebase + Authentication, and sync data using Cloud Firestore. + tags: + - firebase + - android + - ios + - firestore + - real time database + type: codelab + link: + label: Google Codelab + url: https://firebase.google.com/codelabs/firebase-get-to-know-flutter + +- name: Notifications with Firebase Cloud Messaging + description: | + Learn how to develop a multi-platform app with Flutter + and Firebase Cloud Messaging, integrating FCM to send and + receive messages on Android, iOS, and web. + tags: + - firebase + - cloud messaging + - multi platform + - web + - android + - ios + type: codelab + link: + label: Google Codelab + url: https://firebase.google.com/codelabs/firebase-fcm-flutter + +## Games + +- name: Add sound and music to your Flutter game with SoLoud + description: | + The SoLoud package, a free and portable engine, + delivers the low-latency and high-performance sound that's + essential for many games. + tags: + - game + - audio + type: codelab + link: + label: Google Codelab + url: https://codelabs.developers.google.com/codelabs/flutter-codelab-soloud + +- name: Build a 2D physics game with Flutter and Flame + description: | + This codelab guides you through crafting game mechanics in a + Flutter and Flame game using a 2D physics simulation called Forge2D. + tags: + - game + - physics + type: codelab + link: + label: Google Codelab + url: https://codelabs.developers.google.com/codelabs/flutter-flame-forge2d + +- name: Build a word puzzle with Flutter + description: | + This codelab focuses on building word puzzle games, + and dives into using Flutter's background processing + to generate expansive crossword-style grids of interlocking words. + tags: + - game + - isolate + type: codelab + link: + label: Google Codelab + url: https://codelabs.developers.google.com/codelabs/flutter-word-puzzle + +- name: Introduction to Flame with Flutter + description: | + Build a Breakout clone using the Flame 2D game engine and + embed it in a Flutter wrapper. + tags: + - game + - intro + - beginner + type: codelab + link: + label: Google Codelab + url: https://codelabs.developers.google.com/codelabs/flutter-flame-brick-breaker + +## Other techs + +- name: Adding Google Maps to a Flutter app + description: | + Display a Google map in an app, retrieve data from a + web service, and display the data as markers on the map. + tags: + - google maps + - maps + type: codelab + link: + label: Google Codelab + url: https://codelabs.developers.google.com/codelabs/google-maps-in-flutter + +- name: Adding WebView to your Flutter app + description: | + With the WebView Flutter plugin you can add a WebView + widget to your Android or iOS Flutter app. + tags: + - android + - ios + - webview + type: codelab + link: + label: Google Codelab + url: https://codelabs.developers.google.com/codelabs/flutter-webview + +- name: Using FFI in a Flutter plugin + description: | + Learn how to use Dart's FFI (foreign function interface) + library, ffigen, allowing you to leverage + existing native libraries that provide a + C interface. + tags: + - FFI + - interop + - plugin + type: codelab + link: + label: Google Codelab + url: https://codelabs.developers.google.com/codelabs/flutter-ffigen + +# Testing + +- name: How to test a Flutter app + description: | + Start with a simple app that manages state with the Provider package. + Unit test the provider package. Write widget tests for two of the + widgets. Use Flutter Driver to create an integration test. + tags: + - testing + type: codelab + link: + label: Google Codelab + url: https://codelabs.developers.google.com/codelabs/flutter-app-testing/ + +# Platform + +- name: Adding a Home Screen widget to your Flutter app + description: | + Learn how to add a Home Screen widget to your Flutter app + on iOS. This applies to your home screen, lock screen, or the + today view. + tags: + - ios + - home screen widget + type: codelab + link: + label: Google Codelab + url: https://codelabs.developers.google.com/flutter-home-screen-widgets + +- name: Write a Flutter desktop application + description: | + Build a Flutter desktop app (Windows, Linux, or macOS) + that accesses GitHub APIs, and + create and use plugins to interact with native APIs and desktop applications. + tags: + - desktop + - windows + - linux + - macos + type: codelab + link: + label: Google Codelab + url: https://codelabs.developers.google.com/codelabs/flutter-github-client \ No newline at end of file diff --git a/src/data/learning-resources-index/cookbook.yml b/src/data/learning-resources-index/cookbook.yml new file mode 100644 index 00000000000..b77022e2e57 --- /dev/null +++ b/src/data/learning-resources-index/cookbook.yml @@ -0,0 +1,731 @@ +# Animations + +- name: Animate a page route transition + description: Transition between routes by animating the new route into view from the bottom of the screen. + tags: + - animation + - route + type: recipe + link: + label: Flutter docs + url: /cookbook/animation/page-route-animation + +- name: Animate a widget using a physics simulation + description: Learn how to move a widget from a dragged point back to the center using a spring simulation. + tags: + - animation + type: recipe + link: + label: Flutter docs + url: /cookbook/animation/physics-simulation + +- name: Animate the properties of a container + description: Use AnimatedContainer to animate the size, background color, and border radius of a Container. + tags: + - animation + type: recipe + link: + label: Flutter docs + url: /cookbook/animation/animated-container + +- name: Fade a widget in and out + description: The AnimatedOpacity widget makes it easy to perform opacity animations. + tags: + - animation + type: recipe + link: + label: Flutter docs + url: /cookbook/animation/opacity-animation + +# Design + +- name: Add a drawer to a screen + description: Use the Drawer widget in combination with a Scaffold to create a layout with a Material Design drawer. + tags: + - widget + - material + - drawer + - layout + type: recipe + link: + label: Flutter docs + url: /cookbook/design/drawer + +- name: Display a snackbar + description: Use the Snackbar widget to display messages to your users. + type: recipe + link: + label: Flutter docs + url: /cookbook/design/snackbars + +- name: Export fonts from a package + description: Use a font across multiple apps. + type: recipe + link: + label: Flutter docs + url: /cookbook/design/package-fonts + +- name: Update the UI based on orientation + description: Build a list that displays two columns in portrait mode and three columns in landscape mode. + type: recipe + link: + label: Flutter docs + url: /cookbook/design/orientation + +- name: Use a custom font + description: Apply fonts to your entire app or individual widgets. + type: recipe + link: + label: Flutter docs + url: /cookbook/design/fonts + +- name: Use themes to share colors and font styles + description: To share styles across your app, use Themes. + type: recipe + link: + label: Flutter docs + url: /cookbook/design/themes + +- name: Work with tabs + description: Working with tabs is a common pattern in mobile apps that follow the Material Design or Cupertino guidelines. + type: recipe + link: + label: Flutter docs + url: /cookbook/design/tabs + +# Effects + +- name: Create a download button + description: Build a download button that transitions through multiple visual states, based on the status of an app download. + tags: + - ui + - effects + - animations + type: recipe + link: + label: Flutter docs + url: /cookbook/effects/download-button + +- name: Create a nested navigation flow + description: Create top level routes, and routes nested below specific widgets. + tags: + - navigation + - routing + type: recipe + link: + label: Flutter docs + url: /cookbook/effects/nested-nav + +- name: Create a scrolling parallax effect + description: Create the parallax effect by building a list of cards with images that 'move'. + type: recipe + tags: + - design + link: + label: Flutter docs + url: /cookbook/effects/parallax-scrolling + +- name: Create a shimmer loading effect + description: Communicate that data is loading with a chrome color shimmer on the screen. + type: recipe + tags: + - animation + link: + label: Flutter docs + url: /cookbook/effects/shimmer-loading + +- name: Create a staggered menu animation + description: Build a drawer menu with animated content that is staggered and has a button that pops in at the bottom + type: recipe + tags: + - animation + - design + link: + label: Flutter docs + url: /cookbook/effects/staggered-menu-animation + +- name: Create a typing indicator + description: Build a speech bubble typing indicator that animates in and out of view. + type: recipe + link: + label: Flutter docs + url: /cookbook/effects/typing-indicator + +- name: Create an expandable FAB + description: Create a floating action button that spawns other action buttons. + tags: + - ui + - effects + type: recipe + link: + label: Flutter docs + url: /cookbook/effects/expandable-fab + +- name: Drag a UI element + description: Build a drag-and-drop interaction when the user long presses. + tags: + - interaction + - design + type: recipe + link: + label: Flutter docs + url: /cookbook/effects/drag-a-widget + +# Forms +- name: Build a form with validation + description: Learn how to add validation to a form. + tags: + - input + - interaction + type: recipe + link: + label: Flutter docs + url: /cookbook/forms/validation + +- name: Create and style a text field + description: In this recipe, explore how to create and style text fields. + tags: + - input + type: recipe + link: + label: Flutter docs + url: /cookbook/forms/text-input + +- name: Focus and text fields + description: Shift focus to a text field programmatically. + tags: + - input + type: recipe + link: + label: Flutter docs + url: /cookbook/forms/focus + +- name: Handle changes to a text field + description: Listen for changes to a TextField using a callback. + tags: + - input + type: recipe + link: + label: Flutter docs + url: /cookbook/forms/text-field-changes + +- name: Retrieve the value of a text field + description: Learn how to retrieve the text a user has entered into a text field. + tags: + - input + type: recipe + link: + label: Flutter docs + url: /cookbook/forms/retrieve-input + +# Games + +- name: Add achievements and leaderboards to your game + description: Use the games_services package to add leaderboard functionality to your mobile game. + tags: + - games + type: recipe + link: + label: Flutter docs + url: /cookbook/games/achievements-leaderboard + +- name: Add multiplayer support to your Flutter game + description: Use the cloud_firestore package to implement multiplayer capabilities in your game. + tags: + - games + - ads + - firebase + type: recipe + link: + label: Flutter docs + url: /cookbook/games/firestore-multiplayer + +- name: Add ads to your Flutter game + description: Use the google_mobile_ads package to add a banner ad to your app or game. + tags: + - games + - ads + type: recipe + link: + label: Flutter docs + url: /cookbook/plugins/google-mobile-ads + +# Gestures + +- name: Add Material touch ripples + description: Use the Inkwell widget to display a ripple animation. + tags: + - input + - material + type: recipe + link: + label: Flutter docs + url: /cookbook/gestures/ripples + +- name: Handle taps + description: Use the GestureDetector widget to respond to fundamental actions, such as tapping and dragging. + tags: + - input + type: recipe + link: + label: Flutter docs + url: /cookbook/gestures/handling-taps + +- name: Implement swipe to dismiss + description: Learn how to use the Dismissible widget. + tags: + - input + type: recipe + link: + label: Flutter docs + url: /cookbook/gestures/dismissible + +# Images + +- name: Display images from the internet + description: To work with images from a URL, use the Image.network() constructor. + tags: + - image + - beginner + type: recipe + link: + label: Flutter docs + url: /cookbook/images/network-image + +- name: Fade in images with a placeholder + description: Use the FadeInImage widget to show a visual placeholder before an image loads. + tags: + - image + type: recipe + link: + label: Flutter docs + url: /cookbook/images/fading-in-images + +# Lists + +- name: Grid lists + description: Learn to use a GridView widget. + tags: + - list + - layout + type: recipe + link: + label: Flutter docs + url: /cookbook/lists/grid-lists + +- name: Horizontal lists + description: Learn to display items horizontally in a scrollable list. + tags: + - list + - layout + type: recipe + link: + label: Flutter docs + url: /cookbook/lists/horizontal-list + +- name: Lists with different types of items + description: Create a list with headers followed by a few list items. + tags: + - list + - layout + type: recipe + link: + label: Flutter docs + url: /cookbook/lists/mixed-list + +- name: Lists and floating app bars + description: Place a floating app bar or navigation bar above a list. + tags: + - list + - layout + - scrolling + type: recipe + link: + label: Flutter docs + url: /cookbook/lists/floating-app-bar + +- name: Basic lists + description: Learn to display items with the ListView widget. + tags: + - list + - layout + - scrolling + type: recipe + link: + label: Flutter docs + url: /cookbook/lists/basic-list + +- name: Long lists + description: Work with longer lists with the Listview.builder constructor. + tags: + - list + - layout + type: recipe + link: + label: Flutter docs + url: /cookbook/lists/long-lists + +- name: Lists with spaced items + description: Create a list with padding between items. + tags: + - list + - layout + type: recipe + link: + label: Flutter docs + url: /cookbook/lists/spaced-items + +- name: Animate a widget across screens + description: Use the Hero widget to animate a widget from one screen to the next. + tags: + - navigation + - animation + type: recipe + link: + label: Flutter docs + url: /cookbook/navigation/hero-animations + +- name: Navigate to a new screen and back + description: This recipe uses the Navigator to navigate to a new route. + tags: + - navigation + type: recipe + link: + label: Flutter docs + url: /cookbook/navigation/navigation-basics + +- name: Named routes + description: Create named routes and navigate to them. + tags: + - navigation + - routing + type: recipe + link: + label: Flutter docs + url: /cookbook/navigation/named-routes + +- name: Arguments and named routes + description: Pass arguments to a named route and read the arguments on that route. + tags: + - navigation + type: recipe + link: + label: Flutter docs + url: /cookbook/navigation/navigate-with-arguments + +- name: Android app links + description: Set up deep linking on Android + tags: + - navigation + - android + type: recipe + link: + label: Flutter docs + url: /cookbook/navigation/set-up-app-links + +- name: iOS universal links + description: Set up universal links for iOS + tags: + - navigation + - iOS + type: recipe + link: + label: Flutter docs + url: /cookbook/navigation/set-up-universal-links + +- name: Return data from a screen + description: Return data from one screen to another with the Navigator.pop method. + tags: + - navigation + - routing + type: recipe + link: + label: Flutter docs + url: /cookbook/navigation/returning-data + +- name: Send data to a new screen + description: Send data from one screen to new one. + tags: + - navigation + type: recipe + link: + label: Flutter docs + url: /cookbook/navigation/passing-data + +# Networking + +- name: Fetch data from the internet + description: Learn to use HTTP in your app. + tags: + - networking + - http + type: recipe + link: + label: Flutter docs + url: /cookbook/networking/fetch-data + +- name: Make authenticated requests + description: Authorization headers in HTTP + tags: + - networking + - http + - auth + type: recipe + link: + label: Flutter docs + url: /cookbook/networking/authenticated-requests + +- name: Send data to the internet + description: Send HTTP POST requests in your app. + tags: + - networking + - http + type: recipe + link: + label: Flutter docs + url: /cookbook/networking/send-data + +- name: Update data over the internet + description: Send an HTTP put request. + tags: + - networking + - http + type: recipe + link: + label: Flutter docs + url: /cookbook/networking/update-data + +- name: Delete data on the internet + description: Send an HTTP delete request. + tags: + - networking + - http + type: recipe + link: + label: Flutter docs + url: /cookbook/networking/delete-data + +- name: WebSockets + description: Connect to and communicate with a websocket. + tags: + - networking + - websocket + type: recipe + link: + label: Flutter docs + url: /cookbook/networking/web-sockets + +- name: Parse JSON in the background + description: Learn to use Dart's Isolate objects + tags: + - networking + - isolates + - threading + type: recipe + link: + label: Flutter docs + url: /cookbook/networking/background-parsing + +- name: Persist data with SQLite + description: Use the sqflite package. + tags: + - data + - persistence + - SQL + type: recipe + link: + label: Flutter docs + url: /cookbook/persistence/sqlite + +- name: Read and write files + description: Use the dart:io library and path_provider plugin to save files to disk. + tags: + - data + - persistence + - io + type: recipe + link: + label: Flutter docs + url: /cookbook/persistence/reading-writing-files + +- name: Store key-value data on disk + description: Persist data with shared_preferences + tags: + - data + - persistence + type: recipe + link: + label: Flutter docs + url: /cookbook/persistence/key-value + +- name: Play and pause a video + description: Play videos stored on the file system, as an asset, or from the internet. + tags: + - plugins + - video + type: recipe + link: + label: Flutter docs + url: /cookbook/plugins/play-video + +- name: Use the camera + description: Learn to use a devices camera. + tags: + - plugins + type: recipe + link: + label: Flutter docs + url: /cookbook/plugins/picture-using-camera + +- name: Report errors to a service + description: Report errors to Sentry crash reporting. + tags: + - testing + - reporting + type: recipe + link: + label: Flutter docs + url: /cookbook/maintenance/error-reporting + +- name: Performance profiling + description: Write a test that records a performance timeline. + tags: + - testing + - performance + - perf + type: recipe + link: + label: Flutter docs + url: /cookbook/testing/integration/profiling + +- name: Write unit tests + description: An introduction to writing unit tests. + tags: + - testing + - unit testing + type: recipe + link: + label: Flutter docs + url: /cookbook/testing/unit/introduction + +- name: Write widget tests + description: An introduction to writing widget tests. + tags: + - testing + - unit testing + type: recipe + link: + label: Flutter docs + url: /cookbook/testing/widget/introduction + +- name: Mock dependencies in tests + description: The basics of mocking with the Mockito package. + tags: + - testing + - unit testing + type: recipe + link: + label: Flutter docs + url: /cookbook/testing/unit/mocking + +- name: Find widgets in tests + description: This recipe looks at the 'find' constant provided by the flutter_test package. + tags: + - testing + type: recipe + link: + label: Flutter docs + url: /cookbook/testing/widget/finders + +- name: Handle scrolling + description: Learn how to scroll in widget tests. + tags: + - testing + type: recipe + link: + label: Flutter docs + url: /cookbook/testing/widget/scrolling + +- name: App orientation + description: Learn how to check app orientation in widget tests. + tags: + - testing + type: recipe + link: + label: Flutter docs + url: /cookbook/testing/widget/orientation + +- name: Tap, drag, and enter text + description: Interact with widgets in widget tests. + tags: + - testing + type: recipe + link: + label: Flutter docs + url: /cookbook/testing/widget/tap-drag + +- name: Persistent storage architecture - SQL + description: Save complex application data to a user's device with SQL. + tags: + - data + - SQL + - architecture + type: recipe + link: + label: Flutter docs + url: /app-architecture/design-patterns/sql + +- name: Error handling with Result objects + description: Improve error handling across classes with Result objects. + tags: + - error handling + - testing + - architecture + type: recipe + link: + label: Flutter docs + url: /app-architecture/design-patterns/result + +- name: Optimistic state + description: Improve the perception of responsiveness of an application by implementing optimistic state. + tags: + - user experience + - asynchronous dart + - architecture + type: recipe + link: + label: Flutter docs + url: /app-architecture/design-patterns/optimistic-state + +- name: Offline First + description: Implement offline-first support for one feature in an application. + tags: + - user experience + - network + - architecture + type: recipe + link: + label: Flutter docs + url: /app-architecture/design-patterns/offline-first + +- name: Persistent storage architecture - Key-value data + description: Save application data to a user's on-device key-value store. + tags: + - data + - network + - architecture + type: recipe + link: + label: Flutter docs + url: /app-architecture/design-patterns/key-value-data + +- name: Command pattern + description: Simplify view model logic by implementing a Command class. + tags: + - mvvm + - asynchronous dart + - architecture + type: recipe + link: + label: Flutter docs + url: /app-architecture/design-patterns/command diff --git a/src/data/learning-resources-index/demos.yml b/src/data/learning-resources-index/demos.yml new file mode 100644 index 00000000000..c6e3f823be1 --- /dev/null +++ b/src/data/learning-resources-index/demos.yml @@ -0,0 +1,55 @@ +- name: Add-to-app + description: >- + Recommended approaches for adding Flutter to existing apps. + tags: + - platforms + - iOS + - Android + type: demo + link: + url: https://github.com/flutter/samples/tree/main/add_to_app + label: Flutter Github + +- name: Android splash screen + description: >- + A Flutter sample app that exemplifies how to implement an + animated splash screen for Android devices. + tags: + - Android + type: demo + link: + url: https://github.com/flutter/samples/tree/main/android_splash_screen + label: Flutter Github + +- name: iOS app clip + description: >- + A sample project demonstrating integration with iOS App Clip. + tags: + - iOS + type: demo + link: + url: https://github.com/flutter/samples/tree/main/ios_app_clip + label: Flutter Github + +- name: Swift platform view + description: >- + A Flutter sample app that combines a native iOS UIViewController with + a full-screen Flutter view. + tags: + - swift + - ios + type: demo + link: + url: https://github.com/flutter/samples/tree/main/platform_view_swift + label: Flutter Github + +- name: Simplistic editor + description: >- + This sample text editor showcases the use of TextEditingDeltas and + a DeltaTextInputClient to expand and contract styled ranges of text. + tags: + - text + type: quickstart + link: + url: https://github.com/flutter/samples/tree/main/simplistic_editor + label: Flutter Github diff --git a/src/data/learning-resources-index/quickstarts_dart.yml b/src/data/learning-resources-index/quickstarts_dart.yml new file mode 100644 index 00000000000..60d60134527 --- /dev/null +++ b/src/data/learning-resources-index/quickstarts_dart.yml @@ -0,0 +1,78 @@ +- name: Command-line app + description: >- + A command line app that parses command-line options and fetches from GitHub. + tags: + - cli + - Dart + type: quickstart + link: + url: https://github.com/dart-lang/samples/tree/main/command_line + label: Dart Github + +- name: Extension methods + description: >- + Demonstrates Dart's extensions method syntax. + tags: + - syntax + - Dart + type: quickstart + link: + url: https://github.com/dart-lang/samples/tree/main/extension_methods + label: Dart Github + +- name: FFI + description: >- + A series of simple examples demonstrating how to call C libraries from Dart. + tags: + - platforms + - Dart + type: quickstart + link: + url: https://github.com/dart-lang/samples/tree/main/ffi + label: Dart Github + +- name: Isolates (in a CLI) + description: >- + Command line applications that demonstrate how to + work with Concurrency in Dart using isolates. + tags: + - cli + - Dart + type: quickstart + link: + url: https://github.com/dart-lang/samples/tree/main/ffi + label: Dart Github + +- name: Native Dart app + description: >- + A command line application that can be compiled to + native code using `dart compile exe`. + tags: + - cli + - Dart + type: quickstart + link: + url: https://github.com/dart-lang/samples/tree/main/native_app + label: Dart Github + +- name: Server side Dart + description: >- + Examples of running Dart on the server. + tags: + - cli + - Dart + type: quickstart + link: + url: https://github.com/dart-lang/samples/tree/main/server + label: Dart Github + +- name: Package constraint solver + description: >- + Demonstrates best-practices for publishing packages on pub.dev. + tags: + - pub + - Dart + type: quickstart + link: + url: https://github.com/dart-lang/samples/tree/main/server + label: Dart Github \ No newline at end of file diff --git a/src/data/learning-resources-index/quickstarts_flutter.yml b/src/data/learning-resources-index/quickstarts_flutter.yml new file mode 100644 index 00000000000..802aa42efe4 --- /dev/null +++ b/src/data/learning-resources-index/quickstarts_flutter.yml @@ -0,0 +1,257 @@ +- name: Asset transformation + description: >- + Demonstrates how to transform images' color scales and formats. + tags: + - images + - UI + type: quickstart + link: + url: https://github.com/flutter/samples/tree/main/asset_transformation + label: Flutter Github + +- name: Background isolate channels + description: >- + Demonstrates how to use long-lived isolates. + tags: + - performance + type: quickstart + link: + url: https://github.com/flutter/samples/tree/main/background_isolate_channels + label: Flutter Github + +- name: Code sharing + description: >- + Demonstrates how to share business logic between a + Flutter client and Dart server using `package:shelf`. + tags: + - Dart + type: quickstart + link: + url: https://github.com/flutter/samples/tree/main/code_sharing + label: Flutter Github + +- name: Context menus + description: >- + This sample shows how to create and customize cross-platform context menus, + such as the text selection toolbar on mobile or + the right click menu on desktop. + tags: + - macos + - windows + type: quickstart + link: + url: https://github.com/flutter/samples/tree/main/context_menus + label: Flutter Github + +- name: Desktop UI + description: >- + Demonstrates desktop features in both Material and FluentUI design systems. + tags: + - material + - macos + - windows + type: quickstart + link: + url: https://github.com/flutter/samples/tree/main/desktop_photo_search + label: Flutter Github + +- name: AI generated dynamic theme + description: >- + Demonstrates how to call on-device Flutter APIs based on + output from the Gemini API. + tags: + - AI + - google + type: quickstart + link: + url: https://github.com/flutter/samples/tree/main/dynamic_theme + label: Flutter Github + +- name: Form app + description: >- + A sample demonstrating different types of forms and best practices. + tags: + - input + - layout + type: quickstart + link: + url: https://github.com/flutter/samples/tree/main/form_app + label: Flutter Github + +- name: AI todo list + description: >- + A developer sample written in Flutter demonstrating how to + interact with a to-do list in natural language using the Gemini API. + tags: + - AI + - google + type: quickstart + link: + url: https://github.com/flutter/samples/tree/main/gemini_tasks + label: Flutter Github + +- name: Google Maps plugin + description: >- + Demonstrates the Google Maps for Flutter plugin. + tags: + - google + - maps + type: quickstart + link: + url: https://github.com/flutter/samples/tree/main/google_maps + label: Flutter Github + +- name: Infinite list + description: >- + A Flutter sample app that shows an implementation of + the 'infinite list' UX pattern. + tags: + - lists + - layout + - scrolling + type: quickstart + link: + url: https://github.com/flutter/samples/tree/main/infinite_list + label: Flutter Github + +- name: Isolates + description: >- + A sample application that + demonstrate best practices when using isolates. + tags: + - isolates + - perf + - threads + type: quickstart + link: + url: https://github.com/flutter/samples/tree/main/isolate_example + label: Flutter Github + +- name: Navigation and routing + description: >- + A sample that shows how to use `go_router` API to + handle common navigation scenarios. + tags: + - navigation + - routing + type: quickstart + link: + url: https://github.com/flutter/samples/tree/main/navigation_and_routing + label: Flutter Github + +- name: Google Maps Flutter plugin + description: >- + A sample place tracking app that uses the google_apps_flutter plugin. + tags: + - maps + - google + - plugins + type: quickstart + link: + url: https://github.com/flutter/samples/tree/main/place_tracker + label: Flutter Github + +- name: Platform adaptive design + description: >- + This sample project shows a Flutter app that + maximizes application code reuse while adhering to + different design patterns on Android and iOS. + tags: + - adaptive + - design + type: quickstart + link: + url: https://github.com/flutter/samples/tree/main/platform_design + label: Flutter Github + +- name: Counter app with Provider + description: >- + The starter Flutter application, but + using the Provider package to manage state. + tags: + - state management + - architecture + type: quickstart + link: + url: https://github.com/flutter/samples/tree/main/provider_counter + label: Flutter Github + +- name: Shopping app with Provider + description: >- + A Flutter sample app that shows a + state management approach using the Provider package. + tags: + - state management + - architecture + type: quickstart + link: + url: https://github.com/flutter/samples/tree/main/provider_shopper + label: Flutter Github + +- name: Simple shaders + description: >- + A simple Flutter fragment shaders project. + tags: + - shaders + - gpu + type: quickstart + link: + url: https://github.com/flutter/samples/tree/main/simple_shader + label: Flutter Github + +- name: Desktop calculator + description: >- + A calculator sample to demonstrate a simple start for a desktop Flutter app. + tags: + - desktop + type: quickstart + link: + url: https://github.com/flutter/samples/tree/main/simplistic_calculator + label: Flutter Github + +- name: Testing app + description: >- + A sample app that shows different types of testing in Flutter. + tags: + - testing + type: quickstart + link: + url: https://github.com/flutter/samples/tree/main/testing_app + label: Flutter Github + +- name: Web element embedding + description: >- + Modifies the index.html of a flutter app so it + is launched in a custom hostElement. + This is the most basic embedding example. + tags: + - web + type: quickstart + link: + url: https://github.com/flutter/samples/tree/main/web_embedding/element_embedding_demo + label: Flutter Github + +- name: ng-flutter + description: >- + A simple Angular app (and component) that + replicates the element embedding example, but in an Angular app. + tags: + - web + - google + type: quickstart + link: + url: https://github.com/flutter/samples/tree/main/web_embedding/ng-flutter + label: Flutter Github + +- name: Platform channels + description: >- + A sample Flutter app which demonstrates how to use + `MethodChannel`, `EventChannel`, `BasicMessageChannel` and `MessageCodec`. + tags: + - platforms + - android + - ios + type: quickstart + link: + url: https://github.com/flutter/samples/tree/main/platform_channels + label: Flutter Github diff --git a/tool/dash_site/lib/src/utils.dart b/tool/dash_site/lib/src/utils.dart index 2e1a25ab1cf..f3cb366ad64 100644 --- a/tool/dash_site/lib/src/utils.dart +++ b/tool/dash_site/lib/src/utils.dart @@ -33,7 +33,7 @@ int installJasprCliIfNecessary() { 'global', 'activate', 'jaspr_cli', - '^0.21.5', + '^0.21.6', ]); if (activateOutput.exitCode != 0) { From f64e1911ae379d143b1c5b46570c61528be531b8 Mon Sep 17 00:00:00 2001 From: Kilian Schulte Date: Tue, 21 Oct 2025 20:12:58 +0200 Subject: [PATCH 4/6] refactor: simplify resource loading --- .../client/learning_resource_filters.dart | 1 - .../learning_resource_filters_sidebar.dart | 3 +- .../pages/learning_resource_index.dart | 17 ++++++++-- site/lib/src/loaders/data_processor.dart | 32 ------------------- .../src/models/learning_resource_model.dart | 4 +-- 5 files changed, 17 insertions(+), 40 deletions(-) diff --git a/site/lib/src/components/client/learning_resource_filters.dart b/site/lib/src/components/client/learning_resource_filters.dart index 79bfa5e8eb1..f6d994f4e28 100644 --- a/site/lib/src/components/client/learning_resource_filters.dart +++ b/site/lib/src/components/client/learning_resource_filters.dart @@ -13,7 +13,6 @@ import '../../models/learning_resource_model.dart'; import '../util/global_event_listener.dart'; import 'learning_resource_filters_sidebar.dart'; - @client class LearningResourceFilters extends StatefulComponent { const LearningResourceFilters({super.key}); diff --git a/site/lib/src/components/client/learning_resource_filters_sidebar.dart b/site/lib/src/components/client/learning_resource_filters_sidebar.dart index 494d81b3431..559a22e13a4 100644 --- a/site/lib/src/components/client/learning_resource_filters_sidebar.dart +++ b/site/lib/src/components/client/learning_resource_filters_sidebar.dart @@ -183,8 +183,7 @@ class FiltersNotifier extends ChangeNotifier { for (final info in resources) { final matchesTags = - selectedTags.isEmpty || - info.tags.any(filterTags.contains); + selectedTags.isEmpty || info.tags.any(filterTags.contains); if (!matchesTags) { continue; } diff --git a/site/lib/src/components/pages/learning_resource_index.dart b/site/lib/src/components/pages/learning_resource_index.dart index 71db5d42b11..dc4be059049 100644 --- a/site/lib/src/components/pages/learning_resource_index.dart +++ b/site/lib/src/components/pages/learning_resource_index.dart @@ -15,14 +15,25 @@ final class LearningResourceIndex extends StatelessComponent { @override Component build(BuildContext context) { - final resources = - context.page.data['learningResources'] as List? ?? []; + final resourcesData = + context.page.data['learning-resources-index'] as Map?; + + final learningResources = []; + if (resourcesData != null) { + for (final group in resourcesData.values) { + for (final resource in group as List) { + learningResources.add( + LearningResource.fromMap(resource as Map), + ); + } + } + } return div(id: 'resource-index-content', [ div(classes: 'left-col', id: 'resource-index-main-content', [ const LearningResourceFilters(), section(classes: 'card-grid', id: 'all-resources-grid', [ - for (final item in resources) _ResourceCard(item), + for (final item in learningResources) _ResourceCard(item), ]), ]), const LearningResourceFiltersSidebar(), diff --git a/site/lib/src/loaders/data_processor.dart b/site/lib/src/loaders/data_processor.dart index 831dce08de1..37fac337173 100644 --- a/site/lib/src/loaders/data_processor.dart +++ b/site/lib/src/loaders/data_processor.dart @@ -9,7 +9,6 @@ import 'package:jaspr_content/jaspr_content.dart'; import 'package:path/path.dart' as path; import '../data/devtools_releases.dart'; -import '../models/learning_resource_model.dart'; /// A shared data loader to add data to each loaded page. final class DataProcessor implements DataLoader { @@ -17,7 +16,6 @@ final class DataProcessor implements DataLoader { Future loadData(Page page) async { _loadDevToolsReleases(page); _loadLastModified(page); - _processLearningResources(page); } static void _loadDevToolsReleases(Page page) { @@ -51,36 +49,6 @@ final class DataProcessor implements DataLoader { }, ); } - - static List? _cachedLearningResources; - - static void _processLearningResources(Page page) { - if (_cachedLearningResources != null) { - page.apply( - data: {'learningResources': _cachedLearningResources!}, - ); - return; - } - - final resourceGroups = - page.data['learning-resources-index'] as Map?; - if (resourceGroups == null) return; - - final learningResources = _cachedLearningResources = []; - for (final group in resourceGroups.values) { - for (final resource in group as List) { - learningResources.add( - LearningResource.fromMap(resource as Map), - ); - } - } - - page.apply( - data: { - 'learningResources': learningResources, - }, - ); - } } /// Determines the last modified date for a given path diff --git a/site/lib/src/models/learning_resource_model.dart b/site/lib/src/models/learning_resource_model.dart index c3c4e221a8f..12e0224df13 100644 --- a/site/lib/src/models/learning_resource_model.dart +++ b/site/lib/src/models/learning_resource_model.dart @@ -14,7 +14,7 @@ final class LearningResource { this.imageUrl, }); - /// Creates a [LearningResource] from a Map, used on the server + /// Creates a [LearningResource] from a Map, used on the server /// when parsing the yaml data files. factory LearningResource.fromMap(Map map) { return LearningResource( @@ -105,4 +105,4 @@ enum LearningResourceTag { final String label; final List tags; -} \ No newline at end of file +} From 12de486874f6ebc0fdde3c2ec222a8ccfb24d37c Mon Sep 17 00:00:00 2001 From: Kilian Schulte Date: Tue, 21 Oct 2025 20:16:20 +0200 Subject: [PATCH 5/6] fix: set initial filter count --- .../lib/src/components/client/learning_resource_filters.dart | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/site/lib/src/components/client/learning_resource_filters.dart b/site/lib/src/components/client/learning_resource_filters.dart index f6d994f4e28..9993bddb13f 100644 --- a/site/lib/src/components/client/learning_resource_filters.dart +++ b/site/lib/src/components/client/learning_resource_filters.dart @@ -43,12 +43,12 @@ class _LearningResourceFiltersState extends State { } final resourceCards = resourceGrid.querySelectorAll('.card'); - loadResourceInfos(resourceCards); + recreateResources(resourceCards); shuffleCards(resourceGrid); } } - void loadResourceInfos(web.NodeList resourceCards) { + 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); @@ -64,6 +64,7 @@ class _LearningResourceFiltersState extends State { }).toJS, ); } + filteredResourcesCount = resources.length; } void shuffleCards(web.Element container) { From 05a58ced4db821eb3fb10830e1e95de113da7ad3 Mon Sep 17 00:00:00 2001 From: Parker Lougheed Date: Wed, 22 Oct 2025 12:07:11 +0200 Subject: [PATCH 6/6] Update capitalization of GitHub --- .../pages/learning_resource_index.dart | 4 +- .../learning-resources-index/codelabs.yml | 2 +- src/data/learning-resources-index/demos.yml | 10 ++--- .../quickstarts_dart.yml | 14 +++--- .../quickstarts_flutter.yml | 44 +++++++++---------- 5 files changed, 37 insertions(+), 37 deletions(-) diff --git a/site/lib/src/components/pages/learning_resource_index.dart b/site/lib/src/components/pages/learning_resource_index.dart index dc4be059049..e1ec065c494 100644 --- a/site/lib/src/components/pages/learning_resource_index.dart +++ b/site/lib/src/components/pages/learning_resource_index.dart @@ -95,7 +95,7 @@ final class _ResourceCard extends StatelessComponent { } Component _iconForLabel(String label) => switch (label) { - 'Flutter Github' => svg( + 'Flutter GitHub' => svg( classes: 'monochrome-icon', width: 24.px, height: 24.px, @@ -106,7 +106,7 @@ final class _ResourceCard extends StatelessComponent { ), ], ), - 'Dart Github' || 'Dart docs' => img( + 'Dart GitHub' || 'Dart docs' => img( src: '/assets/images/branding/dart/logo.svg', width: 24, alt: 'Dart logo', diff --git a/src/data/learning-resources-index/codelabs.yml b/src/data/learning-resources-index/codelabs.yml index 4fa7114a50b..e992280934d 100644 --- a/src/data/learning-resources-index/codelabs.yml +++ b/src/data/learning-resources-index/codelabs.yml @@ -435,4 +435,4 @@ type: codelab link: label: Google Codelab - url: https://codelabs.developers.google.com/codelabs/flutter-github-client \ No newline at end of file + url: https://codelabs.developers.google.com/codelabs/flutter-github-client diff --git a/src/data/learning-resources-index/demos.yml b/src/data/learning-resources-index/demos.yml index c6e3f823be1..dfc9d47f083 100644 --- a/src/data/learning-resources-index/demos.yml +++ b/src/data/learning-resources-index/demos.yml @@ -8,7 +8,7 @@ type: demo link: url: https://github.com/flutter/samples/tree/main/add_to_app - label: Flutter Github + label: Flutter GitHub - name: Android splash screen description: >- @@ -19,7 +19,7 @@ type: demo link: url: https://github.com/flutter/samples/tree/main/android_splash_screen - label: Flutter Github + label: Flutter GitHub - name: iOS app clip description: >- @@ -29,7 +29,7 @@ type: demo link: url: https://github.com/flutter/samples/tree/main/ios_app_clip - label: Flutter Github + label: Flutter GitHub - name: Swift platform view description: >- @@ -41,7 +41,7 @@ type: demo link: url: https://github.com/flutter/samples/tree/main/platform_view_swift - label: Flutter Github + label: Flutter GitHub - name: Simplistic editor description: >- @@ -52,4 +52,4 @@ type: quickstart link: url: https://github.com/flutter/samples/tree/main/simplistic_editor - label: Flutter Github + label: Flutter GitHub diff --git a/src/data/learning-resources-index/quickstarts_dart.yml b/src/data/learning-resources-index/quickstarts_dart.yml index 60d60134527..258906fe5da 100644 --- a/src/data/learning-resources-index/quickstarts_dart.yml +++ b/src/data/learning-resources-index/quickstarts_dart.yml @@ -7,7 +7,7 @@ type: quickstart link: url: https://github.com/dart-lang/samples/tree/main/command_line - label: Dart Github + label: Dart GitHub - name: Extension methods description: >- @@ -18,7 +18,7 @@ type: quickstart link: url: https://github.com/dart-lang/samples/tree/main/extension_methods - label: Dart Github + label: Dart GitHub - name: FFI description: >- @@ -29,7 +29,7 @@ type: quickstart link: url: https://github.com/dart-lang/samples/tree/main/ffi - label: Dart Github + label: Dart GitHub - name: Isolates (in a CLI) description: >- @@ -41,7 +41,7 @@ type: quickstart link: url: https://github.com/dart-lang/samples/tree/main/ffi - label: Dart Github + label: Dart GitHub - name: Native Dart app description: >- @@ -53,7 +53,7 @@ type: quickstart link: url: https://github.com/dart-lang/samples/tree/main/native_app - label: Dart Github + label: Dart GitHub - name: Server side Dart description: >- @@ -64,7 +64,7 @@ type: quickstart link: url: https://github.com/dart-lang/samples/tree/main/server - label: Dart Github + label: Dart GitHub - name: Package constraint solver description: >- @@ -75,4 +75,4 @@ type: quickstart link: url: https://github.com/dart-lang/samples/tree/main/server - label: Dart Github \ No newline at end of file + label: Dart GitHub diff --git a/src/data/learning-resources-index/quickstarts_flutter.yml b/src/data/learning-resources-index/quickstarts_flutter.yml index 802aa42efe4..774ed0f9c9b 100644 --- a/src/data/learning-resources-index/quickstarts_flutter.yml +++ b/src/data/learning-resources-index/quickstarts_flutter.yml @@ -7,7 +7,7 @@ type: quickstart link: url: https://github.com/flutter/samples/tree/main/asset_transformation - label: Flutter Github + label: Flutter GitHub - name: Background isolate channels description: >- @@ -17,7 +17,7 @@ type: quickstart link: url: https://github.com/flutter/samples/tree/main/background_isolate_channels - label: Flutter Github + label: Flutter GitHub - name: Code sharing description: >- @@ -28,7 +28,7 @@ type: quickstart link: url: https://github.com/flutter/samples/tree/main/code_sharing - label: Flutter Github + label: Flutter GitHub - name: Context menus description: >- @@ -41,7 +41,7 @@ type: quickstart link: url: https://github.com/flutter/samples/tree/main/context_menus - label: Flutter Github + label: Flutter GitHub - name: Desktop UI description: >- @@ -53,7 +53,7 @@ type: quickstart link: url: https://github.com/flutter/samples/tree/main/desktop_photo_search - label: Flutter Github + label: Flutter GitHub - name: AI generated dynamic theme description: >- @@ -65,7 +65,7 @@ type: quickstart link: url: https://github.com/flutter/samples/tree/main/dynamic_theme - label: Flutter Github + label: Flutter GitHub - name: Form app description: >- @@ -76,7 +76,7 @@ type: quickstart link: url: https://github.com/flutter/samples/tree/main/form_app - label: Flutter Github + label: Flutter GitHub - name: AI todo list description: >- @@ -88,7 +88,7 @@ type: quickstart link: url: https://github.com/flutter/samples/tree/main/gemini_tasks - label: Flutter Github + label: Flutter GitHub - name: Google Maps plugin description: >- @@ -99,7 +99,7 @@ type: quickstart link: url: https://github.com/flutter/samples/tree/main/google_maps - label: Flutter Github + label: Flutter GitHub - name: Infinite list description: >- @@ -112,7 +112,7 @@ type: quickstart link: url: https://github.com/flutter/samples/tree/main/infinite_list - label: Flutter Github + label: Flutter GitHub - name: Isolates description: >- @@ -125,7 +125,7 @@ type: quickstart link: url: https://github.com/flutter/samples/tree/main/isolate_example - label: Flutter Github + label: Flutter GitHub - name: Navigation and routing description: >- @@ -137,7 +137,7 @@ type: quickstart link: url: https://github.com/flutter/samples/tree/main/navigation_and_routing - label: Flutter Github + label: Flutter GitHub - name: Google Maps Flutter plugin description: >- @@ -149,7 +149,7 @@ type: quickstart link: url: https://github.com/flutter/samples/tree/main/place_tracker - label: Flutter Github + label: Flutter GitHub - name: Platform adaptive design description: >- @@ -162,7 +162,7 @@ type: quickstart link: url: https://github.com/flutter/samples/tree/main/platform_design - label: Flutter Github + label: Flutter GitHub - name: Counter app with Provider description: >- @@ -174,7 +174,7 @@ type: quickstart link: url: https://github.com/flutter/samples/tree/main/provider_counter - label: Flutter Github + label: Flutter GitHub - name: Shopping app with Provider description: >- @@ -186,7 +186,7 @@ type: quickstart link: url: https://github.com/flutter/samples/tree/main/provider_shopper - label: Flutter Github + label: Flutter GitHub - name: Simple shaders description: >- @@ -197,7 +197,7 @@ type: quickstart link: url: https://github.com/flutter/samples/tree/main/simple_shader - label: Flutter Github + label: Flutter GitHub - name: Desktop calculator description: >- @@ -207,7 +207,7 @@ type: quickstart link: url: https://github.com/flutter/samples/tree/main/simplistic_calculator - label: Flutter Github + label: Flutter GitHub - name: Testing app description: >- @@ -217,7 +217,7 @@ type: quickstart link: url: https://github.com/flutter/samples/tree/main/testing_app - label: Flutter Github + label: Flutter GitHub - name: Web element embedding description: >- @@ -229,7 +229,7 @@ type: quickstart link: url: https://github.com/flutter/samples/tree/main/web_embedding/element_embedding_demo - label: Flutter Github + label: Flutter GitHub - name: ng-flutter description: >- @@ -241,7 +241,7 @@ type: quickstart link: url: https://github.com/flutter/samples/tree/main/web_embedding/ng-flutter - label: Flutter Github + label: Flutter GitHub - name: Platform channels description: >- @@ -254,4 +254,4 @@ type: quickstart link: url: https://github.com/flutter/samples/tree/main/platform_channels - label: Flutter Github + label: Flutter GitHub