Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add "Command Pattern" and "Result Class" design patterns #11444

Merged
merged 36 commits into from
Dec 9, 2024
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
b68a527
WIP command pattern document
miquelbeltran Nov 22, 2024
31f8faf
wip command docs
miquelbeltran Nov 25, 2024
e54e9a4
moved examples without Command to a different file
miquelbeltran Nov 25, 2024
7b9697c
cleanup
miquelbeltran Nov 25, 2024
78c717d
formatted and code excerpts for command pattern
miquelbeltran Nov 26, 2024
5fb346c
Merge branch 'main' into mb-result-command
miquelbeltran Nov 26, 2024
23b0d13
completed command pattern
miquelbeltran Nov 26, 2024
47a90fd
WIP result pattern
miquelbeltran Nov 26, 2024
51206c4
completed result pattern document
miquelbeltran Nov 27, 2024
72ec472
Merge branch 'main' into mb-result-command
miquelbeltran Nov 27, 2024
6fb564b
fix analytics issue
miquelbeltran Nov 27, 2024
e4f57b2
cleanup code
miquelbeltran Nov 27, 2024
0521d6f
Apply suggestion on command.md
miquelbeltran Nov 27, 2024
ebf786f
Apply suggestions in result.md
miquelbeltran Nov 27, 2024
717ed2b
add link to ChangeNotifier
miquelbeltran Nov 27, 2024
490cc49
fix links
miquelbeltran Nov 27, 2024
b60f2bc
renamed function
miquelbeltran Nov 28, 2024
0e43008
Apply suggestions from code review to Command.md
miquelbeltran Dec 2, 2024
7d5205d
Apply suggestions from code review in result.md
miquelbeltran Dec 2, 2024
9fbe7de
use correct capitalization in view, view model and model
miquelbeltran Dec 3, 2024
436911e
result.md: use correct capitalization in view, view model and model
miquelbeltran Dec 3, 2024
aa5f34f
rewrite command pattern introduction
miquelbeltran Dec 3, 2024
ad1f9f4
small typo
miquelbeltran Dec 3, 2024
95ca32c
Merge branch 'main' into mb-result-command
parlough Dec 5, 2024
32cefea
Apply suggestions from code review
miquelbeltran Dec 5, 2024
0b30844
refresh code excerpts
miquelbeltran Dec 5, 2024
c483456
format
miquelbeltran Dec 5, 2024
b90f0d4
Add command typedef doc comments
miquelbeltran Dec 5, 2024
8de404d
command class improvements and refreshed excerpts
miquelbeltran Dec 5, 2024
4c9d193
improved result class and updated code excerpts
miquelbeltran Dec 5, 2024
411ca6d
format
miquelbeltran Dec 5, 2024
9554401
fix result type
miquelbeltran Dec 5, 2024
24f7165
Apply suggestions from code review
miquelbeltran Dec 9, 2024
5453494
finishing touches on Result
miquelbeltran Dec 9, 2024
1b8f39e
finishing touches on Command
miquelbeltran Dec 9, 2024
3d54a39
Update result.md
ericwindmill Dec 9, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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
97 changes: 97 additions & 0 deletions examples/cookbook/architecture/command/lib/command.dart
Original file line number Diff line number Diff line change
@@ -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<T> = Future<Result<T>> Function();
typedef CommandAction1<T, A> = Future<Result<T>> Function(A);
miquelbeltran marked this conversation as resolved.
Show resolved Hide resolved

/// Facilitates interaction with a ViewModel.
miquelbeltran marked this conversation as resolved.
Show resolved Hide resolved
///
/// 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<T> extends ChangeNotifier {
miquelbeltran marked this conversation as resolved.
Show resolved Hide resolved
Command();
miquelbeltran marked this conversation as resolved.
Show resolved Hide resolved

bool _running = false;

/// True when the action is running.
bool get running => _running;
miquelbeltran marked this conversation as resolved.
Show resolved Hide resolved

Result<T>? _result;

