Mobile

19 ago, 2020

Flutter e GetX — Criando uma aplicação para compartilhamento de recursos (parte-1)

Publicidade
Image for post
Telas do aplicativo

Cenário

Com o início da pandemia no final de março de 2020, escolas e redes se viram obrigadas a adotarem plataformas e aplicativos para auxiliarem os alunos a continuarem seus estudos.

No Paraná, assim como em vários estados, o Google ClassRoom foi escolhido para subsidiar as interações entre docentes e discentes.

As CRTEs (Coordenações Regionais de Tecnologias Educacionais) dos núcleos de educação do estado, mesmo com pessoal reduzido e na maioria das vezes sem reconhecimento, ajudaram professores e alunos durante toda a implantação e caminhada, que ainda continua até a data dessa publicação.

Image for post

No entanto, sem tempo para uma formação adequada, vídeos tutoriais foram os recursos mais rápidos para a solução de problemas como por exemplo, permissões de acessos ou a criação de uma atividade dentro da plataforma.

O problema no entanto é a quantidade de vídeos com o tema, muitas vezes desatualizados. Logo, a curadoria demandaria um tempo precioso (todo tempo é precioso !!!).

Como eu mantinha uma planilha com os recursos, categorizados por Alunos, Professores, Problemas entre outros, resolvi desenvolver um aplicativo que apresentava esses recursos e permitia o filtro e a busca de forma mais simples e rápida.

Nesse tutorial explico como construí a aplicação utilizando um package que é um verdadeiro canivete suíço. O GetX que entre outras coisas facilita o gerenciamento de rotas e injeção de dependências.

Instalação das dependências e configurações iniciais

Após a criação no terminal do projeto com o comando flutter create recursos_classroom crie no diretório raiz a pasta assets como ponto de entrada para outras pastas que contém arquivos de imagens e fonts.

Image for post

Após isso, abra o arquivo pubspec.yaml. É nele que definimos os pacotes que utilizaremos no projeto, além da localização do arquivos estáticos.

Logo abaixo de dependencies inclua:

dependencies:
  get: ^3.4.1
  modal_bottom_sheet: ^0.1.5
  flutter_svg: ^0.18.0
  flutter_staggered_grid_view: ^0.3.0
  http: ^0.12.2
  url_launcher: ^5.5.0
  share: ^0.6.4
  google_fonts: ^1.1.0
  flappy_search_bar: ^1.7.2

A primeira dependência ,o GetX, é um pacote que nas palavras da documentação “combina um gerenciador de estado de alta performance, injeção de dependência inteligente e gerenciamento de rotas de uma forma rápida e prática”. Na verdade ele é tudo isso e mais. Contém vários recursos legais que facilitam a separação da lógica da aplicação com a UI.

Os demais são componentes utilitários em interface ou função, explicados durante o desenvolvimento.

Para os arquivos estáticos, na seção assets deixe assim, tomando cuidado com a indentação do parágrafo:

  assets:
    - assets/icons/
    - assets/images/

E finalmente, para utilizar uma fonte personalizada, na seção fonts deixei assim:

 fonts:
    - family: Nunito
      fonts:
        - asset: assets/fonts/Nunito/Nunito-Regular.ttf
        - asset: assets/fonts/Nunito/Nunito-SemiBold.ttf
          weight: 600
        - asset: assets/fonts/Nunito/Nunito-Bold.ttf
          weight: 700

Após salvar, caso você esteja utilizando o VSCode com o plugin do Dart, o comando flutter get pub é disparado para baixar e instalar as dependências.

A próxima configuração também é necessária já que vamos solicitar conexão para acessar uma API.

Na pasta android >> app >> src, encontre o arquivo AndroidManifest.xml. Ele está nas pasta debug, main e profile.

Adicione a tag:

<uses-permission android:name=”android.permission.INTERNET”/>

Pronto, assim damos a permissão para acessar a API.

Vamos abrir o arquivo main.dart que se encontra na pasta lib e apagar o conteúdo exemplo que o Flutter criar ao construir o projeto.

