Flutter

27 mai, 2020

Covid19 API com Flutter, Mobx e Modular (parte 2)

Publicidade

Vamos finalizar a aplicação codificando o controlador e a UI.

(Se você não leu a parte 1, está aqui.)

Antes, vamos instalar um complemento no VSC para ganhar produtividade. Trata-se do flutter_mobx.

Além de snnipets para criar classes, ele possui uma ferramenta que executa o comando: flutter pub run build_runner para geração de classes do Mobx.

Separando a lógica

Com a extensão acima instalada, podemos digitar mobx e enter para que seja criado uma classe MobxX Store.

Codificaremos assim:

import 'package:flutter/material.dart';
import 'package:flutter_covid19/models/mundo_model.dart';
import 'package:flutter_covid19/models/pais_model.dart';
import 'package:flutter_covid19/respositories/covidapi_repository.dart';
import 'package:flutter_modular/flutter_modular.dart';
import 'package:mobx/mobx.dart';
part 'home_controller.g.dart';

class HomeController = _HomeControllerBase with _$HomeController;

abstract class _HomeControllerBase with Store {
  final api = Modular.get<CovidApiRepository>();

  _HomeControllerBase() {
    getInfoMundo();
    getInfoPais();
  }

  @observable
  int currentIndex = 0;
  
  @observable
  TextEditingController textFieldController = TextEditingController();

  @observable
  Mundo mundo;

  @observable
  Pais pais;

  @action
  getInfoMundo() async {
    mundo = await api.getMundo();
  }

  @action
  getInfoPais({String nomePais = 'brazil'}) async {
    
   try{ pais =  await api.getPais(pais: nomePais);}
   catch(exception){
     return Container();
   }
  }


  @action
  changePage(int index) {
    currentIndex = index;
  }
}

Comentando o código

No construtor vamos invocar dois métodos que serão implementados em seguida, getInfoMundo() e getInfoPais()

  _HomeControllerBase() {
    getInfoMundo();
    getInfoPais();
  }

Declaramos quatro objetos. Os que representam os nossos modelos (parte1) mundo e o país, um para controlar o visão na aba (currentIndex) além de um textFieldController para realizarmos a consulta.

Todos estão com a anotação @observable indicando que queremos ser notificados do seu estado.

A IDE apresentará alguns erros até esse momento, pois ainda não estamos fazendo uso das bibliotecas instaladas para geração automática. Para corrigir isso, vamos usar a extensão mais uma vez para executar o comando.

Encontre e clique no rodapé do editor o texto build_runner watch e aguarde um momento. Será executado um script para geração de classes.

Note que será criada a classe home_controller.g.dart.

Não edite essa classe. Toda vez que você alterar o Store essa classe será gerada/atualizada automaticamente.

A implementação do getInfoMundo e getInfoPais são chamadas aos métodos que implementamos na parte 1.

Para os métodos usamos outro tipo de anotação, @action, que basicamente modifica os observables. Assim, os observadores são notificados somente após a conclusão atômica da ação.

A diferença está na passagem de argumentos do getPais que definimos um valor padrão.

  @action
  getInfoPais({String nomePais = 'brazil'}) async {
    try {
      pais = await api.getPais(pais: nomePais);
    } catch (exception) {
      return Container();
    }
  }

Finalmente codificamos um método para controlar a view da barra de navegação.

@action
  changePage(int index) {
    currentIndex = index;
  }

HOME UI

Ainda na pasta pages criaremos o arquivo home_page.dart

Temos inicialmente a assinatura da classe HomePage estendendo um StatefulWidget

class HomePage extends StatefulWidget {
  @override
  _HomePageState createState() => _HomePageState();
}

Em seguida, criamos o estado com a seguinte assinatura:

class _HomePageState extends ModularState<HomePage, HomeController>

Utilizando o modular podemos usar o ModularState, indicando qual é o controlador.

  @override
  Widget build(BuildContext context) {
    return Observer(builder: (_) {
      return Scaffold(
        backgroundColor: Colors.white,
        body: (this.controller.mundo != null && this.controller.pais != null)
            ? _buildBody()
            : Center(child: CircularProgressIndicator()),
        bottomNavigationBar: _buildBottomBar(),
      );
    });
  }

No build temos o primeiro widget Observer (pacote flutter_mobx). Toda vez que esses observáveis ​​mudarem, o Observer reconstrói e renderiza.

Para acessar as propriedades do controlador (HomeController) invocamos o controller.

No body verificamos se os objetos mundo e país estão nulos para decidir a renderização do corpo (_buidBody) ou um widget de espera (CircularProgressIndicator).

Widget _buildBody() {
    return (this.controller.currentIndex == 0)
        ? _buildBodyMundo()
        : _buildBodyPais();
  }

Controlamos o índice para alternar entre o Mundo e o País. O índice é passado no método _buildBottomBar() em currentIndex.

  Widget _buildBottomBar() {
    return BottomNavigationBar(
      selectedItemColor: Colors.grey.shade800,
      unselectedItemColor: Colors.grey,
      currentIndex: this.controller.currentIndex,
      onTap: this.controller.changePage,
      items: [
        BottomNavigationBarItem(
          icon: Icon(Icons.group_work),
          title: Text("Mundo"),
        ),
        BottomNavigationBarItem(
          icon: Icon(Icons.flag),
          title: Text("Pais"),
        ),
      ],
    );
  }

Ao longo da classe criamos outros métodos para padronizar a visualização das informações (ver _buildRow, _buildHeader e _buildTile). A prática mais recomendada seria separar em outros widgets para reutilização em outras partes do projeto, se fosse o caso.

No método _buildBodyPais acessamos os valores dos atributos passando no parâmetro dos métodos acima.

Exemplos: controller.pais.nome, controller.pais.urlBandeira …

Widget _buildBodyPais() {
    return SingleChildScrollView(
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: <Widget>[
          _buildHeader(controller.pais.nome,
              isPais: true, url: controller.pais.urlBandeira),
          const SizedBox(height: 20.0),
          _buildRow(Colors.brown, Icons.person_outline, "Total de Infectados",
              controller.pais.casos.toString()),
          const SizedBox(height: 16.0),
          _buildRow(Colors.red, Icons.person_pin, "Mortes",
              controller.pais.mortes.toString()),
          const SizedBox(height: 16.0),
          _buildRow(Colors.green, Icons.person_add, "Recuperados",
              controller.pais.recuperados.toString()),
        ],
      ),
    );
  }

Finalmente, temos um método que desenha um alert para inserir o nome do país:

_displayDialog(BuildContext context) async {
    return showDialog(
        context: context,
        builder: (context) {
          return AlertDialog(
            title: Text('Situação de outro País'),
            content: TextField(
              controller: controller.textFieldController,
              decoration: InputDecoration(hintText: "INFORME O PAÍS:"),
            ),
            actions: <Widget>[
              RaisedButton(
                onPressed: () {
                  this.controller.getInfoPais(
                      nomePais:
                          this.controller.textFieldController.text.toString());
                  Modular.to.pop();
                },
                child: Text(
                  "OK",
                  style: TextStyle(color: Colors.white),
                ),
                color: const Color(0xFF1BC0C5),
              ),
              new FlatButton(
                child: new Text('CANCELAR'),
                onPressed: () {
                  Modular.to.pop();
                },
              )
            ],
          );
        });
  }

Vinculamos um TextField ao controlador e no onPressed no botão passamos o texto no método getInfoPais.

Bem provável que deixei passar algumas informações da implementação, mas se tiver alguma dúvida, deixe nos comentários.

Se você leu até aqui e gostou, dê um clap!

Código completo do projeto.