From b68a527d5a0690aa7a2216b76df5e9ca610c623a Mon Sep 17 00:00:00 2001 From: Miguel Beltran Date: Fri, 22 Nov 2024 14:34:19 +0100 Subject: [PATCH 01/33] WIP command pattern document --- .../command/analysis_options.yaml | 5 + .../architecture/command/lib/main.dart | 158 ++++++ .../architecture/command/pubspec.yaml | 16 + .../architecture/result/analysis_options.yaml | 5 + .../architecture/result/lib/main.dart | 67 +++ .../architecture/result/lib/result.dart | 54 ++ .../cookbook/architecture/result/pubspec.yaml | 16 + src/content/cookbook/architecture/command.md | 468 ++++++++++++++++++ src/content/cookbook/architecture/result.md | 9 + 9 files changed, 798 insertions(+) create mode 100644 examples/cookbook/architecture/command/analysis_options.yaml create mode 100644 examples/cookbook/architecture/command/lib/main.dart create mode 100644 examples/cookbook/architecture/command/pubspec.yaml create mode 100644 examples/cookbook/architecture/result/analysis_options.yaml create mode 100644 examples/cookbook/architecture/result/lib/main.dart create mode 100644 examples/cookbook/architecture/result/lib/result.dart create mode 100644 examples/cookbook/architecture/result/pubspec.yaml create mode 100644 src/content/cookbook/architecture/command.md create mode 100644 src/content/cookbook/architecture/result.md diff --git a/examples/cookbook/architecture/command/analysis_options.yaml b/examples/cookbook/architecture/command/analysis_options.yaml new file mode 100644 index 0000000000..eee60e0f5a --- /dev/null +++ b/examples/cookbook/architecture/command/analysis_options.yaml @@ -0,0 +1,5 @@ +# Take our settings from the example_utils analysis_options.yaml file. +# If necessary for a particular example, this file can also include +# overrides for individual lints. + +include: package:example_utils/analysis.yaml diff --git a/examples/cookbook/architecture/command/lib/main.dart b/examples/cookbook/architecture/command/lib/main.dart new file mode 100644 index 0000000000..bff0edf2d8 --- /dev/null +++ b/examples/cookbook/architecture/command/lib/main.dart @@ -0,0 +1,158 @@ +import 'package:flutter/material.dart'; + +void main() { + runApp( + MainApp( + viewModel: HomeViewModel(), + ), + ); +} + +class MainApp extends StatelessWidget { + const MainApp({ + super.key, + required this.viewModel, + }); + + final HomeViewModel viewModel; + + @override + Widget build(BuildContext context) { + return MaterialApp( + home: Scaffold( + body: ListenableBuilder( + listenable: viewModel.load, + builder: (context, child) { + if (viewModel.load.running) { + return const Center( + child: CircularProgressIndicator(), + ); + } + + if (viewModel.load.error != null) { + return Center( + child: Text('Error: ${viewModel.load.error}'), + ); + } + + return child!; + }, + child: ListenableBuilder( + listenable: viewModel, + builder: (context, _) { + if (viewModel.user == null) { + return const Center( + child: Text('No user'), + ); + } + + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text('Name: ${viewModel.user!.name}'), + Text('Email: ${viewModel.user!.email}'), + ], + ), + ); + }, + ), + ), + ), + ); + } +} + +class User { + User({required this.name, required this.email}); + + final String name; + final String email; +} + +class HomeViewModel extends ChangeNotifier { + HomeViewModel() { + load = Command(_load)..execute(); + } + + User? _user; + User? get user => _user; + + late final Command load; + + Future _load() async { + // load user + await Future.delayed(const Duration(seconds: 2)); + _user = User(name: 'John Doe', email: 'john@example.com'); + notifyListeners(); + } +} + +// #docregion HomeViewModel2 +// #docregion getUser +// #docregion load1 +// #docregion UiState1 +class HomeViewModel2 extends ChangeNotifier { + // #enddocregion HomeViewModel2 + + User? get user => null; + // #enddocregion getUser + // #enddocregion load1 + + bool get running => false; + + Exception? get error => null; + + // #docregion load1 + void load() { + // load user + } + // #docregion HomeViewModel2 + // #docregion getUser +} +// #enddocregion HomeViewModel2 +// #enddocregion getUser +// #enddocregion load1 +// #enddocregion UiState1 + +class Command extends ChangeNotifier { + Command(this._action); + + bool _running = false; + bool get running => _running; + + Exception? _error; + Exception? get error => _error; + + bool _completed = false; + bool get completed => _completed; + + final Future Function() _action; + + Future execute() async { + if (_running) { + return; + } + + _running = true; + _completed = false; + _error = null; + notifyListeners(); + + try { + await _action(); + _completed = true; + } on Exception catch (error) { + _error = error; + } finally { + _running = false; + notifyListeners(); + } + } + + void clear() { + _running = false; + _error = null; + _completed = false; + } +} diff --git a/examples/cookbook/architecture/command/pubspec.yaml b/examples/cookbook/architecture/command/pubspec.yaml new file mode 100644 index 0000000000..02ace9942d --- /dev/null +++ b/examples/cookbook/architecture/command/pubspec.yaml @@ -0,0 +1,16 @@ +name: command +description: Example for command cookbook recipe + +environment: + sdk: ^3.5.0 + +dependencies: + flutter: + sdk: flutter + +dev_dependencies: + example_utils: + path: ../../../example_utils + +flutter: + uses-material-design: true diff --git a/examples/cookbook/architecture/result/analysis_options.yaml b/examples/cookbook/architecture/result/analysis_options.yaml new file mode 100644 index 0000000000..eee60e0f5a --- /dev/null +++ b/examples/cookbook/architecture/result/analysis_options.yaml @@ -0,0 +1,5 @@ +# Take our settings from the example_utils analysis_options.yaml file. +# If necessary for a particular example, this file can also include +# overrides for individual lints. + +include: package:example_utils/analysis.yaml diff --git a/examples/cookbook/architecture/result/lib/main.dart b/examples/cookbook/architecture/result/lib/main.dart new file mode 100644 index 0000000000..66e7674788 --- /dev/null +++ b/examples/cookbook/architecture/result/lib/main.dart @@ -0,0 +1,67 @@ +import 'package:flutter/material.dart'; + +import 'result.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('Hello World!'), + ), + ), + ); + } +} + +class UserProfile { + final String name; + final String email; + + UserProfile(this.name, this.email); +} + +class UserProfileRepository { + final ApiClientService _apiClientService; + final DatabaseService _databaseService; + + UserProfileRepository( + this._apiClientService, + this._databaseService, + ); + + Future> getUserProfile() async { + final apiResult = await _apiClientService.getUserProfile(); + if (apiResult is Ok) { + return apiResult; + } + + final databaseResult = await _databaseService.createTemporalUser(); + if (databaseResult is Ok) { + return databaseResult; + } + + return Result.error(Exception('Failed to get user profile')); + } +} + +class ApiClientService { + Future> getUserProfile() async { + await Future.delayed(const Duration(seconds: 2)); + return Result.ok(UserProfile('John Doe', 'john@example.com')); + } +} + +class DatabaseService { + Future> createTemporalUser() async { + await Future.delayed(const Duration(seconds: 2)); + return Result.ok(UserProfile('John Doe', 'john@example.com')); + } +} diff --git a/examples/cookbook/architecture/result/lib/result.dart b/examples/cookbook/architecture/result/lib/result.dart new file mode 100644 index 0000000000..5370700b03 --- /dev/null +++ b/examples/cookbook/architecture/result/lib/result.dart @@ -0,0 +1,54 @@ +// Copyright 2024 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/// Utility class to wrap result data +/// +/// Evaluate the result using a switch statement: +/// ```dart +/// switch (result) { +/// case Ok(): { +/// print(result.value); +/// } +/// case Error(): { +/// print(result.error); +/// } +/// } +/// ``` +sealed class Result { + const Result(); + + /// Creates an instance of Result containing a value + factory Result.ok(T value) => Ok(value); + + /// Create an instance of Result containing an error + factory Result.error(Exception error) => Error(error); + + /// Convenience method to cast to Ok + Ok get asOk => this as Ok; + + /// Convenience method to cast to Error + Error get asError => this as Error; +} + +/// Subclass of Result for values +final class Ok extends Result { + const Ok(this.value); + + /// Returned value in result + final T value; + + @override + String toString() => 'Result<$T>.ok($value)'; +} + +/// Subclass of Result for errors +final class Error extends Result { + const Error(this.error); + + /// Returned error in result + final Exception error; + + @override + String toString() => 'Result<$T>.error($error)'; +} diff --git a/examples/cookbook/architecture/result/pubspec.yaml b/examples/cookbook/architecture/result/pubspec.yaml new file mode 100644 index 0000000000..e4563a19af --- /dev/null +++ b/examples/cookbook/architecture/result/pubspec.yaml @@ -0,0 +1,16 @@ +name: result +description: Example for result cookbook recipe + +environment: + sdk: ^3.5.0 + +dependencies: + flutter: + sdk: flutter + +dev_dependencies: + example_utils: + path: ../../../example_utils + +flutter: + uses-material-design: true diff --git a/src/content/cookbook/architecture/command.md b/src/content/cookbook/architecture/command.md new file mode 100644 index 0000000000..ac5bf7c81e --- /dev/null +++ b/src/content/cookbook/architecture/command.md @@ -0,0 +1,468 @@ +--- +title: Command pattern +description: Improve ViewModels with Commands +js: + - defer: true + url: /assets/js/inject_dartpad.js +--- + + + +Model-View-ViewModel (MVVM) is a design pattern +that separates a feature of an application into three parts: +the `Model`, the `ViewModel` and the `View`. +Views and ViewModels make up the UI layer of an application. +Repositories and services represent the data of an application, +or the Model layer of MVVM. + +ViewModels can become very complex +as an application grows +and features become bigger. +The command pattern helps to streamline running actions in ViewModels +by encapsulating some of its complexity and avoiding code repetition. + +In this guide, you will learn +how to use the command pattern +to improve your ViewModels. + +# Challenges when implementing ViewModels + +ViewModel classes in Flutter are typically implemented +by extending the ChangeNotifier class. +This allows ViewModels to call `notifyListeners()` to refresh Views +when data is updated. + + +```dart +class HomeViewModel extends ChangeNotifier { + // ··· +} +``` + +ViewModels contain a representation of the UI state, +including the data being displayed. +For example, this `HomeViewModel` exposes the `User` instance to the View. + + +```dart +class HomeViewModel extends ChangeNotifier { + + User? get user => // ... + // ··· +} +``` + +ViewModels also contain actions typically triggered by the View. +For example, a `load` action in charge of loading the `user`. + + +```dart +class HomeViewModel extends ChangeNotifier { + + User? get user => // ... + // ··· + void load() { + // load user + } +} +``` + +### UI state in ViewModels + +Besides data, ViewModels also contain other types of UI state. +For example, a `running` state or an `error` state. +This allows the View to show to the user +if the action is still running +or if it was completed successfully. + + +```dart +class HomeViewModel extends ChangeNotifier { + + User? get user => // ... + + bool get running => // ... + + Exception? get error => // ... + + void load() { + // load user + } +} +``` + +You can use the running state to display a progress indicator in the View: + +ListenableBuilder( + listenable: viewModel, + builder: (context, child) { + if (viewModel.running) { + return const Center( + child: CircularProgressIndicator(), + ); + } + + // rest of builder + }, +) + +Or use the running state to avoid executing the action multiple times: + +void load() { + if (running) return; + + // load user +} + +Managing the state of an action can get complicated once the ViewModel contains multiple actions. For example, adding an edit() action to the HomeViewModel can lead the following outcome: + +class HomeViewModel extends ChangeNotifier { + + User? get user => ...; + + bool get runningLoad => ...; + + Exception? get errorLoad => ...; + + bool get runningEdit => ...; + + Exception? get errorEdit => ...; + + void load() { + // load user + } + + Void edit(String name) { + // edit user + } +} + +Sharing the running state between the load() and edit() actions may not always work, because you may want to show a different UI component when the load() action runs than when the edit() action runs, and the same problem with the error state. +Triggering UI actions from ViewModels +Another challenge with ViewModel classes is executing UI actions when the ViewModel state changes. For example, you may want to show a SnackBar when an error occurs, or navigate to a different screen when an action completes. + +To implement that, you have to listen to the changes in the ViewModel, and perform the action depending on the state. For example, in the View: + +viewModel.addListener(() { + if (viewModel.error != null) { + // Show SnackBar + } +}); + +With this approach you also need to clear the error state each time you execute this action, otherwise this action will happen each time notifyListeners() is called. + +viewModel.addListener(() { + if (viewModel.error != null) { + viewModel.clearError(); + + // Show SnackBar + } +}); +Command Pattern +You may find yourself repeating the above code over and over, having to implement a different running state for each action in every ViewModel. At that point, it makes sense to extract this code into a reusable pattern: a Command. + +A Command is a class that encapsulates a ViewModel action, and exposes the different states that an action can have. + +class Command extends ChangeNotifier { + Command(this._action); + + bool get running => ...; + + Exception? get error => ...; + + bool get completed => ...; + + Future Function() _action; + + Future execute() async { + // run _action + } + + void clear() { + // clear state + } +} + +In the ViewModel, instead of defining an action directly with a method, you create a Command object: + +class HomeViewModel extends ChangeNotifier { + HomeViewModel() { + load = Command(_load); + } + + User? get user => ...; + + late final Command load; + + void _load() { + // load user + } +} + +The previous load() method becomes _load(), and instead the Command load gets exposed to the View. The previous running and error states can be removed, as they are now part of the Command. +Executing a Command +Instead of calling viewModel.load() to run the load action, now you have to call viewModel.load.execute(). + +The execute() method can also be called from within the ViewModel. For example, to run the load Command when the ViewModel is created: + +HomeViewModel() { + load = Command(_load)..execute(); +} + +The execute() method sets the running state to true and resets the error and completed states. When the action finishes, the running state will change to false and the completed state will be true. + +When the running state is true, it prevents the Command from being launched multiple times before completing. This prevents users from attempting to tap multiple times on a button while an action is executing. + +The Command’s execute() method could also capture any thrown Exceptions by the action implementation automatically and expose them in the error state. + +The following is what a command class might look like. It’s been simplified for demo purposes. You can see a full implementation at the end of this page. + +class Command extends ChangeNotifier { + Command(this._action); + + bool _running = false; + bool get running => _running; + + Exception? _error; + Exception? get error => _error; + + bool _completed = false; + bool get completed => _completed; + + final Future Function() _action; + + Future execute() async { + if (_running) { + return; + } + + _running = true; + _completed = false; + _error = null; + notifyListeners(); + + try { + await _action(); + _completed = true; + } on Exception catch (error) { + _error = error; + } finally { + _running = false; + notifyListeners(); + } + } + + void clear() { + _running = false; + _error = null; + _completed = false; + } +} +Listening to the Command state +The Command class extends from ChangeNotifier, allowing Views to listen to its states. + +In the ListenableBuilder, instead of passing the ViewModel as listenable, pass the Command: + +ListenableBuilder( + listenable: viewModel.load, + builder: (context, child) { + if (viewModel.load.running) { + return const Center( + child: CircularProgressIndicator(), + ); + } + + // rest of builder + }, +) + +As well listen to changes in the Command state in order to run UI actions: + +viewModel.load.addListener(() { + if (viewModel.load.error != null) { + viewModel.load.clear(); + + // Show SnackBar + } +}); + +Combining Command and ViewModel +When combined with the ViewModel, use the child argument to stack multiple ListenableBuilder. For example, you can listen to the running and error states of the Command before showing the ViewModel data. + +ListenableBuilder( + listenable: viewModel.load, + builder: (context, child) { + if (viewModel.load.running) { + return const Center( + child: CircularProgressIndicator(), + ); + } + + if (viewModel.load.error != null) { + return Center( + child: ErrorIndicator(error: viewModel.load.error), + ); + } + + return child; + }, + child: ListenableBuilder( + listenable: viewModel, + builder: (context, _) { + // rest of the screen + }, + ), +) + +You can have multiple Commands in a single ViewModel, simplifying the implementation of ViewModels and minimizing the amount of repeated code. + +class HomeViewModel extends ChangeNotifier { + HomeViewModel() { + load = Command(_load)..execute(); + delete = Command(_delete); + } + + User? get user => ...; + + late final Command load; + late final Command delete; + + Future _load() async { + // load user + } + + Future _delete() async { + // delete user + } +} + +Extending the Command pattern +The Command pattern can be extended in multiple ways, for example, you can have different implementations to support a different number of arguments. + +class HomeViewModel extends ChangeNotifier { + HomeViewModel() { + load = Command0(_load)..execute(); + edit = Command1(_edit); + } + + User? get user => ...; + + // Command0 accepts 0 arguments + late final Command0 load; + + // Command1 accepts 1 argument + late final Command1 edit; + + Future _load() async { + // load user + } + + Future _edit(String name) async { + // edit user + } +} +Putting it all together +In this guide, you have learned how to use the Command design pattern to improve the implementation of ViewModels when using the MVVM design pattern. + +Below you can find the full Command class as implemented in the Compass App example for the Flutter Architecture guidelines. It also uses the Result class, which you can learn more about in the Architecture Cookbook Recipe: Result Class. + +This implementation also includes two types of Command, a Command0, for actions without parameters, and a Command1, which takes one parameter. + +You can also check on pub.dev for different ready-to-use implementations of the Command pattern, like the flutter_command package. + +import 'dart:async'; + +import 'package:flutter/foundation.dart'; + +import 'result.dart'; + +typedef CommandAction0 = Future> Function(); +typedef CommandAction1 = Future> Function(A); + +/// Facilitates interaction with a ViewModel. +/// +/// Encapsulates an action, +/// exposes its running and error states, +/// and ensures that it can't be launched again until it finishes. +/// +/// Use [Command0] for actions without arguments. +/// Use [Command1] for actions with one argument. +/// +/// Actions must return a [Result]. +/// +/// Consume the action result by listening to changes, +/// then call to [clearResult] when the state is consumed. +abstract class Command extends ChangeNotifier { + Command(); + + bool _running = false; + + /// True when the action is running. + bool get running => _running; + + Result? _result; + + /// true if action completed with error + bool get error => _result is Error; + + /// true if action completed successfully + bool get completed => _result is Ok; + + /// Get last action result + Result? get result => _result; + + /// Clear last action result + void clearResult() { + _result = null; + notifyListeners(); + } + + /// Internal execute implementation + Future _execute(CommandAction0 action) async { + // Ensure the action can't launch multiple times. + // e.g. avoid multiple taps on button + if (_running) return; + + // Notify listeners. + // e.g. button shows loading state + _running = true; + _result = null; + notifyListeners(); + + try { + _result = await action(); + } finally { + _running = false; + notifyListeners(); + } + } +} + +/// [Command] without arguments. +/// Takes a [CommandAction0] as action. +class Command0 extends Command { + Command0(this._action); + + final CommandAction0 _action; + + /// Executes the action. + Future execute() async { + await _execute(() => _action()); + } +} + +/// [Command] with one argument. +/// Takes a [CommandAction1] as action. +class Command1 extends Command { + Command1(this._action); + + final CommandAction1 _action; + + /// Executes the action with the argument. + Future execute(A argument) async { + await _execute(() => _action(argument)); + } +} + + + diff --git a/src/content/cookbook/architecture/result.md b/src/content/cookbook/architecture/result.md new file mode 100644 index 0000000000..d5bf6f17ff --- /dev/null +++ b/src/content/cookbook/architecture/result.md @@ -0,0 +1,9 @@ +--- +title: "Persistent storage architecture: SQL" +description: Create a service to store complex data with SQL +js: + - defer: true + url: /assets/js/inject_dartpad.js +--- + + From 31f8faf0daa863e8f4fa9eda87e63cbb1dd6e28d Mon Sep 17 00:00:00 2001 From: Miguel Beltran Date: Mon, 25 Nov 2024 15:57:03 +0100 Subject: [PATCH 02/33] wip command docs --- .../architecture/command/lib/main.dart | 39 ++++++++++ src/content/cookbook/architecture/command.md | 75 ++++++++++++++----- 2 files changed, 94 insertions(+), 20 deletions(-) diff --git a/examples/cookbook/architecture/command/lib/main.dart b/examples/cookbook/architecture/command/lib/main.dart index bff0edf2d8..b2190dbc60 100644 --- a/examples/cookbook/architecture/command/lib/main.dart +++ b/examples/cookbook/architecture/command/lib/main.dart @@ -20,6 +20,7 @@ class MainApp extends StatelessWidget { Widget build(BuildContext context) { return MaterialApp( home: Scaffold( + // #docregion ListenableBuilder body: ListenableBuilder( listenable: viewModel.load, builder: (context, child) { @@ -28,6 +29,7 @@ class MainApp extends StatelessWidget { child: CircularProgressIndicator(), ); } + // #enddocregion ListenableBuilder if (viewModel.load.error != null) { return Center( @@ -57,7 +59,9 @@ class MainApp extends StatelessWidget { ); }, ), + // #docregion ListenableBuilder ), + // #enddocregion ListenableBuilder ), ); } @@ -107,6 +111,19 @@ class HomeViewModel2 extends ChangeNotifier { void load() { // load user } + // #enddocregion load1 + + // #docregion load2 + void load2() { + if (running) { + return; + } + + // load user + } + // #enddocregion load2 + + // #docregion load1 // #docregion HomeViewModel2 // #docregion getUser } @@ -115,6 +132,28 @@ class HomeViewModel2 extends ChangeNotifier { // #enddocregion load1 // #enddocregion UiState1 +// #docregion HomeViewModel3 +class HomeViewModel3 extends ChangeNotifier { + User? get user => null; + + bool get runningLoad => false; + + Exception? get errorLoad => null; + + bool get runningEdit => false; + + Exception? get errorEdit => null; + + void load() { + // load user + } + + void edit(String name) { + // edit user + } +} +// #enddocregion HomeViewModel3 + class Command extends ChangeNotifier { Command(this._action); diff --git a/src/content/cookbook/architecture/command.md b/src/content/cookbook/architecture/command.md index ac5bf7c81e..1b9807e450 100644 --- a/src/content/cookbook/architecture/command.md +++ b/src/content/cookbook/architecture/command.md @@ -64,6 +64,7 @@ class HomeViewModel extends ChangeNotifier { void load() { // load user } + // ··· } ``` @@ -88,11 +89,22 @@ class HomeViewModel extends ChangeNotifier { void load() { // load user } + + void load() { + if (running) { + return; + } + + // load user + } + } ``` You can use the running state to display a progress indicator in the View: + +```dart ListenableBuilder( listenable: viewModel, builder: (context, child) { @@ -101,47 +113,69 @@ ListenableBuilder( child: CircularProgressIndicator(), ); } - - // rest of builder - }, + // ··· ) +``` Or use the running state to avoid executing the action multiple times: -void load() { - if (running) return; + +```dart +void load2() { + if (running) { + return; + } // load user } +``` -Managing the state of an action can get complicated once the ViewModel contains multiple actions. For example, adding an edit() action to the HomeViewModel can lead the following outcome: +Managing the state of an action can get complicated +once the ViewModel contains multiple actions. +For example, adding an `edit()` action to the `HomeViewModel` +can lead the following outcome: + +```dart class HomeViewModel extends ChangeNotifier { - - User? get user => ...; + User? get user => // ... - bool get runningLoad => ...; + bool get runningLoad => // ... - Exception? get errorLoad => ...; + Exception? get errorLoad => // ... - bool get runningEdit => ...; + bool get runningEdit => // ... - Exception? get errorEdit => ...; + Exception? get errorEdit => // ... void load() { // load user } - - Void edit(String name) { + + void edit(String name) { // edit user } } +``` + +Sharing the running state +between the `load()` and `edit()` actions might not always work, +because you might want to show a different UI component +when the `load()` action runs than when the `edit()` action runs, +and the same problem with the `error` state. + +### Triggering UI actions from ViewModels + +Another challenge with ViewModel classes +is executing UI actions +hen the ViewModel state changes. +For example, to show a SnackBar when an error occurs, +or to navigate to a different screen when an action completes. -Sharing the running state between the load() and edit() actions may not always work, because you may want to show a different UI component when the load() action runs than when the edit() action runs, and the same problem with the error state. -Triggering UI actions from ViewModels -Another challenge with ViewModel classes is executing UI actions when the ViewModel state changes. For example, you may want to show a SnackBar when an error occurs, or navigate to a different screen when an action completes. +To implement that, listen to the changes in the ViewModel, +and perform the action depending on the state. -To implement that, you have to listen to the changes in the ViewModel, and perform the action depending on the state. For example, in the View: +For example, in the View: viewModel.addListener(() { if (viewModel.error != null) { @@ -149,7 +183,8 @@ viewModel.addListener(() { } }); -With this approach you also need to clear the error state each time you execute this action, otherwise this action will happen each time notifyListeners() is called. +You need to clear the error state each time you execute this action, +otherwise this action will happen each time `notifyListeners()` is called. viewModel.addListener(() { if (viewModel.error != null) { @@ -159,7 +194,7 @@ viewModel.addListener(() { } }); Command Pattern -You may find yourself repeating the above code over and over, having to implement a different running state for each action in every ViewModel. At that point, it makes sense to extract this code into a reusable pattern: a Command. +You might find yourself repeating the above code over and over, having to implement a different running state for each action in every ViewModel. At that point, it makes sense to extract this code into a reusable pattern: a Command. A Command is a class that encapsulates a ViewModel action, and exposes the different states that an action can have. From e54e9a4a71b3a268dc62c91b3e6a10f18dcaaeb5 Mon Sep 17 00:00:00 2001 From: Miguel Beltran Date: Mon, 25 Nov 2024 16:23:25 +0100 Subject: [PATCH 03/33] moved examples without Command to a different file --- .../architecture/command/lib/main.dart | 45 ++++-- .../architecture/command/lib/no_command.dart | 151 ++++++++++++++++++ src/content/cookbook/architecture/command.md | 81 +++++++--- 3 files changed, 242 insertions(+), 35 deletions(-) create mode 100644 examples/cookbook/architecture/command/lib/no_command.dart diff --git a/examples/cookbook/architecture/command/lib/main.dart b/examples/cookbook/architecture/command/lib/main.dart index b2190dbc60..7e9f700e3f 100644 --- a/examples/cookbook/architecture/command/lib/main.dart +++ b/examples/cookbook/architecture/command/lib/main.dart @@ -8,7 +8,7 @@ void main() { ); } -class MainApp extends StatelessWidget { +class MainApp extends StatefulWidget { const MainApp({ super.key, required this.viewModel, @@ -16,33 +16,52 @@ class MainApp extends StatelessWidget { final HomeViewModel viewModel; + @override + State createState() => _MainAppState(); +} + +class _MainAppState extends State { + // #docregion addListener + @override + void initState() { + super.initState(); + widget.viewModel.addListener(_onViewModelChanged); + } + + @override + void dispose() { + widget.viewModel.removeListener(_onViewModelChanged); + super.dispose(); + } + // #enddocregion addListener + @override Widget build(BuildContext context) { return MaterialApp( home: Scaffold( // #docregion ListenableBuilder body: ListenableBuilder( - listenable: viewModel.load, + listenable: widget.viewModel.load, builder: (context, child) { - if (viewModel.load.running) { + if (widget.viewModel.load.running) { return const Center( child: CircularProgressIndicator(), ); } // #enddocregion ListenableBuilder - if (viewModel.load.error != null) { + if (widget.viewModel.load.error != null) { return Center( - child: Text('Error: ${viewModel.load.error}'), + child: Text('Error: ${widget.viewModel.load.error}'), ); } return child!; }, child: ListenableBuilder( - listenable: viewModel, + listenable: widget.viewModel, builder: (context, _) { - if (viewModel.user == null) { + if (widget.viewModel.user == null) { return const Center( child: Text('No user'), ); @@ -52,8 +71,8 @@ class MainApp extends StatelessWidget { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Text('Name: ${viewModel.user!.name}'), - Text('Email: ${viewModel.user!.email}'), + Text('Name: ${widget.viewModel.user!.name}'), + Text('Email: ${widget.viewModel.user!.email}'), ], ), ); @@ -65,6 +84,14 @@ class MainApp extends StatelessWidget { ), ); } + + // #docregion addListener + void _onViewModelChanged() { + if (widget.viewModel.load.error != null) { + // Show Snackbar + } + } + // #enddocregion addListener } class User { diff --git a/examples/cookbook/architecture/command/lib/no_command.dart b/examples/cookbook/architecture/command/lib/no_command.dart new file mode 100644 index 0000000000..15756222eb --- /dev/null +++ b/examples/cookbook/architecture/command/lib/no_command.dart @@ -0,0 +1,151 @@ +import 'package:flutter/material.dart'; + +class MainApp extends StatefulWidget { + const MainApp({ + super.key, + required this.viewModel, + }); + + final HomeViewModel2 viewModel; + + @override + State createState() => _MainAppState(); +} + +class _MainAppState extends State { + // #docregion addListener + @override + void initState() { + super.initState(); + widget.viewModel.addListener(_onViewModelChanged); + } + + @override + void dispose() { + widget.viewModel.removeListener(_onViewModelChanged); + super.dispose(); + } + // #enddocregion addListener + + @override + Widget build(BuildContext context) { + return MaterialApp( + home: Scaffold( + // #docregion ListenableBuilder + body: ListenableBuilder( + listenable: widget.viewModel, + builder: (context, _) { + if (widget.viewModel.running) { + return const Center( + child: CircularProgressIndicator(), + ); + } + // #enddocregion ListenableBuilder + + if (widget.viewModel.error != null) { + return Center( + child: Text('Error: ${widget.viewModel.load.error}'), + ); + } + + if (widget.viewModel.user == null) { + return const Center( + child: Text('No user'), + ); + } + + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text('Name: ${widget.viewModel.user!.name}'), + Text('Email: ${widget.viewModel.user!.email}'), + ], + ), + ); + // #docregion ListenableBuilder + }, + ), + // #enddocregion ListenableBuilder + ), + ); + } + + // #docregion _onViewModelChanged + void _onViewModelChanged() { + if (widget.viewModel.error != null) { + widget.viewModel.clearError(); + // Show Snackbar + } + } + // #enddocregion _onViewModelChanged +} + +class User { + User({required this.name, required this.email}); + + final String name; + final String email; +} + +// #docregion HomeViewModel2 +// #docregion getUser +// #docregion load1 +// #docregion UiState1 +class HomeViewModel2 extends ChangeNotifier { + // #enddocregion HomeViewModel2 + + User? get user => null; + // #enddocregion getUser + // #enddocregion load1 + + bool get running => false; + + Exception? get error => null; + + // #docregion load1 + void load() { + // load user + } + // #enddocregion load1 + + // #docregion load2 + void load2() { + if (running) { + return; + } + // load user + } + + // #enddocregion load2 + void clearError() {} + // #docregion load1 + // #docregion HomeViewModel2 + // #docregion getUser +} +// #enddocregion HomeViewModel2 +// #enddocregion getUser +// #enddocregion load1 +// #enddocregion UiState1 + +// #docregion HomeViewModel3 +class HomeViewModel3 extends ChangeNotifier { + User? get user => null; + + bool get runningLoad => false; + + Exception? get errorLoad => null; + + bool get runningEdit => false; + + Exception? get errorEdit => null; + + void load() { + // load user + } + + void edit(String name) { + // edit user + } +} +// #enddocregion HomeViewModel3 diff --git a/src/content/cookbook/architecture/command.md b/src/content/cookbook/architecture/command.md index 1b9807e450..f1df32ce58 100644 --- a/src/content/cookbook/architecture/command.md +++ b/src/content/cookbook/architecture/command.md @@ -32,7 +32,7 @@ by extending the ChangeNotifier class. This allows ViewModels to call `notifyListeners()` to refresh Views when data is updated. - + ```dart class HomeViewModel extends ChangeNotifier { // ··· @@ -43,7 +43,7 @@ ViewModels contain a representation of the UI state, including the data being displayed. For example, this `HomeViewModel` exposes the `User` instance to the View. - + ```dart class HomeViewModel extends ChangeNotifier { @@ -55,7 +55,7 @@ class HomeViewModel extends ChangeNotifier { ViewModels also contain actions typically triggered by the View. For example, a `load` action in charge of loading the `user`. - + ```dart class HomeViewModel extends ChangeNotifier { @@ -76,7 +76,7 @@ This allows the View to show to the user if the action is still running or if it was completed successfully. - + ```dart class HomeViewModel extends ChangeNotifier { @@ -94,26 +94,27 @@ class HomeViewModel extends ChangeNotifier { if (running) { return; } - // load user } + void clearError() {} } ``` You can use the running state to display a progress indicator in the View: - + ```dart ListenableBuilder( - listenable: viewModel, - builder: (context, child) { - if (viewModel.running) { + listenable: widget.viewModel, + builder: (context, _) { + if (widget.viewModel.running) { return const Center( child: CircularProgressIndicator(), ); } - // ··· + // ··· + }, ) ``` @@ -135,7 +136,7 @@ once the ViewModel contains multiple actions. For example, adding an `edit()` action to the `HomeViewModel` can lead the following outcome: - + ```dart class HomeViewModel extends ChangeNotifier { User? get user => // ... @@ -169,34 +170,62 @@ and the same problem with the `error` state. Another challenge with ViewModel classes is executing UI actions hen the ViewModel state changes. -For example, to show a SnackBar when an error occurs, + +For example, to show a `SnackBar` when an error occurs, or to navigate to a different screen when an action completes. To implement that, listen to the changes in the ViewModel, and perform the action depending on the state. -For example, in the View: +In the View: -viewModel.addListener(() { - if (viewModel.error != null) { - // Show SnackBar + +```dart +@override +void initState() { + super.initState(); + widget.viewModel.addListener(_onViewModelChanged); +} + +@override +void dispose() { + widget.viewModel.removeListener(_onViewModelChanged); + super.dispose(); +} +``` + + +```dart +void _onViewModelChanged() { + if (widget.viewModel.error != null) { + // Show Snackbar } -}); +} +``` You need to clear the error state each time you execute this action, otherwise this action will happen each time `notifyListeners()` is called. -viewModel.addListener(() { - if (viewModel.error != null) { - viewModel.clearError(); - - // Show SnackBar + +```dart +void _onViewModelChanged() { + if (widget.viewModel.error != null) { + widget.viewModel.clearError(); + // Show Snackbar } -}); -Command Pattern -You might find yourself repeating the above code over and over, having to implement a different running state for each action in every ViewModel. At that point, it makes sense to extract this code into a reusable pattern: a Command. +} +``` + +## Command Pattern + +You might find yourself repeating the above code over and over, +having to implement a different running state +for each action in every ViewModel. +At that point, it makes sense to extract this code +into a reusable pattern: a command. -A Command is a class that encapsulates a ViewModel action, and exposes the different states that an action can have. +A command is a class that encapsulates a ViewModel action, +and exposes the different states that an action can have. class Command extends ChangeNotifier { Command(this._action); From 7b9697c75521476395e2bf4b8172308697deba1b Mon Sep 17 00:00:00 2001 From: Miguel Beltran Date: Mon, 25 Nov 2024 16:46:29 +0100 Subject: [PATCH 04/33] cleanup --- .../architecture/command/lib/main.dart | 70 ------------------- 1 file changed, 70 deletions(-) diff --git a/examples/cookbook/architecture/command/lib/main.dart b/examples/cookbook/architecture/command/lib/main.dart index 7e9f700e3f..963fd69615 100644 --- a/examples/cookbook/architecture/command/lib/main.dart +++ b/examples/cookbook/architecture/command/lib/main.dart @@ -21,7 +21,6 @@ class MainApp extends StatefulWidget { } class _MainAppState extends State { - // #docregion addListener @override void initState() { super.initState(); @@ -33,13 +32,11 @@ class _MainAppState extends State { widget.viewModel.removeListener(_onViewModelChanged); super.dispose(); } - // #enddocregion addListener @override Widget build(BuildContext context) { return MaterialApp( home: Scaffold( - // #docregion ListenableBuilder body: ListenableBuilder( listenable: widget.viewModel.load, builder: (context, child) { @@ -48,7 +45,6 @@ class _MainAppState extends State { child: CircularProgressIndicator(), ); } - // #enddocregion ListenableBuilder if (widget.viewModel.load.error != null) { return Center( @@ -78,20 +74,16 @@ class _MainAppState extends State { ); }, ), - // #docregion ListenableBuilder ), - // #enddocregion ListenableBuilder ), ); } - // #docregion addListener void _onViewModelChanged() { if (widget.viewModel.load.error != null) { // Show Snackbar } } - // #enddocregion addListener } class User { @@ -119,68 +111,6 @@ class HomeViewModel extends ChangeNotifier { } } -// #docregion HomeViewModel2 -// #docregion getUser -// #docregion load1 -// #docregion UiState1 -class HomeViewModel2 extends ChangeNotifier { - // #enddocregion HomeViewModel2 - - User? get user => null; - // #enddocregion getUser - // #enddocregion load1 - - bool get running => false; - - Exception? get error => null; - - // #docregion load1 - void load() { - // load user - } - // #enddocregion load1 - - // #docregion load2 - void load2() { - if (running) { - return; - } - - // load user - } - // #enddocregion load2 - - // #docregion load1 - // #docregion HomeViewModel2 - // #docregion getUser -} -// #enddocregion HomeViewModel2 -// #enddocregion getUser -// #enddocregion load1 -// #enddocregion UiState1 - -// #docregion HomeViewModel3 -class HomeViewModel3 extends ChangeNotifier { - User? get user => null; - - bool get runningLoad => false; - - Exception? get errorLoad => null; - - bool get runningEdit => false; - - Exception? get errorEdit => null; - - void load() { - // load user - } - - void edit(String name) { - // edit user - } -} -// #enddocregion HomeViewModel3 - class Command extends ChangeNotifier { Command(this._action); From 78c717db08df59d405dcdc2d18b1ea857c2da6bc Mon Sep 17 00:00:00 2001 From: Miguel Beltran Date: Tue, 26 Nov 2024 18:10:55 +0100 Subject: [PATCH 05/33] formatted and code excerpts for command pattern --- .../architecture/command/lib/command.dart | 97 ++++++++ .../command/lib/extended_command.dart | 38 +++ .../architecture/command/lib/main.dart | 48 +++- .../architecture/command/lib/result.dart | 54 +++++ .../command/lib/simple_command.dart | 43 ++++ src/_data/sidenav.yml | 4 + src/content/cookbook/architecture/command.md | 229 +++++++++++++----- 7 files changed, 442 insertions(+), 71 deletions(-) create mode 100644 examples/cookbook/architecture/command/lib/command.dart create mode 100644 examples/cookbook/architecture/command/lib/extended_command.dart create mode 100644 examples/cookbook/architecture/command/lib/result.dart create mode 100644 examples/cookbook/architecture/command/lib/simple_command.dart diff --git a/examples/cookbook/architecture/command/lib/command.dart b/examples/cookbook/architecture/command/lib/command.dart new file mode 100644 index 0000000000..2e37434b99 --- /dev/null +++ b/examples/cookbook/architecture/command/lib/command.dart @@ -0,0 +1,97 @@ +// Copyright 2024 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:flutter/foundation.dart'; + +import 'result.dart'; + +typedef CommandAction0 = Future> Function(); +typedef CommandAction1 = Future> Function(A); + +/// Facilitates interaction with a ViewModel. +/// +/// Encapsulates an action, +/// exposes its running and error states, +/// and ensures that it can't be launched again until it finishes. +/// +/// Use [Command0] for actions without arguments. +/// Use [Command1] for actions with one argument. +/// +/// Actions must return a [Result]. +/// +/// Consume the action result by listening to changes, +/// then call to [clearResult] when the state is consumed. +abstract class Command extends ChangeNotifier { + Command(); + + bool _running = false; + + /// True when the action is running. + bool get running => _running; + + Result? _result; + + /// true if action completed with error + bool get error => _result is Error; + + /// true if action completed successfully + bool get completed => _result is Ok; + + /// Get last action result + Result? get result => _result; + + /// Clear last action result + void clearResult() { + _result = null; + notifyListeners(); + } + + /// Internal execute implementation + Future _execute(CommandAction0 action) async { + // Ensure the action can't launch multiple times. + // e.g. avoid multiple taps on button + if (_running) return; + + // Notify listeners. + // e.g. button shows loading state + _running = true; + _result = null; + notifyListeners(); + + try { + _result = await action(); + } finally { + _running = false; + notifyListeners(); + } + } +} + +/// [Command] without arguments. +/// Takes a [CommandAction0] as action. +class Command0 extends Command { + Command0(this._action); + + final CommandAction0 _action; + + /// Executes the action. + Future execute() async { + await _execute(() => _action()); + } +} + +/// [Command] with one argument. +/// Takes a [CommandAction1] as action. +class Command1 extends Command { + Command1(this._action); + + final CommandAction1 _action; + + /// Executes the action with the argument. + Future execute(A argument) async { + await _execute(() => _action(argument)); + } +} diff --git a/examples/cookbook/architecture/command/lib/extended_command.dart b/examples/cookbook/architecture/command/lib/extended_command.dart new file mode 100644 index 0000000000..f1efa8c41f --- /dev/null +++ b/examples/cookbook/architecture/command/lib/extended_command.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; + +// #docregion HomeViewModel +class HomeViewModel extends ChangeNotifier { + HomeViewModel() { + load = Command0(_load)..execute(); + edit = Command1(_edit); + } + + User? get user => null; + + // Command0 accepts 0 arguments + late final Command0 load; + + // Command1 accepts 1 argument + late final Command1 edit; + + Future _load() async { + // load user + } + + Future _edit(String name) async { + // edit user + } +} +// #enddocregion HomeViewModel + +class User {} + +class Command0 { + Command0(Future Function() any); + + void execute() {} +} + +class Command1 { + Command1(Future Function(T) any); +} diff --git a/examples/cookbook/architecture/command/lib/main.dart b/examples/cookbook/architecture/command/lib/main.dart index 963fd69615..4b9f6e27f6 100644 --- a/examples/cookbook/architecture/command/lib/main.dart +++ b/examples/cookbook/architecture/command/lib/main.dart @@ -21,6 +21,7 @@ class MainApp extends StatefulWidget { } class _MainAppState extends State { + // #docregion addListener @override void initState() { super.initState(); @@ -32,11 +33,14 @@ class _MainAppState extends State { widget.viewModel.removeListener(_onViewModelChanged); super.dispose(); } + // #enddocregion addListener @override Widget build(BuildContext context) { return MaterialApp( home: Scaffold( + // #docregion CommandListenable + // #docregion ListenableBuilder body: ListenableBuilder( listenable: widget.viewModel.load, builder: (context, child) { @@ -45,6 +49,7 @@ class _MainAppState extends State { child: CircularProgressIndicator(), ); } + // #enddocregion CommandListenable if (widget.viewModel.load.error != null) { return Center( @@ -57,6 +62,7 @@ class _MainAppState extends State { child: ListenableBuilder( listenable: widget.viewModel, builder: (context, _) { + // #enddocregion ListenableBuilder if (widget.viewModel.user == null) { return const Center( child: Text('No user'), @@ -72,18 +78,25 @@ class _MainAppState extends State { ], ), ); + // #docregion ListenableBuilder }, ), + // #docregion CommandListenable ), + // #enddocregion ListenableBuilder + // #enddocregion CommandListenable ), ); } + // #docregion _onViewModelChanged void _onViewModelChanged() { if (widget.viewModel.load.error != null) { + widget.viewModel.load.clear(); // Show Snackbar } } + // #enddocregion _onViewModelChanged } class User { @@ -93,24 +106,48 @@ class User { final String email; } +// #docregion HomeViewModel class HomeViewModel extends ChangeNotifier { + // #docregion ViewModelInit HomeViewModel() { load = Command(_load)..execute(); } + // #enddocregion ViewModelInit - User? _user; - User? get user => _user; + User? get user => null; late final Command load; Future _load() async { // load user - await Future.delayed(const Duration(seconds: 2)); - _user = User(name: 'John Doe', email: 'john@example.com'); - notifyListeners(); } } +// #enddocregion HomeViewModel + +// #docregion HomeViewModel2 +class HomeViewModel2 extends ChangeNotifier { + HomeViewModel2() { + load = Command(_load)..execute(); + delete = Command(_delete); + } + + User? get user => null; + + late final Command load; + + late final Command delete; + + Future _load() async { + // load user + } + + Future _delete() async { + // delete user + } +} +// #enddocregion HomeViewModel2 +// #docregion Command class Command extends ChangeNotifier { Command(this._action); @@ -152,3 +189,4 @@ class Command extends ChangeNotifier { _completed = false; } } +// #enddocregion Command diff --git a/examples/cookbook/architecture/command/lib/result.dart b/examples/cookbook/architecture/command/lib/result.dart new file mode 100644 index 0000000000..5370700b03 --- /dev/null +++ b/examples/cookbook/architecture/command/lib/result.dart @@ -0,0 +1,54 @@ +// Copyright 2024 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/// Utility class to wrap result data +/// +/// Evaluate the result using a switch statement: +/// ```dart +/// switch (result) { +/// case Ok(): { +/// print(result.value); +/// } +/// case Error(): { +/// print(result.error); +/// } +/// } +/// ``` +sealed class Result { + const Result(); + + /// Creates an instance of Result containing a value + factory Result.ok(T value) => Ok(value); + + /// Create an instance of Result containing an error + factory Result.error(Exception error) => Error(error); + + /// Convenience method to cast to Ok + Ok get asOk => this as Ok; + + /// Convenience method to cast to Error + Error get asError => this as Error; +} + +/// Subclass of Result for values +final class Ok extends Result { + const Ok(this.value); + + /// Returned value in result + final T value; + + @override + String toString() => 'Result<$T>.ok($value)'; +} + +/// Subclass of Result for errors +final class Error extends Result { + const Error(this.error); + + /// Returned error in result + final Exception error; + + @override + String toString() => 'Result<$T>.error($error)'; +} diff --git a/examples/cookbook/architecture/command/lib/simple_command.dart b/examples/cookbook/architecture/command/lib/simple_command.dart new file mode 100644 index 0000000000..89519e93e2 --- /dev/null +++ b/examples/cookbook/architecture/command/lib/simple_command.dart @@ -0,0 +1,43 @@ +// ignore_for_file: unused_field, prefer_final_fields + +import 'package:flutter/material.dart'; + +class User {} + +// #docregion Command +class Command extends ChangeNotifier { + Command(this._action); + + bool get running => false; + + Exception? get error => null; + + bool get completed => false; + + void Function() _action; + + void execute() { + // run _action + } + + void clear() { + // clear state + } +} +// #enddocregion Command + +// #docregion ViewModel +class HomeViewModel extends ChangeNotifier { + HomeViewModel() { + load = Command(_load)..execute(); + } + + User? get user => null; + + late final Command load; + + void _load() { + // load user + } +} +// #enddocregion ViewModel diff --git a/src/_data/sidenav.yml b/src/_data/sidenav.yml index a2b7fb4036..cfc44f5623 100644 --- a/src/_data/sidenav.yml +++ b/src/_data/sidenav.yml @@ -441,6 +441,10 @@ children: - title: Optimistic state permalink: /cookbook/architecture/optimistic-state + - title: Command + permalink: /cookbook/architecture/command + - title: Result + permalink: /cookbook/architecture/result - title: "Persistent storage architecture: Key-value data" permalink: /cookbook/architecture/key-value-data - title: "Persistent storage architecture: SQL" diff --git a/src/content/cookbook/architecture/command.md b/src/content/cookbook/architecture/command.md index f1df32ce58..82d498a109 100644 --- a/src/content/cookbook/architecture/command.md +++ b/src/content/cookbook/architecture/command.md @@ -25,7 +25,7 @@ In this guide, you will learn how to use the command pattern to improve your ViewModels. -# Challenges when implementing ViewModels +## Challenges when implementing ViewModels ViewModel classes in Flutter are typically implemented by extending the ChangeNotifier class. @@ -120,15 +120,15 @@ ListenableBuilder( Or use the running state to avoid executing the action multiple times: - + ```dart -void load2() { +void load() { if (running) { return; } - // load user } + ``` Managing the state of an action can get complicated @@ -204,7 +204,7 @@ void _onViewModelChanged() { ``` You need to clear the error state each time you execute this action, -otherwise this action will happen each time `notifyListeners()` is called. +otherwise this action happens each time `notifyListeners()` is called. ```dart @@ -216,7 +216,7 @@ void _onViewModelChanged() { } ``` -## Command Pattern +## Command pattern You might find yourself repeating the above code over and over, having to implement a different running state @@ -227,18 +227,20 @@ into a reusable pattern: a command. A command is a class that encapsulates a ViewModel action, and exposes the different states that an action can have. + +```dart class Command extends ChangeNotifier { Command(this._action); - bool get running => ...; + bool get running => // ... - Exception? get error => ...; + Exception? get error => // ... - bool get completed => ...; + bool get completed => // ... - Future Function() _action; + void Function() _action; - Future execute() async { + void execute() { // run _action } @@ -246,15 +248,20 @@ class Command extends ChangeNotifier { // clear state } } +``` -In the ViewModel, instead of defining an action directly with a method, you create a Command object: +In the ViewModel, +instead of defining an action directly with a method, +you create a command object: + +```dart class HomeViewModel extends ChangeNotifier { HomeViewModel() { - load = Command(_load); + load = Command(_load)..execute(); } - - User? get user => ...; + + User? get user => // ... late final Command load; @@ -262,34 +269,58 @@ class HomeViewModel extends ChangeNotifier { // load user } } +``` + +The previous `load()` method becomes `_load()`, +and instead the command `load` gets exposed to the View. +The previous `running` and `error` states can be removed, +as they are now part of the command. + +### Executing a command -The previous load() method becomes _load(), and instead the Command load gets exposed to the View. The previous running and error states can be removed, as they are now part of the Command. -Executing a Command -Instead of calling viewModel.load() to run the load action, now you have to call viewModel.load.execute(). +Instead of calling `viewModel.load()` to run the load action, +now you call `viewModel.load.execute()`. -The execute() method can also be called from within the ViewModel. For example, to run the load Command when the ViewModel is created: +The `execute()` method can also be called from within the ViewModel. +For example, to run the `load` command when the ViewModel is created: + +```dart HomeViewModel() { load = Command(_load)..execute(); } +``` -The execute() method sets the running state to true and resets the error and completed states. When the action finishes, the running state will change to false and the completed state will be true. +The `execute()` method sets the running state to `true` +and resets the `error` and `completed` states. +When the action finishes, +the `running` state changes to `false` +and the `completed` state to `true`. -When the running state is true, it prevents the Command from being launched multiple times before completing. This prevents users from attempting to tap multiple times on a button while an action is executing. +When the `running` state is `true`, +it prevents the command from being launched multiple times before completing. +This prevents users from attempting to tap multiple times on a button +while an action is executing. -The Command’s execute() method could also capture any thrown Exceptions by the action implementation automatically and expose them in the error state. +The command’s `execute()` method could also capture any thrown Exceptions` + by the action implementation automatically + and expose them in the `error` state. -The following is what a command class might look like. It’s been simplified for demo purposes. You can see a full implementation at the end of this page. +The following is what a command class might look like. +It’s been simplified for demo purposes. +You can see a full implementation at the end of this page. + +```dart class Command extends ChangeNotifier { Command(this._action); - + bool _running = false; bool get running => _running; Exception? _error; Exception? get error => _error; - + bool _completed = false; bool get completed => _completed; @@ -322,73 +353,111 @@ class Command extends ChangeNotifier { _completed = false; } } -Listening to the Command state -The Command class extends from ChangeNotifier, allowing Views to listen to its states. +``` + +### Listening to the command state + +The `Command` class extends from `ChangeNotifier`, +allowing Views to listen to its states. + +In the `ListenableBuilder`, +instead of passing the ViewModel as listenable, +pass the command: -In the ListenableBuilder, instead of passing the ViewModel as listenable, pass the Command: + +```dart ListenableBuilder( - listenable: viewModel.load, + listenable: widget.viewModel.load, builder: (context, child) { - if (viewModel.load.running) { + if (widget.viewModel.load.running) { return const Center( child: CircularProgressIndicator(), ); } - - // rest of builder - }, + // ··· ) +``` -As well listen to changes in the Command state in order to run UI actions: +As well listen to changes in the command state in order to run UI actions: -viewModel.load.addListener(() { - if (viewModel.load.error != null) { - viewModel.load.clear(); + +```dart +@override +void initState() { + super.initState(); + widget.viewModel.addListener(_onViewModelChanged); +} - // Show SnackBar +@override +void dispose() { + widget.viewModel.removeListener(_onViewModelChanged); + super.dispose(); +} +``` + + +```dart +void _onViewModelChanged() { + if (widget.viewModel.load.error != null) { + widget.viewModel.load.clear(); + // Show Snackbar } -}); +} +``` -Combining Command and ViewModel -When combined with the ViewModel, use the child argument to stack multiple ListenableBuilder. For example, you can listen to the running and error states of the Command before showing the ViewModel data. +### Combining command and ViewModel -ListenableBuilder( - listenable: viewModel.load, +When combined with the ViewModel, +use the child argument to stack multiple `ListenableBuilder`. +For example, +listen to the `running` and `error` states of the command +before showing the ViewModel data. + + +```dart +body: ListenableBuilder( + listenable: widget.viewModel.load, builder: (context, child) { - if (viewModel.load.running) { + if (widget.viewModel.load.running) { return const Center( child: CircularProgressIndicator(), ); } - if (viewModel.load.error != null) { + if (widget.viewModel.load.error != null) { return Center( - child: ErrorIndicator(error: viewModel.load.error), + child: Text('Error: ${widget.viewModel.load.error}'), ); } - return child; + return child!; }, child: ListenableBuilder( - listenable: viewModel, + listenable: widget.viewModel, builder: (context, _) { - // rest of the screen + // ··· }, ), -) +), +``` -You can have multiple Commands in a single ViewModel, simplifying the implementation of ViewModels and minimizing the amount of repeated code. +You can have multiple commands in a single ViewModel, +simplifying the implementation of ViewModels +and minimizing the amount of repeated code. -class HomeViewModel extends ChangeNotifier { - HomeViewModel() { + +```dart +class HomeViewModel2 extends ChangeNotifier { + HomeViewModel2() { load = Command(_load)..execute(); delete = Command(_delete); } - - User? get user => ...; + + User? get user => // ... late final Command load; + late final Command delete; Future _load() async { @@ -399,17 +468,23 @@ class HomeViewModel extends ChangeNotifier { // delete user } } +``` + +### Extending the command pattern -Extending the Command pattern -The Command pattern can be extended in multiple ways, for example, you can have different implementations to support a different number of arguments. +The command pattern can be extended in multiple ways, +for example, +to support a different number of arguments. + +```dart class HomeViewModel extends ChangeNotifier { HomeViewModel() { load = Command0(_load)..execute(); - edit = Command1(_edit); + edit = Command1(_edit); } - - User? get user => ...; + + User? get user => // ... // Command0 accepts 0 arguments late final Command0 load; @@ -425,14 +500,36 @@ class HomeViewModel extends ChangeNotifier { // edit user } } -Putting it all together -In this guide, you have learned how to use the Command design pattern to improve the implementation of ViewModels when using the MVVM design pattern. +``` + +## Putting it all together + +In this guide, +you have learned how to use the command design pattern +to improve the implementation of ViewModels +when using the MVVM design pattern. -Below you can find the full Command class as implemented in the Compass App example for the Flutter Architecture guidelines. It also uses the Result class, which you can learn more about in the Architecture Cookbook Recipe: Result Class. +Below, you can find the full `Command` class +as implemented in the Compass App example +for the Flutter Architecture guidelines. +It also uses the Result class, +which you can learn more about +in the Architecture Cookbook Recipe: Result Class. -This implementation also includes two types of Command, a Command0, for actions without parameters, and a Command1, which takes one parameter. +This implementation also includes two types of command, +a `Command0`, for actions without parameters, +and a `Command1`, which takes one parameter. -You can also check on pub.dev for different ready-to-use implementations of the Command pattern, like the flutter_command package. +:::note +Check pub.dev for different ready-to-use implementations of the command pattern, +like the flutter_command package. +::: + + +```dart +// Copyright 2024 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. import 'dart:async'; @@ -527,6 +624,6 @@ class Command1 extends Command { await _execute(() => _action(argument)); } } - +``` From 23b0d13d40df82888f7f75e0199fc3a8ec35fc4e Mon Sep 17 00:00:00 2001 From: Miguel Beltran Date: Tue, 26 Nov 2024 19:16:21 +0100 Subject: [PATCH 06/33] completed command pattern --- .../architecture/command/lib/no_command.dart | 2 +- src/content/cookbook/architecture/command.md | 19 +++++++++++-------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/examples/cookbook/architecture/command/lib/no_command.dart b/examples/cookbook/architecture/command/lib/no_command.dart index 15756222eb..16f9276fdf 100644 --- a/examples/cookbook/architecture/command/lib/no_command.dart +++ b/examples/cookbook/architecture/command/lib/no_command.dart @@ -44,7 +44,7 @@ class _MainAppState extends State { if (widget.viewModel.error != null) { return Center( - child: Text('Error: ${widget.viewModel.load.error}'), + child: Text('Error: ${widget.viewModel.error}'), ); } diff --git a/src/content/cookbook/architecture/command.md b/src/content/cookbook/architecture/command.md index 82d498a109..640e6baaca 100644 --- a/src/content/cookbook/architecture/command.md +++ b/src/content/cookbook/architecture/command.md @@ -28,7 +28,7 @@ to improve your ViewModels. ## Challenges when implementing ViewModels ViewModel classes in Flutter are typically implemented -by extending the ChangeNotifier class. +by extending the `ChangeNotifier` class. This allows ViewModels to call `notifyListeners()` to refresh Views when data is updated. @@ -510,19 +510,19 @@ to improve the implementation of ViewModels when using the MVVM design pattern. Below, you can find the full `Command` class -as implemented in the Compass App example +as implemented in the [Compass App example][] for the Flutter Architecture guidelines. -It also uses the Result class, -which you can learn more about -in the Architecture Cookbook Recipe: Result Class. +It also uses the [`Result` class][] +to determine if the action completed successfuly or with an error. This implementation also includes two types of command, a `Command0`, for actions without parameters, and a `Command1`, which takes one parameter. :::note -Check pub.dev for different ready-to-use implementations of the command pattern, -like the flutter_command package. +Check [pub.dev][] for different ready-to-use +implementations of the command pattern, +like the [flutter_command][] package. ::: @@ -626,4 +626,7 @@ class Command1 extends Command { } ``` - +[Compass App example]:{{site.repo.samples}}/tree/main/compass_app +[`Result` class]:/cookbook/architecture/result +[pub.dev]:{{site.pub}} +[flutter_command]:{{site.pub-pkg}}/flutter_command From 47a90fda9c5d3cda1c9e5332c3f5dccbafbb95d0 Mon Sep 17 00:00:00 2001 From: Miguel Beltran Date: Tue, 26 Nov 2024 19:51:48 +0100 Subject: [PATCH 07/33] WIP result pattern --- .../architecture/result/lib/no_result.dart | 86 ++++++++++ src/content/cookbook/architecture/result.md | 148 +++++++++++++++++- 2 files changed, 231 insertions(+), 3 deletions(-) create mode 100644 examples/cookbook/architecture/result/lib/no_result.dart diff --git a/examples/cookbook/architecture/result/lib/no_result.dart b/examples/cookbook/architecture/result/lib/no_result.dart new file mode 100644 index 0000000000..28a4ce4f0a --- /dev/null +++ b/examples/cookbook/architecture/result/lib/no_result.dart @@ -0,0 +1,86 @@ +// ignore_for_file: unused_catch_clause, unused_field + +import 'dart:convert'; +import 'dart:io'; + +import 'package:flutter/material.dart'; + +class UserProfile { + final String name; + final String email; + + UserProfile(this.name, this.email); + + static UserProfile fromJson(_) { + return UserProfile('John Doe', 'email@example.com'); + } +} + +// #docregion ApiClientService +class ApiClientService { + // #enddocregion ApiClientService + final HttpClient client = HttpClient(); + final String _host = 'api.example.com'; + final int _port = 443; + // #docregion ApiClientService + + Future getUserProfile() async { + try { + final request = await client.get(_host, _port, '/user'); + final response = await request.close(); + if (response.statusCode == 200) { + final stringData = await response.transform(utf8.decoder).join(); + return UserProfile.fromJson(jsonDecode(stringData)); + } else { + throw const HttpException('Invalid response'); + } + } finally { + client.close(); + } + } +} +// #enddocregion ApiClientService + +// #docregion UserProfileRepository +class UserProfileRepository { + // #enddocregion UserProfileRepository + final ApiClientService _apiClientService = ApiClientService(); + // #docregion UserProfileRepository + + Future getUserProfile() async { + return await _apiClientService.getUserProfile(); + } +} +// #enddocregion UserProfileRepository + +// #docregion UserProfileViewModel +class UserProfileViewModel extends ChangeNotifier { + // #enddocregion UserProfileViewModel + final UserProfileRepository userProfileRepository = UserProfileRepository(); + UserProfile? _userProfile; + // #docregion UserProfileViewModel + + Future load() async { + try { + _userProfile = await userProfileRepository.getUserProfile(); + notifyListeners(); + } on Exception catch (exception) { + // handle exception + } + } +} +// #enddocregion UserProfileViewModel + +// #docregion UserProfileViewModelNoTryCatch +class UserProfileViewModelNoTryCatch extends ChangeNotifier { + // #enddocregion UserProfileViewModelNoTryCatch + final UserProfileRepository userProfileRepository = UserProfileRepository(); + UserProfile? _userProfile; + // #docregion UserProfileViewModelNoTryCatch + + Future load() async { + _userProfile = await userProfileRepository.getUserProfile(); + notifyListeners(); + } +} +// #enddocregion UserProfileViewModelNoTryCatch diff --git a/src/content/cookbook/architecture/result.md b/src/content/cookbook/architecture/result.md index d5bf6f17ff..17827459c8 100644 --- a/src/content/cookbook/architecture/result.md +++ b/src/content/cookbook/architecture/result.md @@ -1,9 +1,151 @@ --- -title: "Persistent storage architecture: SQL" -description: Create a service to store complex data with SQL +title: Result class +description: Handle errors and return values with the result class js: - defer: true url: /assets/js/inject_dartpad.js --- - + + +Dart provides a built-in error handling mechanism +through the ability to throw and catch exceptions. + +As mentioned in the [Error handling documentation][], +Dart’s exceptions are unhandled exceptions, +meaning that methods that throw don’t need to declare them, +and calling methods are not required to catch them either. + +This can lead to situations where exceptions are not handled properly. +In large projects, +developers might forget to catch exceptions, +and the different application layers and components +could throw exceptions that aren’t documented. +This can lead to errors and crashes. + +In this guide, +you will learn about this limitation +and how to mitigate it using the result class pattern. + +## Error flow in Flutter applications + +Applications following the [Flutter Architecture guidelines][] +are usually composed of ViewModels, +Repositories, and Services, among other parts. +When a function in one of these components fails, +it will have to communicate the error to the calling component. + +Typically, that would be done using exceptions. +For example, +an API client Service failing to communicate with the remote server +might throw an HTTP Error Exception. +The calling component, +for example a Repository, +would have to either capture this exception +or ignore it and let the calling ViewModel handle it. + +This can be observed in the following example. Consider these classes: + +- A service named `ApiClientService` +that performs API calls to a remote service. +- A repository named `UserProfileRepository` +that uses the `ApiClientService` to provide the `UserProfile`. +- A ViewModel named `UserProfileViewModel` +that uses the `UserProfileRepository`. + +The `ApiClientService` contains a method named `getUserProfile` + hat can throw exceptions in different situations: + +- The method throws an `HttpException` if the response code isn’t 200. +- The JSON parsing method might throw an exception +if the response is not formatted correctly. +- The Http Client might throw an exception due to networking issues. + +For example: + + +```dart +class ApiClientService { + // ··· + + Future getUserProfile() async { + try { + final request = await client.get(_host, _port, '/user'); + final response = await request.close(); + if (response.statusCode == 200) { + final stringData = await response.transform(utf8.decoder).join(); + return UserProfile.fromJson(jsonDecode(stringData)); + } else { + throw const HttpException('Invalid response'); + } + } finally { + client.close(); + } + } +} +``` + +The `UserProfileRepository` doesn’t need to handle +the exceptions from the `ApiClientService`. +In this example, it just returns the value from the API Client. + + +```dart +class UserProfileRepository { + // ··· + + Future getUserProfile() async { + return await _apiClientService.getUserProfile(); + } +} +``` + +Finally, the `UserProfileViewModel` +should capture all exceptions and handle the errors. + +This can be done by wrapping +the call to the `UserProfileRepository` with a try-catch: + + +```dart +class UserProfileViewModel extends ChangeNotifier { + // ··· + + Future load() async { + try { + _userProfile = await userProfileRepository.getUserProfile(); + notifyListeners(); + } on Exception catch (exception) { + // handle exception + } + } +} +``` +However, as multiple developers can work on the same codebase, +it is not unusual that a developer might forget to properly capture exceptions. +The following code will compile and run, +but will crash if one of the exceptions mentioned previously occurs: + + +```dart +class UserProfileViewModel extends ChangeNotifier { + // ··· + + Future load() async { + _userProfile = await userProfileRepository.getUserProfile(); + notifyListeners(); + } +} +``` + +You can attempt to solve this by documenting the `ApiClientService`, +warning about the possible exceptions it may throw. +However, since the ViewModel doesn’t use the service directly, +this information may be missed by other developers working in the codebase. + +## Using the result pattern + + + +[Error handling documentation]:{{site.dart-site}}/language/error-handling +[Flutter Architecture guidelines]:/app-architecture From 51206c46118998504e751af140339436e9ad32c7 Mon Sep 17 00:00:00 2001 From: Miguel Beltran Date: Wed, 27 Nov 2024 13:48:37 +0100 Subject: [PATCH 08/33] completed result pattern document --- .../architecture/result/lib/main.dart | 70 +++- .../architecture/result/lib/no_result.dart | 28 ++ .../result/lib/simple_result.dart | 25 ++ src/content/cookbook/architecture/result.md | 313 ++++++++++++++++++ 4 files changed, 434 insertions(+), 2 deletions(-) create mode 100644 examples/cookbook/architecture/result/lib/simple_result.dart diff --git a/examples/cookbook/architecture/result/lib/main.dart b/examples/cookbook/architecture/result/lib/main.dart index 66e7674788..7012839578 100644 --- a/examples/cookbook/architecture/result/lib/main.dart +++ b/examples/cookbook/architecture/result/lib/main.dart @@ -1,3 +1,8 @@ +// ignore_for_file: unused_field + +import 'dart:convert'; +import 'dart:io'; + import 'package:flutter/material.dart'; import 'result.dart'; @@ -26,8 +31,35 @@ class UserProfile { final String email; UserProfile(this.name, this.email); + + static UserProfile fromJson(_) { + return UserProfile('John Doe', 'email@example.com'); + } } +// #docregion UserProfileViewModel +class UserProfileViewModel extends ChangeNotifier { + // #enddocregion UserProfileViewModel + final UserProfileRepository userProfileRepository = UserProfileRepository(); + // #docregion UserProfileViewModel + + UserProfile? userProfile; + + Exception? error; + + Future load() async { + final result = await userProfileRepository.getUserProfile(); + switch (result) { + case Ok(): + userProfile = result.value; + case Error(): + error = result.error; + } + notifyListeners(); + } +} +// #enddocregion UserProfileViewModel + class UserProfileRepository { final ApiClientService _apiClientService; final DatabaseService _databaseService; @@ -37,6 +69,13 @@ class UserProfileRepository { this._databaseService, ); + // #docregion getUserProfile1 + Future> getUserProfile1() async { + return await _apiClientService.getUserProfile(); + } + // #enddocregion getUserProfile1 + + // #docregion getUserProfile Future> getUserProfile() async { final apiResult = await _apiClientService.getUserProfile(); if (apiResult is Ok) { @@ -50,14 +89,41 @@ class UserProfileRepository { return Result.error(Exception('Failed to get user profile')); } + // #enddocregion getUserProfile } +// #docregion ApiClientService1 +// #docregion ApiClientService2 class ApiClientService { + // #enddocregion ApiClientService1 + // #enddocregion ApiClientService2 + final HttpClient client = HttpClient(); + final String _host = 'api.example.com'; + final int _port = 443; + // #docregion ApiClientService1 + // #docregion ApiClientService2 + Future> getUserProfile() async { - await Future.delayed(const Duration(seconds: 2)); - return Result.ok(UserProfile('John Doe', 'john@example.com')); + // #enddocregion ApiClientService1 + try { + final request = await client.get(_host, _port, '/user'); + final response = await request.close(); + if (response.statusCode == 200) { + final stringData = await response.transform(utf8.decoder).join(); + return Result.ok(UserProfile.fromJson(jsonDecode(stringData))); + } else { + return Result.error(const HttpException('Invalid response')); + } + } on Exception catch (exception) { + return Result.error(exception); + } finally { + client.close(); + } + // #docregion ApiClientService1 } } +// #enddocregion ApiClientService1 +// #enddocregion ApiClientService2 class DatabaseService { Future> createTemporalUser() async { diff --git a/examples/cookbook/architecture/result/lib/no_result.dart b/examples/cookbook/architecture/result/lib/no_result.dart index 28a4ce4f0a..be66f197ea 100644 --- a/examples/cookbook/architecture/result/lib/no_result.dart +++ b/examples/cookbook/architecture/result/lib/no_result.dart @@ -53,6 +53,27 @@ class UserProfileRepository { } // #enddocregion UserProfileRepository +// #docregion UserProfileRepository2 +class UserProfileRepository2 { + // #enddocregion UserProfileRepository2 + final ApiClientService _apiClientService = ApiClientService(); + final DatabaseService _databaseService = DatabaseService(); + // #docregion UserProfileRepository2 + + Future getUserProfile() async { + try { + return await _apiClientService.getUserProfile(); + } catch (e) { + try { + return await _databaseService.createTemporaryUser(); + } catch (e) { + throw Exception('Failed to get user profile'); + } + } + } +} +// #enddocregion UserProfileRepository2 + // #docregion UserProfileViewModel class UserProfileViewModel extends ChangeNotifier { // #enddocregion UserProfileViewModel @@ -84,3 +105,10 @@ class UserProfileViewModelNoTryCatch extends ChangeNotifier { } } // #enddocregion UserProfileViewModelNoTryCatch + +class DatabaseService { + Future createTemporaryUser() async { + await Future.delayed(const Duration(seconds: 2)); + return UserProfile('John Doe', 'john@example.com'); + } +} diff --git a/examples/cookbook/architecture/result/lib/simple_result.dart b/examples/cookbook/architecture/result/lib/simple_result.dart new file mode 100644 index 0000000000..5d009904e9 --- /dev/null +++ b/examples/cookbook/architecture/result/lib/simple_result.dart @@ -0,0 +1,25 @@ +sealed class Result { + const Result(); + + /// Creates an instance of Result containing a value + factory Result.ok(T value) => Ok(value); + + /// Create an instance of Result containing an error + factory Result.error(Exception error) => Error(error); +} + +/// Subclass of Result for values +final class Ok extends Result { + const Ok(this.value); + + /// Returned value in result + final T value; +} + +/// Subclass of Result for errors +final class Error extends Result { + const Error(this.error); + + /// Returned error in result + final Exception error; +} diff --git a/src/content/cookbook/architecture/result.md b/src/content/cookbook/architecture/result.md index 17827459c8..e19407e45e 100644 --- a/src/content/cookbook/architecture/result.md +++ b/src/content/cookbook/architecture/result.md @@ -145,7 +145,320 @@ this information may be missed by other developers working in the codebase. ## Using the result pattern +An alternative to throwing exceptions +is to wrap the function output into a `Result`. +When the function runs successfully, +the `Result` contains the returned value. +However, if the function did not complete successfully, +the `Result` object will contain the error. + +A `Result` is a `sealed class` +that can either be of the subclass `Ok` or the subclass `Error`. +Return the successful value with the subclass `Ok`, +and the captured error with the subclass `Error`. + +The following is what a `Result` class might look like. +It’s been simplified for demo purposes. +You can see a full implementation at the end of this page. + + +```dart +sealed class Result { + const Result(); + + /// Creates an instance of Result containing a value + factory Result.ok(T value) => Ok(value); + + /// Create an instance of Result containing an error + factory Result.error(Exception error) => Error(error); +} + +/// Subclass of Result for values +final class Ok extends Result { + const Ok(this.value); + + /// Returned value in result + final T value; +} + +/// Subclass of Result for errors +final class Error extends Result { + const Error(this.error); + + /// Returned error in result + final Exception error; +} +``` + +In this example, +the `Result` class uses a generic type `T` to represent any return value, +which can be as simple as a `String` or an `int` +but also can be used for custom data classes like the `UserProfile`. + +### Creating a `Result` object + +For functions using the `Result` class to return values, +instead of a value, +the function returns a `Result` object containing the value. + +For example, in the `ApiClientService`, +`getUserProfile` is changed to return a `Result`: + + +```dart +class ApiClientService { + // ··· + + Future> getUserProfile() async { + // ··· + } +} +``` +Instead of returning the `UserProfile` directly, +it returns a `Result` object containing a `UserProfile`. + +To facilitate using the `Result` class, +it contains two named constructors, `Result.ok` and `Result.error`. +Use them to construct the `Result` depending on desired output. +As well, capture any thrown exceptions by the code +and wrap them into the `Result` object. + +For example, here the `getUserProfile()` method +has been changed to use the `Result` class: + + +```dart +class ApiClientService { + // ··· + + Future> getUserProfile() async { + try { + final request = await client.get(_host, _port, '/user'); + final response = await request.close(); + if (response.statusCode == 200) { + final stringData = await response.transform(utf8.decoder).join(); + return Result.ok(UserProfile.fromJson(jsonDecode(stringData))); + } else { + return Result.error(const HttpException('Invalid response')); + } + } on Exception catch (exception) { + return Result.error(exception); + } finally { + client.close(); + } + } +} +``` + +The original return statement has been replaced +with returning the value using `Result.ok`. +The `throw HttpException()` +has been replaced with returning `Result.error(HttpException())` +wrapping the error into a `Result`. +As well, the method is wrapped with a try-catch +to capture any thrown exception by the Http Client +or the JSON parser into a `Result.error`. + +The repository class also needs to be modified, +and instead of returning a `UserProfile` directly, +now it returns a `Result`. + + +```dart +Future> getUserProfile() async { + return await _apiClientService.getUserProfile(); +} +``` + +### Unwrapping the Result object + +Now the ViewModel doesn't receive the `UserProfile` directly, +but instead it receives a `Result` containing a `UserProfile`. + +This enforces the developer implementing the ViewModel +to unwrap the `Result` to obtain the `UserProfile`, +and avoids having uncaught exceptions. + + +```dart +class UserProfileViewModel extends ChangeNotifier { + // ··· + + UserProfile? userProfile; + + Exception? error; + + Future load() async { + final result = await userProfileRepository.getUserProfile(); + switch (result) { + case Ok(): + userProfile = result.value; + case Error(): + error = result.error; + } + notifyListeners(); + } +} +``` + +The `Result` class is implemented using a `sealed class`, +meaning it can only either be an `Ok` or an `Error`, +so this operation can be done using a switch case. + +In the `Ok` case, +obtain the value using the `value` property. + +In the Error case, +obtain the error object using the `error` property. + +## Improving control flow + +Wrapping code with a try-catch +is a quick way to ensure that any exception thrown +is captured by the calling method. +Catching stops the exception from propagating to other methods, h +owever it also disrupts the normal code flow. + +Consider the following code. + + +```dart +class UserProfileRepository { + // ··· + + Future getUserProfile() async { + try { + return await _apiClientService.getUserProfile(); + } catch (e) { + try { + return await _databaseService.createTemporaryUser(); + } catch (e) { + throw Exception('Failed to get user profile'); + } + } + } +} +``` + +In this method, the `UserProfileRepository` +attempts to obtain the `UserProfile` +using the `ApiClientService`, +and if it fails, +then tries to create a temporary user in a `DatabaseService`. + +Because either Service methods can fail, +the code must catch the exceptions in both cases. + +This can be improved using the `Result` pattern: + + + +```dart +Future> getUserProfile() async { + final apiResult = await _apiClientService.getUserProfile(); + if (apiResult is Ok) { + return apiResult; + } + + final databaseResult = await _databaseService.createTemporalUser(); + if (databaseResult is Ok) { + return databaseResult; + } + + return Result.error(Exception('Failed to get user profile')); +} +``` + +In this case, if the `Result` object is an `Ok`, +then the function returns that object, +otherwise at the end of the function returns a `Result.Error` if none worked. + +## Putting it all together + +In this guide, you have learned +how to use a `Result` class to return result values. + +The key takeaways are: + +- `Result` classes enforce calling methods to check for errors, +reducing the amount of bugs caused by uncaught exceptions. +- `Result` classes help improve control flow compared to try-catch blocks. +- `Result` classes are `sealed classes` that can only be `Ok` or `Error`, +allowing you to use switch statements to unwrap them. + +Below you can find the full `Result` class +as implemented in the [Compass App example][] +for the [Flutter Architecture guidelines][]. + +:::note +Check [pub.dev][] for different ready-to-use +implementations of the `Result` class, +like the [result_dart][], [result_type][] or [multiple_result][] packages. +::: + + +```dart +// Copyright 2024 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/// Utility class to wrap result data +/// +/// Evaluate the result using a switch statement: +/// ```dart +/// switch (result) { +/// case Ok(): { +/// print(result.value); +/// } +/// case Error(): { +/// print(result.error); +/// } +/// } +/// ``` +sealed class Result { + const Result(); + + /// Creates an instance of Result containing a value + factory Result.ok(T value) => Ok(value); + + /// Create an instance of Result containing an error + factory Result.error(Exception error) => Error(error); + + /// Convenience method to cast to Ok + Ok get asOk => this as Ok; + + /// Convenience method to cast to Error + Error get asError => this as Error; +} + +/// Subclass of Result for values +final class Ok extends Result { + const Ok(this.value); + + /// Returned value in result + final T value; + + @override + String toString() => 'Result<$T>.ok($value)'; +} + +/// Subclass of Result for errors +final class Error extends Result { + const Error(this.error); + + /// Returned error in result + final Exception error; + + @override + String toString() => 'Result<$T>.error($error)'; +} +``` [Error handling documentation]:{{site.dart-site}}/language/error-handling [Flutter Architecture guidelines]:/app-architecture +[Compass App example]:{{site.repo.samples}}/tree/main/compass_app +[pub.dev]:{{site.pub}} +[result_dart]:{{site.pub-pkg}}/result_dart +[result_type]:{{site.pub-pkg}}/result_type +[multiple_result]:{{site.pub-pkg}}/multiple_result From 6fb564b978f672fda519cd87680551dc9501e80c Mon Sep 17 00:00:00 2001 From: Miguel Beltran Date: Wed, 27 Nov 2024 13:55:09 +0100 Subject: [PATCH 09/33] fix analytics issue --- examples/cookbook/architecture/result/lib/main.dart | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/examples/cookbook/architecture/result/lib/main.dart b/examples/cookbook/architecture/result/lib/main.dart index 7012839578..281074e02d 100644 --- a/examples/cookbook/architecture/result/lib/main.dart +++ b/examples/cookbook/architecture/result/lib/main.dart @@ -40,7 +40,10 @@ class UserProfile { // #docregion UserProfileViewModel class UserProfileViewModel extends ChangeNotifier { // #enddocregion UserProfileViewModel - final UserProfileRepository userProfileRepository = UserProfileRepository(); + final UserProfileRepository userProfileRepository = UserProfileRepository( + ApiClientService(), + DatabaseService(), + ); // #docregion UserProfileViewModel UserProfile? userProfile; From e4f57b287933d4f55355d276591a174790a53f97 Mon Sep 17 00:00:00 2001 From: Miguel Beltran Date: Wed, 27 Nov 2024 14:06:05 +0100 Subject: [PATCH 10/33] cleanup code --- .../architecture/command/lib/main.dart | 8 -------- .../architecture/command/lib/no_command.dart | 2 ++ .../architecture/result/lib/main.dart | 19 ------------------- src/content/cookbook/architecture/command.md | 10 +--------- 4 files changed, 3 insertions(+), 36 deletions(-) diff --git a/examples/cookbook/architecture/command/lib/main.dart b/examples/cookbook/architecture/command/lib/main.dart index 4b9f6e27f6..0aa78a4790 100644 --- a/examples/cookbook/architecture/command/lib/main.dart +++ b/examples/cookbook/architecture/command/lib/main.dart @@ -1,13 +1,5 @@ import 'package:flutter/material.dart'; -void main() { - runApp( - MainApp( - viewModel: HomeViewModel(), - ), - ); -} - class MainApp extends StatefulWidget { const MainApp({ super.key, diff --git a/examples/cookbook/architecture/command/lib/no_command.dart b/examples/cookbook/architecture/command/lib/no_command.dart index 16f9276fdf..70f2475c3a 100644 --- a/examples/cookbook/architecture/command/lib/no_command.dart +++ b/examples/cookbook/architecture/command/lib/no_command.dart @@ -108,6 +108,7 @@ class HomeViewModel2 extends ChangeNotifier { // load user } // #enddocregion load1 + // #enddocregion UiState1 // #docregion load2 void load2() { @@ -119,6 +120,7 @@ class HomeViewModel2 extends ChangeNotifier { // #enddocregion load2 void clearError() {} + // #docregion UiState1 // #docregion load1 // #docregion HomeViewModel2 // #docregion getUser diff --git a/examples/cookbook/architecture/result/lib/main.dart b/examples/cookbook/architecture/result/lib/main.dart index 281074e02d..e938b0cdc6 100644 --- a/examples/cookbook/architecture/result/lib/main.dart +++ b/examples/cookbook/architecture/result/lib/main.dart @@ -7,25 +7,6 @@ import 'package:flutter/material.dart'; import 'result.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('Hello World!'), - ), - ), - ); - } -} - class UserProfile { final String name; final String email; diff --git a/src/content/cookbook/architecture/command.md b/src/content/cookbook/architecture/command.md index 640e6baaca..54e6d93244 100644 --- a/src/content/cookbook/architecture/command.md +++ b/src/content/cookbook/architecture/command.md @@ -89,15 +89,7 @@ class HomeViewModel extends ChangeNotifier { void load() { // load user } - - void load() { - if (running) { - return; - } - // load user - } - - void clearError() {} + // ··· } ``` From 0521d6f8a41f1e7a779bd1371c03b5c309cfcdc0 Mon Sep 17 00:00:00 2001 From: Miguel Beltran Date: Wed, 27 Nov 2024 17:53:40 +0100 Subject: [PATCH 11/33] Apply suggestion on command.md Co-authored-by: Eric Windmill --- src/content/cookbook/architecture/command.md | 49 ++++++++------------ 1 file changed, 20 insertions(+), 29 deletions(-) diff --git a/src/content/cookbook/architecture/command.md b/src/content/cookbook/architecture/command.md index 54e6d93244..69977636aa 100644 --- a/src/content/cookbook/architecture/command.md +++ b/src/content/cookbook/architecture/command.md @@ -12,7 +12,7 @@ Model-View-ViewModel (MVVM) is a design pattern that separates a feature of an application into three parts: the `Model`, the `ViewModel` and the `View`. Views and ViewModels make up the UI layer of an application. -Repositories and services represent the data of an application, +Repositories and services represent the data layer of an application, or the Model layer of MVVM. ViewModels can become very complex @@ -74,7 +74,7 @@ Besides data, ViewModels also contain other types of UI state. For example, a `running` state or an `error` state. This allows the View to show to the user if the action is still running -or if it was completed successfully. +or if it completed successfully. ```dart @@ -124,7 +124,7 @@ void load() { ``` Managing the state of an action can get complicated -once the ViewModel contains multiple actions. +if the ViewModel contains multiple actions. For example, adding an `edit()` action to the `HomeViewModel` can lead the following outcome: @@ -155,18 +155,16 @@ Sharing the running state between the `load()` and `edit()` actions might not always work, because you might want to show a different UI component when the `load()` action runs than when the `edit()` action runs, -and the same problem with the `error` state. +and you'll have the same problem with the `error` state. ### Triggering UI actions from ViewModels Another challenge with ViewModel classes is executing UI actions -hen the ViewModel state changes. +when the ViewModel state changes. -For example, to show a `SnackBar` when an error occurs, -or to navigate to a different screen when an action completes. - -To implement that, listen to the changes in the ViewModel, +For example, you might want to show a `SnackBar` when an error occurs, +or navigate to a different screen when an action completes. To implement this, listen for changes in the ViewModel, and perform the action depending on the state. In the View: @@ -211,7 +209,7 @@ void _onViewModelChanged() { ## Command pattern You might find yourself repeating the above code over and over, -having to implement a different running state +implementing a different running state for each action in every ViewModel. At that point, it makes sense to extract this code into a reusable pattern: a command. @@ -289,14 +287,12 @@ When the action finishes, the `running` state changes to `false` and the `completed` state to `true`. -When the `running` state is `true`, -it prevents the command from being launched multiple times before completing. -This prevents users from attempting to tap multiple times on a button -while an action is executing. +If the `running` state is `true`, the command cannot begin executing again. +This prevents users from triggering a command multiple times by pressing a button rapidly. -The command’s `execute()` method could also capture any thrown Exceptions` +The command’s `execute()` method also captures any thrown `Exceptions` by the action implementation automatically - and expose them in the `error` state. + and exposes them in the `error` state. The following is what a command class might look like. It’s been simplified for demo purposes. @@ -353,7 +349,7 @@ The `Command` class extends from `ChangeNotifier`, allowing Views to listen to its states. In the `ListenableBuilder`, -instead of passing the ViewModel as listenable, +instead of passing the ViewModel to `ListenableBuilder.listenable`, pass the command: @@ -371,7 +367,7 @@ ListenableBuilder( ) ``` -As well listen to changes in the command state in order to run UI actions: +And listen to changes in the command state in order to run UI actions: ```dart @@ -400,11 +396,7 @@ void _onViewModelChanged() { ### Combining command and ViewModel -When combined with the ViewModel, -use the child argument to stack multiple `ListenableBuilder`. -For example, -listen to the `running` and `error` states of the command -before showing the ViewModel data. +You can stack multiple `ListenableBuilder` widgets to listen to `running` and `error` states before showing the ViewModel data. ```dart @@ -464,9 +456,8 @@ class HomeViewModel2 extends ChangeNotifier { ### Extending the command pattern -The command pattern can be extended in multiple ways, -for example, -to support a different number of arguments. +The command pattern can be extended in multiple ways. +For example, to support a different number of arguments. ```dart @@ -497,19 +488,19 @@ class HomeViewModel extends ChangeNotifier { ## Putting it all together In this guide, -you have learned how to use the command design pattern +you learned how to use the command design pattern to improve the implementation of ViewModels when using the MVVM design pattern. Below, you can find the full `Command` class as implemented in the [Compass App example][] -for the Flutter Architecture guidelines. +for the Flutter architecture guidelines. It also uses the [`Result` class][] to determine if the action completed successfuly or with an error. This implementation also includes two types of command, a `Command0`, for actions without parameters, -and a `Command1`, which takes one parameter. +and a `Command1`, for actions that take one parameter. :::note Check [pub.dev][] for different ready-to-use From ebf786f07658dde57e14fc1f0dce0fe6e77b80e3 Mon Sep 17 00:00:00 2001 From: Miguel Beltran Date: Wed, 27 Nov 2024 17:56:28 +0100 Subject: [PATCH 12/33] Apply suggestions in result.md Co-authored-by: Eric Windmill --- src/content/cookbook/architecture/result.md | 55 ++++++++++----------- 1 file changed, 25 insertions(+), 30 deletions(-) diff --git a/src/content/cookbook/architecture/result.md b/src/content/cookbook/architecture/result.md index e19407e45e..32166d3c5b 100644 --- a/src/content/cookbook/architecture/result.md +++ b/src/content/cookbook/architecture/result.md @@ -29,13 +29,13 @@ and how to mitigate it using the result class pattern. ## Error flow in Flutter applications -Applications following the [Flutter Architecture guidelines][] +Applications following the [Flutter architecture guidelines][] are usually composed of ViewModels, Repositories, and Services, among other parts. When a function in one of these components fails, -it will have to communicate the error to the calling component. +it should communicate the error to the calling component. -Typically, that would be done using exceptions. +Typically, that's done with exceptions. For example, an API client Service failing to communicate with the remote server might throw an HTTP Error Exception. @@ -54,7 +54,7 @@ that uses the `ApiClientService` to provide the `UserProfile`. that uses the `UserProfileRepository`. The `ApiClientService` contains a method named `getUserProfile` - hat can throw exceptions in different situations: + that can throw exceptions in different situations: - The method throws an `HttpException` if the response code isn’t 200. - The JSON parsing method might throw an exception @@ -121,9 +121,8 @@ class UserProfileViewModel extends ChangeNotifier { } } ``` -However, as multiple developers can work on the same codebase, -it is not unusual that a developer might forget to properly capture exceptions. -The following code will compile and run, + +In reality, a developer might forget to properly capture exceptions and end up with the following code. It would compile and run, but will crash if one of the exceptions mentioned previously occurs: @@ -139,28 +138,28 @@ class UserProfileViewModel extends ChangeNotifier { ``` You can attempt to solve this by documenting the `ApiClientService`, -warning about the possible exceptions it may throw. +warning about the possible exceptions it might throw. However, since the ViewModel doesn’t use the service directly, -this information may be missed by other developers working in the codebase. +other developers working in the codebase might miss this information. ## Using the result pattern An alternative to throwing exceptions -is to wrap the function output into a `Result`. +is to wrap the function output into a `Result` object. When the function runs successfully, the `Result` contains the returned value. However, if the function did not complete successfully, the `Result` object will contain the error. -A `Result` is a `sealed class` +A `Result` is a `sealed` class that can either be of the subclass `Ok` or the subclass `Error`. Return the successful value with the subclass `Ok`, and the captured error with the subclass `Error`. The following is what a `Result` class might look like. It’s been simplified for demo purposes. -You can see a full implementation at the end of this page. +A full implementation is at the end of this page. ```dart @@ -193,8 +192,7 @@ final class Error extends Result { In this example, the `Result` class uses a generic type `T` to represent any return value, -which can be as simple as a `String` or an `int` -but also can be used for custom data classes like the `UserProfile`. +which can be a primitive Dart type like `String` or an `int` or a custom class like `UserProfile`. ### Creating a `Result` object @@ -221,7 +219,7 @@ it returns a `Result` object containing a `UserProfile`. To facilitate using the `Result` class, it contains two named constructors, `Result.ok` and `Result.error`. Use them to construct the `Result` depending on desired output. -As well, capture any thrown exceptions by the code +As well, capture any exceptions thrown by the code and wrap them into the `Result` object. For example, here the `getUserProfile()` method @@ -251,13 +249,13 @@ class ApiClientService { } ``` -The original return statement has been replaced -with returning the value using `Result.ok`. +The original return statement was replaced +with a statement that returns the value using `Result.ok`. The `throw HttpException()` -has been replaced with returning `Result.error(HttpException())` +was replaced with a statement that returns `Result.error(HttpException())`, wrapping the error into a `Result`. -As well, the method is wrapped with a try-catch -to capture any thrown exception by the Http Client +As well, the method is wrapped with a `try-catch` block +to capture any exceptions thrown by the Http client or the JSON parser into a `Result.error`. The repository class also needs to be modified, @@ -302,23 +300,21 @@ class UserProfileViewModel extends ChangeNotifier { } ``` -The `Result` class is implemented using a `sealed class`, +The `Result` class is implemented using a `sealed` class, meaning it can only either be an `Ok` or an `Error`, so this operation can be done using a switch case. In the `Ok` case, obtain the value using the `value` property. -In the Error case, +In the `Error` case, obtain the error object using the `error` property. ## Improving control flow -Wrapping code with a try-catch -is a quick way to ensure that any exception thrown -is captured by the calling method. -Catching stops the exception from propagating to other methods, h -owever it also disrupts the normal code flow. +Wrapping code in a `try-catch` block +is a quick way to ensure that the calling method captures any thrown exception. +Catching stops the exception from propagating to other methods, and it disrupts the normal code flow. Consider the following code. @@ -343,9 +339,8 @@ class UserProfileRepository { In this method, the `UserProfileRepository` attempts to obtain the `UserProfile` -using the `ApiClientService`, -and if it fails, -then tries to create a temporary user in a `DatabaseService`. +using the `ApiClientService`. +If it fails, it tries to create a temporary user in a `DatabaseService`. Because either Service methods can fail, the code must catch the exceptions in both cases. From 717ed2b08c480a59bb3d44f888127bd7f0917581 Mon Sep 17 00:00:00 2001 From: Miguel Beltran Date: Wed, 27 Nov 2024 17:58:22 +0100 Subject: [PATCH 13/33] add link to ChangeNotifier --- src/content/cookbook/architecture/command.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/content/cookbook/architecture/command.md b/src/content/cookbook/architecture/command.md index 69977636aa..38e6ffd046 100644 --- a/src/content/cookbook/architecture/command.md +++ b/src/content/cookbook/architecture/command.md @@ -28,7 +28,7 @@ to improve your ViewModels. ## Challenges when implementing ViewModels ViewModel classes in Flutter are typically implemented -by extending the `ChangeNotifier` class. +by extending the [`ChangeNotifier`][] class. This allows ViewModels to call `notifyListeners()` to refresh Views when data is updated. @@ -613,3 +613,4 @@ class Command1 extends Command { [`Result` class]:/cookbook/architecture/result [pub.dev]:{{site.pub}} [flutter_command]:{{site.pub-pkg}}/flutter_command +[`ChangeNotifier`]:/get-started/fundamentals/state-management From 490cc492f0d782a8d3ad11baec68002568558c09 Mon Sep 17 00:00:00 2001 From: Miguel Beltran Date: Wed, 27 Nov 2024 18:00:34 +0100 Subject: [PATCH 14/33] fix links --- src/content/cookbook/architecture/result.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/content/cookbook/architecture/result.md b/src/content/cookbook/architecture/result.md index 32166d3c5b..fc3bcc0640 100644 --- a/src/content/cookbook/architecture/result.md +++ b/src/content/cookbook/architecture/result.md @@ -152,7 +152,7 @@ the `Result` contains the returned value. However, if the function did not complete successfully, the `Result` object will contain the error. -A `Result` is a `sealed` class +A `Result` is a [`sealed`][] class that can either be of the subclass `Ok` or the subclass `Error`. Return the successful value with the subclass `Ok`, and the captured error with the subclass `Error`. @@ -451,9 +451,10 @@ final class Error extends Result { ``` [Error handling documentation]:{{site.dart-site}}/language/error-handling -[Flutter Architecture guidelines]:/app-architecture +[Flutter architecture guidelines]:/app-architecture [Compass App example]:{{site.repo.samples}}/tree/main/compass_app [pub.dev]:{{site.pub}} [result_dart]:{{site.pub-pkg}}/result_dart [result_type]:{{site.pub-pkg}}/result_type [multiple_result]:{{site.pub-pkg}}/multiple_result +[`sealed`]:{{site.dart-site}}/language/class-modifiers#sealed From b60f2bcbe6eb66f5ef8610ce3a2c76bdbc677396 Mon Sep 17 00:00:00 2001 From: Miguel Beltran Date: Thu, 28 Nov 2024 14:56:13 +0100 Subject: [PATCH 15/33] renamed function --- examples/cookbook/architecture/result/lib/main.dart | 4 ++-- src/content/cookbook/architecture/result.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/cookbook/architecture/result/lib/main.dart b/examples/cookbook/architecture/result/lib/main.dart index e938b0cdc6..2c108805ac 100644 --- a/examples/cookbook/architecture/result/lib/main.dart +++ b/examples/cookbook/architecture/result/lib/main.dart @@ -66,7 +66,7 @@ class UserProfileRepository { return apiResult; } - final databaseResult = await _databaseService.createTemporalUser(); + final databaseResult = await _databaseService.createTemporaryUser(); if (databaseResult is Ok) { return databaseResult; } @@ -110,7 +110,7 @@ class ApiClientService { // #enddocregion ApiClientService2 class DatabaseService { - Future> createTemporalUser() async { + Future> createTemporaryUser() async { await Future.delayed(const Duration(seconds: 2)); return Result.ok(UserProfile('John Doe', 'john@example.com')); } diff --git a/src/content/cookbook/architecture/result.md b/src/content/cookbook/architecture/result.md index fc3bcc0640..f2a0d3e5d3 100644 --- a/src/content/cookbook/architecture/result.md +++ b/src/content/cookbook/architecture/result.md @@ -356,7 +356,7 @@ Future> getUserProfile() async { return apiResult; } - final databaseResult = await _databaseService.createTemporalUser(); + final databaseResult = await _databaseService.createTemporaryUser(); if (databaseResult is Ok) { return databaseResult; } From 0e43008ed8e12e2ef051e7dae21d173e82b89020 Mon Sep 17 00:00:00 2001 From: Miguel Beltran Date: Mon, 2 Dec 2024 17:28:53 +0100 Subject: [PATCH 16/33] Apply suggestions from code review to Command.md Co-authored-by: Shams Zakhour (ignore Sfshaza) <44418985+sfshaza2@users.noreply.github.com> --- src/content/cookbook/architecture/command.md | 40 ++++++++++---------- 1 file changed, 19 insertions(+), 21 deletions(-) diff --git a/src/content/cookbook/architecture/command.md b/src/content/cookbook/architecture/command.md index 38e6ffd046..6248055690 100644 --- a/src/content/cookbook/architecture/command.md +++ b/src/content/cookbook/architecture/command.md @@ -52,8 +52,8 @@ class HomeViewModel extends ChangeNotifier { } ``` -ViewModels also contain actions typically triggered by the View. -For example, a `load` action in charge of loading the `user`. +ViewModels also contain actions typically triggered by the View; +for example, a `load` action in charge of loading the `user`. ```dart @@ -70,11 +70,9 @@ class HomeViewModel extends ChangeNotifier { ### UI state in ViewModels -Besides data, ViewModels also contain other types of UI state. -For example, a `running` state or an `error` state. -This allows the View to show to the user -if the action is still running -or if it completed successfully. +A view model also contains UI state besides data, such as +whether the view is running or has experienced an error. +This allows the app to tell the user if the action has completed successfully. ```dart @@ -159,12 +157,12 @@ and you'll have the same problem with the `error` state. ### Triggering UI actions from ViewModels -Another challenge with ViewModel classes -is executing UI actions -when the ViewModel state changes. +View model classes can run into problems when +executing UI actions and the view model's state changes. For example, you might want to show a `SnackBar` when an error occurs, -or navigate to a different screen when an action completes. To implement this, listen for changes in the ViewModel, +or navigate to a different screen when an action completes. +To implement this, listen for changes in the view model, and perform the action depending on the state. In the View: @@ -272,7 +270,8 @@ Instead of calling `viewModel.load()` to run the load action, now you call `viewModel.load.execute()`. The `execute()` method can also be called from within the ViewModel. -For example, to run the `load` command when the ViewModel is created: +The following line of code runs the `load` command when the +view model is created. ```dart @@ -290,12 +289,11 @@ and the `completed` state to `true`. If the `running` state is `true`, the command cannot begin executing again. This prevents users from triggering a command multiple times by pressing a button rapidly. -The command’s `execute()` method also captures any thrown `Exceptions` - by the action implementation automatically - and exposes them in the `error` state. +The command’s `execute()` method captures any thrown `Exceptions` +automatically and exposes them in the `error` state. -The following is what a command class might look like. -It’s been simplified for demo purposes. +The following code shows a sample `Command` class that +has been simplified for demo purposes. You can see a full implementation at the end of this page. @@ -426,8 +424,8 @@ body: ListenableBuilder( ), ``` -You can have multiple commands in a single ViewModel, -simplifying the implementation of ViewModels +You can define multiple commands classes in a single view model, +simplifying its implementation and minimizing the amount of repeated code. @@ -498,12 +496,12 @@ for the Flutter architecture guidelines. It also uses the [`Result` class][] to determine if the action completed successfuly or with an error. -This implementation also includes two types of command, +This implementation also includes two types of commands, a `Command0`, for actions without parameters, and a `Command1`, for actions that take one parameter. :::note -Check [pub.dev][] for different ready-to-use +Check [pub.dev][] for other ready-to-use implementations of the command pattern, like the [flutter_command][] package. ::: From 7d5205df415422a451bb6a8093bdea60a339d0e7 Mon Sep 17 00:00:00 2001 From: Miguel Beltran Date: Mon, 2 Dec 2024 17:32:18 +0100 Subject: [PATCH 17/33] Apply suggestions from code review in result.md Co-authored-by: Shams Zakhour (ignore Sfshaza) <44418985+sfshaza2@users.noreply.github.com> --- src/content/cookbook/architecture/result.md | 69 ++++++++++----------- 1 file changed, 34 insertions(+), 35 deletions(-) diff --git a/src/content/cookbook/architecture/result.md b/src/content/cookbook/architecture/result.md index f2a0d3e5d3..0197b33585 100644 --- a/src/content/cookbook/architecture/result.md +++ b/src/content/cookbook/architecture/result.md @@ -9,12 +9,12 @@ js: Dart provides a built-in error handling mechanism -through the ability to throw and catch exceptions. +with the ability to throw and catch exceptions. As mentioned in the [Error handling documentation][], -Dart’s exceptions are unhandled exceptions, -meaning that methods that throw don’t need to declare them, -and calling methods are not required to catch them either. +Dart’s exceptions are unhandled exceptions. +This means that methods that throw exceptions don’t need to declare them, +and calling methods aren't required to catch them either. This can lead to situations where exceptions are not handled properly. In large projects, @@ -25,7 +25,7 @@ This can lead to errors and crashes. In this guide, you will learn about this limitation -and how to mitigate it using the result class pattern. +and how to mitigate it using the _result_ pattern. ## Error flow in Flutter applications @@ -46,22 +46,20 @@ or ignore it and let the calling ViewModel handle it. This can be observed in the following example. Consider these classes: -- A service named `ApiClientService` -that performs API calls to a remote service. -- A repository named `UserProfileRepository` -that uses the `ApiClientService` to provide the `UserProfile`. -- A ViewModel named `UserProfileViewModel` -that uses the `UserProfileRepository`. +- A service,`ApiClientService`, performs API calls to a remote service. +- A repository, `UserProfileRepository`, + provides the `UserProfile` provided by the `ApiClientService`. +- A view model, `UserProfileViewModel`, uses the `UserProfileRepository`. -The `ApiClientService` contains a method named `getUserProfile` - that can throw exceptions in different situations: +The `ApiClientService` contains a method, `getUserProfile`, +that throws exceptions in certain situations: - The method throws an `HttpException` if the response code isn’t 200. -- The JSON parsing method might throw an exception -if the response is not formatted correctly. -- The Http Client might throw an exception due to networking issues. +- The JSON parsing method throws an exception + if the response isn't formatted correctly. +- The Http client might throw an exception due to networking issues. -For example: +The following code tests for a variety of possible exceptions: ```dart @@ -122,7 +120,8 @@ class UserProfileViewModel extends ChangeNotifier { } ``` -In reality, a developer might forget to properly capture exceptions and end up with the following code. It would compile and run, +In reality, a developer might forget to properly capture exceptions +and end up with the following code. It would compile and run, but will crash if one of the exceptions mentioned previously occurs: @@ -153,12 +152,12 @@ However, if the function did not complete successfully, the `Result` object will contain the error. A `Result` is a [`sealed`][] class -that can either be of the subclass `Ok` or the subclass `Error`. +that can either subclass `Ok` or the `Error` class. Return the successful value with the subclass `Ok`, and the captured error with the subclass `Error`. -The following is what a `Result` class might look like. -It’s been simplified for demo purposes. +The following code shows a sample `Result` class that +has been simplified for demo purposes. A full implementation is at the end of this page. @@ -274,7 +273,7 @@ Future> getUserProfile() async { Now the ViewModel doesn't receive the `UserProfile` directly, but instead it receives a `Result` containing a `UserProfile`. -This enforces the developer implementing the ViewModel +This forces the developer implementing the view model to unwrap the `Result` to obtain the `UserProfile`, and avoids having uncaught exceptions. @@ -301,8 +300,9 @@ class UserProfileViewModel extends ChangeNotifier { ``` The `Result` class is implemented using a `sealed` class, -meaning it can only either be an `Ok` or an `Error`, -so this operation can be done using a switch case. +meaning it can only be of type `Ok` or `Error`. +This allows the code to evaluate the result with a simple +switch statement. In the `Ok` case, obtain the value using the `value` property. @@ -312,9 +312,8 @@ obtain the error object using the `error` property. ## Improving control flow -Wrapping code in a `try-catch` block -is a quick way to ensure that the calling method captures any thrown exception. -Catching stops the exception from propagating to other methods, and it disrupts the normal code flow. +Wrapping code in a `try-catch` block ensures that +thrown exceptions are caught and not propagated to other parts of the code. Consider the following code. @@ -342,7 +341,7 @@ attempts to obtain the `UserProfile` using the `ApiClientService`. If it fails, it tries to create a temporary user in a `DatabaseService`. -Because either Service methods can fail, +Because either Service method can fail, the code must catch the exceptions in both cases. This can be improved using the `Result` pattern: @@ -365,9 +364,9 @@ Future> getUserProfile() async { } ``` -In this case, if the `Result` object is an `Ok`, -then the function returns that object, -otherwise at the end of the function returns a `Result.Error` if none worked. +In this code, if the `Result` object is an `Ok` instance, +then the function returns that object; +otherwise, it returns `Result.Error`. ## Putting it all together @@ -376,11 +375,11 @@ how to use a `Result` class to return result values. The key takeaways are: -- `Result` classes enforce calling methods to check for errors, -reducing the amount of bugs caused by uncaught exceptions. +- `Result` classes force the calling method to check for errors, + reducing the amount of bugs caused by uncaught exceptions. - `Result` classes help improve control flow compared to try-catch blocks. -- `Result` classes are `sealed classes` that can only be `Ok` or `Error`, -allowing you to use switch statements to unwrap them. +- `Result` classes are `sealed` and can only return `Ok` or `Error` instances, + allowing the code to unwrap them with a switch statement. Below you can find the full `Result` class as implemented in the [Compass App example][] From 9fbe7de56aa58eb73735057d77177c9c5661af7f Mon Sep 17 00:00:00 2001 From: Miguel Beltran Date: Tue, 3 Dec 2024 11:42:01 +0100 Subject: [PATCH 18/33] use correct capitalization in view, view model and model --- src/content/cookbook/architecture/command.md | 49 ++++++++++---------- 1 file changed, 25 insertions(+), 24 deletions(-) diff --git a/src/content/cookbook/architecture/command.md b/src/content/cookbook/architecture/command.md index 6248055690..089ab168f1 100644 --- a/src/content/cookbook/architecture/command.md +++ b/src/content/cookbook/architecture/command.md @@ -10,26 +10,26 @@ js: Model-View-ViewModel (MVVM) is a design pattern that separates a feature of an application into three parts: -the `Model`, the `ViewModel` and the `View`. -Views and ViewModels make up the UI layer of an application. +the model, the view model and the view. +Views and view models make up the UI layer of an application. Repositories and services represent the data layer of an application, -or the Model layer of MVVM. +or the model layer of MVVM. -ViewModels can become very complex +View models can become very complex as an application grows and features become bigger. -The command pattern helps to streamline running actions in ViewModels +The command pattern helps to streamline running actions in view models by encapsulating some of its complexity and avoiding code repetition. In this guide, you will learn how to use the command pattern -to improve your ViewModels. +to improve your view models. -## Challenges when implementing ViewModels +## Challenges when implementing view models -ViewModel classes in Flutter are typically implemented +View model classes in Flutter are typically implemented by extending the [`ChangeNotifier`][] class. -This allows ViewModels to call `notifyListeners()` to refresh Views +This allows view models to call `notifyListeners()` to refresh views when data is updated. @@ -39,9 +39,9 @@ class HomeViewModel extends ChangeNotifier { } ``` -ViewModels contain a representation of the UI state, +View models contain a representation of the UI state, including the data being displayed. -For example, this `HomeViewModel` exposes the `User` instance to the View. +For example, this `HomeViewModel` exposes the `User` instance to the view. ```dart @@ -52,7 +52,7 @@ class HomeViewModel extends ChangeNotifier { } ``` -ViewModels also contain actions typically triggered by the View; +View models also contain actions typically triggered by the view; for example, a `load` action in charge of loading the `user`. @@ -68,7 +68,7 @@ class HomeViewModel extends ChangeNotifier { } ``` -### UI state in ViewModels +### UI state in view models A view model also contains UI state besides data, such as whether the view is running or has experienced an error. @@ -91,7 +91,7 @@ class HomeViewModel extends ChangeNotifier { } ``` -You can use the running state to display a progress indicator in the View: +You can use the running state to display a progress indicator in the view: ```dart @@ -122,7 +122,7 @@ void load() { ``` Managing the state of an action can get complicated -if the ViewModel contains multiple actions. +if the view model contains multiple actions. For example, adding an `edit()` action to the `HomeViewModel` can lead the following outcome: @@ -155,7 +155,7 @@ because you might want to show a different UI component when the `load()` action runs than when the `edit()` action runs, and you'll have the same problem with the `error` state. -### Triggering UI actions from ViewModels +### Triggering UI actions from view models View model classes can run into problems when executing UI actions and the view model's state changes. @@ -165,7 +165,7 @@ or navigate to a different screen when an action completes. To implement this, listen for changes in the view model, and perform the action depending on the state. -In the View: +In the view: ```dart @@ -208,11 +208,11 @@ void _onViewModelChanged() { You might find yourself repeating the above code over and over, implementing a different running state -for each action in every ViewModel. +for each action in every view model. At that point, it makes sense to extract this code into a reusable pattern: a command. -A command is a class that encapsulates a ViewModel action, +A command is a class that encapsulates a view model action, and exposes the different states that an action can have. @@ -238,7 +238,7 @@ class Command extends ChangeNotifier { } ``` -In the ViewModel, +In the view model, instead of defining an action directly with a method, you create a command object: @@ -269,7 +269,7 @@ as they are now part of the command. Instead of calling `viewModel.load()` to run the load action, now you call `viewModel.load.execute()`. -The `execute()` method can also be called from within the ViewModel. +The `execute()` method can also be called from within the view model. The following line of code runs the `load` command when the view model is created. @@ -347,7 +347,7 @@ The `Command` class extends from `ChangeNotifier`, allowing Views to listen to its states. In the `ListenableBuilder`, -instead of passing the ViewModel to `ListenableBuilder.listenable`, +instead of passing the view model to `ListenableBuilder.listenable`, pass the command: @@ -394,7 +394,8 @@ void _onViewModelChanged() { ### Combining command and ViewModel -You can stack multiple `ListenableBuilder` widgets to listen to `running` and `error` states before showing the ViewModel data. +You can stack multiple `ListenableBuilder` widgets to listen to `running` +and `error` states before showing the view model data. ```dart @@ -487,7 +488,7 @@ class HomeViewModel extends ChangeNotifier { In this guide, you learned how to use the command design pattern -to improve the implementation of ViewModels +to improve the implementation of view models when using the MVVM design pattern. Below, you can find the full `Command` class From 436911e633bab9df46a3bf009955b5ab67ded93c Mon Sep 17 00:00:00 2001 From: Miguel Beltran Date: Tue, 3 Dec 2024 11:43:34 +0100 Subject: [PATCH 19/33] result.md: use correct capitalization in view, view model and model --- src/content/cookbook/architecture/result.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/content/cookbook/architecture/result.md b/src/content/cookbook/architecture/result.md index 0197b33585..80c0a0eeca 100644 --- a/src/content/cookbook/architecture/result.md +++ b/src/content/cookbook/architecture/result.md @@ -30,8 +30,8 @@ and how to mitigate it using the _result_ pattern. ## Error flow in Flutter applications Applications following the [Flutter architecture guidelines][] -are usually composed of ViewModels, -Repositories, and Services, among other parts. +are usually composed of view models, +repositories, and services, among other parts. When a function in one of these components fails, it should communicate the error to the calling component. @@ -42,7 +42,7 @@ might throw an HTTP Error Exception. The calling component, for example a Repository, would have to either capture this exception -or ignore it and let the calling ViewModel handle it. +or ignore it and let the calling view model handle it. This can be observed in the following example. Consider these classes: @@ -138,7 +138,7 @@ class UserProfileViewModel extends ChangeNotifier { You can attempt to solve this by documenting the `ApiClientService`, warning about the possible exceptions it might throw. -However, since the ViewModel doesn’t use the service directly, +However, since the view model doesn’t use the service directly, other developers working in the codebase might miss this information. ## Using the result pattern @@ -270,7 +270,7 @@ Future> getUserProfile() async { ### Unwrapping the Result object -Now the ViewModel doesn't receive the `UserProfile` directly, +Now the view model doesn't receive the `UserProfile` directly, but instead it receives a `Result` containing a `UserProfile`. This forces the developer implementing the view model From aa5f34f3aa09d6eb1c4aebe29e640aa606a986b5 Mon Sep 17 00:00:00 2001 From: Miguel Beltran Date: Tue, 3 Dec 2024 12:01:50 +0100 Subject: [PATCH 20/33] rewrite command pattern introduction --- src/content/cookbook/architecture/command.md | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/content/cookbook/architecture/command.md b/src/content/cookbook/architecture/command.md index 089ab168f1..71ca0f45e8 100644 --- a/src/content/cookbook/architecture/command.md +++ b/src/content/cookbook/architecture/command.md @@ -15,11 +15,20 @@ Views and view models make up the UI layer of an application. Repositories and services represent the data layer of an application, or the model layer of MVVM. +A command is a class that wraps a method +and helps to handle the different states of that method, +such as running, complete, and error. + +View models can use commands to handle interaction and run actions. +As well, they can be used to display different UI states, +like loading indicators when an action is running is, +or an error dialog when an action failed. + View models can become very complex as an application grows and features become bigger. -The command pattern helps to streamline running actions in view models -by encapsulating some of its complexity and avoiding code repetition. +Commands can help to simplify view models +and reuse code. In this guide, you will learn how to use the command pattern From ad1f9f4abd43139b46b5e4fa858befb02ec26ffb Mon Sep 17 00:00:00 2001 From: Miguel Beltran Date: Tue, 3 Dec 2024 14:18:58 +0100 Subject: [PATCH 21/33] small typo --- src/content/cookbook/architecture/command.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/content/cookbook/architecture/command.md b/src/content/cookbook/architecture/command.md index 71ca0f45e8..b025306370 100644 --- a/src/content/cookbook/architecture/command.md +++ b/src/content/cookbook/architecture/command.md @@ -21,7 +21,7 @@ such as running, complete, and error. View models can use commands to handle interaction and run actions. As well, they can be used to display different UI states, -like loading indicators when an action is running is, +like loading indicators when an action is running, or an error dialog when an action failed. View models can become very complex From 32cefea4dbed5f26a81974d3831f9e9c516e434d Mon Sep 17 00:00:00 2001 From: Miguel Beltran Date: Thu, 5 Dec 2024 08:50:12 +0100 Subject: [PATCH 22/33] Apply suggestions from code review Co-authored-by: Parker Lougheed --- .../architecture/command/lib/command.dart | 25 ++++++++++--------- .../architecture/command/lib/result.dart | 16 ++++++------ 2 files changed, 21 insertions(+), 20 deletions(-) diff --git a/examples/cookbook/architecture/command/lib/command.dart b/examples/cookbook/architecture/command/lib/command.dart index 2e37434b99..b3a8321cac 100644 --- a/examples/cookbook/architecture/command/lib/command.dart +++ b/examples/cookbook/architecture/command/lib/command.dart @@ -11,7 +11,7 @@ import 'result.dart'; typedef CommandAction0 = Future> Function(); typedef CommandAction1 = Future> Function(A); -/// Facilitates interaction with a ViewModel. +/// Facilitates interaction with a view model. /// /// Encapsulates an action, /// exposes its running and error states, @@ -29,27 +29,30 @@ abstract class Command extends ChangeNotifier { bool _running = false; - /// True when the action is running. + /// Whether the action is running. bool get running => _running; Result? _result; - /// true if action completed with error + /// Whether the action completed with an error. bool get error => _result is Error; - /// true if action completed successfully + /// Whether the action completed successfully. bool get completed => _result is Ok; - /// Get last action result + /// The result of the most recent action. + /// + /// Returns `null` if the action is running or completed with an error. Result? get result => _result; - /// Clear last action result + /// Clears the most recent action's result. void clearResult() { _result = null; notifyListeners(); } - /// Internal execute implementation + /// Execute the provided [action], notifying listeners and + /// setting the running and result states as necessary. Future _execute(CommandAction0 action) async { // Ensure the action can't launch multiple times. // e.g. avoid multiple taps on button @@ -70,8 +73,7 @@ abstract class Command extends ChangeNotifier { } } -/// [Command] without arguments. -/// Takes a [CommandAction0] as action. +/// A [Command] that accepts no arguments. class Command0 extends Command { Command0(this._action); @@ -83,14 +85,13 @@ class Command0 extends Command { } } -/// [Command] with one argument. -/// Takes a [CommandAction1] as action. +/// A [Command] that accepts one argument. class Command1 extends Command { Command1(this._action); final CommandAction1 _action; - /// Executes the action with the argument. + /// Executes the action with the specified [argument]. Future execute(A argument) async { await _execute(() => _action(argument)); } diff --git a/examples/cookbook/architecture/command/lib/result.dart b/examples/cookbook/architecture/command/lib/result.dart index 5370700b03..a405e2e053 100644 --- a/examples/cookbook/architecture/command/lib/result.dart +++ b/examples/cookbook/architecture/command/lib/result.dart @@ -18,11 +18,11 @@ sealed class Result { const Result(); - /// Creates an instance of Result containing a value - factory Result.ok(T value) => Ok(value); + /// Creates a successful [Result], completed with the specified [value]. + const factory Result.ok(T value) = Ok; - /// Create an instance of Result containing an error - factory Result.error(Exception error) => Error(error); + /// Creates an error [Result], completed with the specified [error]. + const factory Result.error(Exception error) = Error; /// Convenience method to cast to Ok Ok get asOk => this as Ok; @@ -31,22 +31,22 @@ sealed class Result { Error get asError => this as Error; } -/// Subclass of Result for values +/// A successful [Result] with a returned [value]. final class Ok extends Result { const Ok(this.value); - /// Returned value in result + /// The returned value of this result. final T value; @override String toString() => 'Result<$T>.ok($value)'; } -/// Subclass of Result for errors +/// An error [Result] with a resulting [error]. final class Error extends Result { const Error(this.error); - /// Returned error in result + /// The resulting error of this result. final Exception error; @override From 0b3084483a4fe03b7a032cdab841a4165d857e12 Mon Sep 17 00:00:00 2001 From: Miguel Beltran Date: Thu, 5 Dec 2024 08:51:26 +0100 Subject: [PATCH 23/33] refresh code excerpts --- src/content/cookbook/architecture/command.md | 25 ++++++++++---------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/src/content/cookbook/architecture/command.md b/src/content/cookbook/architecture/command.md index b025306370..6f5a97799a 100644 --- a/src/content/cookbook/architecture/command.md +++ b/src/content/cookbook/architecture/command.md @@ -531,7 +531,7 @@ import 'result.dart'; typedef CommandAction0 = Future> Function(); typedef CommandAction1 = Future> Function(A); -/// Facilitates interaction with a ViewModel. +/// Facilitates interaction with a view model. /// /// Encapsulates an action, /// exposes its running and error states, @@ -549,27 +549,30 @@ abstract class Command extends ChangeNotifier { bool _running = false; - /// True when the action is running. + /// Whether the action is running. bool get running => _running; Result? _result; - /// true if action completed with error + /// Whether the action completed with an error. bool get error => _result is Error; - /// true if action completed successfully + /// Whether the action completed successfully. bool get completed => _result is Ok; - /// Get last action result + /// The result of the most recent action. + /// + /// Returns `null` if the action is running or completed with an error. Result? get result => _result; - /// Clear last action result + /// Clears the most recent action's result. void clearResult() { _result = null; notifyListeners(); } - /// Internal execute implementation + /// Execute the provided [action], notifying listeners and + /// setting the running and result states as necessary. Future _execute(CommandAction0 action) async { // Ensure the action can't launch multiple times. // e.g. avoid multiple taps on button @@ -590,8 +593,7 @@ abstract class Command extends ChangeNotifier { } } -/// [Command] without arguments. -/// Takes a [CommandAction0] as action. +/// A [Command] that accepts no arguments. class Command0 extends Command { Command0(this._action); @@ -603,14 +605,13 @@ class Command0 extends Command { } } -/// [Command] with one argument. -/// Takes a [CommandAction1] as action. +/// A [Command] that accepts one argument. class Command1 extends Command { Command1(this._action); final CommandAction1 _action; - /// Executes the action with the argument. + /// Executes the action with the specified [argument]. Future execute(A argument) async { await _execute(() => _action(argument)); } From c483456dd2db469d3df2b88eeef381886a6cd9af Mon Sep 17 00:00:00 2001 From: Miguel Beltran Date: Thu, 5 Dec 2024 08:53:29 +0100 Subject: [PATCH 24/33] format --- examples/cookbook/architecture/command/lib/command.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/cookbook/architecture/command/lib/command.dart b/examples/cookbook/architecture/command/lib/command.dart index b3a8321cac..e3edf901f0 100644 --- a/examples/cookbook/architecture/command/lib/command.dart +++ b/examples/cookbook/architecture/command/lib/command.dart @@ -41,7 +41,7 @@ abstract class Command extends ChangeNotifier { bool get completed => _result is Ok; /// The result of the most recent action. - /// + /// /// Returns `null` if the action is running or completed with an error. Result? get result => _result; From b90f0d4be3b8ba9271b8cb3fcb59d0333a909d5d Mon Sep 17 00:00:00 2001 From: Miguel Beltran Date: Thu, 5 Dec 2024 08:59:10 +0100 Subject: [PATCH 25/33] Add command typedef doc comments --- examples/cookbook/architecture/command/lib/command.dart | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/examples/cookbook/architecture/command/lib/command.dart b/examples/cookbook/architecture/command/lib/command.dart index e3edf901f0..583d631f2a 100644 --- a/examples/cookbook/architecture/command/lib/command.dart +++ b/examples/cookbook/architecture/command/lib/command.dart @@ -8,7 +8,13 @@ import 'package:flutter/foundation.dart'; import 'result.dart'; +/// Defines a command action that returns a [Result] of type [T]. +/// Used by [Command0] for actions without arguments. typedef CommandAction0 = Future> Function(); + +/// Defines a command action that returns a [Result] of type [T]. +/// Takes an argument of type [A]. +/// Used by [Command1] for actions with one arguments. typedef CommandAction1 = Future> Function(A); /// Facilitates interaction with a view model. From 8de404d8a47e48407d7927e467d4a5a4ec2e3d69 Mon Sep 17 00:00:00 2001 From: Miguel Beltran Date: Thu, 5 Dec 2024 09:06:21 +0100 Subject: [PATCH 26/33] command class improvements and refreshed excerpts --- .../architecture/command/lib/command.dart | 12 ++++++------ src/content/cookbook/architecture/command.md | 16 +++++++++++----- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/examples/cookbook/architecture/command/lib/command.dart b/examples/cookbook/architecture/command/lib/command.dart index 583d631f2a..31a5fc0dcb 100644 --- a/examples/cookbook/architecture/command/lib/command.dart +++ b/examples/cookbook/architecture/command/lib/command.dart @@ -14,7 +14,7 @@ typedef CommandAction0 = Future> Function(); /// Defines a command action that returns a [Result] of type [T]. /// Takes an argument of type [A]. -/// Used by [Command1] for actions with one arguments. +/// Used by [Command1] for actions with one argument. typedef CommandAction1 = Future> Function(A); /// Facilitates interaction with a view model. @@ -26,13 +26,11 @@ typedef CommandAction1 = Future> Function(A); /// Use [Command0] for actions without arguments. /// Use [Command1] for actions with one argument. /// -/// Actions must return a [Result]. +/// Actions must return a [Result] of type [T]. /// /// Consume the action result by listening to changes, /// then call to [clearResult] when the state is consumed. abstract class Command extends ChangeNotifier { - Command(); - bool _running = false; /// Whether the action is running. @@ -80,7 +78,8 @@ abstract class Command extends ChangeNotifier { } /// A [Command] that accepts no arguments. -class Command0 extends Command { +final class Command0 extends Command { + /// Creates a [Command0] with the provided [CommandAction0]. Command0(this._action); final CommandAction0 _action; @@ -92,7 +91,8 @@ class Command0 extends Command { } /// A [Command] that accepts one argument. -class Command1 extends Command { +final class Command1 extends Command { + /// Creates a [Command1] with the provided [CommandAction1]. Command1(this._action); final CommandAction1 _action; diff --git a/src/content/cookbook/architecture/command.md b/src/content/cookbook/architecture/command.md index 6f5a97799a..ebed09e087 100644 --- a/src/content/cookbook/architecture/command.md +++ b/src/content/cookbook/architecture/command.md @@ -528,7 +528,13 @@ import 'package:flutter/foundation.dart'; import 'result.dart'; +/// Defines a command action that returns a [Result] of type [T]. +/// Used by [Command0] for actions without arguments. typedef CommandAction0 = Future> Function(); + +/// Defines a command action that returns a [Result] of type [T]. +/// Takes an argument of type [A]. +/// Used by [Command1] for actions with one argument. typedef CommandAction1 = Future> Function(A); /// Facilitates interaction with a view model. @@ -540,13 +546,11 @@ typedef CommandAction1 = Future> Function(A); /// Use [Command0] for actions without arguments. /// Use [Command1] for actions with one argument. /// -/// Actions must return a [Result]. +/// Actions must return a [Result] of type [T]. /// /// Consume the action result by listening to changes, /// then call to [clearResult] when the state is consumed. abstract class Command extends ChangeNotifier { - Command(); - bool _running = false; /// Whether the action is running. @@ -594,7 +598,8 @@ abstract class Command extends ChangeNotifier { } /// A [Command] that accepts no arguments. -class Command0 extends Command { +final class Command0 extends Command { + /// Creates a [Command0] with the provided [CommandAction0]. Command0(this._action); final CommandAction0 _action; @@ -606,7 +611,8 @@ class Command0 extends Command { } /// A [Command] that accepts one argument. -class Command1 extends Command { +final class Command1 extends Command { + /// Creates a [Command1] with the provided [CommandAction1]. Command1(this._action); final CommandAction1 _action; From 4c9d1938120e8dc2e57320c4d3ab154c78fe19fd Mon Sep 17 00:00:00 2001 From: Miguel Beltran Date: Thu, 5 Dec 2024 09:23:40 +0100 Subject: [PATCH 27/33] improved result class and updated code excerpts --- .../architecture/command/lib/result.dart | 24 ++++++------ .../architecture/result/lib/main.dart | 2 +- .../architecture/result/lib/result.dart | 36 +++++++++--------- src/content/cookbook/architecture/result.md | 38 ++++++++++--------- 4 files changed, 53 insertions(+), 47 deletions(-) diff --git a/examples/cookbook/architecture/command/lib/result.dart b/examples/cookbook/architecture/command/lib/result.dart index a405e2e053..1c6693f34f 100644 --- a/examples/cookbook/architecture/command/lib/result.dart +++ b/examples/cookbook/architecture/command/lib/result.dart @@ -2,7 +2,15 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -/// Utility class to wrap result data +/// Utility class that simplifies handling errors. +/// +/// Return a [Result] from a function to indicate success or failure. +/// +/// A [Result] is either an [Ok] with a value of type [T] +/// or an [Error] with an [Exception]. +/// +/// Use [Result.ok] to create a successful result with a value of type [T]. +/// Use [Result.error] to create an error result with an [Exception]. /// /// Evaluate the result using a switch statement: /// ```dart @@ -19,21 +27,15 @@ sealed class Result { const Result(); /// Creates a successful [Result], completed with the specified [value]. - const factory Result.ok(T value) = Ok; + const factory Result.ok(T value) = Ok._; /// Creates an error [Result], completed with the specified [error]. - const factory Result.error(Exception error) = Error; - - /// Convenience method to cast to Ok - Ok get asOk => this as Ok; - - /// Convenience method to cast to Error - Error get asError => this as Error; + const factory Result.error(Exception error) = Error._; } /// A successful [Result] with a returned [value]. final class Ok extends Result { - const Ok(this.value); + const Ok._(this.value); /// The returned value of this result. final T value; @@ -44,7 +46,7 @@ final class Ok extends Result { /// An error [Result] with a resulting [error]. final class Error extends Result { - const Error(this.error); + const Error._(this.error); /// The resulting error of this result. final Exception error; diff --git a/examples/cookbook/architecture/result/lib/main.dart b/examples/cookbook/architecture/result/lib/main.dart index 2c108805ac..76a0d53409 100644 --- a/examples/cookbook/architecture/result/lib/main.dart +++ b/examples/cookbook/architecture/result/lib/main.dart @@ -96,7 +96,7 @@ class ApiClientService { final stringData = await response.transform(utf8.decoder).join(); return Result.ok(UserProfile.fromJson(jsonDecode(stringData))); } else { - return Result.error(const HttpException('Invalid response')); + return const Result.error(HttpException('Invalid response')); } } on Exception catch (exception) { return Result.error(exception); diff --git a/examples/cookbook/architecture/result/lib/result.dart b/examples/cookbook/architecture/result/lib/result.dart index 5370700b03..1c6693f34f 100644 --- a/examples/cookbook/architecture/result/lib/result.dart +++ b/examples/cookbook/architecture/result/lib/result.dart @@ -2,7 +2,15 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -/// Utility class to wrap result data +/// Utility class that simplifies handling errors. +/// +/// Return a [Result] from a function to indicate success or failure. +/// +/// A [Result] is either an [Ok] with a value of type [T] +/// or an [Error] with an [Exception]. +/// +/// Use [Result.ok] to create a successful result with a value of type [T]. +/// Use [Result.error] to create an error result with an [Exception]. /// /// Evaluate the result using a switch statement: /// ```dart @@ -18,35 +26,29 @@ sealed class Result { const Result(); - /// Creates an instance of Result containing a value - factory Result.ok(T value) => Ok(value); - - /// Create an instance of Result containing an error - factory Result.error(Exception error) => Error(error); - - /// Convenience method to cast to Ok - Ok get asOk => this as Ok; + /// Creates a successful [Result], completed with the specified [value]. + const factory Result.ok(T value) = Ok._; - /// Convenience method to cast to Error - Error get asError => this as Error; + /// Creates an error [Result], completed with the specified [error]. + const factory Result.error(Exception error) = Error._; } -/// Subclass of Result for values +/// A successful [Result] with a returned [value]. final class Ok extends Result { - const Ok(this.value); + const Ok._(this.value); - /// Returned value in result + /// The returned value of this result. final T value; @override String toString() => 'Result<$T>.ok($value)'; } -/// Subclass of Result for errors +/// An error [Result] with a resulting [error]. final class Error extends Result { - const Error(this.error); + const Error._(this.error); - /// Returned error in result + /// The resulting error of this result. final Exception error; @override diff --git a/src/content/cookbook/architecture/result.md b/src/content/cookbook/architecture/result.md index 80c0a0eeca..3f75bb2d0e 100644 --- a/src/content/cookbook/architecture/result.md +++ b/src/content/cookbook/architecture/result.md @@ -237,7 +237,7 @@ class ApiClientService { final stringData = await response.transform(utf8.decoder).join(); return Result.ok(UserProfile.fromJson(jsonDecode(stringData))); } else { - return Result.error(const HttpException('Invalid response')); + return const Result.error(HttpException('Invalid response')); } } on Exception catch (exception) { return Result.error(exception); @@ -397,7 +397,15 @@ like the [result_dart][], [result_type][] or [multiple_result][] packages. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -/// Utility class to wrap result data +/// Utility class that simplifies handling errors. +/// +/// Return a [Result] from a function to indicate success or failure. +/// +/// A [Result] is either an [Ok] with a value of type [T] +/// or an [Error] with an [Exception]. +/// +/// Use [Result.ok] to create a successful result with a value of type [T]. +/// Use [Result.error] to create an error result with an [Exception]. /// /// Evaluate the result using a switch statement: /// ```dart @@ -413,35 +421,29 @@ like the [result_dart][], [result_type][] or [multiple_result][] packages. sealed class Result { const Result(); - /// Creates an instance of Result containing a value - factory Result.ok(T value) => Ok(value); - - /// Create an instance of Result containing an error - factory Result.error(Exception error) => Error(error); + /// Creates a successful [Result], completed with the specified [value]. + const factory Result.ok(T value) = Ok._; - /// Convenience method to cast to Ok - Ok get asOk => this as Ok; - - /// Convenience method to cast to Error - Error get asError => this as Error; + /// Creates an error [Result], completed with the specified [error]. + const factory Result.error(Exception error) = Error._; } -/// Subclass of Result for values +/// A successful [Result] with a returned [value]. final class Ok extends Result { - const Ok(this.value); + const Ok._(this.value); - /// Returned value in result + /// The returned value of this result. final T value; @override String toString() => 'Result<$T>.ok($value)'; } -/// Subclass of Result for errors +/// An error [Result] with a resulting [error]. final class Error extends Result { - const Error(this.error); + const Error._(this.error); - /// Returned error in result + /// The resulting error of this result. final Exception error; @override From 411ca6db8478dcee9fa31acf8d2225c51ee639db Mon Sep 17 00:00:00 2001 From: Miguel Beltran Date: Thu, 5 Dec 2024 09:26:18 +0100 Subject: [PATCH 28/33] format --- examples/cookbook/architecture/command/lib/result.dart | 2 +- examples/cookbook/architecture/result/lib/result.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/cookbook/architecture/command/lib/result.dart b/examples/cookbook/architecture/command/lib/result.dart index 1c6693f34f..497c458f66 100644 --- a/examples/cookbook/architecture/command/lib/result.dart +++ b/examples/cookbook/architecture/command/lib/result.dart @@ -5,7 +5,7 @@ /// Utility class that simplifies handling errors. /// /// Return a [Result] from a function to indicate success or failure. -/// +/// /// A [Result] is either an [Ok] with a value of type [T] /// or an [Error] with an [Exception]. /// diff --git a/examples/cookbook/architecture/result/lib/result.dart b/examples/cookbook/architecture/result/lib/result.dart index 1c6693f34f..497c458f66 100644 --- a/examples/cookbook/architecture/result/lib/result.dart +++ b/examples/cookbook/architecture/result/lib/result.dart @@ -5,7 +5,7 @@ /// Utility class that simplifies handling errors. /// /// Return a [Result] from a function to indicate success or failure. -/// +/// /// A [Result] is either an [Ok] with a value of type [T] /// or an [Error] with an [Exception]. /// From 955440140b0f7aad720cc260d71e10ca85e48106 Mon Sep 17 00:00:00 2001 From: Miguel Beltran Date: Thu, 5 Dec 2024 10:30:32 +0100 Subject: [PATCH 29/33] fix result type --- examples/cookbook/architecture/command/lib/command.dart | 2 +- src/content/cookbook/architecture/command.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/cookbook/architecture/command/lib/command.dart b/examples/cookbook/architecture/command/lib/command.dart index 31a5fc0dcb..0ada1f163a 100644 --- a/examples/cookbook/architecture/command/lib/command.dart +++ b/examples/cookbook/architecture/command/lib/command.dart @@ -47,7 +47,7 @@ abstract class Command extends ChangeNotifier { /// The result of the most recent action. /// /// Returns `null` if the action is running or completed with an error. - Result? get result => _result; + Result? get result => _result; /// Clears the most recent action's result. void clearResult() { diff --git a/src/content/cookbook/architecture/command.md b/src/content/cookbook/architecture/command.md index ebed09e087..c5d33f9040 100644 --- a/src/content/cookbook/architecture/command.md +++ b/src/content/cookbook/architecture/command.md @@ -567,7 +567,7 @@ abstract class Command extends ChangeNotifier { /// The result of the most recent action. /// /// Returns `null` if the action is running or completed with an error. - Result? get result => _result; + Result? get result => _result; /// Clears the most recent action's result. void clearResult() { From 24f716572f42c22ef3eb0b4eb2ba1da135ba8aba Mon Sep 17 00:00:00 2001 From: Miguel Beltran Date: Mon, 9 Dec 2024 14:01:24 +0100 Subject: [PATCH 30/33] Apply suggestions from code review Co-authored-by: Parker Lougheed --- src/content/cookbook/architecture/command.md | 15 ++++--- src/content/cookbook/architecture/result.md | 47 +++++++++++--------- 2 files changed, 33 insertions(+), 29 deletions(-) diff --git a/src/content/cookbook/architecture/command.md b/src/content/cookbook/architecture/command.md index c5d33f9040..070ea904e8 100644 --- a/src/content/cookbook/architecture/command.md +++ b/src/content/cookbook/architecture/command.md @@ -1,6 +1,7 @@ --- title: Command pattern -description: Improve ViewModels with Commands +description: >- + Learn how to improve your view models with commands. js: - defer: true url: /assets/js/inject_dartpad.js @@ -513,7 +514,7 @@ and a `Command1`, for actions that take one parameter. :::note Check [pub.dev][] for other ready-to-use implementations of the command pattern, -like the [flutter_command][] package. +such as the [`flutter_command`][] package. ::: @@ -624,8 +625,8 @@ final class Command1 extends Command { } ``` -[Compass App example]:{{site.repo.samples}}/tree/main/compass_app -[`Result` class]:/cookbook/architecture/result -[pub.dev]:{{site.pub}} -[flutter_command]:{{site.pub-pkg}}/flutter_command -[`ChangeNotifier`]:/get-started/fundamentals/state-management +[Compass App example]: {{site.repo.samples}}/tree/main/compass_app +[`Result` class]: /cookbook/architecture/result +[pub.dev]: {{site.pub}} +[flutter_command]: {{site.pub-pkg}}/flutter_command +[`ChangeNotifier`]: /get-started/fundamentals/state-management diff --git a/src/content/cookbook/architecture/result.md b/src/content/cookbook/architecture/result.md index 3f75bb2d0e..3574af08c9 100644 --- a/src/content/cookbook/architecture/result.md +++ b/src/content/cookbook/architecture/result.md @@ -1,6 +1,7 @@ --- title: Result class -description: Handle errors and return values with the result class +description: >- + Learn to handle errors and return values with a result class. js: - defer: true url: /assets/js/inject_dartpad.js @@ -12,7 +13,7 @@ Dart provides a built-in error handling mechanism with the ability to throw and catch exceptions. As mentioned in the [Error handling documentation][], -Dart’s exceptions are unhandled exceptions. +Dart's exceptions are unhandled exceptions. This means that methods that throw exceptions don’t need to declare them, and calling methods aren't required to catch them either. @@ -37,7 +38,7 @@ it should communicate the error to the calling component. Typically, that's done with exceptions. For example, -an API client Service failing to communicate with the remote server +an API client service failing to communicate with the remote server might throw an HTTP Error Exception. The calling component, for example a Repository, @@ -57,7 +58,7 @@ that throws exceptions in certain situations: - The method throws an `HttpException` if the response code isn’t 200. - The JSON parsing method throws an exception if the response isn't formatted correctly. -- The Http client might throw an exception due to networking issues. +- The HTTP client might throw an exception due to networking issues. The following code tests for a variety of possible exceptions: @@ -120,9 +121,10 @@ class UserProfileViewModel extends ChangeNotifier { } ``` -In reality, a developer might forget to properly capture exceptions -and end up with the following code. It would compile and run, -but will crash if one of the exceptions mentioned previously occurs: +In reality, a developer might forget to properly capture exceptions and +end up with the following code. +It compiles and runs, but crashes if +one of the exceptions mentioned previously occurs: ```dart @@ -139,17 +141,17 @@ class UserProfileViewModel extends ChangeNotifier { You can attempt to solve this by documenting the `ApiClientService`, warning about the possible exceptions it might throw. However, since the view model doesn’t use the service directly, -other developers working in the codebase might miss this information. +other developers working in the codebase might miss this information. ## Using the result pattern An alternative to throwing exceptions -is to wrap the function output into a `Result` object. +is to wrap the function output in a `Result` object. When the function runs successfully, the `Result` contains the returned value. -However, if the function did not complete successfully, -the `Result` object will contain the error. +However, if the function does not complete successfully, +the `Result` object contains the error. A `Result` is a [`sealed`][] class that can either subclass `Ok` or the `Error` class. @@ -212,6 +214,7 @@ class ApiClientService { } } ``` + Instead of returning the `UserProfile` directly, it returns a `Result` object containing a `UserProfile`. @@ -341,7 +344,7 @@ attempts to obtain the `UserProfile` using the `ApiClientService`. If it fails, it tries to create a temporary user in a `DatabaseService`. -Because either Service method can fail, +Because either service method can fail, the code must catch the exceptions in both cases. This can be improved using the `Result` pattern: @@ -383,12 +386,12 @@ The key takeaways are: Below you can find the full `Result` class as implemented in the [Compass App example][] -for the [Flutter Architecture guidelines][]. +for the [Flutter architecture guidelines][]. :::note Check [pub.dev][] for different ready-to-use implementations of the `Result` class, -like the [result_dart][], [result_type][] or [multiple_result][] packages. +such as the [`result_dart`][], [`result_type`][], and [`multiple_result`][] packages. ::: @@ -451,11 +454,11 @@ final class Error extends Result { } ``` -[Error handling documentation]:{{site.dart-site}}/language/error-handling -[Flutter architecture guidelines]:/app-architecture -[Compass App example]:{{site.repo.samples}}/tree/main/compass_app -[pub.dev]:{{site.pub}} -[result_dart]:{{site.pub-pkg}}/result_dart -[result_type]:{{site.pub-pkg}}/result_type -[multiple_result]:{{site.pub-pkg}}/multiple_result -[`sealed`]:{{site.dart-site}}/language/class-modifiers#sealed +[Error handling documentation]: {{site.dart-site}}/language/error-handling +[Flutter architecture guidelines]: /app-architecture +[Compass App example]: {{site.repo.samples}}/tree/main/compass_app +[pub.dev]: {{site.pub}} +[result_dart]: {{site.pub-pkg}}/result_dart +[result_type]: {{site.pub-pkg}}/result_type +[multiple_result]: {{site.pub-pkg}}/multiple_result +[`sealed`]: {{site.dart-site}}/language/class-modifiers#sealed From 54534949fa7d441dd5363518da8a8be8850be70e Mon Sep 17 00:00:00 2001 From: Miguel Beltran Date: Mon, 9 Dec 2024 14:10:53 +0100 Subject: [PATCH 31/33] finishing touches on Result --- .../architecture/result/lib/result.dart | 2 ++ .../result/lib/simple_result.dart | 9 ++++++ src/content/cookbook/architecture/result.md | 28 +++++++++++-------- 3 files changed, 28 insertions(+), 11 deletions(-) diff --git a/examples/cookbook/architecture/result/lib/result.dart b/examples/cookbook/architecture/result/lib/result.dart index 497c458f66..c405dc0936 100644 --- a/examples/cookbook/architecture/result/lib/result.dart +++ b/examples/cookbook/architecture/result/lib/result.dart @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +// #docregion Result /// Utility class that simplifies handling errors. /// /// Return a [Result] from a function to indicate success or failure. @@ -54,3 +55,4 @@ final class Error extends Result { @override String toString() => 'Result<$T>.error($error)'; } +// #enddocregion Result diff --git a/examples/cookbook/architecture/result/lib/simple_result.dart b/examples/cookbook/architecture/result/lib/simple_result.dart index 5d009904e9..5454e9eb9b 100644 --- a/examples/cookbook/architecture/result/lib/simple_result.dart +++ b/examples/cookbook/architecture/result/lib/simple_result.dart @@ -1,3 +1,12 @@ +/// Utility class that simplifies handling errors. +/// +/// Return a [Result] from a function to indicate success or failure. +/// +/// A [Result] is either an [Ok] with a value of type [T] +/// or an [Error] with an [Exception]. +/// +/// Use [Result.ok] to create a successful result with a value of type [T]. +/// Use [Result.error] to create an error result with an [Exception]. sealed class Result { const Result(); diff --git a/src/content/cookbook/architecture/result.md b/src/content/cookbook/architecture/result.md index 3574af08c9..c6f2f675ef 100644 --- a/src/content/cookbook/architecture/result.md +++ b/src/content/cookbook/architecture/result.md @@ -164,6 +164,15 @@ A full implementation is at the end of this page. ```dart +/// Utility class that simplifies handling errors. +/// +/// Return a [Result] from a function to indicate success or failure. +/// +/// A [Result] is either an [Ok] with a value of type [T] +/// or an [Error] with an [Exception]. +/// +/// Use [Result.ok] to create a successful result with a value of type [T]. +/// Use [Result.error] to create an error result with an [Exception]. sealed class Result { const Result(); @@ -304,8 +313,8 @@ class UserProfileViewModel extends ChangeNotifier { The `Result` class is implemented using a `sealed` class, meaning it can only be of type `Ok` or `Error`. -This allows the code to evaluate the result with a simple -switch statement. +This allows the code to evaluate the result with a +[switch result or expression][]. In the `Ok` case, obtain the value using the `value` property. @@ -379,7 +388,7 @@ how to use a `Result` class to return result values. The key takeaways are: - `Result` classes force the calling method to check for errors, - reducing the amount of bugs caused by uncaught exceptions. + reducing the amount of bugs caused by uncaught exceptions[switch result or expression. - `Result` classes help improve control flow compared to try-catch blocks. - `Result` classes are `sealed` and can only return `Ok` or `Error` instances, allowing the code to unwrap them with a switch statement. @@ -394,12 +403,8 @@ implementations of the `Result` class, such as the [`result_dart`][], [`result_type`][], and [`multiple_result`][] packages. ::: - + ```dart -// Copyright 2024 The Flutter team. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - /// Utility class that simplifies handling errors. /// /// Return a [Result] from a function to indicate success or failure. @@ -458,7 +463,8 @@ final class Error extends Result { [Flutter architecture guidelines]: /app-architecture [Compass App example]: {{site.repo.samples}}/tree/main/compass_app [pub.dev]: {{site.pub}} -[result_dart]: {{site.pub-pkg}}/result_dart -[result_type]: {{site.pub-pkg}}/result_type -[multiple_result]: {{site.pub-pkg}}/multiple_result +[`result_dart`]: {{site.pub-pkg}}/result_dart +[`result_type`]: {{site.pub-pkg}}/result_type +[`multiple_result`]: {{site.pub-pkg}}/multiple_result [`sealed`]: {{site.dart-site}}/language/class-modifiers#sealed +[switch result or expression]: {{site.dart-site}}/language/branches#switch-statements From 1b8f39e0bd2b6f12ce5e8bb482b7de3bf3a18a3c Mon Sep 17 00:00:00 2001 From: Miguel Beltran Date: Mon, 9 Dec 2024 14:15:06 +0100 Subject: [PATCH 32/33] finishing touches on Command --- src/content/cookbook/architecture/command.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/content/cookbook/architecture/command.md b/src/content/cookbook/architecture/command.md index 070ea904e8..4269112aee 100644 --- a/src/content/cookbook/architecture/command.md +++ b/src/content/cookbook/architecture/command.md @@ -9,7 +9,7 @@ js: -Model-View-ViewModel (MVVM) is a design pattern +[Model-View-ViewModel (MVVM)][] is a design pattern that separates a feature of an application into three parts: the model, the view model and the view. Views and view models make up the UI layer of an application. @@ -20,7 +20,7 @@ A command is a class that wraps a method and helps to handle the different states of that method, such as running, complete, and error. -View models can use commands to handle interaction and run actions. +[View models][] can use commands to handle interaction and run actions. As well, they can be used to display different UI states, like loading indicators when an action is running, or an error dialog when an action failed. @@ -628,5 +628,7 @@ final class Command1 extends Command { [Compass App example]: {{site.repo.samples}}/tree/main/compass_app [`Result` class]: /cookbook/architecture/result [pub.dev]: {{site.pub}} -[flutter_command]: {{site.pub-pkg}}/flutter_command +[`flutter_command`]: {{site.pub-pkg}}/flutter_command [`ChangeNotifier`]: /get-started/fundamentals/state-management +[Model-View-ViewModel (MVVM)]: /app-architecture/guide#view-models +[View models]: /app-architecture/guide#view-models From 3d54a395f6b42429ab65f3a1a30884d71ee63e31 Mon Sep 17 00:00:00 2001 From: Eric Windmill Date: Mon, 9 Dec 2024 09:01:41 -0500 Subject: [PATCH 33/33] Update result.md --- src/content/cookbook/architecture/result.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/content/cookbook/architecture/result.md b/src/content/cookbook/architecture/result.md index c6f2f675ef..11f8d9b824 100644 --- a/src/content/cookbook/architecture/result.md +++ b/src/content/cookbook/architecture/result.md @@ -388,7 +388,7 @@ how to use a `Result` class to return result values. The key takeaways are: - `Result` classes force the calling method to check for errors, - reducing the amount of bugs caused by uncaught exceptions[switch result or expression. + reducing the amount of bugs caused by uncaught exceptions. - `Result` classes help improve control flow compared to try-catch blocks. - `Result` classes are `sealed` and can only return `Ok` or `Error` instances, allowing the code to unwrap them with a switch statement.