Você está na página 1de 17

3/16/23, 10:20 PM documentation/powersolution-academy-flutter-provider-basics.

adoc at main · PowerSolution-Academy/documentation

PowerSolution-Academy / documentation Private

Code Issues Pull requests Actions Projects Security Insights

main

documentation / frontend / flutter / powersolution-academy-flutter-provider-basics.adoc

MarceloEliasM-PS documentation: API and Frontend integration Basics [completed -… History

1 contributor

Comunicação entre backend e frontend

Controlo de Versões
Table 1. Histórico de Versões
Data Autor Versão Descrição Validado

13 de Marcelo Comunicação entre backend


Fevereiro de Monteiro 0.1 e frontend na Flutter
2023 Framework

Especificações Técnicas
Este manual destina-se a qualquer pessoa que deseje organizar e acolher informações
sobre Providers e Consumers ou da comunicação entre backend e frontend na Flutter
Framework, mas também tem a finalidade de servir de guião para iniciantes e aprendizes.

https://github.com/PowerSolution-Academy/documentation/blob/main/frontend/flutter/powersolution-academy-flutter-provider-basics.adoc 1/17
3/16/23, 10:20 PM documentation/powersolution-academy-flutter-provider-basics.adoc at main · PowerSolution-Academy/documentation

Introdução
A Flutter desenvolveu bibliotecas que promovem soluções digitais. Um deles o mais
poderoso é chamado Provider. Neste guia apresenta-se seus benefícios e a sua utilidade
no desenvolvimento de aplicações UI/UX.

O que é Provider?
Na documentação oficial do Flutter encontra-se esta descrição: “é um wrapper do
InheritedWidget para tornar sua implementação mais fácil e reutilizável”.

Qual a sua função?

A função do Provider é partilhar informação ou dados entre os widgets na árvore.

Imaginemos que quer se partilhar informações através da árvore de widgets e que toda
vez que passamos de uma tela para outra, tivemos que partilhar uma entidade com dados
comuns entre ela. Quão consistente isso pareceria?

Além disso, a equipa do Flutter criou o InheritedWidgets. Estes permitem compartilhar


informações em toda a árvore de widgets e contêm outros widgets.

No entanto, o InheritedWidgets foi difícil de implementar, pois precisava de ferramentas


para consumir e atualizar dados de maneira fácil. Por esse motivo, o pacote Provider
começou como uma extensão do InheritedWidget facilitando o manuseio de informações
em toda a árvore de widgets.

649 lines (487 sloc) 21.7 KB

https://github.com/PowerSolution-Academy/documentation/blob/main/frontend/flutter/powersolution-academy-flutter-provider-basics.adoc 2/17
3/16/23, 10:20 PM documentation/powersolution-academy-flutter-provider-basics.adoc at main · PowerSolution-Academy/documentation

Figure 1. Árvore de Widgets com o Provider


No gráfico da figura 1, pode-se constatar o desempenho do provider sendo o pai de todos
os widgets. Ele pode partilhar as informações com quem precisar, e o Consumer pode
atualizar as informações de seu pai. Agora que entendemos o significado do Provider e sua
finalidade, vamos aprender como configurá-lo e usá-lo.

Como utilizar?
Passo 1 - Adicionar dependência

dependencies:
flutter:
sdk: flutter
provider: <version>

Pode consultar informações da dependência e saber a sua versão mais recente aqui

Então, você tem várias ferramentas à sua disposição:

ChangeNotifier: Este deve ser estendido para fornecer um objeto que pode ser usado para
enviar notificações de alteração para seus ouvintes.

ChangeNotifierProvider: Possui alterações de um ChangeNotifier. Os widgets filhos


podem acessar o objeto de estado e ouvir as alterações.

Consumer: reconstrói parte de uma subárvore quando o estado de escuta muda.

https://github.com/PowerSolution-Academy/documentation/blob/main/frontend/flutter/powersolution-academy-flutter-provider-basics.adoc 3/17
3/16/23, 10:20 PM documentation/powersolution-academy-flutter-provider-basics.adoc at main · PowerSolution-Academy/documentation

Provider.of: permite que widgets descendentes acessem o objeto de estado.

Passo 2 - Construir modelos

No pacote data/models dentro do pacote lib cria-se um novo ficheiro .dart com o
nome da entidade. Vamos utilizar o exemplo de um estudante, então criamos um ficheiro
com nome student.dart .

O modelo deve ter como base o corpo JSON retornado na resposta dada pela API no
backend. Consideremos este exemplo de resposta retornada pela API:

{
"code" : 123456,
"name" : "Joana Patricia Pereira",
"age" : 19,
"course" : "MEDICINE",
"faculty" : "BIO",
"_links" : {
"self" : {
"href" : "http://localhost:8080/students/5a7cc6d9-cc26-4db7-8365-aee937cd0173"
},
"university": {
"href" : "http://localhost:8080/universities/36127889-bf8e-472f-b88f-bda84b27e
}
}
}

Tendo este modelo, vamos definir a classe Student onde seus atributos são os campos
do corpo JSON acima:

class Student {
int? code;
String? name;
int? age;
String? course;
String? faculty;
Links? links;

Student(
{this.code, this.name, this.age, this.course, this.faculty, this.links});

https://github.com/PowerSolution-Academy/documentation/blob/main/frontend/flutter/powersolution-academy-flutter-provider-basics.adoc 4/17
3/16/23, 10:20 PM documentation/powersolution-academy-flutter-provider-basics.adoc at main · PowerSolution-Academy/documentation

Note que o campo _links também contém outros campos como self e university .
Então devemos também criar classes para estes.

class Links {
Self? self;
Self? university;

Links({this.self, this.university});
}

Os campos de self e university têm o mesmo campo href contidos neles, então
podemos definir apenas uma classe para ambas que vamos denominar de Self .

class Self {
String? href;

Self({this.href});

Após definir os campos, iremos criar dois métodos estáticos para cada classe: fromJson e
toJson .

O fromJson serve para serializar os campos retornados pela API em formato JSON para
os campos contidos no modelo que definimos anteriormente. Normalmente utiliza-se para
pedidos do tipo GET quando precisa-se obter dados da API.

O toJson serve para atribuir os campos contidos no modelo definido, para um corpo
JSON para assim enviar no corpo da requisição para a API. É utilizado normalmente para
os pedidos do tipo POST ou PUT quando precisa-se enviar dados para a API.

Para o modelo de Student implementa-se os métodos da seguinte forma:

Student.fromJson(Map<String, dynamic> json) {


code = json['code'];
name = json['name'];
age = json['age'];
course = json['course'];
faculty = json['faculty'];
links = json['_links'] != null ? Links.fromJson(json['_links']) : null;
}

Map<String, dynamic> toJson() {


final Map<String, dynamic> data = Map<String, dynamic>();

https://github.com/PowerSolution-Academy/documentation/blob/main/frontend/flutter/powersolution-academy-flutter-provider-basics.adoc 5/17
3/16/23, 10:20 PM documentation/powersolution-academy-flutter-provider-basics.adoc at main · PowerSolution-Academy/documentation

data['code'] = this.code;
data['name'] = this.name;
data['age'] = this.age;
data['course'] = this.course;
data['faculty'] = this.faculty;
if (this.links != null) {
data['_links'] = this.links!.toJson();
}
return data;
}

Para o modelo de Links também implementa-se os métodos. Faz-se da seguinte forma:

Links.fromJson(Map<String, dynamic> json) {


self = json['self'] != null ? Self.fromJson(json['self']) : null;
university = json['university'] != null
? Self.fromJson(json['university'])
: null;
}

Map<String, dynamic> toJson() {


final Map<String, dynamic> data = Map<String, dynamic>();
if (this.self != null) {
data['self'] = this.self!.toJson();
}
if (this.university != null) {
data['university'] = this.university!.toJson();
}
return data;
}

O mesmo para o modelo Self :

Self.fromJson(Map<String, dynamic> json) {


href = json['href'];
}

Map<String, dynamic> toJson() {


final Map<String, dynamic> data = Map<String, dynamic>();
data['href'] = this.href;
return data;
}

O ficheiro student.dart deve ficar da seguinte forma:

https://github.com/PowerSolution-Academy/documentation/blob/main/frontend/flutter/powersolution-academy-flutter-provider-basics.adoc 6/17
3/16/23, 10:20 PM documentation/powersolution-academy-flutter-provider-basics.adoc at main · PowerSolution-Academy/documentation

class Student {
int? code;
String? name;
int? age;
String? course;
String? faculty;
Links? links;

Student(
{this.code, this.name, this.age, this.course, this.faculty, this.links});

Student.fromJson(Map<String, dynamic> json) {


code = json['code'];
name = json['name'];
age = json['age'];
course = json['course'];
faculty = json['faculty'];
links = json['_links'] != null ? Links.fromJson(json['_links']) : null;
}

Map<String, dynamic> toJson() {


final Map<String, dynamic> data = Map<String, dynamic>();
data['code'] = this.code;
data['name'] = this.name;
data['age'] = this.age;
data['course'] = this.course;
data['faculty'] = this.faculty;
if (this.links != null) {
data['_links'] = this.links!.toJson();
}
return data;
}
}

class Links {
Self? self;
Self? university;

Links({this.self, this.university});

Links.fromJson(Map<String, dynamic> json) {


self = json['self'] != null ? Self.fromJson(json['self']) : null;
university = json['university'] != null
? Self.fromJson(json['university'])
: null;
}

Map<String, dynamic> toJson() {

https://github.com/PowerSolution-Academy/documentation/blob/main/frontend/flutter/powersolution-academy-flutter-provider-basics.adoc 7/17
3/16/23, 10:20 PM documentation/powersolution-academy-flutter-provider-basics.adoc at main · PowerSolution-Academy/documentation

final Map<String, dynamic> data = Map<String, dynamic>();


if (this.self != null) {
data['self'] = this.self!.toJson();
}
if (this.university != null) {
data['university'] = this.university!.toJson();
}
return data;
}
}

class Self {
String? href;

Self({this.href});

Self.fromJson(Map<String, dynamic> json) {


href = json['href'];
}

Map<String, dynamic> toJson() {


final Map<String, dynamic> data = Map<String, dynamic>();
data['href'] = this.href;
return data;
}
}

Passo 3 - Construir serviços

A seguir definimos os métodos que irão ser invocados para comunicação com a API. No
pacote services cria-se um novo ficheiro com o nome <nome_do_modelo>_api.dart . No
nosso caso seria student_api.dart .

Ao abrir este ficheiro criaremos uma classe abstrata StudentApi , onde definiremos alguns
métodos baseados nos métodos dos controladores implementados na backend.

Os métodos mais comuns são: obter um estudante pelo id, obter todos os estudantes,
criar um estudante, atualizar um estudante e eliminar um estudante.

Estes métodos operam de forma assíncrona, quando invocados permitem que outras
operações ocorram antes que a sua operação seja completada. Para isto coloca-se a
keyword async no método.

https://github.com/PowerSolution-Academy/documentation/blob/main/frontend/flutter/powersolution-academy-flutter-provider-basics.adoc 8/17
3/16/23, 10:20 PM documentation/powersolution-academy-flutter-provider-basics.adoc at main · PowerSolution-Academy/documentation

Estes métodos demoram algum tempo, e podem não retornar o resultado imediatamente,
então, estes retornarão a promessa de entregar o resultado quando terminar. Isto é
chamado de Future . Por exemplo, a promessa de carregar um número gravada na base
de dados retornaria um Future<int> . Ou uma lista de filmes de uma pesquisa na Internet
poderiar retornar um Future<List<Movie>> . Um Future<T> é algo que no futuro
retornara um T .

A classe fica definida da seguinte forma:

abstract class StudentApi {


Future<Student> fetchOne(String url) async {
return Future.value();
}

Future<Students> fetchList({String? name}) async {


return Future.value();
}

Future<Student> createStudent(Student student, String universityId) async {


return Future.value();
}

Future<Student> updateStudent(Student student, String universityId) async {


return Future.value();
}

Future<void> delete(String url) async {


return Future.value();
}

Agora definiremos a implementação destes métodos num ficheiro separado no mesmo


pacote services e atribuiremos o nome a este ficheiro de
<nome_do_modelo>_rest_api.dart . Para o presente cenário, seria student_rest_api.dart .

No ficheiro criaremos uma classe que implementará a classe StudentApi :

class RestStudentApi implements StudentApi

Definiremos algumas constantes. A primeira é o endpoint da API ao qual deve ser feito a
requisição. Normalmente cria-se uma classe de configuração no pacote config dentro
do pacote lib denominado AppConfig onde realizai-se as configurações como o DNS e
a porta da API.
https://github.com/PowerSolution-Academy/documentation/blob/main/frontend/flutter/powersolution-academy-flutter-provider-basics.adoc 9/17
3/16/23, 10:20 PM documentation/powersolution-academy-flutter-provider-basics.adoc at main · PowerSolution-Academy/documentation

class AppConfig {

static String apiUrl = 'http://localhost:8080';

Após definir a classe de configuração em lib\config , cria-se uma variável estática que
denomina-se connectionString (pode ter qualquer outro nome no contexto como:
_apiUrlString, _endpointString, …​). Também definimos alguns _headers que irão ser utilizado
nas requisições de obtenção de dados e envio de dados, definidos como getHeaders e
postHeaders respetivamente.

class RestStudentApi implements StudentApi {

static String _connectionString = "${AppConfig.apiUrl}/students";

final Map<String, String> getHeaders = {"Accept": "application/json"};


final Map<String, String> postHeaders = {'Content-Type': 'application/json'};

Deve-se criar uma instância da classe RestStudentApi da seguinte forma:

class RestStudentApi implements StudentApi {

// previous lines of code

static final RestBuildingApi _api = new RestBuildingApi._instance();

factory RestBuildingApi() {
return _api;
}

RestBuildingApi._instance();

// next lines of code

Define-se um método que retorna o cabeçalho de autenticação que normalmente deve


conter o token que irá ser enviado para autenticar o utilizador. Mas isto pode saber mais
tarde na versão avançada deste guia.

https://github.com/PowerSolution-Academy/documentation/blob/main/frontend/flutter/powersolution-academy-flutter-provider-basics.adoc 10/17
3/16/23, 10:20 PM documentation/powersolution-academy-flutter-provider-basics.adoc at main · PowerSolution-Academy/documentation

Agora implementa-se os métodos necessários para comunicação com a API. Por esta ser
uma classe de implementação faz-se o override dos métodos definidos em StudentApi .

@override
Future<Student> fetchOne(String url) async { }

No método cria-se uma instância do modelo ao qual os dados serão preenchidos, e a


seguir, com recurso à biblioteca http , utiliza-se o método get que tem como
argumentos a endpoint no formato Uri e os cabeçalhos. Este deve ser precedido pela
keyword await para este aguardar que o valor prometido seja recebido antes de
prosseguir.

Deve-se validar o status da resposta antes de fazer a serialização recorrendo ao método


fromJson definido anteriormente no modelo. Para casos bem sucedidos são
normalmente retornados status HTTP que variam entre 200 a 300. Caso contrário pode-se
lançar uma exceção para informar que ocorreu um determinado erro ao obter os dados da
API. O jsonDecode faz a conversão da resposta recebida para um objeto do tipo
Map<String, dynamic> .

@override
Future<Student> fetchOne(String url) async {

Student student;

try {

final response = await http.get(Uri.parse("$url"), headers: getHeaders);

if (response.statusCode >= 200 && response.statusCode < 300) {


student = Student.fromJson(jsonDecode(response.body));
} else {
throw Exception(
'Warning: Failed to load data from API | ${response.reasonPhrase}');
}

} catch (e) {
throw Exception('Error: Failed to load data from API | Cause: $e');
}

return student;

https://github.com/PowerSolution-Academy/documentation/blob/main/frontend/flutter/powersolution-academy-flutter-provider-basics.adoc 11/17
3/16/23, 10:20 PM documentation/powersolution-academy-flutter-provider-basics.adoc at main · PowerSolution-Academy/documentation

De forma semelhante para o caso de envio de dados, a diferença está na requisição que
passa a ser http.post cabeçalho que passa a ser postHeaders e no uso do método
toJson . O jsonEncode faz a conversão do objeto do tipo Map<String, dynamic> para um
corpo JSON na requisição.

@override
Future<Student> createStudent(Student student, String universityId) async {

Student studentResponse;
Uri uri = Uri.parse(_connectionString);

// allow any origin header for Chrome


postHeaders.addAll({"Access-Control_Allow_Origin": "*"});

try {

student.links = {};

final postBody = jsonEncode(<String, dynamic> {


"code": student.code,
"name": student.name,
"age": student.age,
"course": student.course,
"faculty": student.faculty,
"universityId": universityId
});

final response = await http.post(uri, body: postBody, headers: postHeaders);

if (response.statusCode >= 200 && response.statusCode < 300) {


studentResponse = Student.fromJson(jsonDecode(response.body));
} else {
throw Exception('Warning: Failed to create new Student');
}
} catch (e) {
throw Exception('Error: Failed to create new Student | $e');
}

return studentResponse;

Nota: Tente implementar os outros métodos que não foram demonstrados.

https://github.com/PowerSolution-Academy/documentation/blob/main/frontend/flutter/powersolution-academy-flutter-provider-basics.adoc 12/17
3/16/23, 10:20 PM documentation/powersolution-academy-flutter-provider-basics.adoc at main · PowerSolution-Academy/documentation

Passo 4 - Construir providers

No pacote providers no pacote lib , criar-se um ficheiro que deve ser denominado de
<nome_do_modelo>_provider.dart e cria-se a classe StudentProvider .

class StudentProvider with ChangeNotifier {}

Como podemos ver no código, a classe estende o ChangeNotifier . Dessa forma, o


StudentProvider partilha os mesmos valores do modelo de um estudante que pode ser
verificado ou modificado por seus descendentes. É necessário notificar os ouvintes usando
o método notifyListeners() na função.

Cria-se uma variável denominada <nomeDoModelo>Details , no nosso cenário


studentDetails para armazenar os dados obtidos/enviados da/para API, também uma
variável booleana loading para que seja definido como true quando os dados ainda não
forem recebidos ou enviados, também uma instância do serviço RestStudentApi .

E define-se as funções para consumir os serviços definidos anteriormente, interagir com a


variável studentDetails e notificar os ouvintes com o notifyListeners() .

class StudentProvider with ChangeNotifier {


Student studentDetail = Student(
code: 0,
name: "",
age: 18,
course: "",
faculty: "",
links: null
);

bool loading = false;


StudentApi _api = RestStudentApi();
bool isNew = true;

getStudentData({required String url}) async {


loading = true;
studentDetail = await _api.fetchOne(url);
loading = false;

notifyListeners();

create(Student studentDetail) async {


loading = true;

https://github.com/PowerSolution-Academy/documentation/blob/main/frontend/flutter/powersolution-academy-flutter-provider-basics.adoc 13/17
3/16/23, 10:20 PM documentation/powersolution-academy-flutter-provider-basics.adoc at main · PowerSolution-Academy/documentation

studentDetail = await _api.createStudent(studentDetail);


isNew = false;
loading = false;

notifyListeners();
}

update(Student studentDetail) async {


loading = true;
studentDetail = await _api.updateStudent(studentDetail);
loading = false;

notifyListeners();
}

delete({required String StudentLinkRel}) async {


loading = true;
await _api.delete(StudentLinkRel);
isNew = false;
loading = false;

notifyListeners();
}
}

A necessidade de vários provedores para desenvolver um app pode ser normal, o que
podemos fazer? Bem, a biblioteca Provider nos deu uma solução chamada
MultiProvider que é uma lista de ChangeNotifierProviders que partilham suas
informações os filhos. Está configurado da seguinte forma no main.dart :

void main() async {

List<SingleChildWidget> providers = await initializeApp();

runApp(
MultiProvider(
providers: providers,
child: MaterialApp(
title: 'University Management',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const Home(),
),
),
);

https://github.com/PowerSolution-Academy/documentation/blob/main/frontend/flutter/powersolution-academy-flutter-provider-basics.adoc 14/17
3/16/23, 10:20 PM documentation/powersolution-academy-flutter-provider-basics.adoc at main · PowerSolution-Academy/documentation

A função initializeApp() irá atribuir à lista de providers todos os providers definidos


para a aplicação da seguinte forma:

initializeApp() async {

List<SingleChildWidget> providers = [
ChangeNotifierProvider(create: (_) => StudentsProvider()),
ChangeNotifierProvider(create: (_) => UniversityProvider()),
];

return providers;

Passo 5 - Consumir dados do provider

Depois de ter os provedores prontos, basta decidir como usá-los de nossos widgets
através de duas opções:

1. Consumer

O Consumer é um widget que escuta um Provider e transfere suas informações para o


construtor. Além disso, pode-se redesenhar a parte da árvore contida pelo consumer
alterado para otimizar a experiência e não reconstrói a visão inteira, mas apenas os
elementos que sofreram alterações. Essa é a maneira de usá-lo:

Consumer<StudentProvider>(builder: (context, provider,_) => Text(provider.name))

O construtor ( builder ) requer a entrada de três parâmetros, dois obrigatórios e um


opcional: BuildContext context , T value , Widget? child . Assim, pode usar o contexto e
o Provider<T> para construir um novo widget na árvore com as funcionalidades já
descritas.

Abaixo temos um exemplo de trecho de um código onde se usa Consumer de mais de um


provider para obter dados, normalmente indica-se a quantidade de providers depois da
palavra Consumer ( Consumer , Consumer2 , Consumer3 , …​, Consumer5 ):

@override
Widget build(BuildContext context) {

https://github.com/PowerSolution-Academy/documentation/blob/main/frontend/flutter/powersolution-academy-flutter-provider-basics.adoc 15/17
3/16/23, 10:20 PM documentation/powersolution-academy-flutter-provider-basics.adoc at main · PowerSolution-Academy/documentation

return Consumer2<StudentProvider, UniversityProvider>(


builder: (context, studentProvider, universityProvider, _) {
return Text("O aluno ${studentProvider.name} é da universidade ${universit
}

Portanto, usar o Consumer pode ser uma boa ideia quando você deseja acessar as
informações para exibir e atualizar sobre as alterações, não precisa alterar os dados do
Provider .
2. Provider.of

O Provider.of é a base do consumidor e a subscrição a um Provider que nos permite


aceder e modificar a informação.

Além disso, pode usá-lo em toda a árvore de widgets. Veja a forma de uso:

class StudentPage extends StatelessWidget {


const StudentPage({Key? key}) : super(key: key);

@override
Widget build(BuildContext context) {
final studentProvider = Provider.of<StudentProvider>(context);
final universityProvider = Provider.of<UniversityProvider>(context);
return Scaffold(
appBar: AppBar(title: const Text('Página do Estudante'),),
body: Column(
children: [
Text('${studentProvider.name} ', style: GoogleFonts.actor(fontSize: 24),),
ElevatedButton(onPressed: (){
studentProvider.name = 'João Lopes';
universityProvider.name = 'Universidade de Cabo Verde';
}, child: Text('Atualizar nomes', style: GoogleFonts.adventPro(fontSize: 2
],
),
);
}
}

Evidentemente, o Provider.of está definido (linhas 6 e 7) e então, em toda a árvore de


widgets, ele pode ser usado para ler (linha 12) ou escrever (linhas 14 e 15) os widgets —
dados entregues pelo provedor.

https://github.com/PowerSolution-Academy/documentation/blob/main/frontend/flutter/powersolution-academy-flutter-provider-basics.adoc 16/17
3/16/23, 10:20 PM documentation/powersolution-academy-flutter-provider-basics.adoc at main · PowerSolution-Academy/documentation

Referências
[1] Flutter Provider: What is it, what is it for, and how to use it? - Medium
(bancolombia-tech)

[2] What is a Future and how do I use it? - Stackoverflow (nvoigt)

[3] Spring Simple app state management - Flutter

Give feedback

https://github.com/PowerSolution-Academy/documentation/blob/main/frontend/flutter/powersolution-academy-flutter-provider-basics.adoc 17/17

Você também pode gostar