About a year ago, the Flutter documentation was updated with new recommendations. Among its suggestions, such as the MVVM architecture I've commented on before, two patterns already known to programmers were indicated: Command and Result. In this article, we will see the advantage of using them, where they fit, and how to implement them in two flows of the same application.
As with every article I write, we will see a little bit of theory before going to the code, but today I will be very brief, I promise.
Knowing the patterns
Command
This pattern was born from the need to reuse code from the same function but without UI components knowing what they are doing. For a "send" button, it only triggers the execution of a command, without the need to execute the function passing any parameter, so the UI can be changed freely without affecting how the data is being executed.

We will then have a Command class, which will be the only one known by our buttons and UI components, and we will have the implementations, for example.

Right, so what?
In Flutter, this pattern is extended to already have information about the request state, indicating if it is being executed and if it finished with an error or success, so that we will have a single ListenableBuilder to react to the request, regardless of who executes it.
Result
The Result is simpler than it seems; I say this because many who start in the area feel that design patterns are something complex and end up imagining an even greater difficulty.
This pattern is simply a standardization of the return type of functions. Okay, it still sounds strange. Let's see an example:
Let's say we have a list of Contacts stored on a server and we want to make a call to it. As soon as we open our app, it searches and returns our list of contacts, so the expected result is a List<Contact>.
Oops, but this request can fail, like a lack of connection error, for example.
In this case, instead of throwing an error (throw Exception), we handle this error and display only a message "We are offline".

