Mudanças entre as edições de "Flutter - CRUD Rest"
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 56: | Linha 57: | ||
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. | 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> | + | <syntaxhighlight lang="dart"> |
class User { | class User { | ||
final int id; | final int id; | ||
Linha 68: | Linha 69: | ||
}); | }); | ||
− | // | + | // Gerar um JSON |
Map<String, dynamic> toMap() { | Map<String, dynamic> toMap() { | ||
return { | return { | ||
Linha 77: | Linha 78: | ||
} | } | ||
− | // | + | // Criar um objeto User a partir de um JSON |
− | User.fromJson(Map json) | + | User.fromJson(Map<String, dynamic> json) |
− | : id = json['id'], | + | : id = json['id'] ?? 0, |
− | name = json['name'], | + | name = json['name'] ?? 'Unknown', |
− | email = json['email']; | + | 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> | </syntaxhighlight> | ||
Linha 89: | Linha 95: | ||
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. | 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 | + | 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> | + | <syntaxhighlight lang="dart"> |
import 'package:http/http.dart' as http; | import 'package:http/http.dart' as http; | ||
import 'dart:convert'; | import 'dart:convert'; | ||
import 'user.dart'; | import 'user.dart'; | ||
− | const url = " | + | 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 { | class API { | ||
static Future getAll() async { | static Future getAll() async { | ||
− | return await http.get(Uri.parse(url)); | + | return await http.get( |
+ | Uri.parse(url), | ||
+ | headers: headers, | ||
+ | ); | ||
} | } | ||
static Future getUser(int id) async { | static Future getUser(int id) async { | ||
− | return await http.get(Uri.parse('$url/$id')); | + | return await http.get( |
+ | Uri.parse('$url/$id'), | ||
+ | headers: headers, | ||
+ | ); | ||
} | } | ||
Linha 110: | Linha 127: | ||
return await http.post( | return await http.post( | ||
Uri.parse(url), | Uri.parse(url), | ||
− | headers: | + | headers: headers, |
body: json.encode(user.toMap()), | body: json.encode(user.toMap()), | ||
); | ); | ||
Linha 118: | Linha 135: | ||
return await http.put( | return await http.put( | ||
Uri.parse(url), | Uri.parse(url), | ||
− | headers: | + | headers: headers, |
body: json.encode(user.toMap()), | body: json.encode(user.toMap()), | ||
); | ); | ||
} | } | ||
− | static Future deleteUser(id) async { | + | static Future deleteUser(int id) async { |
return await http.delete(Uri.parse('$url/$id')); | return await http.delete(Uri.parse('$url/$id')); | ||
} | } | ||
Linha 135: | Linha 152: | ||
<center>[[Image:Appcrud20241.png|300px]]</center> | <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 | + | 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> | + | <syntaxhighlight lang="dart"> |
import 'dart:convert'; | import 'dart:convert'; | ||
import 'package:flutter/material.dart'; | import 'package:flutter/material.dart'; | ||
Linha 151: | Linha 168: | ||
class _UsersCrudState extends State<UsersCrud> { | class _UsersCrudState extends State<UsersCrud> { | ||
− | List<User> users = | + | List<User> users = []; |
bool _enabledId = true; | bool _enabledId = true; | ||
final TextEditingController _id = TextEditingController(); | final TextEditingController _id = TextEditingController(); | ||
Linha 157: | Linha 174: | ||
final TextEditingController _email = TextEditingController(); | final TextEditingController _email = TextEditingController(); | ||
− | + | @override | |
+ | void initState() { | ||
+ | super.initState(); | ||
refreshList(); | refreshList(); | ||
} | } | ||
Linha 163: | Linha 182: | ||
void refreshList() { | void refreshList() { | ||
API.getAll().then((response) { | API.getAll().then((response) { | ||
− | setState(() { | + | if (response.statusCode == 200) { |
− | + | setState(() { | |
− | + | users = User.fromJsonList(json.decode(response.body)); | |
− | } | + | }); |
+ | } else { | ||
+ | // Para simplificação, apenas exibe um print para indicar erro. | ||
+ | showSnackBar("Erro ao carregar usuários: ${response.statusCode}"); | ||
+ | } | ||
}); | }); | ||
} | } | ||
Linha 177: | Linha 200: | ||
_enabledId = true; | _enabledId = true; | ||
}); | }); | ||
+ | } | ||
+ | |||
+ | void showSnackBar(String message) { | ||
+ | ScaffoldMessenger.of(context).showSnackBar( | ||
+ | SnackBar(content: Text(message)), | ||
+ | ); | ||
} | } | ||
Linha 261: | Linha 290: | ||
key: Key(users[index].id.toString()), | key: Key(users[index].id.toString()), | ||
onDismissed: (direction) { | onDismissed: (direction) { | ||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
API.deleteUser(users[index].id); | API.deleteUser(users[index].id); | ||
setState(() => users.removeAt(index)); | setState(() => users.removeAt(index)); | ||
fieldsClenner(); | fieldsClenner(); | ||
+ | showSnackBar('Usuário ${users[index].name} excluído'); | ||
}, | }, | ||
// Agora temos nossos itens da lista | // Agora temos nossos itens da lista |
Edição das 11h27min 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'));
}
}
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 {
// Para simplificação, apenas exibe um print para indicar erro.
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.