Vamos criar o ponto de entrada da aplicação com o seguinte código:

import 'package:flutter/material.dart';
import 'package:get/get.dart';

import 'modules/categoria/categoria_bind.dart';
import 'modules/categoria/categoria_page.dart';
import 'modules/home/home_bind.dart';
import 'modules/home/home_page.dart';
import 'modules/home/splash_page.dart';

void main() => runApp(GetMaterialApp(
      debugShowCheckedModeBanner: false,
      initialRoute: '/',
      defaultTransition: Transition.native,
      locale: Locale('pt', 'BR'),
      getPages: [
        GetPage(
          name: "/",
          page: () => SplashPage(),
        ),
        GetPage(
          name: '/home',
          page: () => HomePage(),
          binding: HomeBind(),
        ),
        GetPage(
            name: '/categoria/:tipo',
            page: () => CategoriaPage(),
            binding: CategoriaBind())
      ],
    ));

Os dois primeiros imports são do core do Flutter e do Get, já os outros são de componentes que vamos criar.

Ao digitar exatamente esse código, vários erros vão surgir. Não se preocupe, alguns arquivos ainda não foram escritos.

No método principal, vamos definir alguns atributos iniciais para o widget GetMaterialApp como a remoção daquele banner chato em debugShowCheckedModeBanner, a definição da rota inicial com initialroute, o tipo de transação padrão em defaultTransition e um array de rotas em getPages.

Instanciamos o primeiro GetPage setando o mesmo name da rota inicial ‘/’ e associando um page que será a tela de entrada. No próximo GetPage o name ‘/home’, uma classe que representa a classe UI logo depois da tela inicial, HomePage() e um binding que é uma funcionalidade do GetX .

Binding permite desacoplar a injeção de dependência, ligando as rotas ao gerenciador de estados e o gerenciador de dependências.

Com isso atribuímos ao Get o controle do gerenciamento de memória, assim quando uma rota não é mais usada, todos os controllers, variáveis e instâncias de objetos relacionados com ela são removidos da memória.

Fazemos o mesmo para categoria, a diferença está na sintaxe do nome da rota ‘/categoria/:tipo’. Com os dois pontos estamos definindo um parâmetro tipo que será passado ao navegar até a rota categoria.

Estruturação, Modelo e Repositório

Vamos estruturar a aplicação criando as seguintes pastas abaixo da lib

Image for post
Estrutura de pastas da aplicação
  • app: Constantes e widgtes comuns a toda a aplicação;
  • models: As classes que representam os modelos do negócio. Nesse aplicativo os atributos do recursos realizados na consulta da API;
  • modules: Pastas e arquivos das interfaces UI e controladores;
  • repositories: Classe com acesso a API.

Em models, crie o arquivo recurso.dart. A classe Recurso mapeia as informações do objeto retornado da API.

class Recurso {
  String publico;
  String titulo;
  String descricao;
  String duracao;
  String autoria;
  String categoria;
  String link;
  String tipo;
  
  Recurso({
    this.publico,
    this.titulo,
    this.descricao,
    this.duracao,
    this.autoria,
    this.categoria,
    this.link,
    this.tipo,
  });
  // Métodos omitidos

Como a resposta é no formato Json, precisamos de um método para atribuir os campos. A implementação está logo abaixo.