In this case, the one responsible for handling our error is the data access function (or class) itself, and whoever calls it (e.g., viewmodel) only needs to worry if the response is an error or success.
Preparing the environment
Starting
Considering you have already executed flutter create <your app>, first let's create a utils directory and add the following files to it: result.dart and command.dart. These two files are available in the flutter documentation (which I will add at the end of the article) and also other libraries like result_dart and command_it.
The official Flutter documentation created two Command classes, Command0 which receives no parameters and Command1 which receives parameters. Feel free to change the names to whatever is easier, for example, CommandWithParam for the second one. Here I will keep the original.
Changing our Result.dart
This is up to the programmer, but I like to add a .fold() function in the Result class, responsible for executing the functions received in case of success or failure, just to facilitate the ViewModel code and not need to write a Switch/Case every time.
Our Result class will be as follows:
sealed class Result<T> {
const Result();
/// Creates a successful [Result], completed with the specified [value].
const factory Result.ok(T value) = Ok._;
/// Creates an error [Result], completed with the specified [error].
const factory Result.error(Exception error) = Error._;
/// Applies one of two functions depending on the result type.
///
/// If this is an [Ok], applies [onOk] to the value.
/// If this is an [Error], applies [onError] to the error.
R fold<R>({
required R Function(T value) onOk,
required R Function(Exception error) onError,
}) {
return switch (this) {
Ok(value: final value) => onOk(value),
Error(error: final error) => onError(error),
};
}
}
Creating the base structure
Just like every new Flutter app, let's remove the home_page.dart file, and create a presentation directory and inside it the home_list_page directory with the home_list_page.dart and home_list_viewmodel.dart files. In addition, inside /lib let's create the repositories folder and the to_do_repository.dart file inside it. Let's also create a models directory in /lib to store to_do.dart.
From the structure, you can see that my focus here is not to present a clean architecture since it is an example, but feel free to implement the architecture and structure that makes you most comfortable.
Our ToDo model will be as follows:
class ToDo {
final String id;
final String title;
final bool isCompleted;
ToDo({required this.id, required this.title, this.isCompleted = false});
ToDo copyWith({String? id, String? title, bool? isCompleted}) {
return ToDo(
id: id ?? this.id,
title: title ?? this.title,
isCompleted: isCompleted ?? this.isCompleted,
);
}
}
Implementing the repository and Result
Our abstract repository class will be as follows:
abstract class ToDoRepository {
Future<Result<List<ToDo>>> getToDos();
Future<Result<void>> addToDo(ToDo toDo);
Future<Result<void>> completeToDo(ToDo toDo);
Future<Result<void>> deleteToDo(String id);
}
Here we already see our Result being used; it will be the base class for data return from our repository (and services if we need them in the future).
I will implement an in-memory repository, but feel free to implement it with Firestore, Isar, or Sqflite. It will be as follows:
class ToDoMemoryRepository implements ToDoRepository {
// In-memory list of to-dos. In a real application, this would be replaced with
// a database or API.
final List<ToDo> _toDos = [ToDo(id: '1', title: 'Sample ToDo')];
@override
Future<Result<List<ToDo>>> getToDos() async {
try {
await Future.delayed(const Duration(seconds: 2));
final List<ToDo> clonedList = List<ToDo>.from(_toDos);
return Result.ok(clonedList);
} catch (error) {
return Result.error(Exception('Failed to get ToDos: $error'));
}
}
@override
Future<Result<void>> completeToDo(ToDo toDo) async {
try {
await Future.delayed(const Duration(seconds: 2));
final index = _toDos.indexWhere((t) => t.id == toDo.id);
if (index != -1) {
_toDos[index] = toDo.copyWith(isCompleted: !toDo.isCompleted);
}
return const Result.ok(null);
} catch (error) {
return Result.error(Exception('Failed to complete ToDo: $error'));
}
}
@override
Future<Result<void>> addToDo(ToDo toDo) async {
try {
await Future.delayed(const Duration(seconds: 2));
_toDos.add(toDo);
return const Result.ok(null);
} catch (error) {
return Result.error(Exception('Failed to add ToDo: $error'));
}
}
@override
Future<Result<void>> deleteToDo(String id) async {
try {
await Future.delayed(const Duration(seconds: 2));
_toDos.removeWhere((toDo) => toDo.id == id);
return const Result.ok(null);
} catch (error) {
return Result.error(Exception('Failed to delete ToDo: $error'));
}
}
}
As said before, with Result, the one responsible for handling errors is the data access class. Here would be the moment to check the request status (if accessed via HTTP) and return a response according to the code, for example.
Implementing the list screen (Command without parameters)
Implementing the ViewModel
Our ViewModel will be simple, since the purpose is only to search the repository using the command. It will be the following code:
class HomeListViewmodel {
final ToDoRepository _toDoRepository;
// Command to load the list of to-dos.
late final loadToDosCommand = Command0<List<ToDo>>(
() => _toDoRepository.getToDos(),
);
// Load the to-dos when the view model is created.
HomeListViewmodel(this._toDoRepository) {
loadToDosCommand.execute();
}
}
This way, as soon as our viewmodel is instantiated, it will execute our search.
Implementing our screen
To implement our screen we will assemble it in this order:
- Instantiating our ViewModel
- Scaffold to contain the screen and add Material configurations
- The ListenableBuilder which will be the widget responsible for listening to our Command
- The RefreshIndicator that will trigger our search when the user wants to update
- The Builder that will read the state of our command and return a component that makes sense
Inside the builder, we will have the following logic:
- If the command is still executing, we will display a CircularProgressIndicator (or any loading widget)
bool isLoading = viewModel.loadToDosCommand.running;
if (isLoading) {
return const Center(child: CircularProgressIndicator());
}
- Return the .fold of our Result to display a component according to its return, displaying EITHER a list OR an error message.
return viewModel.loadToDosCommand.result!.fold(
onOk: (value) {
final toDos = value;
return ListView.builder(
itemCount: toDos.length,
itemBuilder: (context, index) {
final toDo = toDos[index];
return ListTile(
title: Text(toDo.title),
trailing: Checkbox(
value: toDo.isCompleted,
onChanged: (bool? newValue) {
// Handle checkbox state change
},
),
);
},
);
},
onError: (error) {
return Center(child: Text('Error: $error'));
},
);
Thus, our list page:
class HomeListPage extends StatelessWidget {
const HomeListPage({super.key});
@override
Widget build(BuildContext context) {
// Create the view model for the home list page. In a real application, you
// would typically use a dependency injection framework to provide the
// repository and view model, but for this example, we'll just create them directly here.
final viewModel = HomeListViewmodel(ToDoMemoryRepository());
return Scaffold(
appBar: AppBar(title: const Text('To-Do List')),
body: ListenableBuilder(
listenable: viewModel.loadToDosCommand,
builder: (context, child) {
return RefreshIndicator(
child: Builder(
builder: (context) {
bool isLoading = viewModel.loadToDosCommand.running;
if (isLoading) {
return const Center(child: CircularProgressIndicator());
}
return viewModel.loadToDosCommand.result!.fold(
onOk: (value) {
final toDos = value;
return ListView.builder(
itemCount: toDos.length,
itemBuilder: (context, index) {
final toDo = toDos[index];
return ListTile(
title: Text(toDo.title),
trailing: Checkbox(
value: toDo.isCompleted,
onChanged: (bool? newValue) {
// Handle checkbox state change
},
),
);
},
);
},
onError: (error) {
return Center(child: Text('Error: $error'));
},
);
},
),
onRefresh: () {
return viewModel.loadToDosCommand.execute();
},
);
},
),
);
}
}
If you prefer, you can add print or debugPrint to see the data flow in the terminal :)
Implementing the check flow in ToDo (Command with parameters)
Changing the ViewModel
Now that we can use Command without parameters, we will need to send something and we can do it without leaving the screen flow. As we already have the whole structure ready, we will only need to add the command to complete the ToDo in our ViewModel, following the logic of:
- Trigger complete command
- If success, update the ToDo list (execution of the first command)
- If failure, return the error
For this, we will create a function inside the ViewModel and not a lambda as in the first one:
Future<Result<void>> _completeToDo(ToDo toDo) async {
final result = await _toDoRepository.completeToDo(toDo);
return result.fold(
onOk: (_) {
loadToDosCommand.execute();
return const Result.ok(null);
},
onError: (error) {
return Result.error(error);
},
);
}
Thus, our ViewModel will look like this:
class HomeListViewmodel {
final ToDoRepository _toDoRepository;
// Command to load the list of to-dos.
late final loadToDosCommand = Command0<List<ToDo>>(
() => _toDoRepository.getToDos(),
);
// Command to mark a to-do as completed.
late final completeToDoCommand = Command1<void, ToDo>(_completeToDo);
// Load the to-dos when the view model is created.
HomeListViewmodel(this._toDoRepository) {
loadToDosCommand.execute();
}
Future<Result<void>> _completeToDo(ToDo toDo) async {
final result = await _toDoRepository.completeToDo(toDo);
return result.fold(
onOk: (_) {
loadToDosCommand.execute();
// We return a successful result with no value, since the UI doesn't need
// any data from this command, it just needs to know if it succeeded or failed.
return const Result.ok(null);
},
onError: (error) {
return Result.error(error);
},
);
}
}
Implementing the list component
Let's create a widgets directory inside presentation to store our custom components and inside it, list_item.dart.
This component will be a ListenableBuilder that must receive the referring ToDo object and the command it must execute when marking as done. If it is loading, it should display a loading component to prevent the user from tapping multiple times while processing the request. In addition, if an error occurs, it should display a message via Snackbar.
Our component will be as follows:
class ListItem extends StatelessWidget {
final ToDo toDo;
final Command1<void, ToDo> onToggleCompleted;
const ListItem({
super.key,
required this.toDo,
required this.onToggleCompleted,
});
@override
Widget build(BuildContext context) {
return ListenableBuilder(
listenable: onToggleCompleted,
builder: (context, child) {
final isRunning = onToggleCompleted.running;
if (onToggleCompleted.result != null) {
final result = onToggleCompleted.result!;
result.fold(
onOk: (_) {
// Do nothing, the UI will update automatically when the command's
// running state changes, which will cause the checkbox to show the
// new completed state of the to-do.
},
onError: (error) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('Error: $error')));
},
);
}
return ListTile(
title: Text(toDo.title),
trailing: isRunning
? const CircularProgressIndicator()
: Checkbox(
value: toDo.isCompleted,
onChanged: (bool? newValue) {
onToggleCompleted.execute(toDo);
},
),
);
},
);
}
}
Now just swap the ListTile of home_list_page.dart to test passing the completeToDoCommand as a parameter and see the behavior :)
Conclusion
I don't know if you noticed, but the loadToDosCommand command is being called in 3 different places: RefreshIndicator, ViewModel constructor, and _completeToDo function. But no one who calls it needs to know in depth who should call it, and yet they have access to its state and execution. It is precisely for this that we have this pattern.
Just as I said about the State pattern, neither Command nor Result are silver bullets (because there is no perfect pattern), but they are useful for development.
Analyze, implement, and see you next time :)
About a year ago, the Flutter documentation was updated with new recommendations. Among its suggestions, such as the MVVM architecture I've commented on before, two patterns already known to programmers were indicated: Command and Result. In this article, we will see the advantage of using them, where they fit, and how to implement them in two flows of the same application.
As with every article I write, we will see a little bit of theory before going to the code, but today I will be very brief, I promise.
Knowing the patterns
Command
This pattern was born from the need to reuse code from the same function but without UI components knowing what they are doing. For a "send" button, it only triggers the execution of a command, without the need to execute the function passing any parameter, so the UI can be changed freely without affecting how the data is being executed.
We will then have a Command class, which will be the only one known by our buttons and UI components, and we will have the implementations, for example.
Right, so what?
In Flutter, this pattern is extended to already have information about the request state, indicating if it is being executed and if it finished with an error or success, so that we will have a single ListenableBuilder to react to the request, regardless of who executes it.
Result
The Result is simpler than it seems; I say this because many who start in the area feel that design patterns are something complex and end up imagining an even greater difficulty.
This pattern is simply a standardization of the return type of functions. Okay, it still sounds strange. Let's see an example:
Let's say we have a list of Contacts stored on a server and we want to make a call to it. As soon as we open our app, it searches and returns our list of contacts, so the expected result is a List<Contact>.
Oops, but this request can fail, like a lack of connection error, for example.
In this case, instead of throwing an error (throw Exception), we handle this error and display only a message "We are offline".
In this case, the one responsible for handling our error is the data access function (or class) itself, and whoever calls it (e.g., viewmodel) only needs to worry if the response is an error or success.
Preparing the environment
Starting
Considering you have already executed flutter create <your app>, first let's create a utils directory and add the following files to it: result.dart and command.dart. These two files are available in the flutter documentation (which I will add at the end of the article) and also other libraries like result_dart and command_it.
The official Flutter documentation created two Command classes, Command0 which receives no parameters and Command1 which receives parameters. Feel free to change the names to whatever is easier, for example, CommandWithParam for the second one. Here I will keep the original.
Changing our Result.dart
This is up to the programmer, but I like to add a .fold() function in the Result class, responsible for executing the functions received in case of success or failure, just to facilitate the ViewModel code and not need to write a Switch/Case every time.
Our Result class will be as follows:
sealed class Result<T> {
const Result();
/// Creates a successful [Result], completed with the specified [value].
const factory Result.ok(T value) = Ok._;
/// Creates an error [Result], completed with the specified [error].
const factory Result.error(Exception error) = Error._;
/// Applies one of two functions depending on the result type.
///
/// If this is an [Ok], applies [onOk] to the value.
/// If this is an [Error], applies [onError] to the error.
R fold<R>({
required R Function(T value) onOk,
required R Function(Exception error) onError,
}) {
return switch (this) {
Ok(value: final value) => onOk(value),
Error(error: final error) => onError(error),
};
}
}
Creating the base structure
Just like every new Flutter app, let's remove the home_page.dart file, and create a presentation directory and inside it the home_list_page directory with the home_list_page.dart and home_list_viewmodel.dart files. In addition, inside /lib let's create the repositories folder and the to_do_repository.dart file inside it. Let's also create a models directory in /lib to store to_do.dart.
From the structure, you can see that my focus here is not to present a clean architecture since it is an example, but feel free to implement the architecture and structure that makes you most comfortable.
Our ToDo model will be as follows:
class ToDo {
final String id;
final String title;
final bool isCompleted;
ToDo({required this.id, required this.title, this.isCompleted = false});
ToDo copyWith({String? id, String? title, bool? isCompleted}) {
return ToDo(
id: id ?? this.id,
title: title ?? this.title,
isCompleted: isCompleted ?? this.isCompleted,
);
}
}
Implementing the repository and Result
Our abstract repository class will be as follows:
abstract class ToDoRepository {
Future<Result<List<ToDo>>> getToDos();
Future<Result<void>> addToDo(ToDo toDo);
Future<Result<void>> completeToDo(ToDo toDo);
Future<Result<void>> deleteToDo(String id);
}
Here we already see our Result being used; it will be the base class for data return from our repository (and services if we need them in the future).
I will implement an in-memory repository, but feel free to implement it with Firestore, Isar, or Sqflite. It will be as follows:
class ToDoMemoryRepository implements ToDoRepository {
// In-memory list of to-dos. In a real application, this would be replaced with
// a database or API.
final List<ToDo> _toDos = [ToDo(id: '1', title: 'Sample ToDo')];
@override
Future<Result<List<ToDo>>> getToDos() async {
try {
await Future.delayed(const Duration(seconds: 2));
final List<ToDo> clonedList = List<ToDo>.from(_toDos);
return Result.ok(clonedList);
} catch (error) {
return Result.error(Exception('Failed to get ToDos: $error'));
}
}
@override
Future<Result<void>> completeToDo(ToDo toDo) async {
try {
await Future.delayed(const Duration(seconds: 2));
final index = _toDos.indexWhere((t) => t.id == toDo.id);
if (index != -1) {
_toDos[index] = toDo.copyWith(isCompleted: !toDo.isCompleted);
}
return const Result.ok(null);
} catch (error) {
return Result.error(Exception('Failed to complete ToDo: $error'));
}
}
@override
Future<Result<void>> addToDo(ToDo toDo) async {
try {
await Future.delayed(const Duration(seconds: 2));
_toDos.add(toDo);
return const Result.ok(null);
} catch (error) {
return Result.error(Exception('Failed to add ToDo: $error'));
}
}
@override
Future<Result<void>> deleteToDo(String id) async {
try {
await Future.delayed(const Duration(seconds: 2));
_toDos.removeWhere((toDo) => toDo.id == id);
return const Result.ok(null);
} catch (error) {
return Result.error(Exception('Failed to delete ToDo: $error'));
}
}
}
As said before, with Result, the one responsible for handling errors is the data access class. Here would be the moment to check the request status (if accessed via HTTP) and return a response according to the code, for example.
Implementing the list screen (Command without parameters)
Implementing the ViewModel
Our ViewModel will be simple, since the purpose is only to search the repository using the command. It will be the following code:
class HomeListViewmodel {
final ToDoRepository _toDoRepository;
// Command to load the list of to-dos.
late final loadToDosCommand = Command0<List<ToDo>>(
() => _toDoRepository.getToDos(),
);
// Load the to-dos when the view model is created.
HomeListViewmodel(this._toDoRepository) {
loadToDosCommand.execute();
}
}
This way, as soon as our viewmodel is instantiated, it will execute our search.
Implementing our screen
To implement our screen we will assemble it in this order:
- Instantiating our ViewModel
- Scaffold to contain the screen and add Material configurations
- The ListenableBuilder which will be the widget responsible for listening to our Command
- The RefreshIndicator that will trigger our search when the user wants to update
- The Builder that will read the state of our command and return a component that makes sense
Inside the builder, we will have the following logic:
- If the command is still executing, we will display a CircularProgressIndicator (or any loading widget)
bool isLoading = viewModel.loadToDosCommand.running;
if (isLoading) {
return const Center(child: CircularProgressIndicator());
}
- Return the .fold of our Result to display a component according to its return, displaying EITHER a list OR an error message.
return viewModel.loadToDosCommand.result!.fold(
onOk: (value) {
final toDos = value;
return ListView.builder(
itemCount: toDos.length,
itemBuilder: (context, index) {
final toDo = toDos[index];
return ListTile(
title: Text(toDo.title),
trailing: Checkbox(
value: toDo.isCompleted,
onChanged: (bool? newValue) {
// Handle checkbox state change
},
),
);
},
);
},
onError: (error) {
return Center(child: Text('Error: $error'));
},
);
Thus, our list page:
class HomeListPage extends StatelessWidget {
const HomeListPage({super.key});
@override
Widget build(BuildContext context) {
// Create the view model for the home list page. In a real application, you
// would typically use a dependency injection framework to provide the
// repository and view model, but for this example, we'll just create them directly here.
final viewModel = HomeListViewmodel(ToDoMemoryRepository());
return Scaffold(
appBar: AppBar(title: const Text('To-Do List')),
body: ListenableBuilder(
listenable: viewModel.loadToDosCommand,
builder: (context, child) {
return RefreshIndicator(
child: Builder(
builder: (context) {
bool isLoading = viewModel.loadToDosCommand.running;
if (isLoading) {
return const Center(child: CircularProgressIndicator());
}
return viewModel.loadToDosCommand.result!.fold(
onOk: (value) {
final toDos = value;
return ListView.builder(
itemCount: toDos.length,
itemBuilder: (context, index) {
final toDo = toDos[index];
return ListTile(
title: Text(toDo.title),
trailing: Checkbox(
value: toDo.isCompleted,
onChanged: (bool? newValue) {
// Handle checkbox state change
},
),
);
},
);
},
onError: (error) {
return Center(child: Text('Error: $error'));
},
);
},
),
onRefresh: () {
return viewModel.loadToDosCommand.execute();
},
);
},
),
);
}
}
If you prefer, you can add print or debugPrint to see the data flow in the terminal :)
Implementing the check flow in ToDo (Command with parameters)
Changing the ViewModel
Now that we can use Command without parameters, we will need to send something and we can do it without leaving the screen flow. As we already have the whole structure ready, we will only need to add the command to complete the ToDo in our ViewModel, following the logic of:
- Trigger complete command
- If success, update the ToDo list (execution of the first command)
- If failure, return the error
For this, we will create a function inside the ViewModel and not a lambda as in the first one:
Future<Result<void>> _completeToDo(ToDo toDo) async {
final result = await _toDoRepository.completeToDo(toDo);
return result.fold(
onOk: (_) {
loadToDosCommand.execute();
return const Result.ok(null);
},
onError: (error) {
return Result.error(error);
},
);
}
Thus, our ViewModel will look like this:
class HomeListViewmodel {
final ToDoRepository _toDoRepository;
// Command to load the list of to-dos.
late final loadToDosCommand = Command0<List<ToDo>>(
() => _toDoRepository.getToDos(),
);
// Command to mark a to-do as completed.
late final completeToDoCommand = Command1<void, ToDo>(_completeToDo);
// Load the to-dos when the view model is created.
HomeListViewmodel(this._toDoRepository) {
loadToDosCommand.execute();
}
Future<Result<void>> _completeToDo(ToDo toDo) async {
final result = await _toDoRepository.completeToDo(toDo);
return result.fold(
onOk: (_) {
loadToDosCommand.execute();
// We return a successful result with no value, since the UI doesn't need
// any data from this command, it just needs to know if it succeeded or failed.
return const Result.ok(null);
},
onError: (error) {
return Result.error(error);
},
);
}
}
Implementing the list component
Let's create a widgets directory inside presentation to store our custom components and inside it, list_item.dart.
This component will be a ListenableBuilder that must receive the referring ToDo object and the command it must execute when marking as done. If it is loading, it should display a loading component to prevent the user from tapping multiple times while processing the request. In addition, if an error occurs, it should display a message via Snackbar.
Our component will be as follows:
class ListItem extends StatelessWidget {
final ToDo toDo;
final Command1<void, ToDo> onToggleCompleted;
const ListItem({
super.key,
required this.toDo,
required this.onToggleCompleted,
});
@override
Widget build(BuildContext context) {
return ListenableBuilder(
listenable: onToggleCompleted,
builder: (context, child) {
final isRunning = onToggleCompleted.running;
if (onToggleCompleted.result != null) {
final result = onToggleCompleted.result!;
result.fold(
onOk: (_) {
// Do nothing, the UI will update automatically when the command's
// running state changes, which will cause the checkbox to show the
// new completed state of the to-do.
},
onError: (error) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('Error: $error')));
},
);
}
return ListTile(
title: Text(toDo.title),
trailing: isRunning
? const CircularProgressIndicator()
: Checkbox(
value: toDo.isCompleted,
onChanged: (bool? newValue) {
onToggleCompleted.execute(toDo);
},
),
);
},
);
}
}
Now just swap the ListTile of home_list_page.dart to test passing the completeToDoCommand as a parameter and see the behavior :)
Conclusion
I don't know if you noticed, but the loadToDosCommand command is being called in 3 different places: RefreshIndicator, ViewModel constructor, and _completeToDo function. But no one who calls it needs to know in depth who should call it, and yet they have access to its state and execution. It is precisely for this that we have this pattern.
Just as I said about the State pattern, neither Command nor Result are silver bullets (because there is no perfect pattern), but they are useful for development.
Analyze, implement, and see you next time :)
Code
https://github.com/arcbueno/CommandResultArticle
Source
Flutter's command documentation
Flutter's result documentation
Top comments (0)