Flutter - CRUD Rest

De Aulas

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,
  });

  // Gerando um MAP
  Map<String, dynamic> toMap() {
    return {
      'id': id,
      'name': name,
      'email': email,
    };
  }

  // Extraindo os dados do JSON
  User.fromJson(Map json)
      : id = json['id'],
        name = json['name'],
        email = json['email'];
}

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 = "http://localhost:8080/users";

class API {
  static Future getAll() async {
    return await http.get(Uri.parse(url));
  }

  static Future getUser(int id) async {
    return await http.get(Uri.parse('$url/$id'));
  }

  static Future insertUser(User user) async {
    return await http.post(
      Uri.parse(url),
      headers: {"Content-Type": "application/json"},
      body: json.encode(user.toMap()),
    );
  }

  static Future updateUser(User user) async {
    return await http.put(
      Uri.parse(url),
      headers: {"Content-Type": "application/json"},
      body: json.encode(user.toMap()),
    );
  }

  static Future deleteUser(id) async {
    return await http.delete(Uri.parse('$url/$id'));
  }
}

userscrud.dart

Por fim temos a interface do nosso aplicativo.

Appcrud20241.png

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 = List<User>.empty();
  bool _enabledId = true;
  final TextEditingController _id = TextEditingController();
  final TextEditingController _name = TextEditingController();
  final TextEditingController _email = TextEditingController();

  _UsersCrudState() {
    refreshList();
  }

  void refreshList() {
    API.getAll().then((response) {
      setState(() {
        Iterable lista = json.decode(response.body);
        users = lista.map((model) => User.fromJson(model)).toList();
      });
    });
  }

  void fieldsClenner() {
    setState(() {
      _id.text = "";
      _name.text = "";
      _email.text = "";
      _enabledId = true;
    });
  }

  @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) {
                      ScaffoldMessenger.of(context).showSnackBar(
                        SnackBar(
                          content:
                              Text('Usuário ${users[index].name} excluído'),
                        ),
                      );
                      API.deleteUser(users[index].id);
                      setState(() => users.removeAt(index));
                      fieldsClenner();
                    },
                    // 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.