  factory Recurso.fromJson(Map<String, dynamic> map) {
    if (map == null) return null;
  
    return Recurso(
      publico: map['publico'],
      titulo: map['titulo'],
      descricao: map['descricao'],
      duracao: (map['duracao']),
      autoria: map['autoria'],
      categoria: map['categoria'],
      link: map['link'],
      tipo: map['tipo'],
    );
  }

A dica é escrever os mesmos nomes dos atributos da classe com a da API, quando possível. Observe o resultado da chamada à API.

{
"recursos": [

{
"publico": "professor",
"titulo": "Como arquivar turmas duplicadas no Google Classroom pelo COMPUTADOR",
"descricao": "Como arquivar turmas duplicadas no Google Classroom para organizar seu ambiente virtual",
"duracao": "1899-12-30T03:10:01.000Z",
"autoria": "Nirceu Vidal dos Santos",
"categoria": "ClassRoom Atividades",
"link": "https://youtu.be/FZkbpxh5TcQ",
"tipo": "video"
},
{

A implementação da classe completa está aqui.

Também codifiquei a classe Categoria (categoria.dart) que auxiliará na organização das informações.

class Categoria {
  final String nome;
  final int qtd;
  final String image;

  Categoria(this.nome, this.qtd, this.image);
}

A implementação dela é mais simples, pois não é necessária nenhuma conversão.

O próximo passo é criar a classe responsável pelo acesso a API. Na pasta repositories criei o arquivo recurso_api.dart. Segue o código:

import 'dart:convert';

import 'package:course_app/models/recurso.dart';
import 'package:http/http.dart' as http;

class RecursoAPI {
  String url =
      "https://script.google.com/macros/s/AKfycbxGaLN-OmqmY_OebpM1nyuYPNdSAodlNeUjmB0EgEkJEAm22Zg/exec";

  Future<List<Recurso>> getRecursos() async {
    try {
      var response = await http.get(url);
      if (response.statusCode == 200) {
        var data = json.decode(response.body);
        // print(data);
        return List<Recurso>.from(
            data["recursos"].map((x) => Recurso.fromJson(x)));
      } else {
        return List<Recurso>();
      }
    } catch (error) {
      print(error);
      return List<Recurso>();
    }
  }
}

O primeiro atributo é a url da API. Mais informações aqui. O método getRecursos retorna uma lista de objetos de forma assíncrona. Utilizamos o http passando no get o endereço.

Verificamos se ocorreu tudo ok através do código 200 (statusCode) e retornamos a lista, convertendo cada objeto json em um objeto dart em data[“recursos”].map((x) => Recurso.fromJson(x)).

Se o código for diferente ou em caso de um erro retornamos um lista vazia com return List<Recurso>()

Módulos

A aplicação consiste basicamente de 3 telas. Uma tela splash cuja única finalidade é depois de um tempo, redirecionar para a home da aplicação que irá carregar um grid com as quantidades por categoria.

A outra tela é uma lista de recursos da categoria selecionada.

Assim, para simplificar, vamos criar na pasta modules duas pastas que representam nossas funções maiores, home e categoria. Nelas organizaremos todos os artefatos separando os papéis da lógica de negócio e da UI.

Image for post
Módulos

Atenção, para evitar colocar a classe inteira, nas próximas seções colocarei o trecho e comentários a respeito de fragmentos, mas sempre referenciando o código na íntegra.

Em home, vamos criar a página inicial. Dê o nome de splash_page.dart

Abaixo a implementação inicial:

import 'dart:async';

import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:get/get.dart';
import 'package:google_fonts/google_fonts.dart';

class SplashPage extends StatelessWidget {
  SplashPage() {
    startTimeout();
  }

  startTimeout() async {
    return Timer(Duration(seconds: 6), changeScreen);
  }

  changeScreen() async {
    Get.toNamed('/home');
  }

Observe que logo após nomear a classe com SplashPage, herdamos de StatelessWidget já que não precisamos guardar o estado.

No método construtor, aquele que tem a mesma assinatura da classe, invocamos o método startTimeout que retorna após 6 segundos, redireciona para a rota ‘/home’ em return Timer(Duration(seconds: 6), changeScreen).

Note que para navegar até a próxima tela (rota), no método changeScreen, passado no argumento da função anterior, basta chamar Get.toNamed informando o nome da rota que escrevemos no main.dart.

Logo após, temos o método que desenha a tela.

@override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.white,
      body: _buildBody(),
    );
  }

Particularmente, prefiro usar métodos para ajudar na manutenção do código.

Abaixo a tela splash com o métodos responsáveis pera renderização de cada parte.

Image for post

Tela com os métodos em destaques de cada área

A implementação completa da classe SplashPage está aqui.

Vamos ficando por aqui. Na parte 2 iremos escrever os controles e as classes de interface visual da aplicação.

Se você leu até aqui, obrigado 🙂