/// true if action completed with error
bool get error => _result is Error;
miquelbeltran marked this conversation as resolved.
Show resolved Hide resolved

/// true if action completed successfully
miquelbeltran marked this conversation as resolved.
Show resolved Hide resolved
bool get completed => _result is Ok;

/// Get last action result
miquelbeltran marked this conversation as resolved.
Show resolved Hide resolved
Result? get result => _result;
miquelbeltran marked this conversation as resolved.
Show resolved Hide resolved

/// Clear last action result
miquelbeltran marked this conversation as resolved.
Show resolved Hide resolved
void clearResult() {
_result = null;
notifyListeners();
}

/// Internal execute implementation
miquelbeltran marked this conversation as resolved.
Show resolved Hide resolved
Future<void> _execute(CommandAction0<T> 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.
miquelbeltran marked this conversation as resolved.
Show resolved Hide resolved
class Command0<T> extends Command<T> {
miquelbeltran marked this conversation as resolved.
Show resolved Hide resolved
Command0(this._action);
miquelbeltran marked this conversation as resolved.
Show resolved Hide resolved

final CommandAction0<T> _action;

/// Executes the action.
Future<void> execute() async {
await _execute(() => _action());
}
}

/// [Command] with one argument.
/// Takes a [CommandAction1] as action.
miquelbeltran marked this conversation as resolved.
Show resolved Hide resolved
class Command1<T, A> extends Command<T> {
Command1(this._action);
miquelbeltran marked this conversation as resolved.
Show resolved Hide resolved

final CommandAction1<T, A> _action;

/// Executes the action with the argument.
miquelbeltran marked this conversation as resolved.
Show resolved Hide resolved
Future<void> execute(A argument) async {
await _execute(() => _action(argument));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import 'package:flutter/material.dart';

// #docregion HomeViewModel
class HomeViewModel extends ChangeNotifier {
HomeViewModel() {
load = Command0(_load)..execute();
edit = Command1<String>(_edit);
}

User? get user => null;

// Command0 accepts 0 arguments
late final Command0 load;

// Command1 accepts 1 argument
late final Command1 edit;

Future<void> _load() async {
// load user
}

Future<void> _edit(String name) async {
// edit user
}
}
// #enddocregion HomeViewModel

class User {}

class Command0 {
Command0(Future<void> Function() any);

void execute() {}
}

class Command1<T> {
Command1(Future<void> Function(T) any);
}
184 changes: 184 additions & 0 deletions examples/cookbook/architecture/command/lib/main.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
import 'package:flutter/material.dart';

class MainApp extends StatefulWidget {
const MainApp({
super.key,
required this.viewModel,
});

final HomeViewModel viewModel;

@override
State<MainApp> createState() => _MainAppState();
}

class _MainAppState extends State<MainApp> {
// #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 CommandListenable
// #docregion ListenableBuilder
body: ListenableBuilder(
listenable: widget.viewModel.load,
builder: (context, child) {
if (widget.viewModel.load.running) {
return const Center(
child: CircularProgressIndicator(),
);
}
// #enddocregion CommandListenable

if (widget.viewModel.load.error != null) {
return Center(
child: Text('Error: ${widget.viewModel.load.error}'),
);
}

return child!;
},
child: ListenableBuilder(
listenable: widget.viewModel,
builder: (context, _) {
// #enddocregion ListenableBuilder
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
},
),
// #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 {
User({required this.name, required this.email});

final String name;
final String email;
}

// #docregion HomeViewModel
class HomeViewModel extends ChangeNotifier {
// #docregion ViewModelInit
HomeViewModel() {
load = Command(_load)..execute();
}
// #enddocregion ViewModelInit

User? get user => null;

late final Command load;

Future<void> _load() async {
// load user
}
}
// #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<void> _load() async {
// load user
}

Future<void> _delete() async {
// delete user
}
}
// #enddocregion HomeViewModel2

// #docregion Command
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<void> Function() _action;

Future<void> 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;
}
}
// #enddocregion Command
Loading
Loading