Mudanças entre as edições de "Flutter - CRUD Rest"
(Criou página com 'Afluentes: Usabilidade, Desenvolvimento Web, Mobile e Jogos, Desenvolvimento Front-end II = Serviço web = Para que nosso CRUD possa ser funcional, precisamos de um...') |
|||
(7 revisões intermediárias pelo mesmo usuário não estão sendo mostradas) | |||
Linha 1: | Linha 1: | ||
+ | |||
+ | |||
+ | |||
+ | |||
Afluentes: [[Usabilidade, Desenvolvimento Web, Mobile e Jogos]], [[Desenvolvimento Front-end II]] | Afluentes: [[Usabilidade, Desenvolvimento Web, Mobile e Jogos]], [[Desenvolvimento Front-end II]] | ||
Linha 11: | Linha 15: | ||
= Criação do Projeto = | = Criação do Projeto = | ||
+ | |||
+ | Primeiro criamos nosso projeto. Nesse exemplo dei o nome de <code>appcrud</code>, mas vocês podem usar qualquer outro nome. | ||
$ flutter create appcrud | $ flutter create appcrud | ||
+ | |||
+ | Na sequência, entramos dentro da pasta do projeto criado e lá dentro adicionamos a biblioteca <code>http</code> para podermos fazer as chamadas aos serviços web. | ||
+ | |||
$ cd appcrud | $ cd appcrud | ||
$ flutter pub add http | $ flutter pub add http | ||
+ | |||
+ | A partir desse momento, nosso projeto básico já está funcionando. Para executá-lo, basta digitar o comando abaixo dentro da pasta do projeto: | ||
+ | |||
+ | $ flutter run | ||
+ | |||
+ | = Nosso Projeto = | ||
+ | |||
+ | Vamos primeiro alterar o arquivo <code>main.dart</code>, ajustando ele apenas para chamar a nossa janela de CRUD. | ||
+ | |||
+ | == main.dart == | ||
+ | |||
+ | Veja que temos a classe <code>App</code> e nela configuramos o título e no home chamamos a classe <code>UsersCrud</code> que está no arquivo <code>userscrud.dart</code> que deve ser adicionado nos <code>imports</code>· | ||
+ | |||
+ | <syntaxhighlight lang=dart> | ||
+ | import 'package:flutter/material.dart'; | ||
+ | import 'userscrud.dart'; | ||
+ | |||
+ | void main() => runApp(const App()); | ||
+ | |||
+ | class App extends StatelessWidget { | ||
+ | const App({super.key}); | ||
+ | |||
+ | @override | ||
+ | build(context) { | ||
+ | return const MaterialApp( | ||
+ | title: 'Usuários', | ||
+ | home: UsersCrud(), | ||
+ | ); | ||
+ | } | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | == user.dart == | ||
+ | |||
+ | Nossa classe <code>User</code> serve para representar nossos objetos com as informações dos usuários. A classe possui os atributos, o constructor, um Map e um método <code>fromJson</code> que pega os dados do Json e carrega no objeto. | ||
+ | |||
+ | <syntaxhighlight lang="dart"> | ||
+ | class User { | ||
+ | final int id; | ||
+ | final String name; | ||
+ | final String email; | ||
+ | |||
+ | User({ | ||
+ | required this.id, | ||
+ | required this.name, | ||
+ | required this.email, | ||
+ | }); | ||
+ | |||
+ | // Gerar um JSON | ||
+ | Map<String, dynamic> toMap() { | ||
+ | return { | ||
+ | 'id': id, | ||
+ | 'name': name, | ||
+ | 'email': email, | ||
+ | }; | ||
+ | } | ||
+ | |||
+ | // Criar um objeto User a partir de um JSON | ||
+ | User.fromJson(Map<String, dynamic> json) | ||
+ | : id = json['id'] ?? 0, | ||
+ | name = json['name'] ?? 'Unknown', | ||
+ | email = json['email'] ?? 'no-email@example.com'; | ||
+ | |||
+ | // Criar uma lista de usuários a partir de uma lista JSON | ||
+ | static List<User> fromJsonList(List<dynamic> jsonList) { | ||
+ | return jsonList.map((json) => User.fromJson(json)).toList(); | ||
+ | } | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | == api.dart == | ||
+ | |||
+ | A classe <code>API</code> possui apenas métodos estáticos que servem para efetuar as operações no nosso serviço web. Veja que colocamos o link como uma constante chamada <code>url</code>. Pode ser interessante que esse link seja configurável conforme o tipo da aplicação. | ||
+ | |||
+ | Os métodos que estamos buscando são <code>GET</code> sem e com parâmetro, sendo que sem parâmetro retorna a lista de objetos que estão na base de dados remota e o com parâmetro retorna apenas um objeto, com o id passado no parâmetro da chamada da função. Também temos os métodos <code>POST</code>, <code>DELETE</code> e PUT. | ||
+ | |||
+ | <syntaxhighlight lang="dart"> | ||
+ | import 'package:http/http.dart' as http; | ||
+ | import 'dart:convert'; | ||
+ | import 'user.dart'; | ||
+ | |||
+ | const url = 'https://api.arisa.com.br/users'; // URL + endpoint | ||
+ | const headers = { | ||
+ | 'Content-Type': 'application/json; charset=utf-8', // Mensagens JSON | ||
+ | "X-User": "fulano", // Usuário | ||
+ | 'X-API-KEY': "12345", // API-KEY | ||
+ | }; | ||
+ | |||
+ | class API { | ||
+ | static Future getAll() async { | ||
+ | return await http.get( | ||
+ | Uri.parse(url), | ||
+ | headers: headers, | ||
+ | ); | ||
+ | } | ||
+ | |||
+ | static Future getUser(int id) async { | ||
+ | return await http.get( | ||
+ | Uri.parse('$url/$id'), | ||
+ | headers: headers, | ||
+ | ); | ||
+ | } | ||
+ | |||
+ | static Future insertUser(User user) async { | ||
+ | return await http.post( | ||
+ | Uri.parse(url), | ||
+ | headers: headers, | ||
+ | body: json.encode(user.toMap()), | ||
+ | ); | ||
+ | } | ||
+ | |||
+ | static Future updateUser(User user) async { | ||
+ | return await http.put( | ||
+ | Uri.parse(url), | ||
+ | headers: headers, | ||
+ | body: json.encode(user.toMap()), | ||
+ | ); | ||
+ | } | ||
+ | |||
+ | static Future deleteUser(int id) async { | ||
+ | return await http.delete( | ||
+ | Uri.parse('$url/$id'), | ||
+ | headers: headers, | ||
+ | ); | ||
+ | } | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | == userscrud.dart == | ||
+ | |||
+ | Por fim temos a interface do nosso aplicativo. | ||
+ | |||
+ | <center>[[Image:Appcrud20241.png|300px]]</center> | ||
+ | |||
+ | Nesse exemplo temos 3 campos de texto de entrada, sendo o <code>id</code>, o <code>nome</code> e o e-mail. Também temos 2 botões, de <code>gravar</code>, que serve para cadastrar um novo registro ou alterar um existente e o botão <code>limpar</code> que limpa os campos de entrada. Abaixo temos a lista dos elementos cadastrados no banco de dados remoto gerenciado pelo serviço web. | ||
+ | |||
+ | <syntaxhighlight lang="dart"> | ||
+ | import 'dart:convert'; | ||
+ | import 'package:flutter/material.dart'; | ||
+ | import 'user.dart'; | ||
+ | import 'api.dart'; | ||
+ | |||
+ | class UsersCrud extends StatefulWidget { | ||
+ | const UsersCrud({super.key}); | ||
+ | |||
+ | @override | ||
+ | State<UsersCrud> createState() => _UsersCrudState(); | ||
+ | } | ||
+ | |||
+ | class _UsersCrudState extends State<UsersCrud> { | ||
+ | List<User> users = []; | ||
+ | bool _enabledId = true; | ||
+ | final TextEditingController _id = TextEditingController(); | ||
+ | final TextEditingController _name = TextEditingController(); | ||
+ | final TextEditingController _email = TextEditingController(); | ||
+ | |||
+ | @override | ||
+ | void initState() { | ||
+ | super.initState(); | ||
+ | refreshList(); | ||
+ | } | ||
+ | |||
+ | void refreshList() { | ||
+ | API.getAll().then((response) { | ||
+ | if (response.statusCode == 200) { | ||
+ | setState(() { | ||
+ | users = User.fromJsonList(json.decode(response.body)); | ||
+ | }); | ||
+ | } else { | ||
+ | showSnackBar("Erro ao carregar usuários: ${response.statusCode}"); | ||
+ | } | ||
+ | }); | ||
+ | } | ||
+ | |||
+ | void fieldsClenner() { | ||
+ | setState(() { | ||
+ | _id.text = ""; | ||
+ | _name.text = ""; | ||
+ | _email.text = ""; | ||
+ | _enabledId = true; | ||
+ | }); | ||
+ | } | ||
+ | |||
+ | void showSnackBar(String message) { | ||
+ | ScaffoldMessenger.of(context).showSnackBar( | ||
+ | SnackBar(content: Text(message)), | ||
+ | ); | ||
+ | } | ||
+ | |||
+ | @override | ||
+ | Widget build(BuildContext context) { | ||
+ | return Scaffold( | ||
+ | appBar: AppBar( | ||
+ | title: const Text("Usuários"), | ||
+ | ), | ||
+ | body: Column( | ||
+ | children: [ | ||
+ | // Primeiro criamos nossos 3 campos texto do formulário | ||
+ | // No campo ID eu vou precisar do enabled para desabilitar | ||
+ | // sua edição quando buscamos os dados do serviço web para | ||
+ | // alterá-lo, não permitindo alterar o ID. | ||
+ | TextField( | ||
+ | decoration: const InputDecoration( | ||
+ | labelText: 'Id', | ||
+ | ), | ||
+ | controller: _id, | ||
+ | enabled: _enabledId, | ||
+ | ), | ||
+ | TextField( | ||
+ | decoration: const InputDecoration( | ||
+ | labelText: 'Nome', | ||
+ | ), | ||
+ | controller: _name, | ||
+ | ), | ||
+ | TextField( | ||
+ | decoration: const InputDecoration( | ||
+ | labelText: 'E-mail', | ||
+ | ), | ||
+ | controller: _email, | ||
+ | ), | ||
+ | // Depois criamos nossos dois botão. O de gravar, que serve | ||
+ | // tanto para adicionar um novo registro ou alterar, quando | ||
+ | // um registro já foi carregado e o campo ID está desabilitado. | ||
+ | Row( | ||
+ | mainAxisAlignment: MainAxisAlignment.spaceEvenly, | ||
+ | children: [ | ||
+ | ElevatedButton( | ||
+ | child: const Text('Gravar'), | ||
+ | onPressed: () { | ||
+ | var user = User( | ||
+ | id: int.parse(_id.text), | ||
+ | name: _name.text, | ||
+ | email: _email.text, | ||
+ | ); | ||
+ | if (_enabledId == false) { | ||
+ | API.updateUser(user).then((value) { | ||
+ | fieldsClenner(); | ||
+ | refreshList(); | ||
+ | }); | ||
+ | } else { | ||
+ | API.insertUser(user).then((value) { | ||
+ | fieldsClenner(); | ||
+ | refreshList(); | ||
+ | }); | ||
+ | } | ||
+ | }, | ||
+ | ), | ||
+ | ElevatedButton( | ||
+ | style: ElevatedButton.styleFrom( | ||
+ | foregroundColor: Colors.white, | ||
+ | backgroundColor: Colors.green, | ||
+ | ), | ||
+ | child: const Text('Limpar'), | ||
+ | onPressed: () => fieldsClenner(), | ||
+ | ), | ||
+ | ], | ||
+ | ), | ||
+ | // Por último temos uma listview. Veja que colocamos ela dentro de | ||
+ | // um Expanded para que tenhamos uma rolagem fluída. | ||
+ | Expanded( | ||
+ | child: ListView.builder( | ||
+ | shrinkWrap: true, | ||
+ | itemCount: users.length, | ||
+ | itemBuilder: (context, index) { | ||
+ | return Dismissible( | ||
+ | // Vamos usar um elemento dismisseble. Quando dispensarmos | ||
+ | // uma linha, efetuamos a operação delete via serviço web, | ||
+ | // mostramos uma mensagem como SnackBar e liberamos o item | ||
+ | // da nossa lista. | ||
+ | key: Key(users[index].id.toString()), | ||
+ | onDismissed: (direction) { | ||
+ | API.deleteUser(users[index].id); | ||
+ | setState(() => users.removeAt(index)); | ||
+ | fieldsClenner(); | ||
+ | showSnackBar('Usuário ${users[index].name} excluído'); | ||
+ | }, | ||
+ | // Agora temos nossos itens da lista | ||
+ | child: ListTile( | ||
+ | title: Text( | ||
+ | '${users[index].id} - ${users[index].name}', | ||
+ | style: const TextStyle( | ||
+ | fontSize: 20.0, | ||
+ | color: Colors.black, | ||
+ | ), | ||
+ | ), | ||
+ | subtitle: Text(users[index].email), | ||
+ | // Quando clicar, busco as informações via serviço web com | ||
+ | // base no ID do item da lista clicado e carrego os dados | ||
+ | // nos campos de texto. Desabilito o campo do ID porque se | ||
+ | // gravar esses dados, será uma alteração, enão a inserção | ||
+ | // de um novo registro. | ||
+ | onTap: () { | ||
+ | API.getUser(users[index].id).then((response) { | ||
+ | User user = User.fromJson(json.decode(response.body)); | ||
+ | setState(() { | ||
+ | _id.text = user.id.toString(); | ||
+ | _name.text = user.name; | ||
+ | _email.text = user.email; | ||
+ | _enabledId = false; | ||
+ | }); | ||
+ | }); | ||
+ | }, | ||
+ | ), | ||
+ | ); | ||
+ | }), | ||
+ | ), | ||
+ | ], | ||
+ | ), | ||
+ | ); | ||
+ | } | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | = Considerações = | ||
+ | |||
+ | Veja que o código é um exemplo de aprendizagem que em muito falha em eficiência. Esse código serve apenas para demonstrar o uso de alguns recursos. Dessa forma, temos as seguintes observações. | ||
+ | |||
+ | * Se temos uma base de dados grande, não é muito eficiente trazer todos os registros. Podemos trabalhar por paginação ou algo como uma filtragem. | ||
+ | * Quando clicamos em um elemento da nossa lista, podemos apenas carregar nos campos de entrada as informações que estão no objeto já na memória. Nesse caso, um problema é quando o aplicativo é multiusuário. Enquanto você está vendo uma listagem de informações, outra pessoa pode ter alterado os dados de um determinado registro, então você tem uma versão antiga dos dados (problemas de acesso concorrente). | ||
+ | * Quando fazemos qualquer alteração que infere em mudança na nossa ListView, recarregamos ela, o que pode não ser muito eficiente, pois, por exemplo quando excluímos um registro, basta pegar o retorno de exclusão bem sucedida ou não efetuada para realmente excluir ou não aquele item da lista, sem precisar recarregar a lista. Mas, podemos voltar ao problema de acesso concorrente em sistemas distribuídos. |
Edição atual tal como às 14h03min de 19 de novembro de 2024
Afluentes: Usabilidade, Desenvolvimento Web, Mobile e Jogos, Desenvolvimento Front-end II
Serviço web
Para que nosso CRUD possa ser funcional, precisamos de um serviço web com as operações de CRUD (CREATE, READ, UPDATE e DELETE). Para o nosso exemplo aqui, usarei um serviço web desenvolvido em linguagem de programação GO:
Caso você queira reimplementar o serviço em Node.js ou outra linguagem/framework, basta entender o manual de acesso ao serviço web:
Criação do Projeto
Primeiro criamos nosso projeto. Nesse exemplo dei o nome de appcrud
, mas vocês podem usar qualquer outro nome.
$ flutter create appcrud
Na sequência, entramos dentro da pasta do projeto criado e lá dentro adicionamos a biblioteca http
para podermos fazer as chamadas aos serviços web.
$ cd appcrud $ flutter pub add http
A partir desse momento, nosso projeto básico já está funcionando. Para executá-lo, basta digitar o comando abaixo dentro da pasta do projeto:
$ flutter run
Nosso Projeto
Vamos primeiro alterar o arquivo main.dart
, ajustando ele apenas para chamar a nossa janela de CRUD.
main.dart
Veja que temos a classe App
e nela configuramos o título e no home chamamos a classe UsersCrud
que está no arquivo userscrud.dart
que deve ser adicionado nos imports
·
import 'package:flutter/material.dart';
import 'userscrud.dart';
void main() => runApp(const App());
class App extends StatelessWidget {
const App({super.key});
@override
build(context) {
return const MaterialApp(
title: 'Usuários',
home: UsersCrud(),
);
}
}
user.dart
Nossa classe User
serve para representar nossos objetos com as informações dos usuários. A classe possui os atributos, o constructor, um Map e um método fromJson
que pega os dados do Json e carrega no objeto.
class User {
final int id;
final String name;
final String email;
User({
required this.id,
required this.name,
required this.email,
});
// Gerar um JSON
Map<String, dynamic> toMap() {
return {
'id': id,
'name': name,
'email': email,
};
}
// Criar um objeto User a partir de um JSON
User.fromJson(Map<String, dynamic> json)
: id = json['id'] ?? 0,
name = json['name'] ?? 'Unknown',
email = json['email'] ?? 'no-email@example.com';
// Criar uma lista de usuários a partir de uma lista JSON
static List<User> fromJsonList(List<dynamic> jsonList) {
return jsonList.map((json) => User.fromJson(json)).toList();
}
}
api.dart
A classe API
possui apenas métodos estáticos que servem para efetuar as operações no nosso serviço web. Veja que colocamos o link como uma constante chamada url
. Pode ser interessante que esse link seja configurável conforme o tipo da aplicação.
Os métodos que estamos buscando são GET
sem e com parâmetro, sendo que sem parâmetro retorna a lista de objetos que estão na base de dados remota e o com parâmetro retorna apenas um objeto, com o id passado no parâmetro da chamada da função. Também temos os métodos POST
, DELETE
e PUT.
import 'package:http/http.dart' as http;
import 'dart:convert';
import 'user.dart';
const url = 'https://api.arisa.com.br/users'; // URL + endpoint
const headers = {
'Content-Type': 'application/json; charset=utf-8', // Mensagens JSON
"X-User": "fulano", // Usuário
'X-API-KEY': "12345", // API-KEY
};
class API {
static Future getAll() async {
return await http.get(
Uri.parse(url),
headers: headers,
);
}
static Future getUser(int id) async {
return await http.get(
Uri.parse('$url/$id'),
headers: headers,
);
}
static Future insertUser(User user) async {
return await http.post(
Uri.parse(url),
headers: headers,
body: json.encode(user.toMap()),
);
}
static Future updateUser(User user) async {
return await http.put(
Uri.parse(url),
headers: headers,
body: json.encode(user.toMap()),
);
}
static Future deleteUser(int id) async {
return await http.delete(
Uri.parse('$url/$id'),
headers: headers,
);
}
}
userscrud.dart
Por fim temos a interface do nosso aplicativo.
Nesse exemplo temos 3 campos de texto de entrada, sendo o id
, o nome
e o e-mail. Também temos 2 botões, de gravar
, que serve para cadastrar um novo registro ou alterar um existente e o botão limpar
que limpa os campos de entrada. Abaixo temos a lista dos elementos cadastrados no banco de dados remoto gerenciado pelo serviço web.
import 'dart:convert';
import 'package:flutter/material.dart';
import 'user.dart';
import 'api.dart';
class UsersCrud extends StatefulWidget {
const UsersCrud({super.key});
@override
State<UsersCrud> createState() => _UsersCrudState();
}
class _UsersCrudState extends State<UsersCrud> {
List<User> users = [];
bool _enabledId = true;
final TextEditingController _id = TextEditingController();
final TextEditingController _name = TextEditingController();
final TextEditingController _email = TextEditingController();
@override
void initState() {
super.initState();
refreshList();
}
void refreshList() {
API.getAll().then((response) {
if (response.statusCode == 200) {
setState(() {
users = User.fromJsonList(json.decode(response.body));
});
} else {
showSnackBar("Erro ao carregar usuários: ${response.statusCode}");
}
});
}
void fieldsClenner() {
setState(() {
_id.text = "";
_name.text = "";
_email.text = "";
_enabledId = true;
});
}
void showSnackBar(String message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(message)),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Usuários"),
),
body: Column(
children: [
// Primeiro criamos nossos 3 campos texto do formulário
// No campo ID eu vou precisar do enabled para desabilitar
// sua edição quando buscamos os dados do serviço web para
// alterá-lo, não permitindo alterar o ID.
TextField(
decoration: const InputDecoration(
labelText: 'Id',
),
controller: _id,
enabled: _enabledId,
),
TextField(
decoration: const InputDecoration(
labelText: 'Nome',
),
controller: _name,
),
TextField(
decoration: const InputDecoration(
labelText: 'E-mail',
),
controller: _email,
),
// Depois criamos nossos dois botão. O de gravar, que serve
// tanto para adicionar um novo registro ou alterar, quando
// um registro já foi carregado e o campo ID está desabilitado.
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
ElevatedButton(
child: const Text('Gravar'),
onPressed: () {
var user = User(
id: int.parse(_id.text),
name: _name.text,
email: _email.text,
);
if (_enabledId == false) {
API.updateUser(user).then((value) {
fieldsClenner();
refreshList();
});
} else {
API.insertUser(user).then((value) {
fieldsClenner();
refreshList();
});
}
},
),
ElevatedButton(
style: ElevatedButton.styleFrom(
foregroundColor: Colors.white,
backgroundColor: Colors.green,
),
child: const Text('Limpar'),
onPressed: () => fieldsClenner(),
),
],
),
// Por último temos uma listview. Veja que colocamos ela dentro de
// um Expanded para que tenhamos uma rolagem fluída.
Expanded(
child: ListView.builder(
shrinkWrap: true,
itemCount: users.length,
itemBuilder: (context, index) {
return Dismissible(
// Vamos usar um elemento dismisseble. Quando dispensarmos
// uma linha, efetuamos a operação delete via serviço web,
// mostramos uma mensagem como SnackBar e liberamos o item
// da nossa lista.
key: Key(users[index].id.toString()),
onDismissed: (direction) {
API.deleteUser(users[index].id);
setState(() => users.removeAt(index));
fieldsClenner();
showSnackBar('Usuário ${users[index].name} excluído');
},
// Agora temos nossos itens da lista
child: ListTile(
title: Text(
'${users[index].id} - ${users[index].name}',
style: const TextStyle(
fontSize: 20.0,
color: Colors.black,
),
),
subtitle: Text(users[index].email),
// Quando clicar, busco as informações via serviço web com
// base no ID do item da lista clicado e carrego os dados
// nos campos de texto. Desabilito o campo do ID porque se
// gravar esses dados, será uma alteração, enão a inserção
// de um novo registro.
onTap: () {
API.getUser(users[index].id).then((response) {
User user = User.fromJson(json.decode(response.body));
setState(() {
_id.text = user.id.toString();
_name.text = user.name;
_email.text = user.email;
_enabledId = false;
});
});
},
),
);
}),
),
],
),
);
}
}
Considerações
Veja que o código é um exemplo de aprendizagem que em muito falha em eficiência. Esse código serve apenas para demonstrar o uso de alguns recursos. Dessa forma, temos as seguintes observações.
- Se temos uma base de dados grande, não é muito eficiente trazer todos os registros. Podemos trabalhar por paginação ou algo como uma filtragem.
- Quando clicamos em um elemento da nossa lista, podemos apenas carregar nos campos de entrada as informações que estão no objeto já na memória. Nesse caso, um problema é quando o aplicativo é multiusuário. Enquanto você está vendo uma listagem de informações, outra pessoa pode ter alterado os dados de um determinado registro, então você tem uma versão antiga dos dados (problemas de acesso concorrente).
- Quando fazemos qualquer alteração que infere em mudança na nossa ListView, recarregamos ela, o que pode não ser muito eficiente, pois, por exemplo quando excluímos um registro, basta pegar o retorno de exclusão bem sucedida ou não efetuada para realmente excluir ou não aquele item da lista, sem precisar recarregar a lista. Mas, podemos voltar ao problema de acesso concorrente em sistemas distribuídos.