diff --git a/examples/fwe/rolodex/pubspec.yaml b/examples/fwe/rolodex/pubspec.yaml index 5385f42ede..23fb072d3b 100644 --- a/examples/fwe/rolodex/pubspec.yaml +++ b/examples/fwe/rolodex/pubspec.yaml @@ -14,7 +14,7 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - flutter_lints: ^3.0.0 + flutter_lints: ^6.0.0 flutter: uses-material-design: true diff --git a/examples/fwe/wikipedia_reader/README.md b/examples/fwe/wikipedia_reader/README.md new file mode 100644 index 0000000000..b8b0a1613e --- /dev/null +++ b/examples/fwe/wikipedia_reader/README.md @@ -0,0 +1,3 @@ +# wikipedia_reader + +Code excerpts for the Wikipedia reader app in the Flutter tutorial. 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 0000000000..affee140a0 --- /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 0000000000..3dc1fd3d00 --- /dev/null +++ b/examples/fwe/wikipedia_reader/lib/step2_main.dart @@ -0,0 +1,44 @@ +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'); + } + + return Summary.fromJson(jsonDecode(response.body) as Map); + } +} + +// #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 0000000000..ca172369fd --- /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 0000000000..bd22fb7d96 --- /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 0000000000..00e253b4bd --- /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 0000000000..1d5afaf5a2 --- /dev/null +++ b/examples/fwe/wikipedia_reader/lib/step3_main.dart @@ -0,0 +1,71 @@ +// 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...')), + ), + ); + } +} + +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 0000000000..b6ca4d8209 --- /dev/null +++ b/examples/fwe/wikipedia_reader/lib/step3a_main.dart @@ -0,0 +1,42 @@ +// 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 const 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 0000000000..31a89acf26 --- /dev/null +++ b/examples/fwe/wikipedia_reader/lib/step3b_main.dart @@ -0,0 +1,32 @@ +// ignore_for_file: unused_import + +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 0000000000..297128f445 --- /dev/null +++ b/examples/fwe/wikipedia_reader/lib/step3c_main.dart @@ -0,0 +1,35 @@ +// ignore_for_file: unused_import + +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 0000000000..06ac5d980c --- /dev/null +++ b/examples/fwe/wikipedia_reader/lib/step3d_main.dart @@ -0,0 +1,41 @@ +// ignore_for_file: unused_import + +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 0000000000..9f46f8d9e7 --- /dev/null +++ b/examples/fwe/wikipedia_reader/lib/step3e_main.dart @@ -0,0 +1,43 @@ +// 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 0000000000..91d35c72a3 --- /dev/null +++ b/examples/fwe/wikipedia_reader/lib/step3f_main.dart @@ -0,0 +1,34 @@ +// ignore_for_file: unused_import, unused_local_variable + +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 0000000000..d44066b08d --- /dev/null +++ b/examples/fwe/wikipedia_reader/lib/step4_main.dart @@ -0,0 +1,162 @@ +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 main-app +class MainApp extends StatelessWidget { + const MainApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp(home: ArticleView()); + } +} +// #enddocregion main-app + +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 { + final ArticleModel model; + Summary? summary; + Exception? error; + bool isLoading = false; + + ArticleViewModel(this.model) { + fetchArticle(); + } + + Future fetchArticle() async { + isLoading = true; + error = null; + notifyListeners(); + + try { + summary = await model.getRandomArticleSummary(); + } on HttpException catch (e) { + error = e; + } finally { + isLoading = false; + notifyListeners(); + } + } +} + +// #docregion view-model +class ArticleView extends StatelessWidget { + ArticleView({super.key}); + + final ArticleViewModel viewModel = ArticleViewModel(ArticleModel()); + + @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 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!'), + }; + }, + ), + ), + ); + } +} +// #enddocregion view-model + +// #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 SingleChildScrollView( + child: Column( + children: [ + ArticleWidget(summary: summary), + ElevatedButton( + onPressed: nextArticleCallback, + child: const Text('Next random article'), + ), + ], + ), + ); + } +} +// #enddocregion page + +// #docregion 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), + 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), + ], + ), + ); + } +} + +// #enddocregion article 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 0000000000..fbc1dc78cd --- /dev/null +++ b/examples/fwe/wikipedia_reader/lib/step4a_main.dart @@ -0,0 +1,16 @@ +// ignore_for_file: prefer_const_constructors_in_immutables +import 'package:flutter/material.dart'; + +class ArticleView extends StatelessWidget { + ArticleView({super.key}); + + // The view model will be instantiated here next. + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Wikipedia Flutter')), + body: const Center(child: Text('Loading...')), + ); + } +} 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 0000000000..b38fb30141 --- /dev/null +++ b/examples/fwe/wikipedia_reader/lib/step4b_main.dart @@ -0,0 +1,30 @@ +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 view-model +class ArticleView extends StatelessWidget { + ArticleView({super.key}); + + final ArticleViewModel viewModel = ArticleViewModel(ArticleModel()); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Wikipedia Flutter')), + body: const Center(child: Text('Loading...')), + ); + } +} + +// #enddocregion view-model 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 0000000000..ec61358379 --- /dev/null +++ b/examples/fwe/wikipedia_reader/lib/step4c_main.dart @@ -0,0 +1,35 @@ +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 view-model +class ArticleView extends StatelessWidget { + ArticleView({super.key}); + + final ArticleViewModel viewModel = ArticleViewModel(ArticleModel()); + + @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('Loading...')); + }, + ), + ); + } +} + +// #enddocregion view-model 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 0000000000..70d2916ef7 --- /dev/null +++ b/examples/fwe/wikipedia_reader/lib/step4d_main.dart @@ -0,0 +1,24 @@ +import 'package:flutter/material.dart'; + +import 'summary.dart'; + +// #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 Center( + child: Text('Article content will be displayed here...'), + ); + } +} + +// #enddocregion page 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 0000000000..51660c62dd --- /dev/null +++ b/examples/fwe/wikipedia_reader/lib/step4e_main.dart @@ -0,0 +1,26 @@ +import 'package:flutter/material.dart'; + +import 'summary.dart'; + +// #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 SingleChildScrollView( + child: Column( + children: [Text('Article content will be displayed here...')], + ), + ); + } +} + +// #enddocregion page 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 0000000000..cf1ffbfe64 --- /dev/null +++ b/examples/fwe/wikipedia_reader/lib/step4f_main.dart @@ -0,0 +1,17 @@ +import 'package:flutter/material.dart'; + +import 'summary.dart'; + +// #docregion article +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 article 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 0000000000..193fc99c6d --- /dev/null +++ b/examples/fwe/wikipedia_reader/lib/step4g_main.dart @@ -0,0 +1,25 @@ +// ignore_for_file: prefer_const_literals_to_create_immutables, prefer_const_constructors + +import 'package:flutter/material.dart'; + +import 'summary.dart'; + +// #docregion 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), + child: Column( + spacing: 10, + children: [const Text('Article content will be displayed here...')], + ), + ); + } +} + +// #enddocregion article 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 0000000000..259b8151b1 --- /dev/null +++ b/examples/fwe/wikipedia_reader/lib/step4h_main.dart @@ -0,0 +1,26 @@ +import 'package:flutter/material.dart'; + +import 'summary.dart'; + +// #docregion 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), + child: Column( + spacing: 10, + children: [ + if (summary.hasImage) Image.network(summary.originalImage!.source), + const Text('Article content will be displayed here...'), + ], + ), + ); + } +} + +// #enddocregion article diff --git a/examples/fwe/wikipedia_reader/lib/summary.dart b/examples/fwe/wikipedia_reader/lib/summary.dart new file mode 100644 index 0000000000..6f67fe84bc --- /dev/null +++ b/examples/fwe/wikipedia_reader/lib/summary.dart @@ -0,0 +1,249 @@ +// #docregion All +/// Representation of the JSON data returned by the Wikipedia API. +class Summary { + /// Returns a new [Summary] instance. + Summary({ + required this.titles, + required this.pageId, + required this.extract, + required this.extractHtml, + required this.lang, + required this.dir, + required this.url, + this.description, + this.thumbnail, + this.originalImage, + }); + + /// The title information of this article. + final TitlesSet titles; + + /// The page ID of this article. + final int pageId; + + /// The first few sentences of the article in plain text. + final String extract; + + /// The first few sentences of the article in HTML format. + final String extractHtml; + + /// 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; + + /// 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; + + /// 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' + ']'; +} + +// Image path and size, but doesn't contain any Wikipedia descriptions. +// #docregion ImageFile +class ImageFile { + /// Returns a new [ImageFile] instance. + ImageFile({required this.source, required this.width, required this.height}); + + /// The URI of the original image. + final String source; + + /// The width of the original image. + final int width; + + /// 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. + return extension ?? 'err'; + } + + /// Returns a JSON map representation of this [ImageFile]. + Map toJson() { + return { + 'source': source, + 'width': width, + 'height': height, + }; + } + + /// Returns a new [ImageFile] instance with its values populated from [json]. + 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 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) + /// of the article. + final String normalized; + + /// The title as it should be displayed to the user. + final String display; + + /// Returns a new [TitlesSet] instance with its values populated from [json]. + 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 0000000000..057d66c0e0 --- /dev/null +++ b/examples/fwe/wikipedia_reader/pubspec.yaml @@ -0,0 +1,19 @@ +name: wikipedia_reader +description: Code excerpts for the Wikipedia reader app in the Flutter tutorial. +publish_to: none + +environment: + sdk: ^3.11.0 + +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/src/content/learn/pathway/tutorial/change-notifier.md b/src/content/learn/pathway/tutorial/change-notifier.md index 824dc464a6..8578d18623 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` method 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 31bc4c8594..9856fdf213 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 { @@ -133,10 +139,10 @@ 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)); + 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 1ed2b69ba1..11982484ca 100644 --- a/src/content/learn/pathway/tutorial/listenable-builder.md +++ b/src/content/learn/pathway/tutorial/listenable-builder.md @@ -4,7 +4,10 @@ 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,19 +41,20 @@ 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}); + // The view model will be instantiated here 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,21 +64,18 @@ class ArticleView extends StatelessWidget { Create the `ArticleViewModel` in this widget: + ```dart class ArticleView extends StatelessWidget { ArticleView({super.key}); - final viewModel = ArticleViewModel(ArticleModel()); + final ArticleViewModel viewModel = ArticleViewModel(ArticleModel()); @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...')), ); } } @@ -87,15 +88,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()); } } ``` @@ -109,6 +109,7 @@ 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}); @@ -118,13 +119,11 @@ class ArticleView extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar( - title: const Text('Wikipedia Flutter'), - ), + appBar: AppBar(title: const Text('Wikipedia Flutter')), body: ListenableBuilder( listenable: viewModel, builder: (context, child) { - return const Center(child: Text('UI will update here')); + return const Center(child: Text('Loading...')); }, ), ); @@ -147,14 +146,15 @@ 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 ArticleView extends StatelessWidget { ArticleView({super.key}); @@ -164,30 +164,27 @@ class ArticleView extends StatelessWidget { @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, - ), - }; - }, + 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!'), + }; + }, + ), ), ); } @@ -212,6 +209,7 @@ 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({ @@ -225,7 +223,9 @@ class ArticlePage extends StatelessWidget { @override Widget build(BuildContext context) { - return Center(child: Text('Article content will be displayed here')); + return const Center( + child: Text('Article content will be displayed here...'), + ); } } ``` @@ -234,6 +234,7 @@ class ArticlePage extends StatelessWidget { Replace the placeholder with a scrollable column layout: + ```dart class ArticlePage extends StatelessWidget { const ArticlePage({ @@ -247,11 +248,9 @@ class ArticlePage extends StatelessWidget { @override Widget build(BuildContext context) { - return SingleChildScrollView( + return const SingleChildScrollView( child: Column( - children: [ - Text('Article content will be displayed here'), - ], + children: [Text('Article content will be displayed here...')], ), ); } @@ -262,6 +261,7 @@ class ArticlePage extends StatelessWidget { Complete the layout with an article widget and navigation button: + ```dart class ArticlePage extends StatelessWidget { const ArticlePage({ @@ -278,12 +278,10 @@ class ArticlePage extends StatelessWidget { return SingleChildScrollView( child: Column( children: [ - ArticleWidget( - summary: summary, - ), + ArticleWidget(summary: summary), ElevatedButton( onPressed: nextArticleCallback, - child: Text('Next random article'), + child: const Text('Next random article'), ), ], ), @@ -301,6 +299,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}); @@ -309,7 +308,7 @@ class ArticleWidget extends StatelessWidget { @override Widget build(BuildContext context) { - return Text('Article content will be displayed here'); + return const Text('Article content will be displayed here...'); } } ``` @@ -318,6 +317,7 @@ class ArticleWidget extends StatelessWidget { Wrap the content in proper padding and layout: + ```dart class ArticleWidget extends StatelessWidget { const ArticleWidget({super.key, required this.summary}); @@ -327,12 +327,10 @@ 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.0, - children: [ - Text('Article content will be displayed here'), - ], + spacing: 10, + children: [const Text('Article content will be displayed here...')], ), ); } @@ -343,6 +341,7 @@ class ArticleWidget extends StatelessWidget { Add the article image that only shows when available: + ```dart class ArticleWidget extends StatelessWidget { const ArticleWidget({super.key, required this.summary}); @@ -352,15 +351,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.0, + 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...'), ], ), ); @@ -373,6 +369,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}); @@ -382,28 +379,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.0, + 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), ], ), ); 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 93e39ba4d2..b1aa0b938f 100644 --- a/src/content/learn/pathway/tutorial/set-up-state-project.md +++ b/src/content/learn/pathway/tutorial/set-up-state-project.md @@ -86,116 +86,73 @@ $ 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. class Summary { /// Returns a new [Summary] instance. Summary({ required this.titles, - required this.pageid, + required this.pageId, required this.extract, required this.extractHtml, required this.lang, required this.dir, + required this.url, + this.description, this.thumbnail, this.originalImage, - this.url, - - this.description, }); - /// - TitlesSet titles; - - /// The page ID - int pageid; + /// The title information of this article. + final TitlesSet titles; - /// First several sentences of an article in plain text - String extract; + /// The page ID of this article. + final int pageId; - /// First several sentences of an article in simple HTML format - String extractHtml; + /// The first few sentences of the article in plain text. + final String extract; - ImageFile? thumbnail; + /// The first few sentences of the article in HTML format. + final String extractHtml; - /// Url to the article on Wikipedia - String? url; + /// The language code of the article's content, such as "en" for English. + final String lang; - /// - ImageFile? originalImage; + /// The text directionality of the article's content, such as "ltr" or "rtl". + final String dir; - /// The page language code - String lang; + /// The URL of the page. + final String url; - /// The page language direction code - String dir; + /// A description of the article, if available. + final String? description; - /// Wikidata description for the page - String? description; + /// A thumbnail-sized version of the article's primary image, if available. + final ImageFile? thumbnail; - bool get hasImage => - (originalImage != null || thumbnail != null) && preferredSource != null; - - String? get preferredSource { - ImageFile? file; - - if (originalImage != null) { - file = originalImage; - } else { - file = thumbnail; - } - - if (file != null) { - if (acceptableImageFormats.contains(file.extension.toLowerCase())) { - return file.source; - } else { - return null; - } - } + /// The original full-sized article's primary image, if available. + final ImageFile? originalImage; - return null; - } + /// Whether this article has an image. + bool get hasImage => originalImage != null && thumbnail != 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) { { '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, - '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 _}, @@ -203,18 +160,19 @@ class Summary { } => Summary( titles: TitlesSet.fromJson(titles), - pageid: pageid, + 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, + 'pageid': final int pageId, 'extract': final String extract, 'extract_html': final String extractHtml, 'lang': final String lang, @@ -227,7 +185,7 @@ class Summary { } => Summary( titles: TitlesSet.fromJson(titles), - pageid: pageid, + pageId: pageId, extract: extract, extractHtml: extractHtml, lang: lang, @@ -237,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, @@ -249,7 +207,7 @@ class Summary { } => Summary( titles: TitlesSet.fromJson(titles), - pageid: pageid, + pageId: pageId, extract: extract, extractHtml: extractHtml, lang: lang, @@ -264,7 +222,7 @@ class Summary { String toString() => 'Summary[' 'titles=$titles, ' - 'pageid=$pageid, ' + 'pageId=$pageId, ' 'extract=$extract, ' 'extractHtml=$extractHtml, ' 'thumbnail=${thumbnail ?? 'null'}, ' @@ -276,27 +234,27 @@ 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}); - /// Original image URI - String source; + /// The URI of the original image. + final String source; - /// Original image width - int width; + /// The width of the original image. + final int width; - /// Original image height - int 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, @@ -305,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, @@ -331,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 - String canonical; + /// 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 _ - String normalized; + /// 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 - String display; + /// 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, @@ -374,12 +333,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 +359,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...')), ), ); }