From 61c826f37643f6d53e3eb6d354f4282f6e075d1b Mon Sep 17 00:00:00 2001 From: lamek Date: Tue, 17 Mar 2026 00:05:33 +0000 Subject: [PATCH 01/19] Reapply "Update fwe samples (pt2 of 3) (#13156)" (#13159) This reverts commit ff7d8abad3a2090789b192dc575f6b94e19a4e1e. --- examples/fwe/wikipedia_reader/.gitignore | 45 ++++ examples/fwe/wikipedia_reader/.metadata | 45 ++++ examples/fwe/wikipedia_reader/README.md | 3 + .../wikipedia_reader/analysis_options.yaml | 1 + examples/fwe/wikipedia_reader/lib/main.dart | 167 ++++++++++++ .../fwe/wikipedia_reader/lib/step1_main.dart | 34 +++ .../fwe/wikipedia_reader/lib/step2_main.dart | 46 ++++ .../fwe/wikipedia_reader/lib/step2a_main.dart | 34 +++ .../fwe/wikipedia_reader/lib/step2b_main.dart | 43 +++ .../fwe/wikipedia_reader/lib/step2c_main.dart | 47 ++++ .../fwe/wikipedia_reader/lib/step3_main.dart | 71 +++++ .../fwe/wikipedia_reader/lib/step3a_main.dart | 42 +++ .../fwe/wikipedia_reader/lib/step3b_main.dart | 32 +++ .../fwe/wikipedia_reader/lib/step3c_main.dart | 34 +++ .../fwe/wikipedia_reader/lib/step3d_main.dart | 39 +++ .../fwe/wikipedia_reader/lib/step3e_main.dart | 41 +++ .../fwe/wikipedia_reader/lib/step3f_main.dart | 34 +++ .../fwe/wikipedia_reader/lib/step4_main.dart | 169 ++++++++++++ .../fwe/wikipedia_reader/lib/step4a_main.dart | 25 ++ .../fwe/wikipedia_reader/lib/step4b_main.dart | 73 +++++ .../fwe/wikipedia_reader/lib/step4c_main.dart | 27 ++ .../fwe/wikipedia_reader/lib/step4d_main.dart | 40 +++ .../fwe/wikipedia_reader/lib/step4e_main.dart | 18 ++ .../fwe/wikipedia_reader/lib/step4f_main.dart | 24 ++ .../fwe/wikipedia_reader/lib/step4g_main.dart | 27 ++ .../fwe/wikipedia_reader/lib/step4h_main.dart | 38 +++ .../fwe/wikipedia_reader/lib/summary.dart | 252 ++++++++++++++++++ examples/fwe/wikipedia_reader/pubspec.yaml | 20 ++ .../wikipedia_reader/test/widget_test.dart | 13 + .../learn/pathway/tutorial/change-notifier.md | 89 ++++--- .../learn/pathway/tutorial/http-requests.md | 10 +- .../pathway/tutorial/listenable-builder.md | 227 ++++++++-------- .../pathway/tutorial/set-up-state-project.md | 95 ++----- 33 files changed, 1685 insertions(+), 220 deletions(-) create mode 100644 examples/fwe/wikipedia_reader/.gitignore create mode 100644 examples/fwe/wikipedia_reader/.metadata create mode 100644 examples/fwe/wikipedia_reader/README.md create mode 100644 examples/fwe/wikipedia_reader/analysis_options.yaml create mode 100644 examples/fwe/wikipedia_reader/lib/main.dart create mode 100644 examples/fwe/wikipedia_reader/lib/step1_main.dart create mode 100644 examples/fwe/wikipedia_reader/lib/step2_main.dart create mode 100644 examples/fwe/wikipedia_reader/lib/step2a_main.dart create mode 100644 examples/fwe/wikipedia_reader/lib/step2b_main.dart create mode 100644 examples/fwe/wikipedia_reader/lib/step2c_main.dart create mode 100644 examples/fwe/wikipedia_reader/lib/step3_main.dart create mode 100644 examples/fwe/wikipedia_reader/lib/step3a_main.dart create mode 100644 examples/fwe/wikipedia_reader/lib/step3b_main.dart create mode 100644 examples/fwe/wikipedia_reader/lib/step3c_main.dart create mode 100644 examples/fwe/wikipedia_reader/lib/step3d_main.dart create mode 100644 examples/fwe/wikipedia_reader/lib/step3e_main.dart create mode 100644 examples/fwe/wikipedia_reader/lib/step3f_main.dart create mode 100644 examples/fwe/wikipedia_reader/lib/step4_main.dart create mode 100644 examples/fwe/wikipedia_reader/lib/step4a_main.dart create mode 100644 examples/fwe/wikipedia_reader/lib/step4b_main.dart create mode 100644 examples/fwe/wikipedia_reader/lib/step4c_main.dart create mode 100644 examples/fwe/wikipedia_reader/lib/step4d_main.dart create mode 100644 examples/fwe/wikipedia_reader/lib/step4e_main.dart create mode 100644 examples/fwe/wikipedia_reader/lib/step4f_main.dart create mode 100644 examples/fwe/wikipedia_reader/lib/step4g_main.dart create mode 100644 examples/fwe/wikipedia_reader/lib/step4h_main.dart create mode 100644 examples/fwe/wikipedia_reader/lib/summary.dart create mode 100644 examples/fwe/wikipedia_reader/pubspec.yaml create mode 100644 examples/fwe/wikipedia_reader/test/widget_test.dart diff --git a/examples/fwe/wikipedia_reader/.gitignore b/examples/fwe/wikipedia_reader/.gitignore new file mode 100644 index 00000000000..3820a95c65c --- /dev/null +++ b/examples/fwe/wikipedia_reader/.gitignore @@ -0,0 +1,45 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.build/ +.buildlog/ +.history +.svn/ +.swiftpm/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins-dependencies +.pub-cache/ +.pub/ +/build/ +/coverage/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/examples/fwe/wikipedia_reader/.metadata b/examples/fwe/wikipedia_reader/.metadata new file mode 100644 index 00000000000..c9627623eef --- /dev/null +++ b/examples/fwe/wikipedia_reader/.metadata @@ -0,0 +1,45 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "e308d690a1193ea0d93d62aad6efe48e5994bc3c" + channel: "[user-branch]" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: e308d690a1193ea0d93d62aad6efe48e5994bc3c + base_revision: e308d690a1193ea0d93d62aad6efe48e5994bc3c + - platform: android + create_revision: e308d690a1193ea0d93d62aad6efe48e5994bc3c + base_revision: e308d690a1193ea0d93d62aad6efe48e5994bc3c + - platform: ios + create_revision: e308d690a1193ea0d93d62aad6efe48e5994bc3c + base_revision: e308d690a1193ea0d93d62aad6efe48e5994bc3c + - platform: linux + create_revision: e308d690a1193ea0d93d62aad6efe48e5994bc3c + base_revision: e308d690a1193ea0d93d62aad6efe48e5994bc3c + - platform: macos + create_revision: e308d690a1193ea0d93d62aad6efe48e5994bc3c + base_revision: e308d690a1193ea0d93d62aad6efe48e5994bc3c + - platform: web + create_revision: e308d690a1193ea0d93d62aad6efe48e5994bc3c + base_revision: e308d690a1193ea0d93d62aad6efe48e5994bc3c + - platform: windows + create_revision: e308d690a1193ea0d93d62aad6efe48e5994bc3c + base_revision: e308d690a1193ea0d93d62aad6efe48e5994bc3c + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/examples/fwe/wikipedia_reader/README.md b/examples/fwe/wikipedia_reader/README.md new file mode 100644 index 00000000000..2a1fac66791 --- /dev/null +++ b/examples/fwe/wikipedia_reader/README.md @@ -0,0 +1,3 @@ +# wikipedia_reader + +A new Flutter project. diff --git a/examples/fwe/wikipedia_reader/analysis_options.yaml b/examples/fwe/wikipedia_reader/analysis_options.yaml new file mode 100644 index 00000000000..f9b303465f1 --- /dev/null +++ b/examples/fwe/wikipedia_reader/analysis_options.yaml @@ -0,0 +1 @@ +include: package:flutter_lints/flutter.yaml diff --git a/examples/fwe/wikipedia_reader/lib/main.dart b/examples/fwe/wikipedia_reader/lib/main.dart new file mode 100644 index 00000000000..67a457a2e8e --- /dev/null +++ b/examples/fwe/wikipedia_reader/lib/main.dart @@ -0,0 +1,167 @@ +// ignore_for_file: avoid_dynamic_calls + +import 'dart:convert'; +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:http/http.dart'; + +import 'summary.dart'; + +void main() { + runApp(const MainApp()); +} + +class MainApp extends StatelessWidget { + const MainApp({super.key}); + + @override + Widget build(BuildContext context) { + return const MaterialApp(home: ArticleView()); + } +} + +class ArticleModel { + Future getRandomArticleSummary() async { + final uri = Uri.https( + 'en.wikipedia.org', + '/api/rest_v1/page/random/summary', + ); + final response = await get(uri); + + if (response.statusCode != 200) { + throw const HttpException('Failed to update resource'); + } + + return Summary.fromJson(jsonDecode(response.body) as Map); + } +} + +class ArticleViewModel extends ChangeNotifier { + ArticleViewModel(this.model); + final ArticleModel model; + + Summary? summary; + + bool isLoading = false; + + Exception? error; + + Future fetchArticle() async { + isLoading = true; + error = null; + notifyListeners(); + + try { + summary = await model.getRandomArticleSummary(); + error = null; + } on Exception catch (e) { + error = e; + } finally { + isLoading = false; + notifyListeners(); + } + } +} + +class ArticleView extends StatefulWidget { + const ArticleView({super.key}); + + @override + State createState() => _ArticleViewState(); +} + +class _ArticleViewState extends State { + late final ArticleViewModel viewModel; + + @override + void initState() { + super.initState(); + viewModel = ArticleViewModel(ArticleModel()); + viewModel.fetchArticle(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Wikipedia Flutter')), + body: Center( + child: ListenableBuilder( + listenable: viewModel, + builder: (context, _) { + return switch (( + viewModel.isLoading, + viewModel.summary, + viewModel.error, + )) { + (true, _, _) => const CircularProgressIndicator(), + (_, final summary?, _) => ArticlePage( + summary: summary, + nextArticleCallback: viewModel.fetchArticle, + ), + (_, _, final Exception e) => Text('Error: $e'), + _ => const Text('Something went wrong!'), + }; + }, + ), + ), + ); + } +} + +class ArticlePage extends StatelessWidget { + const ArticlePage({ + super.key, + required this.summary, + required this.nextArticleCallback, + }); + + final Summary summary; + final VoidCallback nextArticleCallback; + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + child: Column( + children: [ + ArticleWidget(summary: summary), + ElevatedButton( + onPressed: nextArticleCallback, + child: const Text('Next random article'), + ), + ], + ), + ); + } +} + +class ArticleWidget extends StatelessWidget { + const ArticleWidget({super.key, required this.summary}); + + final Summary summary; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + spacing: 10.0, + children: [ + if (summary.hasImage) Image.network(summary.originalImage!.source), + Text( + summary.titles.normalized, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.displaySmall, + ), + if (summary.description != null) + Text( + summary.description!, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.bodySmall, + ), + Text(summary.extract), + ], + ), + ); + } +} diff --git a/examples/fwe/wikipedia_reader/lib/step1_main.dart b/examples/fwe/wikipedia_reader/lib/step1_main.dart new file mode 100644 index 00000000000..affee140a04 --- /dev/null +++ b/examples/fwe/wikipedia_reader/lib/step1_main.dart @@ -0,0 +1,34 @@ +// ignore_for_file: unused_import + +// #docregion All +import 'dart:convert'; +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:http/http.dart'; + +import 'summary.dart'; + +// #docregion main +void main() { + runApp(const MainApp()); +} +// #enddocregion main + +// #docregion MainApp +class MainApp extends StatelessWidget { + const MainApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + home: Scaffold( + appBar: AppBar(title: const Text('Wikipedia Flutter')), + body: const Center(child: Text('Loading...')), + ), + ); + } +} +// #enddocregion MainApp + +// #enddocregion All diff --git a/examples/fwe/wikipedia_reader/lib/step2_main.dart b/examples/fwe/wikipedia_reader/lib/step2_main.dart new file mode 100644 index 00000000000..e880600cc01 --- /dev/null +++ b/examples/fwe/wikipedia_reader/lib/step2_main.dart @@ -0,0 +1,46 @@ +// ignore_for_file: unused_import, avoid_dynamic_calls + +import 'dart:convert'; +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:http/http.dart'; + +import 'summary.dart'; + +void main() { + runApp(const MainApp()); +} + +class MainApp extends StatelessWidget { + const MainApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + home: Scaffold( + appBar: AppBar(title: const Text('Wikipedia Flutter')), + body: const Center(child: Text('Loading...')), + ), + ); + } +} + +// #docregion ArticleModel +class ArticleModel { + Future getRandomArticleSummary() async { + final uri = Uri.https( + 'en.wikipedia.org', + '/api/rest_v1/page/random/summary', + ); + final response = await get(uri); + + if (response.statusCode != 200) { + throw HttpException('Failed to update resource'); + } + + return Summary.fromJson(jsonDecode(response.body)); + } +} + +// #enddocregion ArticleModel diff --git a/examples/fwe/wikipedia_reader/lib/step2a_main.dart b/examples/fwe/wikipedia_reader/lib/step2a_main.dart new file mode 100644 index 00000000000..ca172369fda --- /dev/null +++ b/examples/fwe/wikipedia_reader/lib/step2a_main.dart @@ -0,0 +1,34 @@ +// ignore_for_file: unused_import + +import 'dart:convert'; +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:http/http.dart'; + +import 'summary.dart'; + +void main() { + runApp(const MainApp()); +} + +class MainApp extends StatelessWidget { + const MainApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + home: Scaffold( + appBar: AppBar(title: const Text('Wikipedia Flutter')), + body: const Center(child: Text('Loading...')), + ), + ); + } +} + +// #docregion ArticleModel +class ArticleModel { + // Properties and methods will be added here. +} + +// #enddocregion ArticleModel diff --git a/examples/fwe/wikipedia_reader/lib/step2b_main.dart b/examples/fwe/wikipedia_reader/lib/step2b_main.dart new file mode 100644 index 00000000000..bd22fb7d969 --- /dev/null +++ b/examples/fwe/wikipedia_reader/lib/step2b_main.dart @@ -0,0 +1,43 @@ +// ignore_for_file: unused_import, unused_local_variable + +import 'dart:convert'; +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:http/http.dart'; + +import 'summary.dart'; + +void main() { + runApp(const MainApp()); +} + +class MainApp extends StatelessWidget { + const MainApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + home: Scaffold( + appBar: AppBar(title: const Text('Wikipedia Flutter')), + body: const Center(child: Text('Loading...')), + ), + ); + } +} + +// #docregion ArticleModel +class ArticleModel { + Future getRandomArticleSummary() async { + final uri = Uri.https( + 'en.wikipedia.org', + '/api/rest_v1/page/random/summary', + ); + final response = await get(uri); + + // TODO: Add error handling and JSON parsing. + throw UnimplementedError(); + } +} + +// #enddocregion ArticleModel diff --git a/examples/fwe/wikipedia_reader/lib/step2c_main.dart b/examples/fwe/wikipedia_reader/lib/step2c_main.dart new file mode 100644 index 00000000000..00e253b4bd9 --- /dev/null +++ b/examples/fwe/wikipedia_reader/lib/step2c_main.dart @@ -0,0 +1,47 @@ +// ignore_for_file: unused_import + +import 'dart:convert'; +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:http/http.dart'; + +import 'summary.dart'; + +void main() { + runApp(const MainApp()); +} + +class MainApp extends StatelessWidget { + const MainApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + home: Scaffold( + appBar: AppBar(title: const Text('Wikipedia Flutter')), + body: const Center(child: Text('Loading...')), + ), + ); + } +} + +// #docregion ArticleModel +class ArticleModel { + Future getRandomArticleSummary() async { + final uri = Uri.https( + 'en.wikipedia.org', + '/api/rest_v1/page/random/summary', + ); + final response = await get(uri); + + if (response.statusCode != 200) { + throw const HttpException('Failed to update resource'); + } + + // TODO: Parse JSON and return Summary. + throw UnimplementedError(); + } +} + +// #enddocregion ArticleModel diff --git a/examples/fwe/wikipedia_reader/lib/step3_main.dart b/examples/fwe/wikipedia_reader/lib/step3_main.dart new file mode 100644 index 00000000000..bb0dd4e88b4 --- /dev/null +++ b/examples/fwe/wikipedia_reader/lib/step3_main.dart @@ -0,0 +1,71 @@ +// ignore_for_file: unused_import, avoid_dynamic_calls + +import 'dart:convert'; +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:http/http.dart'; + +import 'summary.dart'; + +void main() { + runApp(const MainApp()); +} + +class MainApp extends StatelessWidget { + const MainApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + home: Scaffold( + appBar: AppBar(title: const Text('Wikipedia Flutter')), + body: const Center(child: Text('Loading...')), + ), + ); + } +} + +class ArticleModel { + Future getRandomArticleSummary() async { + final uri = Uri.https( + 'en.wikipedia.org', + '/api/rest_v1/page/random/summary', + ); + final response = await get(uri); + + if (response.statusCode != 200) { + throw const HttpException('Failed to update resource'); + } + + return Summary.fromJson(jsonDecode(response.body) as Map); + } +} + +class ArticleViewModel extends ChangeNotifier { + ArticleViewModel({required this.model}); + final ArticleModel model; + + Summary? _summary; + Summary? get summary => _summary; + bool _isLoading = false; + bool get isLoading => _isLoading; + Exception? _error; + Exception? get error => _error; + + Future fetchArticle() async { + _isLoading = true; + _error = null; + notifyListeners(); + + try { + _summary = await model.getRandomArticleSummary(); + _error = null; + } on Exception catch (e) { + _error = e; + } finally { + _isLoading = false; + notifyListeners(); + } + } +} diff --git a/examples/fwe/wikipedia_reader/lib/step3a_main.dart b/examples/fwe/wikipedia_reader/lib/step3a_main.dart new file mode 100644 index 00000000000..b25e824c855 --- /dev/null +++ b/examples/fwe/wikipedia_reader/lib/step3a_main.dart @@ -0,0 +1,42 @@ +// ignore_for_file: unused_import, avoid_dynamic_calls + +import 'dart:convert'; +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:http/http.dart'; + +import 'summary.dart'; + +void main() { + runApp(const MainApp()); +} + +class MainApp extends StatelessWidget { + const MainApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + home: Scaffold(body: Center(child: Text('Loading...'))), + ); + } +} + +class ArticleModel { + Future getRandomArticleSummary() async { + return throw UnimplementedError(); + } +} + +// #docregion ArticleViewModel +class ArticleViewModel extends ChangeNotifier { + final ArticleModel model; + Summary? summary; + Exception? error; + bool isLoading = false; + + ArticleViewModel(this.model); +} + +// #enddocregion ArticleViewModel diff --git a/examples/fwe/wikipedia_reader/lib/step3b_main.dart b/examples/fwe/wikipedia_reader/lib/step3b_main.dart new file mode 100644 index 00000000000..f457a60ba92 --- /dev/null +++ b/examples/fwe/wikipedia_reader/lib/step3b_main.dart @@ -0,0 +1,32 @@ +// ignore_for_file: unused_import, avoid_dynamic_calls + +import 'dart:convert'; +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:http/http.dart'; + +import 'summary.dart'; + +class ArticleModel { + Future getRandomArticleSummary() async { + throw UnimplementedError(); + } +} + +// #docregion ArticleViewModel +class ArticleViewModel extends ChangeNotifier { + final ArticleModel model; + Summary? summary; + Exception? error; + bool isLoading = false; + + ArticleViewModel(this.model) { + fetchArticle(); + } + + // Methods will be added next. + Future fetchArticle() async {} +} + +// #enddocregion ArticleViewModel diff --git a/examples/fwe/wikipedia_reader/lib/step3c_main.dart b/examples/fwe/wikipedia_reader/lib/step3c_main.dart new file mode 100644 index 00000000000..88a9516b63a --- /dev/null +++ b/examples/fwe/wikipedia_reader/lib/step3c_main.dart @@ -0,0 +1,34 @@ +// ignore_for_file: unused_import, avoid_dynamic_calls + +import 'package:flutter/material.dart'; +import 'summary.dart'; + +class ArticleModel { + Future getRandomArticleSummary() async { + throw UnimplementedError(); + } +} + +// #docregion ArticleViewModel +class ArticleViewModel extends ChangeNotifier { + final ArticleModel model; + Summary? summary; + Exception? error; + bool isLoading = false; + + ArticleViewModel(this.model) { + fetchArticle(); + } + + Future fetchArticle() async { + isLoading = true; + notifyListeners(); + + // TODO: Add data fetching logic + + isLoading = false; + notifyListeners(); + } +} + +// #enddocregion ArticleViewModel diff --git a/examples/fwe/wikipedia_reader/lib/step3d_main.dart b/examples/fwe/wikipedia_reader/lib/step3d_main.dart new file mode 100644 index 00000000000..67065479c7c --- /dev/null +++ b/examples/fwe/wikipedia_reader/lib/step3d_main.dart @@ -0,0 +1,39 @@ +// ignore_for_file: unused_import, avoid_dynamic_calls + +import 'dart:io'; +import 'package:flutter/material.dart'; +import 'summary.dart'; + +class ArticleModel { + Future getRandomArticleSummary() async { + throw UnimplementedError(); + } +} + +// #docregion ArticleViewModel +class ArticleViewModel extends ChangeNotifier { + final ArticleModel model; + Summary? summary; + Exception? error; + bool isLoading = false; + + ArticleViewModel(this.model) { + fetchArticle(); + } + + Future fetchArticle() async { + isLoading = true; + notifyListeners(); + try { + summary = await model.getRandomArticleSummary(); + error = null; // Clear any previous errors. + } on HttpException catch (e) { + error = e; + summary = null; + } + isLoading = false; + notifyListeners(); + } +} + +// #enddocregion ArticleViewModel diff --git a/examples/fwe/wikipedia_reader/lib/step3e_main.dart b/examples/fwe/wikipedia_reader/lib/step3e_main.dart new file mode 100644 index 00000000000..edc069525c8 --- /dev/null +++ b/examples/fwe/wikipedia_reader/lib/step3e_main.dart @@ -0,0 +1,41 @@ +// ignore_for_file: unused_import, avoid_dynamic_calls, avoid_print + +import 'dart:io'; +import 'package:flutter/material.dart'; +import 'summary.dart'; + +class ArticleModel { + Future getRandomArticleSummary() async { + throw UnimplementedError(); + } +} + +class ArticleViewModel extends ChangeNotifier { + final ArticleModel model; + Summary? summary; + Exception? error; + bool isLoading = false; + + ArticleViewModel(this.model) { + fetchArticle(); + } + + // #docregion fetchArticle + Future fetchArticle() async { + isLoading = true; + notifyListeners(); + try { + summary = await model.getRandomArticleSummary(); + print('Article loaded: ${summary!.titles.normalized}'); // Temporary + error = null; // Clear any previous errors. + } on HttpException catch (e) { + print('Error loading article: ${e.message}'); // Temporary + error = e; + summary = null; + } + isLoading = false; + notifyListeners(); + } + + // #enddocregion fetchArticle +} diff --git a/examples/fwe/wikipedia_reader/lib/step3f_main.dart b/examples/fwe/wikipedia_reader/lib/step3f_main.dart new file mode 100644 index 00000000000..7208169e83e --- /dev/null +++ b/examples/fwe/wikipedia_reader/lib/step3f_main.dart @@ -0,0 +1,34 @@ +// ignore_for_file: unused_local_variable +// ignore_for_file: unused_import, avoid_dynamic_calls + +import 'package:flutter/material.dart'; +import 'summary.dart'; + +class ArticleModel {} + +class ArticleViewModel { + ArticleViewModel(ArticleModel model); +} + +// #docregion MainApp +class MainApp extends StatelessWidget { + const MainApp({super.key}); + + @override + Widget build(BuildContext context) { + // Instantiate your `ArticleViewModel` to test its HTTP requests. + final viewModel = ArticleViewModel(ArticleModel()); + + return MaterialApp( + home: Scaffold( + appBar: AppBar(title: const Text('Wikipedia Flutter')), + body: const Center(child: Text('Check console for article data')), + ), + ); + } +} +// #enddocregion MainApp + +void main() { + runApp(const MainApp()); +} diff --git a/examples/fwe/wikipedia_reader/lib/step4_main.dart b/examples/fwe/wikipedia_reader/lib/step4_main.dart new file mode 100644 index 00000000000..749c77ca033 --- /dev/null +++ b/examples/fwe/wikipedia_reader/lib/step4_main.dart @@ -0,0 +1,169 @@ +// ignore_for_file: avoid_dynamic_calls + +import 'dart:convert'; +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:http/http.dart'; + +import 'summary.dart'; + +void main() { + runApp(const MainApp()); +} + +// #docregion MainApp +class MainApp extends StatelessWidget { + const MainApp({super.key}); + + @override + Widget build(BuildContext context) { + return const MaterialApp(home: ArticleView()); + } +} +// #enddocregion MainApp + +class ArticleModel { + Future getRandomArticleSummary() async { + final uri = Uri.https( + 'en.wikipedia.org', + '/api/rest_v1/page/random/summary', + ); + final response = await get(uri); + + if (response.statusCode != 200) { + throw const HttpException('Failed to update resource'); + } + + return Summary.fromJson(jsonDecode(response.body) as Map); + } +} + +class ArticleViewModel extends ChangeNotifier { + ArticleViewModel(this.model); + final ArticleModel model; + + Summary? summary; + + bool isLoading = false; + + Exception? error; + + Future fetchArticle() async { + isLoading = true; + error = null; + notifyListeners(); + + try { + summary = await model.getRandomArticleSummary(); + error = null; + } on Exception catch (e) { + error = e; + } finally { + isLoading = false; + notifyListeners(); + } + } +} + +class ArticleView extends StatefulWidget { + const ArticleView({super.key}); + + @override + State createState() => _ArticleViewState(); +} + +class _ArticleViewState extends State { + late final ArticleViewModel viewModel; + + @override + void initState() { + super.initState(); + viewModel = ArticleViewModel(ArticleModel()); + viewModel.fetchArticle(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Wikipedia Flutter')), + body: Center( + child: ListenableBuilder( + listenable: viewModel, + builder: (context, _) { + return switch (( + viewModel.isLoading, + viewModel.summary, + viewModel.error, + )) { + (true, _, _) => const CircularProgressIndicator(), + (_, final summary?, _) => ArticlePage( + summary: summary, + nextArticleCallback: viewModel.fetchArticle, + ), + (_, _, final Exception e) => Text('Error: $e'), + _ => const Text('Something went wrong!'), + }; + }, + ), + ), + ); + } +} + +class ArticlePage extends StatelessWidget { + const ArticlePage({ + super.key, + required this.summary, + required this.nextArticleCallback, + }); + + final Summary summary; + final VoidCallback nextArticleCallback; + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + child: Column( + children: [ + ArticleWidget(summary: summary), + ElevatedButton( + onPressed: nextArticleCallback, + child: const Text('Next random article'), + ), + ], + ), + ); + } +} + +class ArticleWidget extends StatelessWidget { + const ArticleWidget({super.key, required this.summary}); + + final Summary summary; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + spacing: 10.0, + children: [ + if (summary.hasImage) Image.network(summary.originalImage!.source), + Text( + summary.titles.normalized, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.displaySmall, + ), + if (summary.description != null) + Text( + summary.description!, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.bodySmall, + ), + Text(summary.extract), + ], + ), + ); + } +} diff --git a/examples/fwe/wikipedia_reader/lib/step4a_main.dart b/examples/fwe/wikipedia_reader/lib/step4a_main.dart new file mode 100644 index 00000000000..2d08e714e98 --- /dev/null +++ b/examples/fwe/wikipedia_reader/lib/step4a_main.dart @@ -0,0 +1,25 @@ +// ignore_for_file: unused_import + +import 'package:flutter/material.dart'; + +// #docregion ArticleView +class ArticleView extends StatefulWidget { + const ArticleView({super.key}); + + @override + State createState() => _ArticleViewState(); +} + +class _ArticleViewState extends State { + // viewModel will be instantiated next + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Wikipedia Flutter')), + body: const Center(child: Text('Loading...')), + ); + } +} + +// #enddocregion ArticleView diff --git a/examples/fwe/wikipedia_reader/lib/step4b_main.dart b/examples/fwe/wikipedia_reader/lib/step4b_main.dart new file mode 100644 index 00000000000..0862a870ce7 --- /dev/null +++ b/examples/fwe/wikipedia_reader/lib/step4b_main.dart @@ -0,0 +1,73 @@ +// ignore_for_file: unused_import + +import 'package:flutter/material.dart'; +import 'summary.dart'; + +class ArticleViewModel extends ChangeNotifier { + ArticleViewModel(ArticleModel model); + bool get isLoading => false; + Summary? get summary => null; + Exception? get error => null; + Future fetchArticle() async {} +} + +class ArticleModel {} + +// #docregion ArticleView +class ArticleView extends StatefulWidget { + const ArticleView({super.key}); + + @override + State createState() => _ArticleViewState(); +} + +class _ArticleViewState extends State { + late final ArticleViewModel viewModel; + + @override + void initState() { + super.initState(); + viewModel = ArticleViewModel(ArticleModel()); + viewModel.fetchArticle(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Wikipedia Flutter')), + body: Center( + child: ListenableBuilder( + listenable: viewModel, + builder: (context, _) { + return switch (( + viewModel.isLoading, + viewModel.summary, + viewModel.error, + )) { + (true, _, _) => const CircularProgressIndicator(), + (_, final summary?, _) => ArticlePage( + summary: summary, + nextArticleCallback: viewModel.fetchArticle, + ), + (_, _, final Exception e) => Text('Error: $e'), + _ => const Text('Something went wrong!'), + }; + }, + ), + ), + ); + } +} +// #enddocregion ArticleView + +class ArticlePage extends StatelessWidget { + const ArticlePage({ + super.key, + required this.summary, + required this.nextArticleCallback, + }); + final Summary summary; + final VoidCallback nextArticleCallback; + @override + Widget build(BuildContext context) => const Placeholder(); +} diff --git a/examples/fwe/wikipedia_reader/lib/step4c_main.dart b/examples/fwe/wikipedia_reader/lib/step4c_main.dart new file mode 100644 index 00000000000..c61c9829aaa --- /dev/null +++ b/examples/fwe/wikipedia_reader/lib/step4c_main.dart @@ -0,0 +1,27 @@ +// ignore_for_file: unused_import + +import 'package:flutter/material.dart'; +import 'summary.dart'; + +// #docregion ArticlePage +class ArticlePage extends StatelessWidget { + const ArticlePage({ + super.key, + required this.summary, + required this.nextArticleCallback, + }); + + final Summary summary; + final VoidCallback nextArticleCallback; + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + child: Column( + children: [const Text('Article content will be displayed here')], + ), + ); + } +} + +// #enddocregion ArticlePage diff --git a/examples/fwe/wikipedia_reader/lib/step4d_main.dart b/examples/fwe/wikipedia_reader/lib/step4d_main.dart new file mode 100644 index 00000000000..abc61fbffe1 --- /dev/null +++ b/examples/fwe/wikipedia_reader/lib/step4d_main.dart @@ -0,0 +1,40 @@ +// ignore_for_file: unused_import, prefer_const_constructors, prefer_const_literals_to_create_immutables + +import 'package:flutter/material.dart'; +import 'summary.dart'; + +class ArticleWidget extends StatelessWidget { + const ArticleWidget({super.key, required this.summary}); + final Summary summary; + @override + Widget build(BuildContext context) => const Placeholder(); +} + +// #docregion ArticlePage +class ArticlePage extends StatelessWidget { + const ArticlePage({ + super.key, + required this.summary, + required this.nextArticleCallback, + }); + + final Summary summary; + final VoidCallback nextArticleCallback; + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + child: Column( + children: [ + ArticleWidget(summary: summary), + ElevatedButton( + onPressed: nextArticleCallback, + child: const Text('Next random article'), + ), + ], + ), + ); + } +} + +// #enddocregion ArticlePage diff --git a/examples/fwe/wikipedia_reader/lib/step4e_main.dart b/examples/fwe/wikipedia_reader/lib/step4e_main.dart new file mode 100644 index 00000000000..db908d2b68d --- /dev/null +++ b/examples/fwe/wikipedia_reader/lib/step4e_main.dart @@ -0,0 +1,18 @@ +// ignore_for_file: unused_import, prefer_const_constructors, prefer_const_literals_to_create_immutables + +import 'package:flutter/material.dart'; +import 'summary.dart'; + +// #docregion ArticleWidget +class ArticleWidget extends StatelessWidget { + const ArticleWidget({super.key, required this.summary}); + + final Summary summary; + + @override + Widget build(BuildContext context) { + return const Text('Article content will be displayed here'); + } +} + +// #enddocregion ArticleWidget diff --git a/examples/fwe/wikipedia_reader/lib/step4f_main.dart b/examples/fwe/wikipedia_reader/lib/step4f_main.dart new file mode 100644 index 00000000000..8c7a915ef90 --- /dev/null +++ b/examples/fwe/wikipedia_reader/lib/step4f_main.dart @@ -0,0 +1,24 @@ +// ignore_for_file: unused_import, prefer_const_constructors, prefer_const_literals_to_create_immutables + +import 'package:flutter/material.dart'; +import 'summary.dart'; + +// #docregion ArticleWidget +class ArticleWidget extends StatelessWidget { + const ArticleWidget({super.key, required this.summary}); + + final Summary summary; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + spacing: 10.0, + children: [const Text('Article content will be displayed here')], + ), + ); + } +} + +// #enddocregion ArticleWidget diff --git a/examples/fwe/wikipedia_reader/lib/step4g_main.dart b/examples/fwe/wikipedia_reader/lib/step4g_main.dart new file mode 100644 index 00000000000..0bc4872e8ae --- /dev/null +++ b/examples/fwe/wikipedia_reader/lib/step4g_main.dart @@ -0,0 +1,27 @@ +// ignore_for_file: unused_import, prefer_const_constructors, prefer_const_literals_to_create_immutables + +import 'package:flutter/material.dart'; +import 'summary.dart'; + +// #docregion ArticleWidget +class ArticleWidget extends StatelessWidget { + const ArticleWidget({super.key, required this.summary}); + + final Summary summary; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + spacing: 10.0, + children: [ + if (summary.hasImage) Image.network(summary.originalImage!.source), + const Text('Article content will be displayed here'), + ], + ), + ); + } +} + +// #enddocregion ArticleWidget diff --git a/examples/fwe/wikipedia_reader/lib/step4h_main.dart b/examples/fwe/wikipedia_reader/lib/step4h_main.dart new file mode 100644 index 00000000000..349b08c0d19 --- /dev/null +++ b/examples/fwe/wikipedia_reader/lib/step4h_main.dart @@ -0,0 +1,38 @@ +// ignore_for_file: unused_import + +import 'package:flutter/material.dart'; +import 'summary.dart'; + +// #docregion ArticleWidget +class ArticleWidget extends StatelessWidget { + const ArticleWidget({super.key, required this.summary}); + + final Summary summary; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + spacing: 10.0, + children: [ + if (summary.hasImage) Image.network(summary.originalImage!.source), + Text( + summary.titles.normalized, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.displaySmall, + ), + if (summary.description != null) + Text( + summary.description!, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.bodySmall, + ), + Text(summary.extract), + ], + ), + ); + } +} + +// #enddocregion ArticleWidget diff --git a/examples/fwe/wikipedia_reader/lib/summary.dart b/examples/fwe/wikipedia_reader/lib/summary.dart new file mode 100644 index 00000000000..36c512c19e5 --- /dev/null +++ b/examples/fwe/wikipedia_reader/lib/summary.dart @@ -0,0 +1,252 @@ +// ignore_for_file: directives_ordering + +// #docregion All + +// Representation of the JSON data returned by the Wikipedia API. +// #docregion Summary +class Summary { + /// Returns a new [Summary] instance. + Summary({ + required this.titles, + required this.pageid, + required this.extract, + required this.extractHtml, + this.thumbnail, + this.originalImage, + required this.lang, + required this.dir, + this.description, + required this.url, + }); + + /// Titles + TitlesSet titles; + + /// Page ID + int pageid; + + /// Extract + String extract; + + /// Extract HTML + String extractHtml; + + /// Thumbnail image + ImageFile? thumbnail; + + /// Original image + ImageFile? originalImage; + + /// Language + String lang; + + /// Directionality + String dir; + + /// Description + String? description; + + /// URL + String url; + + bool get hasImage => originalImage != null && thumbnail != null; + + /// Returns a new [Summary] instance and imports its values from a JSON map + static Summary fromJson(Map json) { + return switch (json) { + { + 'titles': final Map titles, + 'pageid': final int pageid, + 'extract': final String extract, + 'extract_html': final String extractHtml, + 'thumbnail': final Map thumbnail, + 'originalimage': final Map originalImage, + 'lang': final String lang, + 'dir': final String dir, + 'description': final String description, + 'content_urls': { + 'desktop': {'page': final String url}, + 'mobile': {'page': String _}, + }, + } => + Summary( + titles: TitlesSet.fromJson(titles), + pageid: pageid, + extract: extract, + extractHtml: extractHtml, + thumbnail: ImageFile.fromJson(thumbnail), + originalImage: ImageFile.fromJson(originalImage), + lang: lang, + dir: dir, + description: description, + url: url, + ), + { + 'titles': final Map titles, + 'pageid': final int pageid, + 'extract': final String extract, + 'extract_html': final String extractHtml, + 'lang': final String lang, + 'dir': final String dir, + 'description': final String description, + 'content_urls': { + 'desktop': {'page': final String url}, + 'mobile': {'page': String _}, + }, + } => + Summary( + titles: TitlesSet.fromJson(titles), + pageid: pageid, + extract: extract, + extractHtml: extractHtml, + lang: lang, + dir: dir, + description: description, + url: url, + ), + { + 'titles': final Map titles, + 'pageid': final int pageid, + 'extract': final String extract, + 'extract_html': final String extractHtml, + 'lang': final String lang, + 'dir': final String dir, + 'content_urls': { + 'desktop': {'page': final String url}, + 'mobile': {'page': String _}, + }, + } => + Summary( + titles: TitlesSet.fromJson(titles), + pageid: pageid, + extract: extract, + extractHtml: extractHtml, + lang: lang, + dir: dir, + url: url, + ), + _ => throw FormatException('Could not deserialize Summary, json=$json'), + }; + } + + @override + String toString() => + 'Summary[' + 'titles=$titles, ' + 'pageid=$pageid, ' + 'extract=$extract, ' + 'extractHtml=$extractHtml, ' + 'thumbnail=${thumbnail ?? 'null'}, ' + 'originalImage=${originalImage ?? 'null'}, ' + 'lang=$lang, ' + 'dir=$dir, ' + 'description=$description' + ']'; +} +// #enddocregion Summary + +// Image path and size, but doesn't contain any Wikipedia descriptions. +/// +/// For images with metadata, see [WikipediaImage] +// #docregion ImageFile +class ImageFile { + /// Returns a new [ImageFile] instance. + ImageFile({required this.source, required this.width, required this.height}); + + /// Original image URI + String source; + + /// Original image width + int width; + + /// Original image height + int height; + + String get extension { + final extension = getFileExtension(source); + // by default, return a non-viable image extension + return extension ?? 'err'; + } + + Map toJson() { + return { + 'source': source, + 'width': width, + 'height': height, + }; + } + + /// Returns a new [ImageFile] instance + // ignore: prefer_constructors_over_static_methods + static ImageFile fromJson(Map json) { + if (json case { + 'source': final String source, + 'height': final int height, + 'width': final int width, + }) { + return ImageFile(source: source, width: width, height: height); + } + throw FormatException('Could not deserialize OriginalImage, json=$json'); + } + + @override + String toString() => + 'OriginalImage[source_=$source, width=$width, height=$height]'; +} +// #enddocregion ImageFile + +// #docregion TitlesSet +class TitlesSet { + /// Returns a new [TitlesSet] instance. + TitlesSet({ + required this.canonical, + required this.normalized, + required this.display, + }); + + /// the DB key (non-prefixed), e.g. may have _ instead of spaces, + /// best for making request URIs, still requires Percent-encoding + String canonical; + + /// the normalized title (https://www.mediawiki.org/wiki/API:Query#Example_2:_Title_normalization), + /// e.g. may have spaces instead of _ + String normalized; + + /// the title as it should be displayed to the user + String display; + + /// Returns a new [TitlesSet] instance and imports its values from a JSON map + static TitlesSet fromJson(Map json) { + if (json case { + 'canonical': final String canonical, + 'normalized': final String normalized, + 'display': final String display, + }) { + return TitlesSet( + canonical: canonical, + normalized: normalized, + display: display, + ); + } + throw FormatException('Could not deserialize TitleSet, json=$json'); + } + + @override + String toString() => + 'TitlesSet[' + 'canonical=$canonical, ' + 'normalized=$normalized, ' + 'display=$display' + ']'; +} +// #enddocregion TitlesSet + +String? getFileExtension(String file) { + final segments = file.split('.'); + if (segments.isNotEmpty) return segments.last; + return null; +} + +const acceptableImageFormats = ['png', 'jpg', 'jpeg']; + +// #enddocregion All diff --git a/examples/fwe/wikipedia_reader/pubspec.yaml b/examples/fwe/wikipedia_reader/pubspec.yaml new file mode 100644 index 00000000000..2d1682fa422 --- /dev/null +++ b/examples/fwe/wikipedia_reader/pubspec.yaml @@ -0,0 +1,20 @@ +name: wikipedia_reader +description: "A new Flutter project." +publish_to: 'none' +version: 0.1.0+1 + +environment: + sdk: ^3.12.0-113.2.beta + +dependencies: + flutter: + sdk: flutter + http: ^1.6.0 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^6.0.0 + +flutter: + uses-material-design: true diff --git a/examples/fwe/wikipedia_reader/test/widget_test.dart b/examples/fwe/wikipedia_reader/test/widget_test.dart new file mode 100644 index 00000000000..e8cb3289cce --- /dev/null +++ b/examples/fwe/wikipedia_reader/test/widget_test.dart @@ -0,0 +1,13 @@ +// ignore_for_file: avoid_relative_lib_imports +import 'package:flutter_test/flutter_test.dart'; +import '../lib/main.dart'; + +void main() { + testWidgets('Wikipedia Reader smoke test', (WidgetTester tester) async { + // Build our app and trigger a frame. + await tester.pumpWidget(const MainApp()); + + // Verify that the title is Wikipedia Flutter. + expect(find.text('Wikipedia Flutter'), findsOneWidget); + }); +} diff --git a/src/content/learn/pathway/tutorial/change-notifier.md b/src/content/learn/pathway/tutorial/change-notifier.md index 824dc464a6d..306d04f8425 100644 --- a/src/content/learn/pathway/tutorial/change-notifier.md +++ b/src/content/learn/pathway/tutorial/change-notifier.md @@ -42,12 +42,13 @@ which triggers UI rebuilds when called. Create the `ArticleViewModel` class with its basic structure and state properties: + ```dart class ArticleViewModel extends ChangeNotifier { final ArticleModel model; Summary? summary; - String? errorMessage; - bool loading = false; + Exception? error; + bool isLoading = false; ArticleViewModel(this.model); } @@ -56,26 +57,28 @@ class ArticleViewModel extends ChangeNotifier { The `ArticleViewModel` holds three pieces of state: - `summary`: The current Wikipedia article data. -- `errorMessage`: Any error that occurred during data fetching. -- `loading`: A flag to show progress indicators. +- `error`: Any error that occurred during data fetching. +- `isLoading`: A flag to show progress indicators. ### Add constructor initialization Update the constructor to automatically fetch content when the `ArticleViewModel` is created: + ```dart class ArticleViewModel extends ChangeNotifier { final ArticleModel model; Summary? summary; - String? errorMessage; - bool loading = false; + Exception? error; + bool isLoading = false; ArticleViewModel(this.model) { - getRandomArticleSummary(); + fetchArticle(); } // Methods will be added next. + Future fetchArticle() async {} } ``` @@ -84,69 +87,71 @@ a `ArticleViewModel` object is created. Because constructors can't be asynchronous, it delegates initial content fetching to a separate method. -### Set up the `getRandomArticleSummary` method +### Set up the `fetchArticle` method -Add the `getRandomArticleSummary` that fetches data and manages state updates: +Add the `fetchArticle` that fetches data and manages state updates: + ```dart class ArticleViewModel extends ChangeNotifier { final ArticleModel model; Summary? summary; - String? errorMessage; - bool loading = false; + Exception? error; + bool isLoading = false; ArticleViewModel(this.model) { - getRandomArticleSummary(); + fetchArticle(); } - Future getRandomArticleSummary() async { - loading = true; + Future fetchArticle() async { + isLoading = true; notifyListeners(); // TODO: Add data fetching logic - loading = false; + isLoading = false; notifyListeners(); } } ``` -The ViewModel updates the `loading` property and +The ViewModel updates the `isLoading` property and calls `notifyListeners()` to inform the UI of the update. When the operation completes, it toggles the property back. -When you build the UI, you'll use this `loading` property to +When you build the UI, you'll use this `isLoading` property to show a loading indicator while fetching a new article. ### Retrieve an article from the `ArticleModel` -Complete the `getRandomArticleSummary` method to fetch an article summary. +Complete the `fetchArticle` method to fetch an article summary. Use a [try-catch block][] to gracefully handle network errors and store error messages that the UI can display to users. The method clears previous errors on success and clears the previous article summary on error to maintain a consistent state. + ```dart class ArticleViewModel extends ChangeNotifier { final ArticleModel model; Summary? summary; - String? errorMessage; - bool loading = false; + Exception? error; + bool isLoading = false; ArticleViewModel(this.model) { - getRandomArticleSummary(); + fetchArticle(); } - Future getRandomArticleSummary() async { - loading = true; + Future fetchArticle() async { + isLoading = true; notifyListeners(); try { summary = await model.getRandomArticleSummary(); - errorMessage = null; // Clear any previous errors. - } on HttpException catch (error) { - errorMessage = error.message; + error = null; // Clear any previous errors. + } on HttpException catch (e) { + error = e; summary = null; } - loading = false; + isLoading = false; notifyListeners(); } } @@ -158,30 +163,32 @@ class ArticleViewModel extends ChangeNotifier { Before building the full UI, test that your HTTP requests work by printing results to the console. -First, update the `getRandomArticleSummary` method to +First, update the `fetchArticle` method to print the results: + ```dart -Future getRandomArticleSummary() async { - loading = true; +Future fetchArticle() async { + isLoading = true; notifyListeners(); try { summary = await model.getRandomArticleSummary(); print('Article loaded: ${summary!.titles.normalized}'); // Temporary - errorMessage = null; // Clear any previous errors. - } on HttpException catch (error) { - print('Error loading article: ${error.message}'); // Temporary - errorMessage = error.message; + error = null; // Clear any previous errors. + } on HttpException catch (e) { + print('Error loading article: ${e.message}'); // Temporary + error = e; summary = null; } - loading = false; + isLoading = false; notifyListeners(); } ``` Then, update the `MainApp` widget to create the `ArticleViewModel`, -which calls the `getRandomArticleSummary` method on creation: +which calls the `fetchArticle` method on creation: + ```dart class MainApp extends StatelessWidget { const MainApp({super.key}); @@ -193,12 +200,8 @@ class MainApp extends StatelessWidget { return MaterialApp( home: Scaffold( - appBar: AppBar( - title: const Text('Wikipedia Flutter'), - ), - body: const Center( - child: Text('Check console for article data'), - ), + appBar: AppBar(title: const Text('Wikipedia Flutter')), + body: const Center(child: Text('Check console for article data')), ), ); } @@ -227,7 +230,7 @@ items: icon: toggle_on details: >- Your ViewModel tracks three pieces of state: - `loading`, `summary`, and `errorMessage`. + `isLoading`, `summary`, and `error`. Using `try` and `catch`, you handle network errors gracefully and maintain consistent state for each possible outcome. - title: Used notifyListeners to signal UI updates diff --git a/src/content/learn/pathway/tutorial/http-requests.md b/src/content/learn/pathway/tutorial/http-requests.md index 31bc4c85940..eaf85290b02 100644 --- a/src/content/learn/pathway/tutorial/http-requests.md +++ b/src/content/learn/pathway/tutorial/http-requests.md @@ -49,7 +49,8 @@ A model doesn't usually need to import Flutter libraries. Create an empty `ArticleModel` class in your `main.dart` file: -```dart title="lib/main.dart" + +```dart class ArticleModel { // Properties and methods will be added here. } @@ -66,6 +67,7 @@ https://en.wikipedia.org/api/rest_v1/page/random/summary Add a method to fetch a random Wikipedia article summary: + ```dart class ArticleModel { Future getRandomArticleSummary() async { @@ -76,6 +78,7 @@ class ArticleModel { final response = await get(uri); // TODO: Add error handling and JSON parsing. + throw UnimplementedError(); } } ``` @@ -99,6 +102,7 @@ A status code of **200** indicates success, while other codes indicate errors. If the status code isn't **200**, the model throws an error for the UI to display to users. + ```dart class ArticleModel { Future getRandomArticleSummary() async { @@ -109,10 +113,11 @@ class ArticleModel { final response = await get(uri); if (response.statusCode != 200) { - throw HttpException('Failed to update resource'); + throw const HttpException('Failed to update resource'); } // TODO: Parse JSON and return Summary. + throw UnimplementedError(); } } ``` @@ -123,6 +128,7 @@ The [Wikipedia API][] returns [JSON][] data that you decode into a `Summary` class Complete the `getRandomArticleSummary` method: + ```dart class ArticleModel { Future getRandomArticleSummary() async { diff --git a/src/content/learn/pathway/tutorial/listenable-builder.md b/src/content/learn/pathway/tutorial/listenable-builder.md index 8da714f9185..6c5fe0c56fc 100644 --- a/src/content/learn/pathway/tutorial/listenable-builder.md +++ b/src/content/learn/pathway/tutorial/listenable-builder.md @@ -38,19 +38,23 @@ Create the `ArticleView` widget that manages the overall page layout and state handling. Start with the basic class structure and widgets: + ```dart -class ArticleView extends StatelessWidget { - ArticleView({super.key}); +class ArticleView extends StatefulWidget { + const ArticleView({super.key}); + + @override + State createState() => _ArticleViewState(); +} + +class _ArticleViewState extends State { + // viewModel will be instantiated next @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar( - title: const Text('Wikipedia Flutter'), - ), - body: const Center( - child: Text('UI will update here'), - ), + appBar: AppBar(title: const Text('Wikipedia Flutter')), + body: const Center(child: Text('Loading...')), ); } } @@ -60,20 +64,48 @@ class ArticleView extends StatelessWidget { Create the `ArticleViewModel` in this widget: + ```dart -class ArticleView extends StatelessWidget { - ArticleView({super.key}); +class ArticleView extends StatefulWidget { + const ArticleView({super.key}); - final viewModel = ArticleViewModel(ArticleModel()); + @override + State createState() => _ArticleViewState(); +} + +class _ArticleViewState extends State { + late final ArticleViewModel viewModel; + + @override + void initState() { + super.initState(); + viewModel = ArticleViewModel(ArticleModel()); + viewModel.fetchArticle(); + } @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar( - title: const Text('Wikipedia Flutter'), - ), - body: const Center( - child: Text('UI will update here'), + appBar: AppBar(title: const Text('Wikipedia Flutter')), + body: Center( + child: ListenableBuilder( + listenable: viewModel, + builder: (context, _) { + return switch (( + viewModel.isLoading, + viewModel.summary, + viewModel.error, + )) { + (true, _, _) => const CircularProgressIndicator(), + (_, final summary?, _) => ArticlePage( + summary: summary, + nextArticleCallback: viewModel.fetchArticle, + ), + (_, _, final Exception e) => Text('Error: $e'), + _ => const Text('Something went wrong!'), + }; + }, + ), ), ); } @@ -86,23 +118,23 @@ Wrap your UI in a [`ListenableBuilder`][] to listen for state changes, and pass it a `ChangeNotifier` object. In this case, the `ArticleViewModel` extends `ChangeNotifier`. + ```dart -class ArticleView extends StatelessWidget { - ArticleView({super.key}); +class ArticlePage extends StatelessWidget { + const ArticlePage({ + super.key, + required this.summary, + required this.nextArticleCallback, + }); - final ArticleViewModel viewModel = ArticleViewModel(ArticleModel()); + final Summary summary; + final VoidCallback nextArticleCallback; @override Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text('Wikipedia Flutter'), - ), - body: ListenableBuilder( - listenable: viewModel, - builder: (context, child) { - return const Center(child: Text('UI will update here')); - }, + return SingleChildScrollView( + child: Column( + children: [const Text('Article content will be displayed here')], ), ); } @@ -132,39 +164,29 @@ the UI can display different widgets. Use Dart's support for [switch expressions][] to handle all possible combinations in a clean, readable way: + ```dart -class ArticleView extends StatelessWidget { - ArticleView({super.key}); +class ArticlePage extends StatelessWidget { + const ArticlePage({ + super.key, + required this.summary, + required this.nextArticleCallback, + }); - final ArticleViewModel viewModel = ArticleViewModel(ArticleModel()); + final Summary summary; + final VoidCallback nextArticleCallback; @override Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text('Wikipedia Flutter'), - actions: [], - ), - body: ListenableBuilder( - listenable: viewModel, - builder: (context, child) { - return switch (( - viewModel.loading, - viewModel.summary, - viewModel.errorMessage, - )) { - (true, _, _) => CircularProgressIndicator(), - (false, _, String message) => Center(child: Text(message)), - (false, null, null) => Center( - child: Text('An unknown error has occurred'), - ), - // The summary must be non-null in this switch case. - (false, Summary summary, null) => ArticlePage( - summary: summary, - nextArticleCallback: viewModel.getRandomArticleSummary, - ), - }; - }, + return SingleChildScrollView( + child: Column( + children: [ + ArticleWidget(summary: summary), + ElevatedButton( + onPressed: nextArticleCallback, + child: const Text('Next random article'), + ), + ], ), ); } @@ -189,20 +211,16 @@ by the view model to build the UI. Now create a `ArticlePage` widget that displays the actual article content. This reusable widget takes summary data and a callback function: + ```dart -class ArticlePage extends StatelessWidget { - const ArticlePage({ - super.key, - required this.summary, - required this.nextArticleCallback, - }); +class ArticleWidget extends StatelessWidget { + const ArticleWidget({super.key, required this.summary}); final Summary summary; - final VoidCallback nextArticleCallback; @override Widget build(BuildContext context) { - return Center(child: Text('Article content will be displayed here')); + return const Text('Article content will be displayed here'); } } ``` @@ -211,24 +229,20 @@ class ArticlePage extends StatelessWidget { Replace the placeholder with a scrollable column layout: + ```dart -class ArticlePage extends StatelessWidget { - const ArticlePage({ - super.key, - required this.summary, - required this.nextArticleCallback, - }); +class ArticleWidget extends StatelessWidget { + const ArticleWidget({super.key, required this.summary}); final Summary summary; - final VoidCallback nextArticleCallback; @override Widget build(BuildContext context) { - return SingleChildScrollView( + return Padding( + padding: const EdgeInsets.all(8.0), child: Column( - children: [ - Text('Article content will be displayed here'), - ], + spacing: 10.0, + children: [const Text('Article content will be displayed here')], ), ); } @@ -239,29 +253,22 @@ class ArticlePage extends StatelessWidget { Complete the layout with an article widget and navigation button: + ```dart -class ArticlePage extends StatelessWidget { - const ArticlePage({ - super.key, - required this.summary, - required this.nextArticleCallback, - }); +class ArticleWidget extends StatelessWidget { + const ArticleWidget({super.key, required this.summary}); final Summary summary; - final VoidCallback nextArticleCallback; @override Widget build(BuildContext context) { - return SingleChildScrollView( + return Padding( + padding: const EdgeInsets.all(8.0), child: Column( + spacing: 10.0, children: [ - ArticleWidget( - summary: summary, - ), - ElevatedButton( - onPressed: nextArticleCallback, - child: Text('Next random article'), - ), + if (summary.hasImage) Image.network(summary.originalImage!.source), + const Text('Article content will be displayed here'), ], ), ); @@ -278,6 +285,7 @@ with proper styling and conditional rendering. Start with the widget that accepts a `summary` parameter: + ```dart class ArticleWidget extends StatelessWidget { const ArticleWidget({super.key, required this.summary}); @@ -286,7 +294,27 @@ class ArticleWidget extends StatelessWidget { @override Widget build(BuildContext context) { - return Text('Article content will be displayed here'); + return Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + spacing: 10.0, + children: [ + if (summary.hasImage) Image.network(summary.originalImage!.source), + Text( + summary.titles.normalized, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.displaySmall, + ), + if (summary.description != null) + Text( + summary.description!, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.bodySmall, + ), + Text(summary.extract), + ], + ), + ); } } ``` @@ -295,23 +323,14 @@ class ArticleWidget extends StatelessWidget { Wrap the content in proper padding and layout: + ```dart -class ArticleWidget extends StatelessWidget { - const ArticleWidget({super.key, required this.summary}); - - final Summary summary; +class MainApp extends StatelessWidget { + const MainApp({super.key}); @override Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.all(8.0), - child: Column( - spacing: 10.0, - children: [ - Text('Article content will be displayed here'), - ], - ), - ); + return const MaterialApp(home: ArticleView()); } } ``` diff --git a/src/content/learn/pathway/tutorial/set-up-state-project.md b/src/content/learn/pathway/tutorial/set-up-state-project.md index 93e39ba4d2d..b6dc86ad778 100644 --- a/src/content/learn/pathway/tutorial/set-up-state-project.md +++ b/src/content/learn/pathway/tutorial/set-up-state-project.md @@ -88,7 +88,10 @@ for Wikipedia article summaries. This file has no special logic, and is simply a collection of classes that represent the data returned by the Wikipedia API. Its sufficient to copy the code below into the file and then ignore it. If you aren't comfortable basic Dart classes, you should read the [Dart Getting Started][] tutorial first. + ```dart title="lib/summary.dart" collapsed + +// Representation of the JSON data returned by the Wikipedia API. class Summary { /// Returns a new [Summary] instance. Summary({ @@ -96,68 +99,47 @@ class Summary { required this.pageid, required this.extract, required this.extractHtml, - required this.lang, - required this.dir, this.thumbnail, this.originalImage, - this.url, - + required this.lang, + required this.dir, this.description, + required this.url, }); - /// + /// Titles TitlesSet titles; - /// The page ID + /// Page ID int pageid; - /// First several sentences of an article in plain text + /// Extract String extract; - /// First several sentences of an article in simple HTML format + /// Extract HTML String extractHtml; + /// Thumbnail image ImageFile? thumbnail; - /// Url to the article on Wikipedia - String? url; - - /// + /// Original image ImageFile? originalImage; - /// The page language code + /// Language String lang; - /// The page language direction code + /// Directionality String dir; - /// Wikidata description for the page + /// Description String? description; - bool get hasImage => - (originalImage != null || thumbnail != null) && preferredSource != null; - - String? get preferredSource { - ImageFile? file; - - if (originalImage != null) { - file = originalImage; - } else { - file = thumbnail; - } + /// URL + String url; - if (file != null) { - if (acceptableImageFormats.contains(file.extension.toLowerCase())) { - return file.source; - } else { - return null; - } - } + bool get hasImage => originalImage != null && thumbnail != null; - return null; - } - - /// Returns a new [Summary] instance + /// Returns a new [Summary] instance and imports its values from a JSON map static Summary fromJson(Map json) { return switch (json) { { @@ -165,37 +147,11 @@ class Summary { 'pageid': final int pageid, 'extract': final String extract, 'extract_html': final String extractHtml, - 'lang': final String lang, - 'dir': final String dir, - 'content_urls': { - 'desktop': {'page': final String url}, - 'mobile': {'page': String _}, - }, - 'description': final String description, 'thumbnail': final Map thumbnail, 'originalimage': final Map originalImage, - } => - Summary( - titles: TitlesSet.fromJson(titles), - pageid: pageid, - extract: extract, - extractHtml: extractHtml, - thumbnail: ImageFile.fromJson(thumbnail), - originalImage: ImageFile.fromJson(originalImage), - lang: lang, - dir: dir, - url: url, - description: description, - ), - { - 'titles': final Map titles, - 'pageid': final int pageid, - 'extract': final String extract, - 'extract_html': final String extractHtml, 'lang': final String lang, 'dir': final String dir, - 'thumbnail': final Map thumbnail, - 'originalimage': final Map originalImage, + 'description': final String description, 'content_urls': { 'desktop': {'page': final String url}, 'mobile': {'page': String _}, @@ -210,6 +166,7 @@ class Summary { originalImage: ImageFile.fromJson(originalImage), lang: lang, dir: dir, + description: description, url: url, ), { @@ -374,12 +331,12 @@ String? getFileExtension(String file) { } const acceptableImageFormats = ['png', 'jpg', 'jpeg']; - ``` Then, open `lib/main.dart` and replace the existing code with this basic structure, which adds required imports that the app uses: + ```dart title="lib/main.dart" import 'dart:convert'; import 'dart:io'; @@ -400,12 +357,8 @@ class MainApp extends StatelessWidget { Widget build(BuildContext context) { return MaterialApp( home: Scaffold( - appBar: AppBar( - title: const Text('Wikipedia Flutter'), - ), - body: const Center( - child: Text('Loading...'), - ), + appBar: AppBar(title: const Text('Wikipedia Flutter')), + body: const Center(child: Text('Loading...')), ), ); } From 51fb792d05df54a380789bef6cae228d6a364dde Mon Sep 17 00:00:00 2001 From: lamek Date: Tue, 17 Mar 2026 00:23:16 +0000 Subject: [PATCH 02/19] Fix PR issues and sync tutorial excerpt --- .../fwe/wikipedia_reader/lib/step2_main.dart | 2 +- .../fwe/wikipedia_reader/lib/step4_main.dart | 11 +++++-- .../fwe/wikipedia_reader/lib/step4f_main.dart | 5 +-- .../fwe/wikipedia_reader/lib/step4g_main.dart | 6 ++-- .../fwe/wikipedia_reader/lib/step4h_main.dart | 11 +++++-- .../fwe/wikipedia_reader/lib/summary.dart | 32 +++++++++---------- .../learn/pathway/tutorial/http-requests.md | 2 +- .../pathway/tutorial/listenable-builder.md | 32 ++++++++++++------- .../pathway/tutorial/set-up-state-project.md | 32 +++++++++---------- 9 files changed, 77 insertions(+), 56 deletions(-) diff --git a/examples/fwe/wikipedia_reader/lib/step2_main.dart b/examples/fwe/wikipedia_reader/lib/step2_main.dart index e880600cc01..8ffa84c3038 100644 --- a/examples/fwe/wikipedia_reader/lib/step2_main.dart +++ b/examples/fwe/wikipedia_reader/lib/step2_main.dart @@ -39,7 +39,7 @@ class ArticleModel { throw HttpException('Failed to update resource'); } - return Summary.fromJson(jsonDecode(response.body)); + return Summary.fromJson(jsonDecode(response.body) as Map); } } diff --git a/examples/fwe/wikipedia_reader/lib/step4_main.dart b/examples/fwe/wikipedia_reader/lib/step4_main.dart index 749c77ca033..4ed212ac64a 100644 --- a/examples/fwe/wikipedia_reader/lib/step4_main.dart +++ b/examples/fwe/wikipedia_reader/lib/step4_main.dart @@ -147,20 +147,25 @@ class ArticleWidget extends StatelessWidget { return Padding( padding: const EdgeInsets.all(8.0), child: Column( - spacing: 10.0, children: [ - if (summary.hasImage) Image.network(summary.originalImage!.source), + if (summary.hasImage) ...[ + Image.network(summary.originalImage!.source), + const SizedBox(height: 10.0), + ], Text( summary.titles.normalized, overflow: TextOverflow.ellipsis, style: Theme.of(context).textTheme.displaySmall, ), - if (summary.description != null) + const SizedBox(height: 10.0), + if (summary.description != null) ...[ Text( summary.description!, overflow: TextOverflow.ellipsis, style: Theme.of(context).textTheme.bodySmall, ), + const SizedBox(height: 10.0), + ], Text(summary.extract), ], ), diff --git a/examples/fwe/wikipedia_reader/lib/step4f_main.dart b/examples/fwe/wikipedia_reader/lib/step4f_main.dart index 8c7a915ef90..74f6b8c7251 100644 --- a/examples/fwe/wikipedia_reader/lib/step4f_main.dart +++ b/examples/fwe/wikipedia_reader/lib/step4f_main.dart @@ -14,8 +14,9 @@ class ArticleWidget extends StatelessWidget { return Padding( padding: const EdgeInsets.all(8.0), child: Column( - spacing: 10.0, - children: [const Text('Article content will be displayed here')], + children: [ + const Text('Article content will be displayed here'), + ], ), ); } diff --git a/examples/fwe/wikipedia_reader/lib/step4g_main.dart b/examples/fwe/wikipedia_reader/lib/step4g_main.dart index 0bc4872e8ae..63acce0bf5b 100644 --- a/examples/fwe/wikipedia_reader/lib/step4g_main.dart +++ b/examples/fwe/wikipedia_reader/lib/step4g_main.dart @@ -14,9 +14,11 @@ class ArticleWidget extends StatelessWidget { return Padding( padding: const EdgeInsets.all(8.0), child: Column( - spacing: 10.0, children: [ - if (summary.hasImage) Image.network(summary.originalImage!.source), + if (summary.hasImage) ...[ + Image.network(summary.originalImage!.source), + const SizedBox(height: 10.0), + ], const Text('Article content will be displayed here'), ], ), diff --git a/examples/fwe/wikipedia_reader/lib/step4h_main.dart b/examples/fwe/wikipedia_reader/lib/step4h_main.dart index 349b08c0d19..b61f400f241 100644 --- a/examples/fwe/wikipedia_reader/lib/step4h_main.dart +++ b/examples/fwe/wikipedia_reader/lib/step4h_main.dart @@ -14,20 +14,25 @@ class ArticleWidget extends StatelessWidget { return Padding( padding: const EdgeInsets.all(8.0), child: Column( - spacing: 10.0, children: [ - if (summary.hasImage) Image.network(summary.originalImage!.source), + if (summary.hasImage) ...[ + Image.network(summary.originalImage!.source), + const SizedBox(height: 10.0), + ], Text( summary.titles.normalized, overflow: TextOverflow.ellipsis, style: Theme.of(context).textTheme.displaySmall, ), - if (summary.description != null) + const SizedBox(height: 10.0), + if (summary.description != null) ...[ Text( summary.description!, overflow: TextOverflow.ellipsis, style: Theme.of(context).textTheme.bodySmall, ), + const SizedBox(height: 10.0), + ], Text(summary.extract), ], ), diff --git a/examples/fwe/wikipedia_reader/lib/summary.dart b/examples/fwe/wikipedia_reader/lib/summary.dart index 36c512c19e5..e7b853db095 100644 --- a/examples/fwe/wikipedia_reader/lib/summary.dart +++ b/examples/fwe/wikipedia_reader/lib/summary.dart @@ -20,34 +20,34 @@ class Summary { }); /// Titles - TitlesSet titles; + final TitlesSet titles; /// Page ID - int pageid; + final int pageid; /// Extract - String extract; + final String extract; /// Extract HTML - String extractHtml; + final String extractHtml; /// Thumbnail image - ImageFile? thumbnail; + final ImageFile? thumbnail; /// Original image - ImageFile? originalImage; + final ImageFile? originalImage; /// Language - String lang; + final String lang; /// Directionality - String dir; + final String dir; /// Description - String? description; + final String? description; /// URL - String url; + final String url; bool get hasImage => originalImage != null && thumbnail != null; @@ -154,13 +154,13 @@ class ImageFile { ImageFile({required this.source, required this.width, required this.height}); /// Original image URI - String source; + final String source; /// Original image width - int width; + final int width; /// Original image height - int height; + final int height; String get extension { final extension = getFileExtension(source); @@ -206,14 +206,14 @@ class TitlesSet { /// the DB key (non-prefixed), e.g. may have _ instead of spaces, /// best for making request URIs, still requires Percent-encoding - String canonical; + final String canonical; /// the normalized title (https://www.mediawiki.org/wiki/API:Query#Example_2:_Title_normalization), /// e.g. may have spaces instead of _ - String normalized; + final String normalized; /// the title as it should be displayed to the user - String display; + final String display; /// Returns a new [TitlesSet] instance and imports its values from a JSON map static TitlesSet fromJson(Map json) { diff --git a/src/content/learn/pathway/tutorial/http-requests.md b/src/content/learn/pathway/tutorial/http-requests.md index eaf85290b02..bdd3023562b 100644 --- a/src/content/learn/pathway/tutorial/http-requests.md +++ b/src/content/learn/pathway/tutorial/http-requests.md @@ -142,7 +142,7 @@ class ArticleModel { throw HttpException('Failed to update resource'); } - return Summary.fromJson(jsonDecode(response.body)); + return Summary.fromJson(jsonDecode(response.body) as Map); } } ``` diff --git a/src/content/learn/pathway/tutorial/listenable-builder.md b/src/content/learn/pathway/tutorial/listenable-builder.md index 6c5fe0c56fc..7f6546d55e2 100644 --- a/src/content/learn/pathway/tutorial/listenable-builder.md +++ b/src/content/learn/pathway/tutorial/listenable-builder.md @@ -241,8 +241,9 @@ class ArticleWidget extends StatelessWidget { return Padding( padding: const EdgeInsets.all(8.0), child: Column( - spacing: 10.0, - children: [const Text('Article content will be displayed here')], + children: [ + const Text('Article content will be displayed here'), + ], ), ); } @@ -265,9 +266,11 @@ class ArticleWidget extends StatelessWidget { return Padding( padding: const EdgeInsets.all(8.0), child: Column( - spacing: 10.0, children: [ - if (summary.hasImage) Image.network(summary.originalImage!.source), + if (summary.hasImage) ...[ + Image.network(summary.originalImage!.source), + const SizedBox(height: 10.0), + ], const Text('Article content will be displayed here'), ], ), @@ -297,20 +300,25 @@ class ArticleWidget extends StatelessWidget { return Padding( padding: const EdgeInsets.all(8.0), child: Column( - spacing: 10.0, children: [ - if (summary.hasImage) Image.network(summary.originalImage!.source), + if (summary.hasImage) ...[ + Image.network(summary.originalImage!.source), + const SizedBox(height: 10.0), + ], Text( summary.titles.normalized, overflow: TextOverflow.ellipsis, style: Theme.of(context).textTheme.displaySmall, ), - if (summary.description != null) + const SizedBox(height: 10.0), + if (summary.description != null) ...[ Text( summary.description!, overflow: TextOverflow.ellipsis, style: Theme.of(context).textTheme.bodySmall, ), + const SizedBox(height: 10.0), + ], Text(summary.extract), ], ), @@ -350,7 +358,7 @@ class ArticleWidget extends StatelessWidget { return Padding( padding: const EdgeInsets.all(8.0), child: Column( - spacing: 10.0, + children: [ if (summary.hasImage) Image.network( @@ -380,7 +388,7 @@ class ArticleWidget extends StatelessWidget { return Padding( padding: const EdgeInsets.all(8.0), child: Column( - spacing: 10.0, + children: [ if (summary.hasImage) Image.network( @@ -413,8 +421,8 @@ This widget demonstrates a few important UI concepts: The `if` statements show content only when available. - **Text styling**: Different text styles create visual hierarchy using Flutter's theme system. -- **Proper spacing**: - The `spacing` parameter provides consistent vertical spacing. + + - **Overflow handling**: `TextOverflow.ellipsis` prevents text from breaking the layout. @@ -482,7 +490,7 @@ items: details: >- You created `ArticleView`, `ArticlePage`, and `ArticleWidget` with conditional rendering, text styling, - proper spacing, and overflow handling. + and overflow handling. These are core UI patterns you'll use in every Flutter app. - title: Completed the MVVM architecture icon: celebration diff --git a/src/content/learn/pathway/tutorial/set-up-state-project.md b/src/content/learn/pathway/tutorial/set-up-state-project.md index b6dc86ad778..0460c92ad54 100644 --- a/src/content/learn/pathway/tutorial/set-up-state-project.md +++ b/src/content/learn/pathway/tutorial/set-up-state-project.md @@ -108,34 +108,34 @@ class Summary { }); /// Titles - TitlesSet titles; + final TitlesSet titles; /// Page ID - int pageid; + final int pageid; /// Extract - String extract; + final String extract; /// Extract HTML - String extractHtml; + final String extractHtml; /// Thumbnail image - ImageFile? thumbnail; + final ImageFile? thumbnail; /// Original image - ImageFile? originalImage; + final ImageFile? originalImage; /// Language - String lang; + final String lang; /// Directionality - String dir; + final String dir; /// Description - String? description; + final String? description; /// URL - String url; + final String url; bool get hasImage => originalImage != null && thumbnail != null; @@ -240,13 +240,13 @@ class ImageFile { ImageFile({required this.source, required this.width, required this.height}); /// Original image URI - String source; + final String source; /// Original image width - int width; + final int width; /// Original image height - int height; + final int height; String get extension { final extension = getFileExtension(source); @@ -290,14 +290,14 @@ class TitlesSet { /// the DB key (non-prefixed), e.g. may have _ instead of spaces, /// best for making request URIs, still requires Percent-encoding - String canonical; + final String canonical; /// the normalized title (https://www.mediawiki.org/wiki/API:Query#Example_2:_Title_normalization), /// e.g. may have spaces instead of _ - String normalized; + final String normalized; /// the title as it should be displayed to the user - String display; + final String display; /// Returns a new [TitlesSet] instance and imports its values from a JSON map static TitlesSet fromJson(Map json) { From da1d8bccd26ca3a39601f41c17b5b9be0a1d3cd6 Mon Sep 17 00:00:00 2001 From: lamek Date: Wed, 18 Mar 2026 19:59:28 +0000 Subject: [PATCH 03/19] Format step4f_main.dart --- examples/fwe/wikipedia_reader/lib/step4f_main.dart | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/examples/fwe/wikipedia_reader/lib/step4f_main.dart b/examples/fwe/wikipedia_reader/lib/step4f_main.dart index 74f6b8c7251..5d22ba9884a 100644 --- a/examples/fwe/wikipedia_reader/lib/step4f_main.dart +++ b/examples/fwe/wikipedia_reader/lib/step4f_main.dart @@ -14,9 +14,7 @@ class ArticleWidget extends StatelessWidget { return Padding( padding: const EdgeInsets.all(8.0), child: Column( - children: [ - const Text('Article content will be displayed here'), - ], + children: [const Text('Article content will be displayed here')], ), ); } From 02cc4399e23b3d3f1ae203d94ae4ff2213408ab0 Mon Sep 17 00:00:00 2001 From: lamek Date: Wed, 18 Mar 2026 20:02:24 +0000 Subject: [PATCH 04/19] Refresh excerpts after dart format --- src/content/learn/pathway/tutorial/listenable-builder.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/content/learn/pathway/tutorial/listenable-builder.md b/src/content/learn/pathway/tutorial/listenable-builder.md index 7f6546d55e2..fe5bc5b9dee 100644 --- a/src/content/learn/pathway/tutorial/listenable-builder.md +++ b/src/content/learn/pathway/tutorial/listenable-builder.md @@ -241,9 +241,7 @@ class ArticleWidget extends StatelessWidget { return Padding( padding: const EdgeInsets.all(8.0), child: Column( - children: [ - const Text('Article content will be displayed here'), - ], + children: [const Text('Article content will be displayed here')], ), ); } From bd2b6af73acca130ca5848c11e6fe58cd655a14b Mon Sep 17 00:00:00 2001 From: Parker Lougheed Date: Sat, 18 Apr 2026 22:10:39 +0200 Subject: [PATCH 05/19] Apply some changes from #13286 Co-authored-by: lamek --- examples/fwe/wikipedia_reader/lib/main.dart | 4 +--- examples/fwe/wikipedia_reader/lib/summary.dart | 2 -- examples/fwe/wikipedia_reader/pubspec.yaml | 7 +++---- src/content/learn/pathway/tutorial/set-up-state-project.md | 2 -- 4 files changed, 4 insertions(+), 11 deletions(-) diff --git a/examples/fwe/wikipedia_reader/lib/main.dart b/examples/fwe/wikipedia_reader/lib/main.dart index 67a457a2e8e..a42a3ac0c5d 100644 --- a/examples/fwe/wikipedia_reader/lib/main.dart +++ b/examples/fwe/wikipedia_reader/lib/main.dart @@ -1,5 +1,3 @@ -// ignore_for_file: avoid_dynamic_calls - import 'dart:convert'; import 'dart:io'; @@ -145,7 +143,7 @@ class ArticleWidget extends StatelessWidget { return Padding( padding: const EdgeInsets.all(8.0), child: Column( - spacing: 10.0, + spacing: 10, children: [ if (summary.hasImage) Image.network(summary.originalImage!.source), Text( diff --git a/examples/fwe/wikipedia_reader/lib/summary.dart b/examples/fwe/wikipedia_reader/lib/summary.dart index e7b853db095..c7c6699b08e 100644 --- a/examples/fwe/wikipedia_reader/lib/summary.dart +++ b/examples/fwe/wikipedia_reader/lib/summary.dart @@ -146,8 +146,6 @@ class Summary { // #enddocregion Summary // Image path and size, but doesn't contain any Wikipedia descriptions. -/// -/// For images with metadata, see [WikipediaImage] // #docregion ImageFile class ImageFile { /// Returns a new [ImageFile] instance. diff --git a/examples/fwe/wikipedia_reader/pubspec.yaml b/examples/fwe/wikipedia_reader/pubspec.yaml index 2d1682fa422..829dc7cced0 100644 --- a/examples/fwe/wikipedia_reader/pubspec.yaml +++ b/examples/fwe/wikipedia_reader/pubspec.yaml @@ -1,10 +1,9 @@ name: wikipedia_reader -description: "A new Flutter project." -publish_to: 'none' -version: 0.1.0+1 +description: Code excerpts for the Wikipedia reader app in FWE. +publish_to: none environment: - sdk: ^3.12.0-113.2.beta + sdk: ^3.11.0 dependencies: flutter: diff --git a/src/content/learn/pathway/tutorial/set-up-state-project.md b/src/content/learn/pathway/tutorial/set-up-state-project.md index 0460c92ad54..1670339f7f7 100644 --- a/src/content/learn/pathway/tutorial/set-up-state-project.md +++ b/src/content/learn/pathway/tutorial/set-up-state-project.md @@ -233,8 +233,6 @@ class Summary { } // Image path and size, but doesn't contain any Wikipedia descriptions. -/// -/// For images with metadata, see [WikipediaImage] class ImageFile { /// Returns a new [ImageFile] instance. ImageFile({required this.source, required this.width, required this.height}); From 06f15c05c74866a07379fe68bc1190303e058d92 Mon Sep 17 00:00:00 2001 From: Parker Lougheed Date: Sat, 18 Apr 2026 22:13:24 +0200 Subject: [PATCH 06/19] Remove some unnecessary files --- examples/fwe/wikipedia_reader/.gitignore | 45 ------------------- examples/fwe/wikipedia_reader/.metadata | 45 ------------------- examples/fwe/wikipedia_reader/README.md | 2 +- examples/fwe/wikipedia_reader/pubspec.yaml | 2 +- .../wikipedia_reader/test/widget_test.dart | 13 ------ 5 files changed, 2 insertions(+), 105 deletions(-) delete mode 100644 examples/fwe/wikipedia_reader/.gitignore delete mode 100644 examples/fwe/wikipedia_reader/.metadata delete mode 100644 examples/fwe/wikipedia_reader/test/widget_test.dart diff --git a/examples/fwe/wikipedia_reader/.gitignore b/examples/fwe/wikipedia_reader/.gitignore deleted file mode 100644 index 3820a95c65c..00000000000 --- a/examples/fwe/wikipedia_reader/.gitignore +++ /dev/null @@ -1,45 +0,0 @@ -# Miscellaneous -*.class -*.log -*.pyc -*.swp -.DS_Store -.atom/ -.build/ -.buildlog/ -.history -.svn/ -.swiftpm/ -migrate_working_dir/ - -# IntelliJ related -*.iml -*.ipr -*.iws -.idea/ - -# The .vscode folder contains launch configuration and tasks you configure in -# VS Code which you may wish to be included in version control, so this line -# is commented out by default. -#.vscode/ - -# Flutter/Dart/Pub related -**/doc/api/ -**/ios/Flutter/.last_build_id -.dart_tool/ -.flutter-plugins-dependencies -.pub-cache/ -.pub/ -/build/ -/coverage/ - -# Symbolication related -app.*.symbols - -# Obfuscation related -app.*.map.json - -# Android Studio will place build artifacts here -/android/app/debug -/android/app/profile -/android/app/release diff --git a/examples/fwe/wikipedia_reader/.metadata b/examples/fwe/wikipedia_reader/.metadata deleted file mode 100644 index c9627623eef..00000000000 --- a/examples/fwe/wikipedia_reader/.metadata +++ /dev/null @@ -1,45 +0,0 @@ -# This file tracks properties of this Flutter project. -# Used by Flutter tool to assess capabilities and perform upgrades etc. -# -# This file should be version controlled and should not be manually edited. - -version: - revision: "e308d690a1193ea0d93d62aad6efe48e5994bc3c" - channel: "[user-branch]" - -project_type: app - -# Tracks metadata for the flutter migrate command -migration: - platforms: - - platform: root - create_revision: e308d690a1193ea0d93d62aad6efe48e5994bc3c - base_revision: e308d690a1193ea0d93d62aad6efe48e5994bc3c - - platform: android - create_revision: e308d690a1193ea0d93d62aad6efe48e5994bc3c - base_revision: e308d690a1193ea0d93d62aad6efe48e5994bc3c - - platform: ios - create_revision: e308d690a1193ea0d93d62aad6efe48e5994bc3c - base_revision: e308d690a1193ea0d93d62aad6efe48e5994bc3c - - platform: linux - create_revision: e308d690a1193ea0d93d62aad6efe48e5994bc3c - base_revision: e308d690a1193ea0d93d62aad6efe48e5994bc3c - - platform: macos - create_revision: e308d690a1193ea0d93d62aad6efe48e5994bc3c - base_revision: e308d690a1193ea0d93d62aad6efe48e5994bc3c - - platform: web - create_revision: e308d690a1193ea0d93d62aad6efe48e5994bc3c - base_revision: e308d690a1193ea0d93d62aad6efe48e5994bc3c - - platform: windows - create_revision: e308d690a1193ea0d93d62aad6efe48e5994bc3c - base_revision: e308d690a1193ea0d93d62aad6efe48e5994bc3c - - # User provided section - - # List of Local paths (relative to this file) that should be - # ignored by the migrate tool. - # - # Files that are not part of the templates will be ignored by default. - unmanaged_files: - - 'lib/main.dart' - - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/examples/fwe/wikipedia_reader/README.md b/examples/fwe/wikipedia_reader/README.md index 2a1fac66791..b8b0a1613e2 100644 --- a/examples/fwe/wikipedia_reader/README.md +++ b/examples/fwe/wikipedia_reader/README.md @@ -1,3 +1,3 @@ # wikipedia_reader -A new Flutter project. +Code excerpts for the Wikipedia reader app in the Flutter tutorial. diff --git a/examples/fwe/wikipedia_reader/pubspec.yaml b/examples/fwe/wikipedia_reader/pubspec.yaml index 829dc7cced0..057d66c0e0c 100644 --- a/examples/fwe/wikipedia_reader/pubspec.yaml +++ b/examples/fwe/wikipedia_reader/pubspec.yaml @@ -1,5 +1,5 @@ name: wikipedia_reader -description: Code excerpts for the Wikipedia reader app in FWE. +description: Code excerpts for the Wikipedia reader app in the Flutter tutorial. publish_to: none environment: diff --git a/examples/fwe/wikipedia_reader/test/widget_test.dart b/examples/fwe/wikipedia_reader/test/widget_test.dart deleted file mode 100644 index e8cb3289cce..00000000000 --- a/examples/fwe/wikipedia_reader/test/widget_test.dart +++ /dev/null @@ -1,13 +0,0 @@ -// ignore_for_file: avoid_relative_lib_imports -import 'package:flutter_test/flutter_test.dart'; -import '../lib/main.dart'; - -void main() { - testWidgets('Wikipedia Reader smoke test', (WidgetTester tester) async { - // Build our app and trigger a frame. - await tester.pumpWidget(const MainApp()); - - // Verify that the title is Wikipedia Flutter. - expect(find.text('Wikipedia Flutter'), findsOneWidget); - }); -} From 9e666f8160e5b2ced87e54af727d2d3a47dd31a0 Mon Sep 17 00:00:00 2001 From: Parker Lougheed Date: Sat, 18 Apr 2026 22:14:24 +0200 Subject: [PATCH 07/19] Remove custom analysis options file --- examples/fwe/wikipedia_reader/analysis_options.yaml | 1 - examples/fwe/wikipedia_reader/lib/step2_main.dart | 2 +- examples/fwe/wikipedia_reader/lib/step3a_main.dart | 2 +- examples/fwe/wikipedia_reader/lib/step4c_main.dart | 4 ++-- src/content/learn/pathway/tutorial/http-requests.md | 2 +- src/content/learn/pathway/tutorial/listenable-builder.md | 4 ++-- 6 files changed, 7 insertions(+), 8 deletions(-) delete mode 100644 examples/fwe/wikipedia_reader/analysis_options.yaml diff --git a/examples/fwe/wikipedia_reader/analysis_options.yaml b/examples/fwe/wikipedia_reader/analysis_options.yaml deleted file mode 100644 index f9b303465f1..00000000000 --- a/examples/fwe/wikipedia_reader/analysis_options.yaml +++ /dev/null @@ -1 +0,0 @@ -include: package:flutter_lints/flutter.yaml diff --git a/examples/fwe/wikipedia_reader/lib/step2_main.dart b/examples/fwe/wikipedia_reader/lib/step2_main.dart index 8ffa84c3038..1082639f908 100644 --- a/examples/fwe/wikipedia_reader/lib/step2_main.dart +++ b/examples/fwe/wikipedia_reader/lib/step2_main.dart @@ -36,7 +36,7 @@ class ArticleModel { final response = await get(uri); if (response.statusCode != 200) { - throw HttpException('Failed to update resource'); + throw const HttpException('Failed to update resource'); } return Summary.fromJson(jsonDecode(response.body) as Map); diff --git a/examples/fwe/wikipedia_reader/lib/step3a_main.dart b/examples/fwe/wikipedia_reader/lib/step3a_main.dart index b25e824c855..5bfa64f8155 100644 --- a/examples/fwe/wikipedia_reader/lib/step3a_main.dart +++ b/examples/fwe/wikipedia_reader/lib/step3a_main.dart @@ -17,7 +17,7 @@ class MainApp extends StatelessWidget { @override Widget build(BuildContext context) { - return MaterialApp( + return const MaterialApp( home: Scaffold(body: Center(child: Text('Loading...'))), ); } diff --git a/examples/fwe/wikipedia_reader/lib/step4c_main.dart b/examples/fwe/wikipedia_reader/lib/step4c_main.dart index c61c9829aaa..be7dce9aa82 100644 --- a/examples/fwe/wikipedia_reader/lib/step4c_main.dart +++ b/examples/fwe/wikipedia_reader/lib/step4c_main.dart @@ -16,9 +16,9 @@ class ArticlePage extends StatelessWidget { @override Widget build(BuildContext context) { - return SingleChildScrollView( + return const SingleChildScrollView( child: Column( - children: [const Text('Article content will be displayed here')], + children: [Text('Article content will be displayed here')], ), ); } diff --git a/src/content/learn/pathway/tutorial/http-requests.md b/src/content/learn/pathway/tutorial/http-requests.md index bdd3023562b..9856fdf2133 100644 --- a/src/content/learn/pathway/tutorial/http-requests.md +++ b/src/content/learn/pathway/tutorial/http-requests.md @@ -139,7 +139,7 @@ class ArticleModel { final response = await get(uri); if (response.statusCode != 200) { - throw HttpException('Failed to update resource'); + throw const HttpException('Failed to update resource'); } return Summary.fromJson(jsonDecode(response.body) as Map); diff --git a/src/content/learn/pathway/tutorial/listenable-builder.md b/src/content/learn/pathway/tutorial/listenable-builder.md index 6c702c02200..305d8550b0e 100644 --- a/src/content/learn/pathway/tutorial/listenable-builder.md +++ b/src/content/learn/pathway/tutorial/listenable-builder.md @@ -155,9 +155,9 @@ class ArticlePage extends StatelessWidget { @override Widget build(BuildContext context) { - return SingleChildScrollView( + return const SingleChildScrollView( child: Column( - children: [const Text('Article content will be displayed here')], + children: [Text('Article content will be displayed here')], ), ); } From 016003cdd1d5ab6ad4cf6bf65b775557eafde178 Mon Sep 17 00:00:00 2001 From: Parker Lougheed Date: Sat, 18 Apr 2026 22:20:22 +0200 Subject: [PATCH 08/19] Add back pre-existing and some improved API doc comments --- .../fwe/wikipedia_reader/lib/step4c_main.dart | 4 +-- .../fwe/wikipedia_reader/lib/summary.dart | 26 +++++++++--------- .../learn/pathway/tutorial/change-notifier.md | 2 +- .../pathway/tutorial/listenable-builder.md | 4 +-- .../pathway/tutorial/set-up-state-project.md | 27 ++++++++++--------- 5 files changed, 29 insertions(+), 34 deletions(-) diff --git a/examples/fwe/wikipedia_reader/lib/step4c_main.dart b/examples/fwe/wikipedia_reader/lib/step4c_main.dart index be7dce9aa82..440ef060031 100644 --- a/examples/fwe/wikipedia_reader/lib/step4c_main.dart +++ b/examples/fwe/wikipedia_reader/lib/step4c_main.dart @@ -17,9 +17,7 @@ class ArticlePage extends StatelessWidget { @override Widget build(BuildContext context) { return const SingleChildScrollView( - child: Column( - children: [Text('Article content will be displayed here')], - ), + child: Column(children: [Text('Article content will be displayed here')]), ); } } diff --git a/examples/fwe/wikipedia_reader/lib/summary.dart b/examples/fwe/wikipedia_reader/lib/summary.dart index c7c6699b08e..309b44c0f51 100644 --- a/examples/fwe/wikipedia_reader/lib/summary.dart +++ b/examples/fwe/wikipedia_reader/lib/summary.dart @@ -1,9 +1,7 @@ // ignore_for_file: directives_ordering // #docregion All - -// Representation of the JSON data returned by the Wikipedia API. -// #docregion Summary +/// Representation of the JSON data returned by the Wikipedia API. class Summary { /// Returns a new [Summary] instance. Summary({ @@ -19,36 +17,37 @@ class Summary { required this.url, }); - /// Titles + /// The title information of this article. final TitlesSet titles; - /// Page ID + /// The page ID of this article. final int pageid; - /// Extract + /// The first few sentences of the article in plain text. final String extract; - /// Extract HTML + /// The first few sentences of the article in HTML format. final String extractHtml; - /// Thumbnail image + /// A thumbnail-sized version of the article's primary image, if available. final ImageFile? thumbnail; - /// Original image + /// The original full-sized article's primary image, if available. final ImageFile? originalImage; - /// Language + /// The language code of the article's content, such as "en" for English. final String lang; - /// Directionality + /// The text directionality of the article's content, such as "ltr" or "rtl". final String dir; - /// Description + /// A description of the article, if available. final String? description; - /// URL + /// The URL of the page. final String url; + /// Whether this article has an image. bool get hasImage => originalImage != null && thumbnail != null; /// Returns a new [Summary] instance and imports its values from a JSON map @@ -143,7 +142,6 @@ class Summary { 'description=$description' ']'; } -// #enddocregion Summary // Image path and size, but doesn't contain any Wikipedia descriptions. // #docregion ImageFile diff --git a/src/content/learn/pathway/tutorial/change-notifier.md b/src/content/learn/pathway/tutorial/change-notifier.md index 306d04f8425..8578d186237 100644 --- a/src/content/learn/pathway/tutorial/change-notifier.md +++ b/src/content/learn/pathway/tutorial/change-notifier.md @@ -89,7 +89,7 @@ it delegates initial content fetching to a separate method. ### Set up the `fetchArticle` method -Add the `fetchArticle` that fetches data and manages state updates: +Add the `fetchArticle` method that fetches data and manages state updates: ```dart diff --git a/src/content/learn/pathway/tutorial/listenable-builder.md b/src/content/learn/pathway/tutorial/listenable-builder.md index 305d8550b0e..9cad6e396be 100644 --- a/src/content/learn/pathway/tutorial/listenable-builder.md +++ b/src/content/learn/pathway/tutorial/listenable-builder.md @@ -156,9 +156,7 @@ class ArticlePage extends StatelessWidget { @override Widget build(BuildContext context) { return const SingleChildScrollView( - child: Column( - children: [Text('Article content will be displayed here')], - ), + child: Column(children: [Text('Article content will be displayed here')]), ); } } diff --git a/src/content/learn/pathway/tutorial/set-up-state-project.md b/src/content/learn/pathway/tutorial/set-up-state-project.md index 1670339f7f7..bfce1bc7806 100644 --- a/src/content/learn/pathway/tutorial/set-up-state-project.md +++ b/src/content/learn/pathway/tutorial/set-up-state-project.md @@ -86,12 +86,12 @@ $ cd wikipedia_reader && flutter pub add http First, create a new file `lib/summary.dart` to define the data model for Wikipedia article summaries. This file has no special logic, and is simply a collection of classes that represent the data returned by the -Wikipedia API. Its sufficient to copy the code below into the file and then ignore it. If you aren't comfortable basic Dart classes, you should read the [Dart Getting Started][] tutorial first. +Wikipedia API. It's sufficient to copy the code below into the file and then ignore it. +If you aren't comfortable with basic Dart classes, you should read the [Dart Getting Started][] tutorial first. ```dart title="lib/summary.dart" collapsed - -// Representation of the JSON data returned by the Wikipedia API. +/// Representation of the JSON data returned by the Wikipedia API. class Summary { /// Returns a new [Summary] instance. Summary({ @@ -107,36 +107,37 @@ class Summary { required this.url, }); - /// Titles + /// The title information of this article. final TitlesSet titles; - /// Page ID + /// The page ID of this article. final int pageid; - /// Extract + /// The first few sentences of the article in plain text. final String extract; - /// Extract HTML + /// The first few sentences of the article in HTML format. final String extractHtml; - /// Thumbnail image + /// A thumbnail-sized version of the article's primary image, if available. final ImageFile? thumbnail; - /// Original image + /// The original full-sized article's primary image, if available. final ImageFile? originalImage; - /// Language + /// The language code of the article's content, such as "en" for English. final String lang; - /// Directionality + /// The text directionality of the article's content, such as "ltr" or "rtl". final String dir; - /// Description + /// A description of the article, if available. final String? description; - /// URL + /// The URL of the page. final String url; + /// Whether this article has an image. bool get hasImage => originalImage != null && thumbnail != null; /// Returns a new [Summary] instance and imports its values from a JSON map From a0ef0f1cd5d98655a64eab1d0ede4d100541c01d Mon Sep 17 00:00:00 2001 From: Parker Lougheed Date: Sat, 18 Apr 2026 22:22:42 +0200 Subject: [PATCH 09/19] Remove unnecessary dependency --- examples/fwe/rolodex/pubspec.yaml | 1 - examples/fwe/wikipedia_reader/pubspec.yaml | 1 - 2 files changed, 2 deletions(-) diff --git a/examples/fwe/rolodex/pubspec.yaml b/examples/fwe/rolodex/pubspec.yaml index 5385f42eded..b6885636504 100644 --- a/examples/fwe/rolodex/pubspec.yaml +++ b/examples/fwe/rolodex/pubspec.yaml @@ -14,7 +14,6 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - flutter_lints: ^3.0.0 flutter: uses-material-design: true diff --git a/examples/fwe/wikipedia_reader/pubspec.yaml b/examples/fwe/wikipedia_reader/pubspec.yaml index 057d66c0e0c..acd1eb156c7 100644 --- a/examples/fwe/wikipedia_reader/pubspec.yaml +++ b/examples/fwe/wikipedia_reader/pubspec.yaml @@ -13,7 +13,6 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - flutter_lints: ^6.0.0 flutter: uses-material-design: true From 07d0d71eb6a3202f83da61fee75fe0c7cc85b4a7 Mon Sep 17 00:00:00 2001 From: Parker Lougheed Date: Sat, 18 Apr 2026 22:31:06 +0200 Subject: [PATCH 10/19] Add back usage of spacing argument as it does exist --- .../learn/pathway/tutorial/listenable-builder.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/content/learn/pathway/tutorial/listenable-builder.md b/src/content/learn/pathway/tutorial/listenable-builder.md index 9cad6e396be..4d972d24e69 100644 --- a/src/content/learn/pathway/tutorial/listenable-builder.md +++ b/src/content/learn/pathway/tutorial/listenable-builder.md @@ -377,7 +377,7 @@ class ArticleWidget extends StatelessWidget { return Padding( padding: const EdgeInsets.all(8.0), child: Column( - + spacing: 10, children: [ if (summary.hasImage) Image.network( @@ -407,7 +407,7 @@ class ArticleWidget extends StatelessWidget { return Padding( padding: const EdgeInsets.all(8.0), child: Column( - + spacing: 10, children: [ if (summary.hasImage) Image.network( @@ -440,8 +440,8 @@ This widget demonstrates a few important UI concepts: The `if` statements show content only when available. - **Text styling**: Different text styles create visual hierarchy using Flutter's theme system. - - +- **Proper spacing**: + The `spacing` parameter provides consistent vertical spacing. - **Overflow handling**: `TextOverflow.ellipsis` prevents text from breaking the layout. @@ -486,7 +486,7 @@ items: details: >- You created `ArticleView`, `ArticlePage`, and `ArticleWidget` with conditional rendering, text styling, - and overflow handling. + proper spacing, and overflow handling. These are core UI patterns you'll use in every Flutter app. - title: Completed the MVVM architecture icon: celebration From a6208617d544996f55f949af0afae969575ab194 Mon Sep 17 00:00:00 2001 From: Parker Lougheed Date: Sat, 18 Apr 2026 22:53:28 +0200 Subject: [PATCH 11/19] Switch back to stateless widget to keep PR simpler --- .../fwe/wikipedia_reader/lib/step4_main.dart | 31 ++++-------- .../fwe/wikipedia_reader/lib/step4a_main.dart | 13 ++--- .../fwe/wikipedia_reader/lib/step4b_main.dart | 20 ++------ .../fwe/wikipedia_reader/lib/step4c_main.dart | 4 +- .../fwe/wikipedia_reader/lib/step4d_main.dart | 2 + .../fwe/wikipedia_reader/lib/step4e_main.dart | 2 +- .../fwe/wikipedia_reader/lib/step4f_main.dart | 6 +-- .../fwe/wikipedia_reader/lib/step4g_main.dart | 2 +- .../fwe/wikipedia_reader/lib/step4h_main.dart | 4 +- .../pathway/tutorial/listenable-builder.md | 49 ++++++------------- 10 files changed, 45 insertions(+), 88 deletions(-) diff --git a/examples/fwe/wikipedia_reader/lib/step4_main.dart b/examples/fwe/wikipedia_reader/lib/step4_main.dart index 4ed212ac64a..97cd566e0ac 100644 --- a/examples/fwe/wikipedia_reader/lib/step4_main.dart +++ b/examples/fwe/wikipedia_reader/lib/step4_main.dart @@ -18,7 +18,7 @@ class MainApp extends StatelessWidget { @override Widget build(BuildContext context) { - return const MaterialApp(home: ArticleView()); + return MaterialApp(home: ArticleView()); } } // #enddocregion MainApp @@ -40,14 +40,14 @@ class ArticleModel { } class ArticleViewModel extends ChangeNotifier { - ArticleViewModel(this.model); final ArticleModel model; - Summary? summary; - + Exception? error; bool isLoading = false; - Exception? error; + ArticleViewModel(this.model) { + fetchArticle(); + } Future fetchArticle() async { isLoading = true; @@ -56,8 +56,7 @@ class ArticleViewModel extends ChangeNotifier { try { summary = await model.getRandomArticleSummary(); - error = null; - } on Exception catch (e) { + } on HttpException catch (e) { error = e; } finally { isLoading = false; @@ -66,22 +65,10 @@ class ArticleViewModel extends ChangeNotifier { } } -class ArticleView extends StatefulWidget { - const ArticleView({super.key}); +class ArticleView extends StatelessWidget { + ArticleView({super.key}); - @override - State createState() => _ArticleViewState(); -} - -class _ArticleViewState extends State { - late final ArticleViewModel viewModel; - - @override - void initState() { - super.initState(); - viewModel = ArticleViewModel(ArticleModel()); - viewModel.fetchArticle(); - } + final ArticleViewModel viewModel = ArticleViewModel(ArticleModel()); @override Widget build(BuildContext context) { diff --git a/examples/fwe/wikipedia_reader/lib/step4a_main.dart b/examples/fwe/wikipedia_reader/lib/step4a_main.dart index 2d08e714e98..beb5d8c50c1 100644 --- a/examples/fwe/wikipedia_reader/lib/step4a_main.dart +++ b/examples/fwe/wikipedia_reader/lib/step4a_main.dart @@ -1,17 +1,12 @@ -// ignore_for_file: unused_import +// ignore_for_file: prefer_const_constructors_in_immutables, unused_import import 'package:flutter/material.dart'; // #docregion ArticleView -class ArticleView extends StatefulWidget { - const ArticleView({super.key}); +class ArticleView extends StatelessWidget { + ArticleView({super.key}); - @override - State createState() => _ArticleViewState(); -} - -class _ArticleViewState extends State { - // viewModel will be instantiated next + // The view model will be instantiated here next. @override Widget build(BuildContext context) { diff --git a/examples/fwe/wikipedia_reader/lib/step4b_main.dart b/examples/fwe/wikipedia_reader/lib/step4b_main.dart index 0862a870ce7..8fe5164c134 100644 --- a/examples/fwe/wikipedia_reader/lib/step4b_main.dart +++ b/examples/fwe/wikipedia_reader/lib/step4b_main.dart @@ -14,22 +14,10 @@ class ArticleViewModel extends ChangeNotifier { class ArticleModel {} // #docregion ArticleView -class ArticleView extends StatefulWidget { - const ArticleView({super.key}); +class ArticleView extends StatelessWidget { + ArticleView({super.key}); - @override - State createState() => _ArticleViewState(); -} - -class _ArticleViewState extends State { - late final ArticleViewModel viewModel; - - @override - void initState() { - super.initState(); - viewModel = ArticleViewModel(ArticleModel()); - viewModel.fetchArticle(); - } + final ArticleViewModel viewModel = ArticleViewModel(ArticleModel()); @override Widget build(BuildContext context) { @@ -66,8 +54,10 @@ class ArticlePage extends StatelessWidget { required this.summary, required this.nextArticleCallback, }); + final Summary summary; final VoidCallback nextArticleCallback; + @override Widget build(BuildContext context) => const Placeholder(); } diff --git a/examples/fwe/wikipedia_reader/lib/step4c_main.dart b/examples/fwe/wikipedia_reader/lib/step4c_main.dart index 440ef060031..8534882ddba 100644 --- a/examples/fwe/wikipedia_reader/lib/step4c_main.dart +++ b/examples/fwe/wikipedia_reader/lib/step4c_main.dart @@ -17,7 +17,9 @@ class ArticlePage extends StatelessWidget { @override Widget build(BuildContext context) { return const SingleChildScrollView( - child: Column(children: [Text('Article content will be displayed here')]), + child: Column( + children: [Text('Article content will be displayed here...')], + ), ); } } diff --git a/examples/fwe/wikipedia_reader/lib/step4d_main.dart b/examples/fwe/wikipedia_reader/lib/step4d_main.dart index abc61fbffe1..388f1e95219 100644 --- a/examples/fwe/wikipedia_reader/lib/step4d_main.dart +++ b/examples/fwe/wikipedia_reader/lib/step4d_main.dart @@ -5,7 +5,9 @@ import 'summary.dart'; class ArticleWidget extends StatelessWidget { const ArticleWidget({super.key, required this.summary}); + final Summary summary; + @override Widget build(BuildContext context) => const Placeholder(); } diff --git a/examples/fwe/wikipedia_reader/lib/step4e_main.dart b/examples/fwe/wikipedia_reader/lib/step4e_main.dart index db908d2b68d..e0eec70c68f 100644 --- a/examples/fwe/wikipedia_reader/lib/step4e_main.dart +++ b/examples/fwe/wikipedia_reader/lib/step4e_main.dart @@ -11,7 +11,7 @@ class ArticleWidget extends StatelessWidget { @override Widget build(BuildContext context) { - return const Text('Article content will be displayed here'); + return const Text('Article content will be displayed here...'); } } diff --git a/examples/fwe/wikipedia_reader/lib/step4f_main.dart b/examples/fwe/wikipedia_reader/lib/step4f_main.dart index 5d22ba9884a..1dd77e9a6c4 100644 --- a/examples/fwe/wikipedia_reader/lib/step4f_main.dart +++ b/examples/fwe/wikipedia_reader/lib/step4f_main.dart @@ -11,10 +11,10 @@ class ArticleWidget extends StatelessWidget { @override Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.all(8.0), + return const Padding( + padding: EdgeInsets.all(8.0), child: Column( - children: [const Text('Article content will be displayed here')], + children: [Text('Article content will be displayed here...')], ), ); } diff --git a/examples/fwe/wikipedia_reader/lib/step4g_main.dart b/examples/fwe/wikipedia_reader/lib/step4g_main.dart index 63acce0bf5b..bcdc73fb05f 100644 --- a/examples/fwe/wikipedia_reader/lib/step4g_main.dart +++ b/examples/fwe/wikipedia_reader/lib/step4g_main.dart @@ -14,10 +14,10 @@ class ArticleWidget extends StatelessWidget { return Padding( padding: const EdgeInsets.all(8.0), child: Column( + spacing: 10, children: [ if (summary.hasImage) ...[ Image.network(summary.originalImage!.source), - const SizedBox(height: 10.0), ], const Text('Article content will be displayed here'), ], diff --git a/examples/fwe/wikipedia_reader/lib/step4h_main.dart b/examples/fwe/wikipedia_reader/lib/step4h_main.dart index b61f400f241..d064f4ba6a5 100644 --- a/examples/fwe/wikipedia_reader/lib/step4h_main.dart +++ b/examples/fwe/wikipedia_reader/lib/step4h_main.dart @@ -14,24 +14,22 @@ class ArticleWidget extends StatelessWidget { return Padding( padding: const EdgeInsets.all(8.0), child: Column( + spacing: 10, children: [ if (summary.hasImage) ...[ Image.network(summary.originalImage!.source), - const SizedBox(height: 10.0), ], Text( summary.titles.normalized, overflow: TextOverflow.ellipsis, style: Theme.of(context).textTheme.displaySmall, ), - const SizedBox(height: 10.0), if (summary.description != null) ...[ Text( summary.description!, overflow: TextOverflow.ellipsis, style: Theme.of(context).textTheme.bodySmall, ), - const SizedBox(height: 10.0), ], Text(summary.extract), ], diff --git a/src/content/learn/pathway/tutorial/listenable-builder.md b/src/content/learn/pathway/tutorial/listenable-builder.md index 4d972d24e69..62f0fa1fa53 100644 --- a/src/content/learn/pathway/tutorial/listenable-builder.md +++ b/src/content/learn/pathway/tutorial/listenable-builder.md @@ -40,15 +40,10 @@ Start with the basic class structure and widgets: ```dart -class ArticleView extends StatefulWidget { - const ArticleView({super.key}); +class ArticleView extends StatelessWidget { + ArticleView({super.key}); - @override - State createState() => _ArticleViewState(); -} - -class _ArticleViewState extends State { - // viewModel will be instantiated next + // The view model will be instantiated here next. @override Widget build(BuildContext context) { @@ -66,22 +61,10 @@ Create the `ArticleViewModel` in this widget: ```dart -class ArticleView extends StatefulWidget { - const ArticleView({super.key}); - - @override - State createState() => _ArticleViewState(); -} +class ArticleView extends StatelessWidget { + ArticleView({super.key}); -class _ArticleViewState extends State { - late final ArticleViewModel viewModel; - - @override - void initState() { - super.initState(); - viewModel = ArticleViewModel(ArticleModel()); - viewModel.fetchArticle(); - } + final ArticleViewModel viewModel = ArticleViewModel(ArticleModel()); @override Widget build(BuildContext context) { @@ -156,7 +139,9 @@ class ArticlePage extends StatelessWidget { @override Widget build(BuildContext context) { return const SingleChildScrollView( - child: Column(children: [Text('Article content will be displayed here')]), + child: Column( + children: [Text('Article content will be displayed here...')], + ), ); } } @@ -241,7 +226,7 @@ class ArticleWidget extends StatelessWidget { @override Widget build(BuildContext context) { - return const Text('Article content will be displayed here'); + return const Text('Article content will be displayed here...'); } } ``` @@ -259,10 +244,10 @@ class ArticleWidget extends StatelessWidget { @override Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.all(8.0), + return const Padding( + padding: EdgeInsets.all(8.0), child: Column( - children: [const Text('Article content will be displayed here')], + children: [Text('Article content will be displayed here...')], ), ); } @@ -285,10 +270,10 @@ class ArticleWidget extends StatelessWidget { return Padding( padding: const EdgeInsets.all(8.0), child: Column( + spacing: 10, children: [ if (summary.hasImage) ...[ Image.network(summary.originalImage!.source), - const SizedBox(height: 10.0), ], const Text('Article content will be displayed here'), ], @@ -319,24 +304,22 @@ class ArticleWidget extends StatelessWidget { return Padding( padding: const EdgeInsets.all(8.0), child: Column( + spacing: 10, children: [ if (summary.hasImage) ...[ Image.network(summary.originalImage!.source), - const SizedBox(height: 10.0), ], Text( summary.titles.normalized, overflow: TextOverflow.ellipsis, style: Theme.of(context).textTheme.displaySmall, ), - const SizedBox(height: 10.0), if (summary.description != null) ...[ Text( summary.description!, overflow: TextOverflow.ellipsis, style: Theme.of(context).textTheme.bodySmall, ), - const SizedBox(height: 10.0), ], Text(summary.extract), ], @@ -357,7 +340,7 @@ class MainApp extends StatelessWidget { @override Widget build(BuildContext context) { - return const MaterialApp(home: ArticleView()); + return MaterialApp(home: ArticleView()); } } ``` From 095aa0af9e4909648759e3ad742cceca0bae2145 Mon Sep 17 00:00:00 2001 From: Parker Lougheed Date: Sat, 18 Apr 2026 23:58:28 +0200 Subject: [PATCH 12/19] Keep listenable-builder changes to excerpts for easier reviewing --- examples/fwe/rolodex/pubspec.yaml | 1 + .../fwe/wikipedia_reader/lib/step4_main.dart | 29 +-- .../fwe/wikipedia_reader/lib/step4a_main.dart | 7 +- .../fwe/wikipedia_reader/lib/step4b_main.dart | 40 +-- .../fwe/wikipedia_reader/lib/step4c_main.dart | 37 +-- .../fwe/wikipedia_reader/lib/step4d_main.dart | 28 +-- .../fwe/wikipedia_reader/lib/step4e_main.dart | 22 +- .../fwe/wikipedia_reader/lib/step4f_main.dart | 14 +- .../fwe/wikipedia_reader/lib/step4g_main.dart | 16 +- .../fwe/wikipedia_reader/lib/step4h_main.dart | 27 +- examples/fwe/wikipedia_reader/pubspec.yaml | 1 + .../pathway/tutorial/listenable-builder.md | 234 ++++++++---------- 12 files changed, 188 insertions(+), 268 deletions(-) diff --git a/examples/fwe/rolodex/pubspec.yaml b/examples/fwe/rolodex/pubspec.yaml index b6885636504..23fb072d3b7 100644 --- a/examples/fwe/rolodex/pubspec.yaml +++ b/examples/fwe/rolodex/pubspec.yaml @@ -14,6 +14,7 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter + flutter_lints: ^6.0.0 flutter: uses-material-design: true diff --git a/examples/fwe/wikipedia_reader/lib/step4_main.dart b/examples/fwe/wikipedia_reader/lib/step4_main.dart index 97cd566e0ac..d44066b08d0 100644 --- a/examples/fwe/wikipedia_reader/lib/step4_main.dart +++ b/examples/fwe/wikipedia_reader/lib/step4_main.dart @@ -1,5 +1,3 @@ -// ignore_for_file: avoid_dynamic_calls - import 'dart:convert'; import 'dart:io'; @@ -12,7 +10,7 @@ void main() { runApp(const MainApp()); } -// #docregion MainApp +// #docregion main-app class MainApp extends StatelessWidget { const MainApp({super.key}); @@ -21,7 +19,7 @@ class MainApp extends StatelessWidget { return MaterialApp(home: ArticleView()); } } -// #enddocregion MainApp +// #enddocregion main-app class ArticleModel { Future getRandomArticleSummary() async { @@ -65,6 +63,7 @@ class ArticleViewModel extends ChangeNotifier { } } +// #docregion view-model class ArticleView extends StatelessWidget { ArticleView({super.key}); @@ -84,11 +83,12 @@ class ArticleView extends StatelessWidget { viewModel.error, )) { (true, _, _) => const CircularProgressIndicator(), + (_, _, final Exception e) => Text('Error: $e'), + // The summary must be non-null in this switch case. (_, final summary?, _) => ArticlePage( summary: summary, nextArticleCallback: viewModel.fetchArticle, ), - (_, _, final Exception e) => Text('Error: $e'), _ => const Text('Something went wrong!'), }; }, @@ -97,7 +97,9 @@ class ArticleView extends StatelessWidget { ); } } +// #enddocregion view-model +// #docregion page class ArticlePage extends StatelessWidget { const ArticlePage({ super.key, @@ -123,7 +125,9 @@ class ArticlePage extends StatelessWidget { ); } } +// #enddocregion page +// #docregion article class ArticleWidget extends StatelessWidget { const ArticleWidget({super.key, required this.summary}); @@ -132,30 +136,27 @@ class ArticleWidget extends StatelessWidget { @override Widget build(BuildContext context) { return Padding( - padding: const EdgeInsets.all(8.0), + padding: const EdgeInsets.all(8), child: Column( + spacing: 10, children: [ - if (summary.hasImage) ...[ - Image.network(summary.originalImage!.source), - const SizedBox(height: 10.0), - ], + if (summary.hasImage) Image.network(summary.originalImage!.source), Text( summary.titles.normalized, overflow: TextOverflow.ellipsis, style: Theme.of(context).textTheme.displaySmall, ), - const SizedBox(height: 10.0), - if (summary.description != null) ...[ + if (summary.description != null) Text( summary.description!, overflow: TextOverflow.ellipsis, style: Theme.of(context).textTheme.bodySmall, ), - const SizedBox(height: 10.0), - ], Text(summary.extract), ], ), ); } } + +// #enddocregion article diff --git a/examples/fwe/wikipedia_reader/lib/step4a_main.dart b/examples/fwe/wikipedia_reader/lib/step4a_main.dart index beb5d8c50c1..e31588eed84 100644 --- a/examples/fwe/wikipedia_reader/lib/step4a_main.dart +++ b/examples/fwe/wikipedia_reader/lib/step4a_main.dart @@ -1,10 +1,7 @@ -// ignore_for_file: prefer_const_constructors_in_immutables, unused_import - import 'package:flutter/material.dart'; -// #docregion ArticleView class ArticleView extends StatelessWidget { - ArticleView({super.key}); + const ArticleView({super.key}); // The view model will be instantiated here next. @@ -16,5 +13,3 @@ class ArticleView extends StatelessWidget { ); } } - -// #enddocregion ArticleView diff --git a/examples/fwe/wikipedia_reader/lib/step4b_main.dart b/examples/fwe/wikipedia_reader/lib/step4b_main.dart index 8fe5164c134..ea9d6f8a2f4 100644 --- a/examples/fwe/wikipedia_reader/lib/step4b_main.dart +++ b/examples/fwe/wikipedia_reader/lib/step4b_main.dart @@ -1,5 +1,3 @@ -// ignore_for_file: unused_import - import 'package:flutter/material.dart'; import 'summary.dart'; @@ -13,7 +11,7 @@ class ArticleViewModel extends ChangeNotifier { class ArticleModel {} -// #docregion ArticleView +// #docregion view-model class ArticleView extends StatelessWidget { ArticleView({super.key}); @@ -23,41 +21,9 @@ class ArticleView extends StatelessWidget { Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Wikipedia Flutter')), - body: Center( - child: ListenableBuilder( - listenable: viewModel, - builder: (context, _) { - return switch (( - viewModel.isLoading, - viewModel.summary, - viewModel.error, - )) { - (true, _, _) => const CircularProgressIndicator(), - (_, final summary?, _) => ArticlePage( - summary: summary, - nextArticleCallback: viewModel.fetchArticle, - ), - (_, _, final Exception e) => Text('Error: $e'), - _ => const Text('Something went wrong!'), - }; - }, - ), - ), + body: const Center(child: Text('Loading...')), ); } } -// #enddocregion ArticleView - -class ArticlePage extends StatelessWidget { - const ArticlePage({ - super.key, - required this.summary, - required this.nextArticleCallback, - }); - - final Summary summary; - final VoidCallback nextArticleCallback; - @override - Widget build(BuildContext context) => const Placeholder(); -} +// #enddocregion view-model diff --git a/examples/fwe/wikipedia_reader/lib/step4c_main.dart b/examples/fwe/wikipedia_reader/lib/step4c_main.dart index 8534882ddba..d6ebdd5cb0c 100644 --- a/examples/fwe/wikipedia_reader/lib/step4c_main.dart +++ b/examples/fwe/wikipedia_reader/lib/step4c_main.dart @@ -1,27 +1,34 @@ -// ignore_for_file: unused_import - import 'package:flutter/material.dart'; import 'summary.dart'; -// #docregion ArticlePage -class ArticlePage extends StatelessWidget { - const ArticlePage({ - super.key, - required this.summary, - required this.nextArticleCallback, - }); +class ArticleViewModel extends ChangeNotifier { + ArticleViewModel(ArticleModel model); + bool get isLoading => false; + Summary? get summary => null; + Exception? get error => null; + Future fetchArticle() async {} +} + +class ArticleModel {} + +// #docregion view-model +class ArticleView extends StatelessWidget { + ArticleView({super.key}); - final Summary summary; - final VoidCallback nextArticleCallback; + final ArticleViewModel viewModel = ArticleViewModel(ArticleModel()); @override Widget build(BuildContext context) { - return const SingleChildScrollView( - child: Column( - children: [Text('Article content will be displayed here...')], + return Scaffold( + appBar: AppBar(title: const Text('Wikipedia Flutter')), + body: ListenableBuilder( + listenable: viewModel, + builder: (context, child) { + return const Center(child: Text('Loading...')); + }, ), ); } } -// #enddocregion ArticlePage +// #enddocregion view-model diff --git a/examples/fwe/wikipedia_reader/lib/step4d_main.dart b/examples/fwe/wikipedia_reader/lib/step4d_main.dart index 388f1e95219..70d2916ef7a 100644 --- a/examples/fwe/wikipedia_reader/lib/step4d_main.dart +++ b/examples/fwe/wikipedia_reader/lib/step4d_main.dart @@ -1,18 +1,8 @@ -// ignore_for_file: unused_import, prefer_const_constructors, prefer_const_literals_to_create_immutables - import 'package:flutter/material.dart'; -import 'summary.dart'; - -class ArticleWidget extends StatelessWidget { - const ArticleWidget({super.key, required this.summary}); - - final Summary summary; - @override - Widget build(BuildContext context) => const Placeholder(); -} +import 'summary.dart'; -// #docregion ArticlePage +// #docregion page class ArticlePage extends StatelessWidget { const ArticlePage({ super.key, @@ -25,18 +15,10 @@ class ArticlePage extends StatelessWidget { @override Widget build(BuildContext context) { - return SingleChildScrollView( - child: Column( - children: [ - ArticleWidget(summary: summary), - ElevatedButton( - onPressed: nextArticleCallback, - child: const Text('Next random article'), - ), - ], - ), + return const Center( + child: Text('Article content will be displayed here...'), ); } } -// #enddocregion ArticlePage +// #enddocregion page diff --git a/examples/fwe/wikipedia_reader/lib/step4e_main.dart b/examples/fwe/wikipedia_reader/lib/step4e_main.dart index e0eec70c68f..51660c62ddb 100644 --- a/examples/fwe/wikipedia_reader/lib/step4e_main.dart +++ b/examples/fwe/wikipedia_reader/lib/step4e_main.dart @@ -1,18 +1,26 @@ -// ignore_for_file: unused_import, prefer_const_constructors, prefer_const_literals_to_create_immutables - import 'package:flutter/material.dart'; + import 'summary.dart'; -// #docregion ArticleWidget -class ArticleWidget extends StatelessWidget { - const ArticleWidget({super.key, required this.summary}); +// #docregion page +class ArticlePage extends StatelessWidget { + const ArticlePage({ + super.key, + required this.summary, + required this.nextArticleCallback, + }); final Summary summary; + final VoidCallback nextArticleCallback; @override Widget build(BuildContext context) { - return const Text('Article content will be displayed here...'); + return const SingleChildScrollView( + child: Column( + children: [Text('Article content will be displayed here...')], + ), + ); } } -// #enddocregion ArticleWidget +// #enddocregion page diff --git a/examples/fwe/wikipedia_reader/lib/step4f_main.dart b/examples/fwe/wikipedia_reader/lib/step4f_main.dart index 1dd77e9a6c4..cf1ffbfe642 100644 --- a/examples/fwe/wikipedia_reader/lib/step4f_main.dart +++ b/examples/fwe/wikipedia_reader/lib/step4f_main.dart @@ -1,9 +1,8 @@ -// ignore_for_file: unused_import, prefer_const_constructors, prefer_const_literals_to_create_immutables - import 'package:flutter/material.dart'; + import 'summary.dart'; -// #docregion ArticleWidget +// #docregion article class ArticleWidget extends StatelessWidget { const ArticleWidget({super.key, required this.summary}); @@ -11,13 +10,8 @@ class ArticleWidget extends StatelessWidget { @override Widget build(BuildContext context) { - return const Padding( - padding: EdgeInsets.all(8.0), - child: Column( - children: [Text('Article content will be displayed here...')], - ), - ); + return const Text('Article content will be displayed here...'); } } -// #enddocregion ArticleWidget +// #enddocregion article diff --git a/examples/fwe/wikipedia_reader/lib/step4g_main.dart b/examples/fwe/wikipedia_reader/lib/step4g_main.dart index bcdc73fb05f..6c1cf1df0ed 100644 --- a/examples/fwe/wikipedia_reader/lib/step4g_main.dart +++ b/examples/fwe/wikipedia_reader/lib/step4g_main.dart @@ -1,9 +1,10 @@ -// ignore_for_file: unused_import, prefer_const_constructors, prefer_const_literals_to_create_immutables +// ignore_for_file: prefer_const_constructors import 'package:flutter/material.dart'; + import 'summary.dart'; -// #docregion ArticleWidget +// #docregion article class ArticleWidget extends StatelessWidget { const ArticleWidget({super.key, required this.summary}); @@ -12,18 +13,13 @@ class ArticleWidget extends StatelessWidget { @override Widget build(BuildContext context) { return Padding( - padding: const EdgeInsets.all(8.0), + padding: const EdgeInsets.all(8), child: Column( spacing: 10, - children: [ - if (summary.hasImage) ...[ - Image.network(summary.originalImage!.source), - ], - const Text('Article content will be displayed here'), - ], + children: [const Text('Article content will be displayed here...')], ), ); } } -// #enddocregion ArticleWidget +// #enddocregion article diff --git a/examples/fwe/wikipedia_reader/lib/step4h_main.dart b/examples/fwe/wikipedia_reader/lib/step4h_main.dart index d064f4ba6a5..259b8151b15 100644 --- a/examples/fwe/wikipedia_reader/lib/step4h_main.dart +++ b/examples/fwe/wikipedia_reader/lib/step4h_main.dart @@ -1,9 +1,8 @@ -// ignore_for_file: unused_import - import 'package:flutter/material.dart'; + import 'summary.dart'; -// #docregion ArticleWidget +// #docregion article class ArticleWidget extends StatelessWidget { const ArticleWidget({super.key, required this.summary}); @@ -12,30 +11,16 @@ class ArticleWidget extends StatelessWidget { @override Widget build(BuildContext context) { return Padding( - padding: const EdgeInsets.all(8.0), + padding: const EdgeInsets.all(8), child: Column( spacing: 10, children: [ - if (summary.hasImage) ...[ - Image.network(summary.originalImage!.source), - ], - Text( - summary.titles.normalized, - overflow: TextOverflow.ellipsis, - style: Theme.of(context).textTheme.displaySmall, - ), - if (summary.description != null) ...[ - Text( - summary.description!, - overflow: TextOverflow.ellipsis, - style: Theme.of(context).textTheme.bodySmall, - ), - ], - Text(summary.extract), + if (summary.hasImage) Image.network(summary.originalImage!.source), + const Text('Article content will be displayed here...'), ], ), ); } } -// #enddocregion ArticleWidget +// #enddocregion article diff --git a/examples/fwe/wikipedia_reader/pubspec.yaml b/examples/fwe/wikipedia_reader/pubspec.yaml index acd1eb156c7..057d66c0e0c 100644 --- a/examples/fwe/wikipedia_reader/pubspec.yaml +++ b/examples/fwe/wikipedia_reader/pubspec.yaml @@ -13,6 +13,7 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter + flutter_lints: ^6.0.0 flutter: uses-material-design: true diff --git a/src/content/learn/pathway/tutorial/listenable-builder.md b/src/content/learn/pathway/tutorial/listenable-builder.md index 62f0fa1fa53..0e055fbaad6 100644 --- a/src/content/learn/pathway/tutorial/listenable-builder.md +++ b/src/content/learn/pathway/tutorial/listenable-builder.md @@ -4,7 +4,8 @@ description: Instructions on how to manage state with ChangeNotifiers. layout: tutorial --- -Learn to use ListenableBuilder to automatically rebuild UI and handle all possible states with switch expressions. +Learn to use ListenableBuilder to automatically rebuild UI and +handle all possible states with switch expressions. title: What you'll accomplish @@ -38,10 +39,12 @@ Create the `ArticleView` widget that manages the overall page layout and state handling. Start with the basic class structure and widgets: - + ```dart +import 'package:flutter/material.dart'; + class ArticleView extends StatelessWidget { - ArticleView({super.key}); + const ArticleView({super.key}); // The view model will be instantiated here next. @@ -59,8 +62,8 @@ class ArticleView extends StatelessWidget { Create the `ArticleViewModel` in this widget: - -```dart + +```dart highlightLines=4 class ArticleView extends StatelessWidget { ArticleView({super.key}); @@ -70,26 +73,7 @@ class ArticleView extends StatelessWidget { Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Wikipedia Flutter')), - body: Center( - child: ListenableBuilder( - listenable: viewModel, - builder: (context, _) { - return switch (( - viewModel.isLoading, - viewModel.summary, - viewModel.error, - )) { - (true, _, _) => const CircularProgressIndicator(), - (_, final summary?, _) => ArticlePage( - summary: summary, - nextArticleCallback: viewModel.fetchArticle, - ), - (_, _, final Exception e) => Text('Error: $e'), - _ => const Text('Something went wrong!'), - }; - }, - ), - ), + body: const Center(child: Text('Loading...')), ); } } @@ -102,15 +86,14 @@ include your completed `ArticleView`. Replace your existing `MainApp` with this updated version: + ```dart class MainApp extends StatelessWidget { const MainApp({super.key}); @override Widget build(BuildContext context) { - return MaterialApp( - home: ArticleView(), - ); + return MaterialApp(home: ArticleView()); } } ``` @@ -124,23 +107,22 @@ Wrap your UI in a [`ListenableBuilder`][] to listen for state changes, and pass it a `ChangeNotifier` object. In this case, the `ArticleViewModel` extends `ChangeNotifier`. - -```dart -class ArticlePage extends StatelessWidget { - const ArticlePage({ - super.key, - required this.summary, - required this.nextArticleCallback, - }); + +```dart highlightLines=12-17 +class ArticleView extends StatelessWidget { + ArticleView({super.key}); - final Summary summary; - final VoidCallback nextArticleCallback; + final ArticleViewModel viewModel = ArticleViewModel(ArticleModel()); @override Widget build(BuildContext context) { - return const SingleChildScrollView( - child: Column( - children: [Text('Article content will be displayed here...')], + return Scaffold( + appBar: AppBar(title: const Text('Wikipedia Flutter')), + body: ListenableBuilder( + listenable: viewModel, + builder: (context, child) { + return const Center(child: Text('Loading...')); + }, ), ); } @@ -162,37 +144,45 @@ Recall the `ArticleViewModel`, which has three properties that the UI is interested in: - `Summary? summary` -- `bool loading` -- `String? errorMessage` +- `bool isLoading` +- `Exception? error` Depending on the combined state of these properties, the UI can display different widgets. Use Dart's support for [switch expressions][] to handle all possible combinations in a clean, readable way: - + ```dart -class ArticlePage extends StatelessWidget { - const ArticlePage({ - super.key, - required this.summary, - required this.nextArticleCallback, - }); +class ArticleView extends StatelessWidget { + ArticleView({super.key}); - final Summary summary; - final VoidCallback nextArticleCallback; + final ArticleViewModel viewModel = ArticleViewModel(ArticleModel()); @override Widget build(BuildContext context) { - return SingleChildScrollView( - child: Column( - children: [ - ArticleWidget(summary: summary), - ElevatedButton( - onPressed: nextArticleCallback, - child: const Text('Next random article'), - ), - ], + return Scaffold( + appBar: AppBar(title: const Text('Wikipedia Flutter')), + body: Center( + child: ListenableBuilder( + listenable: viewModel, + builder: (context, _) { + return switch (( + viewModel.isLoading, + viewModel.summary, + viewModel.error, + )) { + (true, _, _) => const CircularProgressIndicator(), + (_, _, final Exception e) => Text('Error: $e'), + // The summary must be non-null in this switch case. + (_, final summary?, _) => ArticlePage( + summary: summary, + nextArticleCallback: viewModel.fetchArticle, + ), + _ => const Text('Something went wrong!'), + }; + }, + ), ), ); } @@ -217,16 +207,23 @@ by the view model to build the UI. Now create a `ArticlePage` widget that displays the actual article content. This reusable widget takes summary data and a callback function: - + ```dart -class ArticleWidget extends StatelessWidget { - const ArticleWidget({super.key, required this.summary}); +class ArticlePage extends StatelessWidget { + const ArticlePage({ + super.key, + required this.summary, + required this.nextArticleCallback, + }); final Summary summary; + final VoidCallback nextArticleCallback; @override Widget build(BuildContext context) { - return const Text('Article content will be displayed here...'); + return const Center( + child: Text('Article content will be displayed here...'), + ); } } ``` @@ -235,17 +232,21 @@ class ArticleWidget extends StatelessWidget { Replace the placeholder with a scrollable column layout: - + ```dart -class ArticleWidget extends StatelessWidget { - const ArticleWidget({super.key, required this.summary}); +class ArticlePage extends StatelessWidget { + const ArticlePage({ + super.key, + required this.summary, + required this.nextArticleCallback, + }); final Summary summary; + final VoidCallback nextArticleCallback; @override Widget build(BuildContext context) { - return const Padding( - padding: EdgeInsets.all(8.0), + return const SingleChildScrollView( child: Column( children: [Text('Article content will be displayed here...')], ), @@ -258,24 +259,28 @@ class ArticleWidget extends StatelessWidget { Complete the layout with an article widget and navigation button: - + ```dart -class ArticleWidget extends StatelessWidget { - const ArticleWidget({super.key, required this.summary}); +class ArticlePage extends StatelessWidget { + const ArticlePage({ + super.key, + required this.summary, + required this.nextArticleCallback, + }); final Summary summary; + final VoidCallback nextArticleCallback; @override Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.all(8.0), + return SingleChildScrollView( child: Column( - spacing: 10, children: [ - if (summary.hasImage) ...[ - Image.network(summary.originalImage!.source), - ], - const Text('Article content will be displayed here'), + ArticleWidget(summary: summary), + ElevatedButton( + onPressed: nextArticleCallback, + child: const Text('Next random article'), + ), ], ), ); @@ -292,7 +297,7 @@ with proper styling and conditional rendering. Start with the widget that accepts a `summary` parameter: - + ```dart class ArticleWidget extends StatelessWidget { const ArticleWidget({super.key, required this.summary}); @@ -301,30 +306,7 @@ class ArticleWidget extends StatelessWidget { @override Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.all(8.0), - child: Column( - spacing: 10, - children: [ - if (summary.hasImage) ...[ - Image.network(summary.originalImage!.source), - ], - Text( - summary.titles.normalized, - overflow: TextOverflow.ellipsis, - style: Theme.of(context).textTheme.displaySmall, - ), - if (summary.description != null) ...[ - Text( - summary.description!, - overflow: TextOverflow.ellipsis, - style: Theme.of(context).textTheme.bodySmall, - ), - ], - Text(summary.extract), - ], - ), - ); + return const Text('Article content will be displayed here...'); } } ``` @@ -333,14 +315,22 @@ class ArticleWidget extends StatelessWidget { Wrap the content in proper padding and layout: - + ```dart -class MainApp extends StatelessWidget { - const MainApp({super.key}); +class ArticleWidget extends StatelessWidget { + const ArticleWidget({super.key, required this.summary}); + + final Summary summary; @override Widget build(BuildContext context) { - return MaterialApp(home: ArticleView()); + return Padding( + padding: const EdgeInsets.all(8), + child: Column( + spacing: 10, + children: [const Text('Article content will be displayed here...')], + ), + ); } } ``` @@ -349,6 +339,7 @@ class MainApp extends StatelessWidget { Add the article image that only shows when available: + ```dart class ArticleWidget extends StatelessWidget { const ArticleWidget({super.key, required this.summary}); @@ -358,15 +349,12 @@ class ArticleWidget extends StatelessWidget { @override Widget build(BuildContext context) { return Padding( - padding: const EdgeInsets.all(8.0), + padding: const EdgeInsets.all(8), child: Column( spacing: 10, children: [ - if (summary.hasImage) - Image.network( - summary.originalImage!.source, - ), - Text('Article content will be displayed here'), + if (summary.hasImage) Image.network(summary.originalImage!.source), + const Text('Article content will be displayed here...'), ], ), ); @@ -379,6 +367,7 @@ class ArticleWidget extends StatelessWidget { Replace the placeholder text with a properly styled title, description, and extract: + ```dart class ArticleWidget extends StatelessWidget { const ArticleWidget({super.key, required this.summary}); @@ -388,28 +377,23 @@ class ArticleWidget extends StatelessWidget { @override Widget build(BuildContext context) { return Padding( - padding: const EdgeInsets.all(8.0), + padding: const EdgeInsets.all(8), child: Column( spacing: 10, children: [ - if (summary.hasImage) - Image.network( - summary.originalImage!.source, - ), + if (summary.hasImage) Image.network(summary.originalImage!.source), Text( summary.titles.normalized, overflow: TextOverflow.ellipsis, - style: TextTheme.of(context).displaySmall, + style: Theme.of(context).textTheme.displaySmall, ), if (summary.description != null) Text( summary.description!, overflow: TextOverflow.ellipsis, - style: TextTheme.of(context).bodySmall, + style: Theme.of(context).textTheme.bodySmall, ), - Text( - summary.extract, - ), + Text(summary.extract), ], ), ); From cee19434e95a8ed86ccaba604091cee8b0d8521f Mon Sep 17 00:00:00 2001 From: Parker Lougheed Date: Sat, 18 Apr 2026 23:59:44 +0200 Subject: [PATCH 13/19] Simplify and organize imports --- examples/fwe/wikipedia_reader/lib/step3c_main.dart | 1 + examples/fwe/wikipedia_reader/lib/step3d_main.dart | 2 ++ examples/fwe/wikipedia_reader/lib/step3e_main.dart | 2 ++ examples/fwe/wikipedia_reader/lib/step3f_main.dart | 1 + examples/fwe/wikipedia_reader/lib/step4b_main.dart | 1 + examples/fwe/wikipedia_reader/lib/step4c_main.dart | 1 + src/content/learn/pathway/tutorial/listenable-builder.md | 4 ++-- 7 files changed, 10 insertions(+), 2 deletions(-) diff --git a/examples/fwe/wikipedia_reader/lib/step3c_main.dart b/examples/fwe/wikipedia_reader/lib/step3c_main.dart index 88a9516b63a..78cc732cc81 100644 --- a/examples/fwe/wikipedia_reader/lib/step3c_main.dart +++ b/examples/fwe/wikipedia_reader/lib/step3c_main.dart @@ -1,6 +1,7 @@ // ignore_for_file: unused_import, avoid_dynamic_calls import 'package:flutter/material.dart'; + import 'summary.dart'; class ArticleModel { diff --git a/examples/fwe/wikipedia_reader/lib/step3d_main.dart b/examples/fwe/wikipedia_reader/lib/step3d_main.dart index 67065479c7c..0df71ec6c38 100644 --- a/examples/fwe/wikipedia_reader/lib/step3d_main.dart +++ b/examples/fwe/wikipedia_reader/lib/step3d_main.dart @@ -1,7 +1,9 @@ // ignore_for_file: unused_import, avoid_dynamic_calls import 'dart:io'; + import 'package:flutter/material.dart'; + import 'summary.dart'; class ArticleModel { diff --git a/examples/fwe/wikipedia_reader/lib/step3e_main.dart b/examples/fwe/wikipedia_reader/lib/step3e_main.dart index edc069525c8..9f46f8d9e7a 100644 --- a/examples/fwe/wikipedia_reader/lib/step3e_main.dart +++ b/examples/fwe/wikipedia_reader/lib/step3e_main.dart @@ -1,7 +1,9 @@ // ignore_for_file: unused_import, avoid_dynamic_calls, avoid_print import 'dart:io'; + import 'package:flutter/material.dart'; + import 'summary.dart'; class ArticleModel { diff --git a/examples/fwe/wikipedia_reader/lib/step3f_main.dart b/examples/fwe/wikipedia_reader/lib/step3f_main.dart index 7208169e83e..6cf6ebc32ff 100644 --- a/examples/fwe/wikipedia_reader/lib/step3f_main.dart +++ b/examples/fwe/wikipedia_reader/lib/step3f_main.dart @@ -2,6 +2,7 @@ // ignore_for_file: unused_import, avoid_dynamic_calls import 'package:flutter/material.dart'; + import 'summary.dart'; class ArticleModel {} diff --git a/examples/fwe/wikipedia_reader/lib/step4b_main.dart b/examples/fwe/wikipedia_reader/lib/step4b_main.dart index ea9d6f8a2f4..b38fb301410 100644 --- a/examples/fwe/wikipedia_reader/lib/step4b_main.dart +++ b/examples/fwe/wikipedia_reader/lib/step4b_main.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; + import 'summary.dart'; class ArticleViewModel extends ChangeNotifier { diff --git a/examples/fwe/wikipedia_reader/lib/step4c_main.dart b/examples/fwe/wikipedia_reader/lib/step4c_main.dart index d6ebdd5cb0c..ec613583791 100644 --- a/examples/fwe/wikipedia_reader/lib/step4c_main.dart +++ b/examples/fwe/wikipedia_reader/lib/step4c_main.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; + import 'summary.dart'; class ArticleViewModel extends ChangeNotifier { diff --git a/src/content/learn/pathway/tutorial/listenable-builder.md b/src/content/learn/pathway/tutorial/listenable-builder.md index 0e055fbaad6..fd314552d35 100644 --- a/src/content/learn/pathway/tutorial/listenable-builder.md +++ b/src/content/learn/pathway/tutorial/listenable-builder.md @@ -63,7 +63,7 @@ class ArticleView extends StatelessWidget { Create the `ArticleViewModel` in this widget: -```dart highlightLines=4 +```dart class ArticleView extends StatelessWidget { ArticleView({super.key}); @@ -108,7 +108,7 @@ and pass it a `ChangeNotifier` object. In this case, the `ArticleViewModel` extends `ChangeNotifier`. -```dart highlightLines=12-17 +```dart class ArticleView extends StatelessWidget { ArticleView({super.key}); From 0fb266dcef96c55e80c91505f248147ba8aa39ef Mon Sep 17 00:00:00 2001 From: Parker Lougheed Date: Sun, 19 Apr 2026 00:02:35 +0200 Subject: [PATCH 14/19] Remove unnecessary lint ignores --- examples/fwe/wikipedia_reader/lib/step2_main.dart | 2 -- examples/fwe/wikipedia_reader/lib/step3_main.dart | 2 +- examples/fwe/wikipedia_reader/lib/step3a_main.dart | 2 +- examples/fwe/wikipedia_reader/lib/step3b_main.dart | 2 +- examples/fwe/wikipedia_reader/lib/step3c_main.dart | 2 +- examples/fwe/wikipedia_reader/lib/step3d_main.dart | 2 +- examples/fwe/wikipedia_reader/lib/step3f_main.dart | 3 +-- examples/fwe/wikipedia_reader/lib/step4g_main.dart | 2 +- examples/fwe/wikipedia_reader/lib/summary.dart | 2 -- 9 files changed, 7 insertions(+), 12 deletions(-) diff --git a/examples/fwe/wikipedia_reader/lib/step2_main.dart b/examples/fwe/wikipedia_reader/lib/step2_main.dart index 1082639f908..3dc1fd3d00d 100644 --- a/examples/fwe/wikipedia_reader/lib/step2_main.dart +++ b/examples/fwe/wikipedia_reader/lib/step2_main.dart @@ -1,5 +1,3 @@ -// ignore_for_file: unused_import, avoid_dynamic_calls - import 'dart:convert'; import 'dart:io'; diff --git a/examples/fwe/wikipedia_reader/lib/step3_main.dart b/examples/fwe/wikipedia_reader/lib/step3_main.dart index bb0dd4e88b4..1d5afaf5a2e 100644 --- a/examples/fwe/wikipedia_reader/lib/step3_main.dart +++ b/examples/fwe/wikipedia_reader/lib/step3_main.dart @@ -1,4 +1,4 @@ -// ignore_for_file: unused_import, avoid_dynamic_calls +// ignore_for_file: unused_import import 'dart:convert'; import 'dart:io'; diff --git a/examples/fwe/wikipedia_reader/lib/step3a_main.dart b/examples/fwe/wikipedia_reader/lib/step3a_main.dart index 5bfa64f8155..b6ca4d8209d 100644 --- a/examples/fwe/wikipedia_reader/lib/step3a_main.dart +++ b/examples/fwe/wikipedia_reader/lib/step3a_main.dart @@ -1,4 +1,4 @@ -// ignore_for_file: unused_import, avoid_dynamic_calls +// ignore_for_file: unused_import import 'dart:convert'; import 'dart:io'; diff --git a/examples/fwe/wikipedia_reader/lib/step3b_main.dart b/examples/fwe/wikipedia_reader/lib/step3b_main.dart index f457a60ba92..31a89acf265 100644 --- a/examples/fwe/wikipedia_reader/lib/step3b_main.dart +++ b/examples/fwe/wikipedia_reader/lib/step3b_main.dart @@ -1,4 +1,4 @@ -// ignore_for_file: unused_import, avoid_dynamic_calls +// ignore_for_file: unused_import import 'dart:convert'; import 'dart:io'; diff --git a/examples/fwe/wikipedia_reader/lib/step3c_main.dart b/examples/fwe/wikipedia_reader/lib/step3c_main.dart index 78cc732cc81..297128f4453 100644 --- a/examples/fwe/wikipedia_reader/lib/step3c_main.dart +++ b/examples/fwe/wikipedia_reader/lib/step3c_main.dart @@ -1,4 +1,4 @@ -// ignore_for_file: unused_import, avoid_dynamic_calls +// ignore_for_file: unused_import import 'package:flutter/material.dart'; diff --git a/examples/fwe/wikipedia_reader/lib/step3d_main.dart b/examples/fwe/wikipedia_reader/lib/step3d_main.dart index 0df71ec6c38..06ac5d980c7 100644 --- a/examples/fwe/wikipedia_reader/lib/step3d_main.dart +++ b/examples/fwe/wikipedia_reader/lib/step3d_main.dart @@ -1,4 +1,4 @@ -// ignore_for_file: unused_import, avoid_dynamic_calls +// ignore_for_file: unused_import import 'dart:io'; diff --git a/examples/fwe/wikipedia_reader/lib/step3f_main.dart b/examples/fwe/wikipedia_reader/lib/step3f_main.dart index 6cf6ebc32ff..91d35c72a36 100644 --- a/examples/fwe/wikipedia_reader/lib/step3f_main.dart +++ b/examples/fwe/wikipedia_reader/lib/step3f_main.dart @@ -1,5 +1,4 @@ -// ignore_for_file: unused_local_variable -// ignore_for_file: unused_import, avoid_dynamic_calls +// ignore_for_file: unused_import, unused_local_variable import 'package:flutter/material.dart'; diff --git a/examples/fwe/wikipedia_reader/lib/step4g_main.dart b/examples/fwe/wikipedia_reader/lib/step4g_main.dart index 6c1cf1df0ed..193fc99c6dd 100644 --- a/examples/fwe/wikipedia_reader/lib/step4g_main.dart +++ b/examples/fwe/wikipedia_reader/lib/step4g_main.dart @@ -1,4 +1,4 @@ -// ignore_for_file: prefer_const_constructors +// ignore_for_file: prefer_const_literals_to_create_immutables, prefer_const_constructors import 'package:flutter/material.dart'; diff --git a/examples/fwe/wikipedia_reader/lib/summary.dart b/examples/fwe/wikipedia_reader/lib/summary.dart index 309b44c0f51..bb526096707 100644 --- a/examples/fwe/wikipedia_reader/lib/summary.dart +++ b/examples/fwe/wikipedia_reader/lib/summary.dart @@ -1,5 +1,3 @@ -// ignore_for_file: directives_ordering - // #docregion All /// Representation of the JSON data returned by the Wikipedia API. class Summary { From 2f7f0c538d5090d28773987bbaf78e3fb607ec33 Mon Sep 17 00:00:00 2001 From: Parker Lougheed Date: Sun, 19 Apr 2026 00:05:15 +0200 Subject: [PATCH 15/19] Remove unused main.dart file --- examples/fwe/wikipedia_reader/lib/main.dart | 165 -------------------- 1 file changed, 165 deletions(-) delete mode 100644 examples/fwe/wikipedia_reader/lib/main.dart diff --git a/examples/fwe/wikipedia_reader/lib/main.dart b/examples/fwe/wikipedia_reader/lib/main.dart deleted file mode 100644 index a42a3ac0c5d..00000000000 --- a/examples/fwe/wikipedia_reader/lib/main.dart +++ /dev/null @@ -1,165 +0,0 @@ -import 'dart:convert'; -import 'dart:io'; - -import 'package:flutter/material.dart'; -import 'package:http/http.dart'; - -import 'summary.dart'; - -void main() { - runApp(const MainApp()); -} - -class MainApp extends StatelessWidget { - const MainApp({super.key}); - - @override - Widget build(BuildContext context) { - return const MaterialApp(home: ArticleView()); - } -} - -class ArticleModel { - Future getRandomArticleSummary() async { - final uri = Uri.https( - 'en.wikipedia.org', - '/api/rest_v1/page/random/summary', - ); - final response = await get(uri); - - if (response.statusCode != 200) { - throw const HttpException('Failed to update resource'); - } - - return Summary.fromJson(jsonDecode(response.body) as Map); - } -} - -class ArticleViewModel extends ChangeNotifier { - ArticleViewModel(this.model); - final ArticleModel model; - - Summary? summary; - - bool isLoading = false; - - Exception? error; - - Future fetchArticle() async { - isLoading = true; - error = null; - notifyListeners(); - - try { - summary = await model.getRandomArticleSummary(); - error = null; - } on Exception catch (e) { - error = e; - } finally { - isLoading = false; - notifyListeners(); - } - } -} - -class ArticleView extends StatefulWidget { - const ArticleView({super.key}); - - @override - State createState() => _ArticleViewState(); -} - -class _ArticleViewState extends State { - late final ArticleViewModel viewModel; - - @override - void initState() { - super.initState(); - viewModel = ArticleViewModel(ArticleModel()); - viewModel.fetchArticle(); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar(title: const Text('Wikipedia Flutter')), - body: Center( - child: ListenableBuilder( - listenable: viewModel, - builder: (context, _) { - return switch (( - viewModel.isLoading, - viewModel.summary, - viewModel.error, - )) { - (true, _, _) => const CircularProgressIndicator(), - (_, final summary?, _) => ArticlePage( - summary: summary, - nextArticleCallback: viewModel.fetchArticle, - ), - (_, _, final Exception e) => Text('Error: $e'), - _ => const Text('Something went wrong!'), - }; - }, - ), - ), - ); - } -} - -class ArticlePage extends StatelessWidget { - const ArticlePage({ - super.key, - required this.summary, - required this.nextArticleCallback, - }); - - final Summary summary; - final VoidCallback nextArticleCallback; - - @override - Widget build(BuildContext context) { - return SingleChildScrollView( - child: Column( - children: [ - ArticleWidget(summary: summary), - ElevatedButton( - onPressed: nextArticleCallback, - child: const Text('Next random article'), - ), - ], - ), - ); - } -} - -class ArticleWidget extends StatelessWidget { - const ArticleWidget({super.key, required this.summary}); - - final Summary summary; - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.all(8.0), - child: Column( - spacing: 10, - children: [ - if (summary.hasImage) Image.network(summary.originalImage!.source), - Text( - summary.titles.normalized, - overflow: TextOverflow.ellipsis, - style: Theme.of(context).textTheme.displaySmall, - ), - if (summary.description != null) - Text( - summary.description!, - overflow: TextOverflow.ellipsis, - style: Theme.of(context).textTheme.bodySmall, - ), - Text(summary.extract), - ], - ), - ); - } -} From 1866c60d7e2d81445ab2ea10dbe629d8ec1bb898 Mon Sep 17 00:00:00 2001 From: Parker Lougheed Date: Sun, 19 Apr 2026 00:17:04 +0200 Subject: [PATCH 16/19] Clean up other doc comments in the summary.dart file --- .../fwe/wikipedia_reader/lib/summary.dart | 27 ++++++++++--------- .../pathway/tutorial/set-up-state-project.md | 27 ++++++++++--------- 2 files changed, 30 insertions(+), 24 deletions(-) diff --git a/examples/fwe/wikipedia_reader/lib/summary.dart b/examples/fwe/wikipedia_reader/lib/summary.dart index bb526096707..57a0f49cf0c 100644 --- a/examples/fwe/wikipedia_reader/lib/summary.dart +++ b/examples/fwe/wikipedia_reader/lib/summary.dart @@ -147,21 +147,23 @@ class ImageFile { /// Returns a new [ImageFile] instance. ImageFile({required this.source, required this.width, required this.height}); - /// Original image URI + /// The URI of the original image. final String source; - /// Original image width + /// The width of the original image. final int width; - /// Original image height + /// The height of the original image. final int height; + /// The file extension of the image, or 'err' if one can't be determined. String get extension { final extension = getFileExtension(source); - // by default, return a non-viable image extension + // By default, return a non-viable image extension. return extension ?? 'err'; } + /// Returns a JSON map representation of this [ImageFile]. Map toJson() { return { 'source': source, @@ -170,8 +172,7 @@ class ImageFile { }; } - /// Returns a new [ImageFile] instance - // ignore: prefer_constructors_over_static_methods + /// Returns a new [ImageFile] instance with its values populated from [json]. static ImageFile fromJson(Map json) { if (json case { 'source': final String source, @@ -198,18 +199,20 @@ class TitlesSet { required this.display, }); - /// the DB key (non-prefixed), e.g. may have _ instead of spaces, - /// best for making request URIs, still requires Percent-encoding + /// The non-prefixed DB key for the article. + /// + /// Might contain changes such as underscores instead of spaces. + /// Best suited for making request URIs, but still requires percent-encoding. final String canonical; - /// the normalized title (https://www.mediawiki.org/wiki/API:Query#Example_2:_Title_normalization), - /// e.g. may have spaces instead of _ + /// The [normalized title](https://www.mediawiki.org/wiki/API:Query#Example_2:_Title_normalization) + /// of the article. final String normalized; - /// the title as it should be displayed to the user + /// The title as it should be displayed to the user. final String display; - /// Returns a new [TitlesSet] instance and imports its values from a JSON map + /// Returns a new [TitlesSet] instance with its values populated from [json]. static TitlesSet fromJson(Map json) { if (json case { 'canonical': final String canonical, diff --git a/src/content/learn/pathway/tutorial/set-up-state-project.md b/src/content/learn/pathway/tutorial/set-up-state-project.md index bfce1bc7806..67fd3d6c4fa 100644 --- a/src/content/learn/pathway/tutorial/set-up-state-project.md +++ b/src/content/learn/pathway/tutorial/set-up-state-project.md @@ -238,21 +238,23 @@ class ImageFile { /// Returns a new [ImageFile] instance. ImageFile({required this.source, required this.width, required this.height}); - /// Original image URI + /// The URI of the original image. final String source; - /// Original image width + /// The width of the original image. final int width; - /// Original image height + /// The height of the original image. final int height; + /// The file extension of the image, or 'err' if one can't be determined. String get extension { final extension = getFileExtension(source); - // by default, return a non-viable image extension + // By default, return a non-viable image extension. return extension ?? 'err'; } + /// Returns a JSON map representation of this [ImageFile]. Map toJson() { return { 'source': source, @@ -261,8 +263,7 @@ class ImageFile { }; } - /// Returns a new [ImageFile] instance - // ignore: prefer_constructors_over_static_methods + /// Returns a new [ImageFile] instance with its values populated from [json]. static ImageFile fromJson(Map json) { if (json case { 'source': final String source, @@ -287,18 +288,20 @@ class TitlesSet { required this.display, }); - /// the DB key (non-prefixed), e.g. may have _ instead of spaces, - /// best for making request URIs, still requires Percent-encoding + /// The non-prefixed DB key for the article. + /// + /// Might contain changes such as underscores instead of spaces. + /// Best suited for making request URIs, but still requires percent-encoding. final String canonical; - /// the normalized title (https://www.mediawiki.org/wiki/API:Query#Example_2:_Title_normalization), - /// e.g. may have spaces instead of _ + /// The [normalized title](https://www.mediawiki.org/wiki/API:Query#Example_2:_Title_normalization) + /// of the article. final String normalized; - /// the title as it should be displayed to the user + /// The title as it should be displayed to the user. final String display; - /// Returns a new [TitlesSet] instance and imports its values from a JSON map + /// Returns a new [TitlesSet] instance with its values populated from [json]. static TitlesSet fromJson(Map json) { if (json case { 'canonical': final String canonical, From 70ed67bdc7466af4b821427ead8c75f76738f3dd Mon Sep 17 00:00:00 2001 From: Parker Lougheed Date: Sun, 19 Apr 2026 00:19:44 +0200 Subject: [PATCH 17/19] Correct naming of pageId field --- examples/fwe/wikipedia_reader/lib/summary.dart | 18 +++++++++--------- .../pathway/tutorial/set-up-state-project.md | 18 +++++++++--------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/examples/fwe/wikipedia_reader/lib/summary.dart b/examples/fwe/wikipedia_reader/lib/summary.dart index 57a0f49cf0c..943b1cfd3e6 100644 --- a/examples/fwe/wikipedia_reader/lib/summary.dart +++ b/examples/fwe/wikipedia_reader/lib/summary.dart @@ -4,7 +4,7 @@ class Summary { /// Returns a new [Summary] instance. Summary({ required this.titles, - required this.pageid, + required this.pageId, required this.extract, required this.extractHtml, this.thumbnail, @@ -19,7 +19,7 @@ class Summary { final TitlesSet titles; /// The page ID of this article. - final int pageid; + final int pageId; /// The first few sentences of the article in plain text. final String extract; @@ -53,7 +53,7 @@ class Summary { return switch (json) { { 'titles': final Map titles, - 'pageid': final int pageid, + 'pageid': final int pageId, 'extract': final String extract, 'extract_html': final String extractHtml, 'thumbnail': final Map thumbnail, @@ -68,7 +68,7 @@ class Summary { } => Summary( titles: TitlesSet.fromJson(titles), - pageid: pageid, + pageId: pageId, extract: extract, extractHtml: extractHtml, thumbnail: ImageFile.fromJson(thumbnail), @@ -80,7 +80,7 @@ class Summary { ), { 'titles': final Map titles, - 'pageid': final int pageid, + 'pageid': final int pageId, 'extract': final String extract, 'extract_html': final String extractHtml, 'lang': final String lang, @@ -93,7 +93,7 @@ class Summary { } => Summary( titles: TitlesSet.fromJson(titles), - pageid: pageid, + pageId: pageId, extract: extract, extractHtml: extractHtml, lang: lang, @@ -103,7 +103,7 @@ class Summary { ), { 'titles': final Map titles, - 'pageid': final int pageid, + 'pageid': final int pageId, 'extract': final String extract, 'extract_html': final String extractHtml, 'lang': final String lang, @@ -115,7 +115,7 @@ class Summary { } => Summary( titles: TitlesSet.fromJson(titles), - pageid: pageid, + pageId: pageId, extract: extract, extractHtml: extractHtml, lang: lang, @@ -130,7 +130,7 @@ class Summary { String toString() => 'Summary[' 'titles=$titles, ' - 'pageid=$pageid, ' + 'pageId=$pageId, ' 'extract=$extract, ' 'extractHtml=$extractHtml, ' 'thumbnail=${thumbnail ?? 'null'}, ' diff --git a/src/content/learn/pathway/tutorial/set-up-state-project.md b/src/content/learn/pathway/tutorial/set-up-state-project.md index 67fd3d6c4fa..32b0c1424b6 100644 --- a/src/content/learn/pathway/tutorial/set-up-state-project.md +++ b/src/content/learn/pathway/tutorial/set-up-state-project.md @@ -96,7 +96,7 @@ class Summary { /// Returns a new [Summary] instance. Summary({ required this.titles, - required this.pageid, + required this.pageId, required this.extract, required this.extractHtml, this.thumbnail, @@ -111,7 +111,7 @@ class Summary { final TitlesSet titles; /// The page ID of this article. - final int pageid; + final int pageId; /// The first few sentences of the article in plain text. final String extract; @@ -145,7 +145,7 @@ class Summary { return switch (json) { { 'titles': final Map titles, - 'pageid': final int pageid, + 'pageid': final int pageId, 'extract': final String extract, 'extract_html': final String extractHtml, 'thumbnail': final Map thumbnail, @@ -160,7 +160,7 @@ class Summary { } => Summary( titles: TitlesSet.fromJson(titles), - pageid: pageid, + pageId: pageId, extract: extract, extractHtml: extractHtml, thumbnail: ImageFile.fromJson(thumbnail), @@ -172,7 +172,7 @@ class Summary { ), { 'titles': final Map titles, - 'pageid': final int pageid, + 'pageid': final int pageId, 'extract': final String extract, 'extract_html': final String extractHtml, 'lang': final String lang, @@ -185,7 +185,7 @@ class Summary { } => Summary( titles: TitlesSet.fromJson(titles), - pageid: pageid, + pageId: pageId, extract: extract, extractHtml: extractHtml, lang: lang, @@ -195,7 +195,7 @@ class Summary { ), { 'titles': final Map titles, - 'pageid': final int pageid, + 'pageid': final int pageId, 'extract': final String extract, 'extract_html': final String extractHtml, 'lang': final String lang, @@ -207,7 +207,7 @@ class Summary { } => Summary( titles: TitlesSet.fromJson(titles), - pageid: pageid, + pageId: pageId, extract: extract, extractHtml: extractHtml, lang: lang, @@ -222,7 +222,7 @@ class Summary { String toString() => 'Summary[' 'titles=$titles, ' - 'pageid=$pageid, ' + 'pageId=$pageId, ' 'extract=$extract, ' 'extractHtml=$extractHtml, ' 'thumbnail=${thumbnail ?? 'null'}, ' From 97e11e66ecce9354c9276c8c521914fdf921e448 Mon Sep 17 00:00:00 2001 From: Parker Lougheed Date: Sun, 19 Apr 2026 00:22:34 +0200 Subject: [PATCH 18/19] Avoid const that has to soon be removed --- examples/fwe/wikipedia_reader/lib/step4a_main.dart | 3 ++- src/content/learn/pathway/tutorial/listenable-builder.md | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/examples/fwe/wikipedia_reader/lib/step4a_main.dart b/examples/fwe/wikipedia_reader/lib/step4a_main.dart index e31588eed84..fbc1dc78cd8 100644 --- a/examples/fwe/wikipedia_reader/lib/step4a_main.dart +++ b/examples/fwe/wikipedia_reader/lib/step4a_main.dart @@ -1,7 +1,8 @@ +// ignore_for_file: prefer_const_constructors_in_immutables import 'package:flutter/material.dart'; class ArticleView extends StatelessWidget { - const ArticleView({super.key}); + ArticleView({super.key}); // The view model will be instantiated here next. diff --git a/src/content/learn/pathway/tutorial/listenable-builder.md b/src/content/learn/pathway/tutorial/listenable-builder.md index fd314552d35..11982484ca3 100644 --- a/src/content/learn/pathway/tutorial/listenable-builder.md +++ b/src/content/learn/pathway/tutorial/listenable-builder.md @@ -4,6 +4,8 @@ description: Instructions on how to manage state with ChangeNotifiers. layout: tutorial --- + + Learn to use ListenableBuilder to automatically rebuild UI and handle all possible states with switch expressions. @@ -44,7 +46,7 @@ Start with the basic class structure and widgets: import 'package:flutter/material.dart'; class ArticleView extends StatelessWidget { - const ArticleView({super.key}); + ArticleView({super.key}); // The view model will be instantiated here next. From 5110dc6013ad319e96e1577b090d94a53cb03797 Mon Sep 17 00:00:00 2001 From: Parker Lougheed Date: Sun, 19 Apr 2026 00:24:46 +0200 Subject: [PATCH 19/19] Keep required parameters together in Summary constructor --- .../fwe/wikipedia_reader/lib/summary.dart | 22 +++++++++---------- .../pathway/tutorial/set-up-state-project.md | 22 +++++++++---------- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/examples/fwe/wikipedia_reader/lib/summary.dart b/examples/fwe/wikipedia_reader/lib/summary.dart index 943b1cfd3e6..6f67fe84bc9 100644 --- a/examples/fwe/wikipedia_reader/lib/summary.dart +++ b/examples/fwe/wikipedia_reader/lib/summary.dart @@ -7,12 +7,12 @@ class Summary { required this.pageId, required this.extract, required this.extractHtml, - this.thumbnail, - this.originalImage, required this.lang, required this.dir, - this.description, required this.url, + this.description, + this.thumbnail, + this.originalImage, }); /// The title information of this article. @@ -27,23 +27,23 @@ class Summary { /// The first few sentences of the article in HTML format. final String extractHtml; - /// A thumbnail-sized version of the article's primary image, if available. - final ImageFile? thumbnail; - - /// The original full-sized article's primary image, if available. - final ImageFile? originalImage; - /// The language code of the article's content, such as "en" for English. final String lang; /// The text directionality of the article's content, such as "ltr" or "rtl". final String dir; + /// The URL of the page. + final String url; + /// A description of the article, if available. final String? description; - /// The URL of the page. - final String url; + /// A thumbnail-sized version of the article's primary image, if available. + final ImageFile? thumbnail; + + /// The original full-sized article's primary image, if available. + final ImageFile? originalImage; /// Whether this article has an image. bool get hasImage => originalImage != null && thumbnail != null; diff --git a/src/content/learn/pathway/tutorial/set-up-state-project.md b/src/content/learn/pathway/tutorial/set-up-state-project.md index 32b0c1424b6..b1aa0b938f2 100644 --- a/src/content/learn/pathway/tutorial/set-up-state-project.md +++ b/src/content/learn/pathway/tutorial/set-up-state-project.md @@ -99,12 +99,12 @@ class Summary { required this.pageId, required this.extract, required this.extractHtml, - this.thumbnail, - this.originalImage, required this.lang, required this.dir, - this.description, required this.url, + this.description, + this.thumbnail, + this.originalImage, }); /// The title information of this article. @@ -119,23 +119,23 @@ class Summary { /// The first few sentences of the article in HTML format. final String extractHtml; - /// A thumbnail-sized version of the article's primary image, if available. - final ImageFile? thumbnail; - - /// The original full-sized article's primary image, if available. - final ImageFile? originalImage; - /// The language code of the article's content, such as "en" for English. final String lang; /// The text directionality of the article's content, such as "ltr" or "rtl". final String dir; + /// The URL of the page. + final String url; + /// A description of the article, if available. final String? description; - /// The URL of the page. - final String url; + /// A thumbnail-sized version of the article's primary image, if available. + final ImageFile? thumbnail; + + /// The original full-sized article's primary image, if available. + final ImageFile? originalImage; /// Whether this article has an image. bool get hasImage => originalImage != null && thumbnail != null;