· 5 years ago · Nov 25, 2020, 07:38 PM
1// Importa bibliotecas.
2import 'package:flutter/material.dart';
3import 'package:flutter/widgets.dart';
4import 'controls/apiClient.dart';
5import 'controls/databaseClient.dart';
6import 'models/dog.dart';
7
8void main() => runApp(MyApp());
9
10class MyApp extends StatelessWidget {
11 @override
12 Widget build(BuildContext context) {
13 return MaterialApp(
14 theme: ThemeData(
15 primarySwatch: Colors.cyan,
16 ),
17 home: DogPage(),
18 );
19 }
20}
21
22class DogPage extends StatefulWidget {
23 @override
24 _DogPageState createState() => _DogPageState();
25}
26
27class _DogPageState extends State<DogPage> {
28 // Isso identificará de forma exclusiva o formulário com o qual estamos
29 // trabalhando e nos permitirá validar o formulário posteriormente
30 final GlobalKey<FormState> _formStateKey = GlobalKey<FormState>();
31
32 // Utilizado para armazenar o context e assim utilizarmos os snackbars
33 // em qualquer lugar da aplicação.
34 final _scaffoldKey = GlobalKey<ScaffoldState>();
35
36 // Armazenará a lista de cachorros lidos do banco.
37 Future<List<Dog>> dogs;
38
39 // Lista auxiliares que serão utilizadas para armazenar dados da API e do BD.
40 List<Dog> dogsFromApi;
41 List<Dog> dogsFromDb;
42
43 // Armazenarão os dados de um objeto que será manipulado no app (ao criar ou atualizar).
44 String _dogId;
45 String _dogName;
46 int _dogAge;
47
48 // Indica se a operação é de update (true) ou insert (false).
49 bool isUpdate = false;
50
51 // Armazena o id do cachorro que será atualizado em uma operação de update.
52 String dogIdForUpdate;
53
54 // Armazena objeto para manipular o banco.
55 DatabaseClient db;
56
57 // Armazena objeto para manipular a API.
58 ApiClient api;
59
60 // Controladores que vão manipular os textos dos TextEdits.
61 final _dogIdController = TextEditingController();
62 final _dogNameController = TextEditingController();
63 final _dogAgeController = TextEditingController();
64
65 // Executada ao inicializar o aplicativo.
66 @override
67 void initState() {
68 super.initState();
69 // Instancia objeto da classe DatabaseClient.
70 db = DatabaseClient();
71 // Instancia objeto da classe ApiClient.
72 api = ApiClient();
73 // Chama método para abrir o banco.
74 openDatabase();
75 }
76
77 // Método que chama a função open da classe "databaseClient"
78 openDatabase() async {
79 await db.open();
80 // Chama método para atualizar a listagem de cachorros já cadastrados no banco.
81 await refreshDogList();
82 }
83
84 // Método que atualiza a listagem de cachorros já cadastrados no banco.
85 refreshDogList() {
86 setState(() {
87 dogs = db.getDogs();
88 });
89 }
90
91 // Método responsável por comandar o envio de um objeto Dog para a API.
92 Future<Dog> uploadOnApi(dog) async {
93 // Tenta fazer a operação.
94 try {
95 Dog newDog = await api.insertDog(dog);
96
97 // Mostra snackbar de sucesso.
98 final snackBar = SnackBar(
99 content: Text('Dados enviados com sucesso para a API!'),
100 duration: Duration(milliseconds: 500),
101 backgroundColor: Colors.green,
102 );
103
104 _scaffoldKey.currentState.showSnackBar(snackBar);
105
106 // Ao receber o dado, a API retorna o objeto que foi salvo nela,
107 // incluindo o id que ela utilizou (auto incremento), assim poderemos pegar
108 // este objeto e salvar posteriormente no BD.
109 return newDog;
110 }
111 // Caso tenha erro ao realizar a operação.
112 catch (error) {
113 // Mostra snackbar de erro.
114 final snackBar = SnackBar(
115 content: Text('Erro ao enviar dado para a API!'),
116 duration: Duration(milliseconds: 500),
117 backgroundColor: Colors.red,
118 );
119 _scaffoldKey.currentState.showSnackBar(snackBar);
120
121 return null;
122 }
123 }
124
125 // Método responsável por comandar o envio de um objeto Dog para o BD.
126 uploadOnDb(dog) async {
127 // Tenta fazer a operação.
128 try {
129 await db.insertDog(dog);
130 final snackBar = SnackBar(
131 content: Text('Dados enviados com sucesso para a BD!'),
132 duration: Duration(milliseconds: 500),
133 backgroundColor: Colors.green,
134 );
135 _scaffoldKey.currentState.showSnackBar(snackBar);
136 }
137 // Caso tenha erro ao realizar a operação.
138 catch (error) {
139 // Mostra snackbar de erro.
140 final snackBar = SnackBar(
141 content: Text('Erro ao enviar dado para o BD!'),
142 duration: Duration(milliseconds: 500),
143 backgroundColor: Colors.red,
144 );
145 _scaffoldKey.currentState.showSnackBar(snackBar);
146 }
147 }
148
149 // Método responsável por comandar a remoção de um objeto Dog na API.
150 deleteOnApi(Dog dog) async {
151 // Tenta fazer a operação.
152 try {
153 await api.deleteDog(dog.id);
154 final snackBar = SnackBar(
155 content: Text('Dado removido com sucesso da API!'),
156 duration: Duration(milliseconds: 500),
157 backgroundColor: Colors.green,
158 );
159 _scaffoldKey.currentState.showSnackBar(snackBar);
160 }
161 // Caso tenha erro ao realizar a operação.
162 catch (error) {
163 // Mostra snackbar de erro.
164 final snackBar = SnackBar(
165 content: Text('Erro ao remover dado da API!'),
166 duration: Duration(milliseconds: 500),
167 backgroundColor: Colors.red,
168 );
169 _scaffoldKey.currentState.showSnackBar(snackBar);
170 }
171 }
172
173 // Método responsável por comandar a remoção de um objeto Dog no BD.
174 deleteOnDb(Dog dog) async {
175 // Tenta fazer a operação.
176 try {
177 await db.deleteDog(dog.id);
178 final snackBar = SnackBar(
179 content: Text('Dado removido com sucesso do BD!'),
180 duration: Duration(milliseconds: 500),
181 backgroundColor: Colors.green,
182 );
183 _scaffoldKey.currentState.showSnackBar(snackBar);
184 }
185 // Caso tenha erro ao realizar a operação.
186 catch (error) {
187 // Mostra snackbar de erro.
188 final snackBar = SnackBar(
189 content: Text('Erro ao remover dado do BD!'),
190 duration: Duration(milliseconds: 500),
191 backgroundColor: Colors.red,
192 );
193 _scaffoldKey.currentState.showSnackBar(snackBar);
194 }
195 }
196
197 // Método responsável por comandar a atualização de um objeto Dog na API.
198 Future<Dog> updateOnApi(Dog dog) async {
199 // Tenta fazer a operação.
200 try {
201 Dog newDog = await api.updateDog(dog);
202
203 // Mostra snackbar de sucesso.
204 final snackBar = SnackBar(
205 content: Text('Dado atualizado com sucesso da API!'),
206 duration: Duration(milliseconds: 500),
207 backgroundColor: Colors.green,
208 );
209 _scaffoldKey.currentState.showSnackBar(snackBar);
210
211 // Ao receber o dado, a API retorna o objeto que foi atualizado nela,
212 // incluindo o id que ela utilizou (auto incremento), assim poderemos pegar
213 // este objeto e atualizar posteriormente também no BD.
214 return newDog;
215 }
216 // Caso tenha erro ao realizar a operação.
217 catch (error) {
218 // Mostra snackbar de erro.
219 final snackBar = SnackBar(
220 content: Text('Erro ao atualizar dado na API!'),
221 duration: Duration(milliseconds: 500),
222 backgroundColor: Colors.red,
223 );
224 _scaffoldKey.currentState.showSnackBar(snackBar);
225
226 return null;
227 }
228 }
229
230 // Método responsável por comandar a atualização de um objeto Dog no BD.
231 updateOnDb(Dog dog) async {
232 // Tenta fazer a operação.
233 try {
234 await db.updateDog(dog);
235 final snackBar = SnackBar(
236 content: Text('Dado atualizado com sucesso do BD!'),
237 duration: Duration(milliseconds: 500),
238 backgroundColor: Colors.green,
239 );
240 _scaffoldKey.currentState.showSnackBar(snackBar);
241 }
242 // Caso tenha erro ao realizar a operação.
243 catch (error) {
244 // Mostra snackbar de erro.
245 final snackBar = SnackBar(
246 content: Text('Erro ao atualizar dado no BD!'),
247 duration: Duration(milliseconds: 500),
248 backgroundColor: Colors.red,
249 );
250 _scaffoldKey.currentState.showSnackBar(snackBar);
251 }
252 }
253
254 // Método responsável por solicitar o download de todos os dados da API e então
255 // atualizar a base de dados local.
256 downloadFromApi() async {
257 // Variável utilizada para indicar se ocorreu erros nesta operação.
258 var errorDownload = false;
259
260 // Tenta fazer a leitura dos dados da API.
261 try {
262 dogsFromApi = await api.getDogs();
263
264 // Mostra snackbar de sucesso.
265 final snackBar = SnackBar(
266 content: Text('Dados lidos com sucesso na API!'),
267 duration: Duration(milliseconds: 500),
268 backgroundColor: Colors.green,
269 );
270 _scaffoldKey.currentState.showSnackBar(snackBar);
271 }
272 // Caso dê erro (ex: url incorreta ou sem internet)
273 catch (erro) {
274 // Indica que ocorreu um erro na variável.
275 errorDownload = true;
276
277 // Mostra snackbar de erro.
278 final snackBar = SnackBar(
279 content: Text('Erro ao comunicar com a API!'),
280 duration: Duration(milliseconds: 500),
281 backgroundColor: Colors.red,
282 );
283 _scaffoldKey.currentState.showSnackBar(snackBar);
284 }
285
286 // Se a operação de busca de todos os dados da API foi feita sem erros.
287 if (errorDownload == false) {
288 // Solicita remoção de todos os dados do BD.
289 await db.deleteAllDogs();
290 // Solicita escrita de todos os dados lidos na API no BD.
291 await db.insertDogs(dogsFromApi);
292 // Atualiza lista Dogs para exibição no grid.
293 refreshDogList();
294
295 // Mostra snackbar de sucesso.
296 final snackBar = SnackBar(
297 content: Text('Dados armazenados com sucesso no BD!'),
298 duration: Duration(milliseconds: 500),
299 backgroundColor: Colors.green,
300 );
301 _scaffoldKey.currentState.showSnackBar(snackBar);
302 }
303 }
304
305 // Método que vai construir a interface do app.
306 @override
307 Widget build(BuildContext context) {
308 return Scaffold(
309 key: _scaffoldKey,
310 // Cria AppBar
311 appBar: AppBar(title: Text('App Dog'), actions: <Widget>[
312 // Adiciona botão para download dos dados da API.
313 Padding(
314 padding: EdgeInsets.only(right: 20.0),
315 child: GestureDetector(
316 onTap: () {
317 // Ao clicar chama método para obtenção de todos os dados da API.
318 downloadFromApi();
319 },
320 child: Icon(Icons.cloud_download),
321 )),
322 ]),
323
324 // Corpo do app.
325 body: Column(
326 children: <Widget>[
327 // Cria um formulário.
328 Form(
329 // Atribui a chave para identificar este formulário.
330 key: _formStateKey,
331 // Indica que a validação dos campos deve ser verificada automaticamente.
332 autovalidate: true,
333 // Adiciona Padding
334 child: Column(
335 children: <Widget>[
336 Padding(
337 padding: EdgeInsets.only(left: 10, right: 10, bottom: 10),
338 // Adiciona TextForm para inserir o Id do cachorro.
339 child: TextFormField(
340 enabled: false,
341 // Quando o formulário for enviado, salva o valor preenchido em _dogId.
342 onSaved: (value) {
343 _dogId = value;
344 },
345 // Indica quem é o controlador deste TextForm.
346 controller: _dogIdController,
347 // Indica que o teclado padrão deve ser numérico.
348 keyboardType: TextInputType.number,
349 // Personaliza o TextForm
350 decoration: InputDecoration(
351 disabledBorder: new UnderlineInputBorder(
352 borderSide: new BorderSide(
353 color: Colors.grey,
354 width: 2,
355 style: BorderStyle.solid)),
356 // Rótulo do TextForm
357 labelText: "Id do cachorro",
358 // Ícone que será exibido ao lado do TextForm
359 icon: Icon(
360 Icons.vpn_key,
361 color: Colors.grey,
362 ),
363 // Cor de fundo do TextForm
364 fillColor: Colors.white,
365 // Altera a cor da fonte do rótulo
366 labelStyle: TextStyle(
367 color: Colors.grey,
368 )),
369 ),
370 ),
371
372 // Repete passos anteriores, porém agora, para o TextForm do nome.
373 Padding(
374 padding: EdgeInsets.only(left: 10, right: 10, bottom: 10),
375 child: TextFormField(
376 validator: (value) {
377 if (value.isEmpty) {
378 return 'Por favor insira o nome do cachorro';
379 }
380 if (value.trim() == "")
381 return "Somente espaço não é válido!";
382 return null;
383 },
384 onSaved: (value) {
385 _dogName = value;
386 },
387 controller: _dogNameController,
388 decoration: InputDecoration(
389 focusedBorder: new UnderlineInputBorder(
390 borderSide: new BorderSide(
391 color: Colors.cyan,
392 width: 2,
393 style: BorderStyle.solid)),
394 labelText: "Nome do cachorro",
395 icon: Icon(
396 Icons.pets,
397 color: Colors.cyan,
398 ),
399 fillColor: Colors.white,
400 labelStyle: TextStyle(
401 color: Colors.cyan,
402 )),
403 ),
404 ),
405
406 // Repete passos anteriores, porém agora, para o TextForm da idade.
407 Padding(
408 padding: EdgeInsets.only(left: 10, right: 10, bottom: 10),
409 child: TextFormField(
410 validator: (value) {
411 if (value.isEmpty) {
412 return 'Por favor insira a idade do cachorro';
413 }
414 if (value.trim() == "")
415 return "Somente espaço não é válido!";
416 return null;
417 },
418 onSaved: (value) {
419 _dogAge = int.parse(value);
420 },
421 controller: _dogAgeController,
422 keyboardType: TextInputType.number,
423 decoration: InputDecoration(
424 focusedBorder: new UnderlineInputBorder(
425 borderSide: new BorderSide(
426 color: Colors.cyan,
427 width: 2,
428 style: BorderStyle.solid)),
429 labelText: "Idade do cachorro",
430 icon: Icon(
431 Icons.calendar_today,
432 color: Colors.cyan,
433 ),
434 fillColor: Colors.white,
435 labelStyle: TextStyle(
436 color: Colors.cyan,
437 )),
438 ),
439 ),
440 ],
441 ),
442 ),
443
444 // Após o fórmulário, organiza na horizontal os botões de ação.
445 Row(
446 // Centraliza os botões.
447 mainAxisAlignment: MainAxisAlignment.center,
448 // Adiciona botões.
449 children: <Widget>[
450 // Botão de dupla função (Atualizar ou Adicionar).
451 RaisedButton(
452 color: Colors.cyan,
453 // Se a operação for de atualização mostra "ATUALIZAR", caso
454 // contrário, mostra "ADICIONAR".
455 child: Text(
456 (isUpdate ? 'ATUALIZAR' : 'ADICIONAR'),
457 style: TextStyle(color: Colors.white),
458 ),
459 // Programa função do clique no botão
460 onPressed: () async {
461 FocusScope.of(context).requestFocus(new FocusNode());
462
463 // Se a operação for de atualização
464 if (isUpdate) {
465 // Verifica se os dados dos TextForms estão válidos.
466 if (_formStateKey.currentState.validate()) {
467 // Se sim, salva os dados nas variáveis.
468 _formStateKey.currentState.save();
469 // Tenta fazer atualização dos dados.
470 try {
471 // Cria objeto Dog com os dados dos campos de formulário.
472 Dog dog = Dog(
473 id: dogIdForUpdate, name: _dogName, age: _dogAge);
474
475 // Solicita primeiro a atualização na API, ela então retornará
476 // um objeto com os dados atualizados nela, em seguida
477 // chama método para atualizar este objeto no banco
478 await updateOnDb(await updateOnApi(dog));
479 // Solicita que a lista Dogs seja atualizada (para o grid).
480 refreshDogList();
481 // Indica que a atualização acabou (assim os botões voltam ao estado de adicionar).
482 setState(() {
483 isUpdate = false;
484 });
485 }
486 // Caso encontre algum erro nas funções acima.
487 catch (error) {
488 print(error);
489 final snackBar = SnackBar(
490 content: Text('Erro ao comunicar com a API!'),
491 duration: Duration(milliseconds: 500),
492 backgroundColor: Colors.red,
493 );
494 _scaffoldKey.currentState.showSnackBar(snackBar);
495 }
496 }
497 }
498 // Caso a operação a ser executada seja a de inserção de um novo dado.
499 else {
500 // Verifica se os dados dos TextForms estão válidos.
501 if (_formStateKey.currentState.validate()) {
502 // Se sim, salva os dados nas variáveis.
503 _formStateKey.currentState.save();
504 // Tenta fazer inserção dos dados.
505 try {
506 // Cria objeto Dog com os dados dos campos de formulário.
507 Dog dog = Dog(id: _dogId, name: _dogName, age: _dogAge);
508 // Solicita primeiro a inserção na API, ela então retornará
509 // um objeto com os dados inseridos nela, em seguida
510 // chama método para inserir este objeto no banco.
511 // Isso é necessário visto que o id é auto_increment na API,
512 // desta maneira o objeto retornado pela API já tem o id correto.
513 await uploadOnDb(await uploadOnApi(dog));
514
515 // Solicita que a lista Dogs seja atualizada (para o grid).
516 refreshDogList();
517 }
518 // Caso encontre algum erro nas funções acima.
519 catch (error) {
520 final snackBar = SnackBar(
521 content: Text('Erro ao comunicar com a API!'),
522 duration: Duration(milliseconds: 500),
523 backgroundColor: Colors.red,
524 );
525 _scaffoldKey.currentState.showSnackBar(snackBar);
526 }
527 }
528 }
529 // Após a atualização ou inserção, limpa os textos dos TextForms
530 _dogIdController.text = '';
531 _dogNameController.text = '';
532 _dogAgeController.text = '';
533 },
534 ),
535 Padding(
536 padding: EdgeInsets.all(10),
537 ),
538 // Botão com dupla função: cancelar a atualização ou limpar os campos
539 RaisedButton(
540 color: Colors.red,
541 // Se a operação for de atualização mostra "CANCELAR ATUALIZAÇÃO",
542 // caso contrário, mostra "CANCELAR".
543 child: Text(
544 (isUpdate ? 'CANCELAR ATUALIZAÇÃO' : 'LIMPAR'),
545 style: TextStyle(color: Colors.white),
546 ),
547 // Programa o que ocorrerá quando o botão for clicado
548 onPressed: () {
549 // Em ambas as situações limpa os dados dos TextForms
550 _dogIdController.text = '';
551 _dogNameController.text = '';
552 _dogAgeController.text = '';
553
554 setState(() {
555 // Indica que não está ocorrendo mais uma atualização
556 isUpdate = false;
557 // Limpa a variável que armazenaria o id a ser atualizado
558 dogIdForUpdate = null;
559 });
560 },
561 ),
562 ],
563 ),
564 const Divider(
565 height: 20.0,
566 ),
567 // Componente que exibirá a listagem com os cachorros cadastrados.
568 Expanded(
569 // Para saber +: https://tinyurl.com/y44rl6ce
570 child: FutureBuilder(
571 future: dogs,
572 builder: (context, snapshot) {
573 print(snapshot.connectionState);
574 // Caso não exista dados a serem exibidos, mostra uma mensagem
575 if (snapshot.data == null || snapshot.data.length == 0) {
576 return Text('Sem dados para exibir');
577 }
578 // Se o banco tiver retornado dados para exibir, ou seja, dados
579 // cadastrados, chama método para gerar a lista de cachorros e
580 // e posteriormente exibir tal lista na tela.
581 else if (snapshot.hasData) {
582 return generateList(snapshot.data);
583 }
584 // Retorna um widget circular que será apresentado enquanto a
585 // operação de recuperação de dados estiver sendo processada.
586 return CircularProgressIndicator();
587 },
588 ),
589 ),
590 ],
591 ),
592 );
593 }
594
595 // Método que retorna um widget com a lista de cachorros cadastrados no banco.
596 SingleChildScrollView generateList(List<Dog> dogs) {
597 return SingleChildScrollView(
598 scrollDirection: Axis.vertical,
599 child: SingleChildScrollView(
600 scrollDirection: Axis.horizontal,
601 child: DataTable(
602 columns: [
603 DataColumn(
604 label: Text('ID', style: TextStyle(fontStyle: FontStyle.italic)),
605 ),
606 DataColumn(
607 label:
608 Text('NOME', style: TextStyle(fontStyle: FontStyle.italic)),
609 ),
610 DataColumn(
611 label:
612 Text('IDADE', style: TextStyle(fontStyle: FontStyle.italic)),
613 ),
614 DataColumn(
615 label: Text('DELETAR',
616 style: TextStyle(fontStyle: FontStyle.italic)),
617 )
618 ],
619 // Monta linhas
620 rows: dogs
621 .map(
622 (dog) => DataRow(
623 cells: [
624 // Célula que exibirá o id do cachorro.
625 DataCell(
626 Text(dog.id.toString()),
627 // Quando o usuário clicar nesta célula ativa a operação
628 // de atualização.
629 onTap: () {
630 setState(() {
631 // Indica que a operação de atualização está ativada
632 isUpdate = true;
633 // Salva o id do objeto apresentado na linha selecionada
634 dogIdForUpdate = dog.id;
635 });
636 // Preenche, com os dados do cachorro selecionado, os TextForms
637 _dogIdController.text = dog.id.toString();
638 _dogNameController.text = dog.name;
639 _dogAgeController.text = dog.age.toString();
640 },
641 ),
642 // Célula que exibirá o nome do cachorro.
643 DataCell(
644 Text(dog.name),
645 onTap: () {
646 setState(() {
647 isUpdate = true;
648 dogIdForUpdate = dog.id;
649 });
650 _dogIdController.text = dog.id.toString();
651 _dogNameController.text = dog.name;
652 _dogAgeController.text = dog.age.toString();
653 },
654 ),
655 // Célula que exibirá a idade do cachorro.
656 DataCell(
657 Text(dog.age.toString()),
658 onTap: () {
659 setState(() {
660 isUpdate = true;
661 dogIdForUpdate = dog.id;
662 });
663 _dogIdController.text = dog.id.toString();
664 _dogNameController.text = dog.name;
665 _dogAgeController.text = dog.age.toString();
666 },
667 ),
668 // Célula que exibirá o botão para deletar uma linha do banco
669 DataCell(
670 IconButton(
671 icon: Icon(Icons.delete),
672 // Coordena a remoção de um dado do grid, API e BD.
673 onPressed: () {
674 // Tenta fazer deleção dos dados.
675 try {
676 // Solicita que o objeto seja removido da API.
677 deleteOnApi(dog);
678 // Solicita que o objeto seja removido do BD.
679 deleteOnDb(dog);
680 // Solicita que a lista Dogs seja atualizada (para o grid).
681 refreshDogList();
682 }
683 // Caso encontre algum erro nas funções acima.
684 catch (error) {
685 final snackBar = SnackBar(
686 content: Text('Erro ao comunicar com a API!'),
687 duration: Duration(milliseconds: 500),
688 backgroundColor: Colors.red,
689 );
690 _scaffoldKey.currentState.showSnackBar(snackBar);
691 }
692 },
693 ),
694 ),
695 ],
696 ),
697 )
698 .toList(),
699 ),
700 ),
701 );
702 }
703}
704