Livro 4 - Aplicativo Previsão do Tempo

Site: IFSC
Curso: 2025-2 - PROGRAMAÇÃO PARA DISPOSITIVOS MÓVEIS
Livro: Livro 4 - Aplicativo Previsão do Tempo
Impresso por:
Data: Friday, 19 Dec 2025, 15:11

1. Introdução

No Livro 3, exploraremos componentes avançados para o desenvolvimento de aplicativos com o Android Jetpack. Vamos criar um aplicativo de previsão do tempo utilizando os dados gratuitos disponíveis na API OpenWeatherMap. Durante este processo, aprenderemos sobre novos recursos do Android, como fragmentos, listas e o padrão ViewHolder. Além disso, abordaremos a manipulação de dados JSON e o download de imagens com a biblioteca Glide.

O aplicativo faz uso dos serviços web REST fornecidos gratuitamente pela OpenWeatherMap para obter dados de previsão do tempo de 5 dias para uma cidade específica. Esses dados climáticos são retornados no formato JSON (Javascript Object Notation). A lista de informações climáticas é apresentada em uma componente ListView, que é responsável por exibir uma lista de itens.

No contexto deste aplicativo, implementaremos um layout personalizado para cada item na lista, permitindo que cada um deles apresente:

  • Um ícone de condição climática
  • O dia da semana com uma descrição textual do clima
  • A temperatura mais alta e mais baixa (em ºC)
  • A umidade (em %)

Para mais informações API gratuita do OpenWeatherMap acesse o site https://openweathermap.org/ 

2. Recursos Envolvidos

Neste capítulo, exploraremos os conceitos essenciais necessários para o desenvolvimento do nosso projeto. Vamos aprender sobre Web Services e o processamento de JSON. Além disso, aprenderemos como usar listas no ambiente Android, um elemento fundamental para a construção de aplicativos para criar e exibir listas de informações em nosso aplicativo. Também abordaremos alguns componentes gráficos inovadores.

2.1. WebServices OpenWeatherMap

O que são Web Services?

Web Services são um conjunto de tecnologias e padrões que permitem que sistemas diferentes se comuniquem e compartilhem dados pela internet. Eles são uma forma de interação entre aplicativos ou sistemas de software, permitindo que eles se comuniquem e troquem informações de maneira padronizada, independente da linguagem de programação ou plataforma em que foram desenvolvidos.

Existem vários tipos de web services, mas dois dos mais comuns são:

  1. SOAP (Simple Object Access Protocol): Um protocolo baseado em XML que define uma estrutura para mensagens e como elas devem ser processadas. Os web services SOAP são geralmente mais rígidos em termos de estrutura de mensagem e exigem bibliotecas específicas para implementar e consumir.

  2. REST (Representational State Transfer): Um estilo arquitetônico que utiliza os métodos HTTP (como GET, POST, PUT e DELETE) para operações CRUD (Create, Read, Update, Delete). Os web services RESTful são frequentemente baseados em URLs e recursos, sendo mais leves e flexíveis que os serviços SOAP.

Web Services são amplamente utilizados para integração de sistemas, permitindo que diferentes aplicativos se comuniquem e compartilhem dados de maneira eficaz. Eles são fundamentais para a construção de sistemas distribuídos e para a criação de ecossistemas de aplicativos que podem funcionar em conjunto na web. Além disso, são uma parte essencial da arquitetura de muitas aplicações modernas, incluindo sistemas de comércio eletrônico, aplicativos móveis, serviços em nuvem e muito mais.

Os web services podem ser usados para buscar informações de um banco de dados, compartilhar dados entre aplicativos, acessar serviços de terceiros (por exemplo, serviços de pagamento, serviços de mapas, previsão do tempo), autenticar usuários e realizar muitas outras tarefas. Eles são uma parte essencial da infraestrutura que permite a comunicação entre sistemas distribuídos na era digital.

As operações disponíveis e os parâmetros que podem ser usados são documentados nas APIs (Interfaces de Programação de Aplicativos) de cada Web Service. Algumas APIs populares são Google Maps, Facebook, Twitter, Youtube, OpenWeatherMap e LinkedIn.

O uso de um Web Service geralmente requer uma chave de API. Essa chave é usada para identificar o cliente, que pode ser um aplicativo ou usuário, e também para confirmar a permissão de uso e rastrear a utilização do serviço. Chaves gratuitas geralmente têm limitações, como um limite de 50 acessos por segundo, o que as torna adequadas para aplicativos de pequeno porte, como o WeatherApp. No entanto, se o seu aplicativo for usado comercialmente, é provável que o número de acessos aumente, exigindo o pagamento por uma chave para continuar usando o serviço.

OpenWeatherMap

OpenWeatherMap é um serviço de dados meteorológicos e previsão do tempo amplamente utilizado, que fornece informações meteorológicas em tempo real e previsões para locais em todo o mundo. É uma plataforma de dados abertos que permite a desenvolvedores de software, empresas e indivíduos acessar dados meteorológicos por meio de uma API (Application Programming Interface) para uso em aplicativos, sites e outros serviços.

As principais características e funcionalidades do OpenWeatherMap incluem:

  1. Dados Meteorológicos em Tempo Real: O OpenWeatherMap oferece acesso a uma ampla gama de informações meteorológicas em tempo real, incluindo temperatura, umidade, velocidade do vento, pressão atmosférica, condições de precipitação e muito mais.

  2. Previsões do Tempo: Além de dados atuais, o serviço fornece previsões meteorológicas de curto e longo prazo para ajudar os usuários a planejar suas atividades com antecedência.

  3. Localizações Globais: O OpenWeatherMap oferece dados para locais em todo o mundo, tornando-o uma fonte valiosa de informações meteorológicas globais.

  4. Suporte a Diferentes Formatos de Dados: Os dados meteorológicos podem ser acessados em vários formatos, incluindo JSON, XML e outros, para se adequar às necessidades dos desenvolvedores.

  5. API de Acesso: O OpenWeatherMap fornece uma API que permite aos desenvolvedores integrar facilmente informações meteorológicas em seus aplicativos e sites. A API é acessível mediante registro e gera uma chave de API que os desenvolvedores usam para fazer solicitações de dados meteorológicos.

  6. Planos de Assinatura: O OpenWeatherMap oferece vários planos de assinatura, incluindo planos gratuitos e pagos, com diferentes limites de acesso e recursos, para atender às necessidades de diferentes tipos de usuários.

Muitos aplicativos e serviços populares, como aplicativos de previsão do tempo, sites de notícias, aplicativos de viagem e plataformas de IoT (Internet das Coisas), usam o OpenWeatherMap para fornecer informações meteorológicas aos usuários. É uma fonte confiável e acessível de dados meteorológicos que ajuda as pessoas a se manterem informadas sobre as condições meteorológicas em tempo real e a planejar suas atividades com base nas previsões.

Acesso a API

Os serviços da API OpenWeatherMap utilizados neste projeto fazem uso de uma chave de API gratuita, que está disponível neste documento. Caso você deseje criar uma conta pessoal e utilizar sua própria chave, você pode se registrar no site oficial da API aqui

Neste aplicativo, utilizaremos a API "Call 5 day/3 hour forecast data" para obter os dados da previsão do tempo de uma cidade específica. Para isso, precisamos utilizar a URL: "api.openweathermap.org/data/2.5/forecast?q=" seguida pelo nome da cidade e campos opcionais, incluindo o número de dias, unidade de medida e idioma. Para obter mais detalhes sobre a API, você pode acessar o link aqui.

Teste Rápido

api.openweathermap.org/data/2.5/forecast?q=Jaraguá do Sul&appId=3b8b9363d5ddb0ad63fa396b70ed67be&units=metric&lang=pt_br

Chave acesso temporária: 3b8b9363d5ddb0ad63fa396b70ed67be

    2.2. JSON

    O que é JSON?

    JSON (JavaScript Object Notation) é um formato de representação de dados que é amplamente utilizado para armazenar e trocar informações estruturadas entre sistemas. Ele é baseado em um subconjunto da notação de objeto JavaScript e é independente de linguagem, o que significa que pode ser utilizado em uma variedade de linguagens de programação.

    A estrutura do JSON é simples e legível por humanos, o que o torna ideal para troca de dados entre sistemas e para configuração de arquivos. As informações no JSON são organizadas em pares de chave-valor, onde cada chave é uma string que identifica um campo específico e o valor associado pode ser um número, uma string, um booleano, um objeto JSON, uma matriz (array) ou até mesmo o valor nulo. Alguns dos conceitos-chave do JSON incluem:

    • Objeto JSON: É delimitado por chaves ({}) e contém pares de chave-valor. As chaves são strings que identificam os campos e os valores podem ser de qualquer tipo JSON válido.

    • Array: É uma coleção ordenada de valores que é delimitada por colchetes ([]). Os valores de um array podem ser de qualquer tipo JSON, incluindo objetos, arrays, números, strings, booleanos, etc.

    • Valor: Pode ser um número, uma string (delimitada por aspas duplas), um booleano (true ou false), um objeto JSON, um array, ou nulo.

    • Chave: Uma string que identifica um campo ou propriedade em um objeto JSON.

    O JSON é amplamente utilizado em aplicativos da web para transmitir dados entre um servidor e um cliente, sendo uma alternativa popular ao formato XML. APIs da web, como as oferecidas pelo OpenWeatherMap, frequentemente retornam dados no formato JSON para facilitar o processamento desses dados pelos aplicativos.

    Exemplo de um objeto JSON simples:


    JSON desempenha um papel fundamental na troca de informações entre aplicativos, sistemas e serviços na era da web. Ele é fácil de entender e de utilizar, tornando-o uma escolha popular para representação de dados estruturados.

    Objeto JSON da API OpenWeatherMap

    Existem diferentes maneiras de obter informações do tempo de serviços da web, como o OpenWeatherMap. Alguns desses serviços oferecem respostas nos formatos JSON ou XML. Neste projeto, escolhemos o formato JSON porque é mais simples e eficiente.

    Dentro do objeto JSON retornado pela API, encontramos um elemento fundamental chamado "list". Este elemento contém uma lista de previsões que são atualizadas a cada 3 horas. Você pode obter previsões para até 5 dias consecutivos a partir dessa lista.

    As partes mais importantes dessa informação são:

    • "dt": Este é um número representando o carimbo de data (timestamp). Ele representa a data e hora em que a previsão foi feita, medido em segundos desde o dia 1º de janeiro de 1970, no fuso horário GMT. Usaremos essa informação para identificar o dia correspondente a previsão do tempo.

    • "main": Este é outro conjunto de informações dentro do JSON. Ele contém as temperaturas mínima ("temp_min") e máxima ("temp_max"). Também podemos verificar a umidade ("humidity").

    • "weather": Dentro deste campo, encontramos a "description," que nos fornece uma descrição da condição do tempo. Além disso, temos a propriedade "icon," que nos ajuda a identificar a condição climática por meio de um ícone.

    Como podem ver, o serviço da OpenWeatherMap fornece informações detalhadas em um formato organizado, facilitando a exibição de previsões climáticas em nosso aplicativo.

    2.3. Pacotes e classes

    Neste projeto, três partes fundamentais do processo envolvem a comunicação com o web service REST da OpenWeatherMap e a execução de operações em segundo plano para manter a responsividade do aplicativo. Vamos abordar esses conceitos em detalhes.

    Pacote org.json

    Dentro do pacote org.json, você encontrará as seguintes classes que serão usadas para processar os dados JSON recebidos pelo aplicativo:

    • JSONObject: Esta classe contém métodos para trabalhar com objetos JSON. Um dos construtores da classe pode converter uma String formatada em JSON em um objeto JSONObject. O JSONObject contém um mapeamento das propriedades para seus respectivos valores. As propriedades podem ter valores dos tipos int, long, double, boolean, String, JSONObject e JSONArray. Você pode acessar as propriedades usando os métodos get e set desta classe.
    • JSONArray: Essa classe é usada para manipular arrays de objetos JSON. Ela fornece métodos para acessar os elementos dentro do array. No contexto deste projeto, a propriedade "list" na resposta da API OpenWeatherMap será tratada como um JSONArray, permitindo o acesso aos elementos dessa lista.

    Essas classes são essenciais para analisar e manipular os dados JSON que serão obtidos do serviço da OpenWeatherMap.

    Classe HttpUrlConnection e comunicação com um web service REST

    Para ativar o web service, você precisará fazer uma solicitação HTTP à API da OpenWeatherMap. Isso é feito em Java com a classe HttpURLConnection, que está presente no pacotejava.net . Você começará com uma String representando a URL do serviço e, em seguida, criará um objeto. URL para abrir uma nova conexãoHttpURLConnection . Essa conexão será responsável por fazer a solicitação HTTP ao serviço da OpenWeatherMap e receber a resposta, que normalmente será um objeto JSON. A leitura dos dados de resposta será feita usando uma classe InputStream , que receberá o conteúdo linha por linha ou string por string do objeto JSON. Depois de obter os dados, você converterá os dados lidos em um objeto JSONObject para processamento posterior.

    AsyncTask

    A comunicação com um serviço externo, como a leitura de arquivos, bancos de dados ou web services, pode ser uma operação demorada e, se realizada na thread principal do aplicativo, pode bloquear a interface do usuário e causar o aparecimento da mensagem "Activity Not Responding (ANR)". Portanto, essas operações devem ser executadas em segundo plano para manter a responsividade do aplicativo.
    No Android, uma maneira comum de realizar tarefas em segundo plano é usando a classe AsyncTask, que está no pacote android.os. O AsyncTask executa operações de longa duração em uma thread separada e , ao mesmo tempo, fornece a capacidade de transmitir os resultados de volta para a thread principal, onde a interface do usuário está sendo executada.

    No contexto deste aplicativo, você usará duas subclasses de AsyncTask:

    1. Uma para ativar o web service e fazer as solicitações HTTP para obter os dados da previsão do tempo.
    2. Outra para baixar uma imagem que representa a condição climática.

    Isso ajudará a manter o aplicativo responsivo e evitar problemas de bloqueio durante a comunicação com o web service e o download de recursos, como imagens.

    2.4. Manipulação de Listas

    No desenvolvimento do aplicativo vamos aprender a usar o componente RecyclerView, uma versão melhorada do componente clássico ListViewEsse componente trabalha em conjunto com um adaptador para exibir uma lista de itens rolável no aplicativo.

    Trabalhar com listas no Android é conceitualmente diferente de um aplicativo desktop devido ao tamanho limitado na tela e a capacidade de processamento do dispositivo. Não vamos renderizar mil itens se eles não podem ser mostrados na tela. Por isso adotamos um padrão de design chamado ViewHolder.

    É importante compreender como o RecyclerView, RecyclerViewAdapter e o padrão ViewHolder funcionam no contexto do aplicativo. Aqui estão alguns detalhes explicando esses conceitos.

    O RecyclerView é um componente de interface do usuário no Android que exibe uma lista rolante de itens, como a lista de contatos no seu aplicativo. É altamente eficiente, pois recicla automaticamente as visualizações fora da tela, o que economiza memória e melhora o desempenho. Cada item na lista é chamado de "item de RecyclerView," e eles são exibidos em um layout de lista.

    O RecyclerViewAdapter é uma classe que atua como uma ponte entre os dados (por exemplo, uma lista de contatos) e o RecyclerView. Ele ajuda a preencher os dados nos itens individuais da RecyclerView e gerencia a criação e reciclagem de visualizações, tornando o processo eficiente. Você deve criar uma subclasse de RecyclerViewAdapter personalizada para o seu aplicativo. Essa subclasse conterá lógica para vincular os dados aos itens da lista e criar novos itens conforme necessário.

    O padrão ViewHolder é uma técnica usada para melhorar o desempenho ao criar e exibir itens em um RecyclerView. Em vez de criar uma nova visualização para cada item na lista, você cria um objeto ViewHolder que contém referências às visualizações individuais que precisam ser atualizadas. Isso reduz a sobrecarga de criar novas visualizações repetidamente. Quando um item é rolado para fora da tela e reciclado, o ViewHolder permite que você reutilize a visualização existente, apenas atualizando os dados exibidos.

    O processo geral é o seguinte:

    1. Você cria uma subclasse do RecyclerViewAdapter e, dentro dela, define a classe ViewHolder.

    2. No método onCreateViewHolder do adaptador, você infla o layout da visualização de item e cria uma instância do ViewHolder. Neste ponto, você pode configurar os manipuladores de visualizações e quaisquer outros elementos da interface do usuário dentro do ViewHolder.

    3. No método onBindViewHolder, você vincula os dados do item atual aos elementos da visualização dentro do ViewHolder.

    4. O método getItemCount retorna o número total de itens na lista.

    Quando os dados são atualizados ou a lista é rolada, o RecyclerViewAdapter chama os métodos onCreateViewHolder e onBindViewHolder conforme necessário, permitindo que você atualize os itens da lista de forma eficiente.

    Em resumo, o uso do RecyclerView, RecyclerViewAdapter e padrão ViewHolder é fundamental para criar uma lista de rolagem eficiente e responsiva em seu aplicativo. Certifique-se de implementar esses conceitos de acordo com as necessidades específicas do seu aplicativo e, se tiver mais perguntas ou precisar de esclarecimentos adicionais, sinta-se à vontade para perguntar.

    2.5. Novos componentes gráficos

    No desenvolvimento do aplicativo vamos introduzir os seguintes novos componentes gráficos.

    TextInputLayout

    O TextInputLayout é um componente importante para melhorar a experiência do usuário ao lidar com caixas de texto (EditText) em um aplicativo Android. Ele faz parte da Android Design Support Library e tem várias funcionalidades úteis, incluindo o tratamento de dicas, animações e interações de forma elegante. Aqui estão algumas das características do TextInputLayout:

    1. Dicas animadas: O TextInputLayout permite que você defina dicas para as caixas de texto (EditText) que são exibidas de maneira especial. Quando o elemento EditText recebe foco, a dica é animada e move-se para a parte superior da caixa de texto, ficando visível ao usuário. Isso ajuda o usuário a entender claramente o propósito da caixa de texto e o que deve ser inserido.

    2. Visibilidade contínua: Ao contrário das dicas tradicionais que desaparecem quando o usuário começa a digitar, as dicas no TextInputLayout permanecem visíveis mesmo após o usuário começar a digitar. Isso é útil para que o usuário possa sempre se referir à dica, se necessário.

    3. Espaço para erro: Além das dicas, o TextInputLayout fornece espaço para exibir mensagens de erro. Isso é útil para fornecer feedback imediato ao usuário quando ocorrem erros de validação ou entrada incorreta.

    4. Personalização: Você pode personalizar o TextInputLayout para se adequar ao design do seu aplicativo, como alterar cores, fontes e tamanhos.

    Aqui está um exemplo de como usar o TextInputLayout em seu layout XML:


    Neste exemplo, você envolve a caixa de texto (EditText) com o TextInputLayout. A propriedade hint no TextInputLayout define a dica que será exibida animadamente.

    Para exibir uma mensagem de erro, você pode usar o método setError no TextInputLayout.


    TextInputLayout é uma adição valiosa ao desenvolvimento de aplicativos Android, pois ajuda a tornar as caixas de texto mais informativas e interativas, melhorando a usabilidade e a experiência geral do usuário.

    FloatActionButton

    Os botões de ação flutuantes (Floating Action Buttons ou FABs) são elementos de interface de usuário que desempenham um papel importante no Material Design, um conjunto de diretrizes de design do Google para criar aplicativos Android atraentes e coesos. Aqui estão alguns pontos importantes sobre os FABs:

    1. Elevação e Flutuação: Os FABs têm uma elevação maior em comparação com outros elementos da interface do usuário, criando a sensação de flutuação. Isso ajuda a destacar a ação principal que o  representa.

    2. Ações Importantes: Os FABs são projetados para representar uma ação única e importante no contexto do aplicativo. Eles geralmente executam a ação primária ou mais frequente que um usuário pode realizar em uma tela.

    3. Uso de Ícones: Os FABs geralmente contêm ícones em vez de texto. Isso economiza espaço na interface do usuário e torna a ação mais reconhecível visualmente.

    4. Posicionamento: As diretrizes do Material Design recomendam que os FABs sejam posicionados a pelo menos 16dp das margens do dispositivo em uma tela de telefone e a pelo menos 24dp nas margens em uma tela de tablet. Isso ajuda a garantir que os FABs sejam facilmente acessíveis e não se sobreponham a outros elementos da interface do usuário.

    5. Tamanho e Cores: Os FABs devem ser de um tamanho suficiente para serem facilmente tocados em dispositivos móveis. Além disso, as cores e animações dos FABs devem seguir as diretrizes de cores e animações do Material Design para criar uma experiência de usuário consistente.

    6. Android Design Support Library: Os FABs foram introduzidos no Android 6.0 (API de nível 23) e estão disponíveis por meio da Android Design Support Library. Isso permite que os desenvolvedores usem FABs em versões mais antigas do Android, garantindo uma experiência consistente em diferentes dispositivos.

    7. Ações Contextuais: Dependendo do contexto, os FABs podem ter ações diferentes. Por exemplo, em um aplicativo de e-mail, um FAB pode ser usado para compor um novo e-mail, enquanto em um aplicativo de câmera, o mesmo FAB pode ser usado para tirar uma foto.

    No contexto do aplicativo WeatherApp, o uso de um FAB para acionar a busca das condições climáticas de uma cidade é apropriado, pois representa uma ação principal do aplicativo. Os FABs são uma parte importante do Material Design e ajudam os desenvolvedores a criar interfaces de usuário atraentes e intuitivas em aplicativos Android.

    SnackBar

    O Snackbar é uma componente importante no Material Design que pode ser usado para fornecer mensagens informativas e interativas aos usuários do seu aplicativo Android. Aqui estão algumas características essenciais do Snackbar:

    1. Semelhante a um Toast: O Snackbar é conceitualmente semelhante a um Toast, pois exibe mensagens temporárias na tela. No entanto, ele segue as diretrizes do Material Design e oferece recursos adicionais em comparação com um Toast padrão.

    2. Interativo: Uma das vantagens do Snackbar é a sua interatividade. Os usuários podem interagir com ele de várias maneiras:

      • Deslizar para descartar: Os usuários podem descartar o Snackbar rapidamente deslizando-o para fora da tela.
      • Toque para ação: Além da mensagem informativa, o Snackbar pode incluir uma ação. Quando os usuários tocam no Snackbar, a ação é acionada. Isso é útil para permitir que os usuários tomem medidas com base na mensagem exibida.
    3. Tempo limitado: O Snackbar é exibido na tela por um tempo limitado e, após esse período, desaparece automaticamente. Normalmente, ele não requer intervenção do usuário para ser fechado.

    4. Uso para mensagens informativas: O Snackbar é frequentemente usado para exibir mensagens curtas e informativas, como confirmações de ação, erros ou feedback. Por exemplo, você pode usar um Snackbar para confirmar que uma ação foi executada com sucesso ou para informar ao usuário que ocorreu um erro.

    Aqui está um exemplo de como usar um Snackbar em seu aplicativo:


    Neste exemplo:

    • view: É a visualização associada ao contexto onde o Snackbar será exibido. Pode ser a raiz da atividade ou qualquer outra visualização apropriada.
    • "Mensagem informativa": É a mensagem que você deseja exibir no Snackbar.
    • Snackbar.LENGTH_SHORT: Define o tempo de exibição do Snackbar. Você pode usar Snackbar.LENGTH_LONG se preferir um tempo mais longo.

    A ação é definida usando o método setAction, que permite especificar o texto da ação e o código a ser executado quando a ação é acionada.

    Em resumo, o Snackbar é uma maneira eficaz e interativa de fornecer feedback e informações aos usuários do seu aplicativo, seguindo as diretrizes de design do Material Design. Ele é útil para melhorar a experiência do usuário e tornar o aplicativo mais informativo e envolvente.

    3. Criando o projeto

    Para começar o Projeto 3 vamos criar o projeto base de maneira similar ao Projeto 1. Siga os mesmos passos do Capitulo 3 do Livro 1 mas coloque as seguintes configurações:

    • Escolha a template do projeto como Basic Views Activity.
    • Defina o nome do projeto para WeatherApp
    • Especifique o campo Package name de acordo com as regras apresentadas no Livro 1
    • Modifique o local de criação do projeto se achar necessário (Save Location)
    • Na opção Language certifique-se de estar selecionado a linguagem Kotlin
    • O mesmo com campo Mininum SDK. Certifique-se que está selecionada a API 23 na caixa de seleção.
    • Clique em Finish e aguarde a criação do novo projeto.


    4. Configurações Iniciais

    A template Basic Activity inicializa o projeto com os seguintes arquivos:

    • Classes MainActivity, First Fragment e Second Fragment
    • Arquivos de layout: o activity_main.xml, content_main.xml, fragment_first.xml e fragment_second.xml
    • Um arquivo menu_main.xml que define as opções do menu da MainActivity
    • Um grafo de navegação

    O projeto WeatherApp não faz o uso de várias telas portanto não usaremos todos estes recursos gerados pela template básica. 


    Durante as configurações iniciais iremos remover alguns dos elementos, mas primeiramente vamos adicionar todas as configurações necessárias ao projeto.

    (VERIFICAR) Para etapa inicial de configuração do projeto altere o arquivo colors.xml para mudar a tonalidade da cor de realce. Lembre-se que essa cor é usada nos FloatActionButton da aplicação, assim para gerar um melhor contraste com o ícone que iremos adicionar, vamos definir essa cor como um azul mais claro (#448AFF).

    4.1. Recurso strings.xml

    Para este aplicativo, usaremos recursos de string para a interface do usuário, como é comum, mas também adicionaremos alguns campos para serem usados na lógica de programação. Em particular, registraremos a URL da API como um recurso de string. Dessa forma, se houver uma alteração no endereço da API no futuro, será necessário apenas atualizar o valor do recurso de string, sem a necessidade de modificar o código em si.

    Abra o arquivo strings.xml e adicione os valores abaixo:

    Nome do recurso

    Valor

    api_key

    Escolha sua chave pessoal ou use: 7c0cffe2e25bc681909553be76d060c0

    web_service_url

    https://api.openweathermap.org/data/2.5/forecast?q=

    invalid_url

    A URL digitada é inválida.

    weather_condition_image

    Uma representação gráfica das condições climáticas.

    high_temp

    Máxima: %s

    low_temp

    Mínima: %s

    day_description

    %1$s: %2$s

    humidity

    Umidade: %s

    hint_text

    Digite uma cidade (ex: Jaraguá do Sul, SC)

    read_error

    Não foi possível ler os dados da previsão do tempo.

    connect_error

    Não foi possível se conectar a OpenWeatherMap.org



    4.2. Manifesto Android

    O aplicativo WeatherApp requer permissão de acesso à Internet para se comunicar com o web service. A partir da versão Android 6.0, a permissão de Internet é automaticamente concedida ao aplicativo, pois o acesso à Internet é considerado essencial na maioria dos aplicativos atuais. De acordo com as diretrizes do Google, permissões como a de Internet, entre outras, não representam um 'grande risco para a privacidade ou segurança do usuário' e são classificadas na categoria 'PROTECTION_NORMAL'. Portanto, elas são concedidas automaticamente durante a instalação do aplicativo. No entanto, mesmo que não seja necessário verificar a permissão no aplicativo, é importante solicitar essa permissão no arquivo AndroidManifest.xml.

    Para fazer isso, abra o arquivo AndroidManifest.xml e adicione a seguinte linha '<uses-permission ...':

    <?xml version="1.0" encoding="utf-8"?>
    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

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

    <application
    android:allowBackup="true"
    android:dataExtractionRules="@xml/data_extraction_rules"
    android:fullBackupContent="@xml/backup_rules"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:roundIcon="@mipmap/ic_launcher_round"
    android:supportsRtl="true"
    android:theme="@style/Theme.WeatherApp"
    tools:targetApi="31">
    <activity
    android:name=".MainActivity"
    android:exported="true"
    android:label="@string/app_name"
    android:theme="@style/Theme.WeatherApp">
    <intent-filter>
    <action android:name="android.intent.action.MAIN" />

    <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
    </activity>
    </application>

    </manifest>

    4.3. Adicionando Ícones

    O Material Design opera nativamente no Android Studio e oferece diferentes tipos de recursos para serem incluídos nas aplicações Android. Um desses recursos é o uso de ícones. O Material Design Icons é um kit com diversos tipos de ícones e está presente dentro do Android Studio pronto para ser utilizado. A vantagem de se usar esses ícones é que eles são imagens vetoriais, isto é, não perdem a qualidade ao escalar o tamanho na tela.

    Para adicionar um novo ícone ao projeto no Android Studio, siga os seguintes passos:

    • Selecione File > New > Vector Asset para abrir a ferramenta Vector Asset Studio




    • Clique no botão ao lado do campo Clip Art e na próxima janela procure o ícone com nome “done”, clique em OK



    • Altera o nome do ícone para “baseline_done_24dp” e modifique o campo Color para branco (#FFFFFF).  Clique em Next Finish a seguir



    Lembre-se que, imagens vetoriais não são exatamente figuras, mas sim definições de formas. Assim, é possível modificar suas propriedades como cor dos elementos e plano de fundo. Neste projeto, vamos usar esses ícone para o FABs (Float Action Button) por isso colocamos a cor branca para dar um melhor contraste na cor de fundo.

    Como resultado final o ícone será adicionado na pasta drawable dos recursos do projeto:




    5. Desenho da Interface Gráfica (UI)

    Para construir a interface gráfica do aplicativo vamos precisar apenas de um fragmento portanto vamos mexer apenas no arquivo fragment_first.xml.

    A tela principal do dispositivo vai apresentar uma caixa de texto, um botão flutuante e uma lista de itens contendo os dados da previsão climática. No caso da lista de itens, vamos criar um novo arquivo de layout chamado list_item.xml que ficará encarregado do design dos dados exibidos para cada item.

    5.1. Layout da Atividade Principal

    Primeiramente vamos configurar o arquivo activity_main.xml. 

    • Selecione o elemento CoordinatorLayout e adicione a propriedade id: coordinatorLayout. Vamos usar isso para especificar em qual componente a Snackbar será exibida.
    • Remova o componente FloatingActionButton 

    O resultado parcial não deve mostrar muita coisa pois ainda precisamos desenhar o fragmento para refletir nas mudanças da atividade principal. Continue com os próximos passos para finalizar o desenho da UI.

    5.2. Layout de Conteúdo

    O arquivo content_main.xml é um dos componentes fundamentais criados pela template 'Basic Activity'. Esse arquivo é referenciado na atividade principal e serve como uma "tela em branco" para criar a UI da atividade principal. Quando usado em conjunto com os fragmentos, o content_main.xml atua como um recipiente em branco que facilita a transição entre os fragmentos, que representam as diferentes telas do aplicativo, conforme definido no grafo de navegação.

    Para criar esse recipiente, usamos o componente NavHostFragment, que faz parte do pacote androidx.navigation.fragment. O NavHostFragment desempenha um papel essencial na navegação do aplicativo, pois gerencia a troca de fragmentos, permitindo que o usuário navegue entre as diferentes telas de forma suave e intuitiva.

    Nosso trabalho está pronto e não será necessário realizar nenhuma alteração neste arquivo pois o Android Studio já fez a configuração correta entre o arquivo de navegação (nav_graph) e o componente NavHostFragment.


    5.3. Fragmento Principal

    Neste aplicativo vamos utilizar apenas uma fragmento pois só é necessário mostrar uma tela contendo uma caixa de texto, uma lista de itens e um fab. Portanto, vamos escolher o arquivo fragment_first, que já está configurado como a tela inicial do aplicativo para ser o nosso arquivo de desenho da interface gráfica (UI).

    Abra o arquivo fragment_first.xml e realiza o passos a seguir.

    • Remova o ConstraintLayout criado automaticamento no inicio do projeto
    • Abra o arquivo em modo código e renomei NestedScrollView para FrameLayout
    • Adicione um novo componente FloatingActionButton e:
      • Modifique sua propriedade gravity de “bottom|end” para “top”end”. 
      • Modifique também a propriedade srcCompat com o botão “...” ao lado e selecione o ícone adicionado anteriormente ao projeto (baseline_done_24dp).
      • A fim de mover o botão a direita do elemento EditText, configure a propriedade layout_margin_top com o valor 60dp.
    • Adicione um novo LinearLayout. Configure a a propriedade orientation  para Vertical
    • Insira um componente TextInputLayout dentro do LinearLayout. Esse novo componente de texto está localizado na paleta na aba Text na última posição. Configure a propriedades do novo componente como:
      • layout:width -> match_parent
      • layout:height -> wrap_content 
    • Selecione o TextInputEditText dentro do TextInputLayout e modique as suas propriedades:
      • id: locationEditText
      • singleLine: true
      • hint: @string/hint_text
    • Por fim, adicione um elemento RecyclerView (Aba Commom) também dentro do LinearLayout e configure suas propriedades:
      • id: weatherRecyclerView
      • layout_width: 0dp
      • layout_weight: 1

    Como resultado final aparecerá o seguinte layout:




    5.4. Layout dos Itens da Listas

    Vamos definir um layout personalizado para exibir dados climáticos em uma item de RecyclerView. Esse layout será inflado pelo adaptador WeatherListAdapter a fim de criar a interface do usuário para novos itens de RecyclerView.


    Primeiramente, vamos adicionar um novo recurso de layout ao projeto. Para tanto:

    • Clique com menu File -> New - > Layout resource file

    • Na janela New Resource File digite o nome list_item. Certifique-se que no campo root elemento está especificado o ConstraintLayout e clique em OK.


    Abra o arquivo criado e adicione os elementos com as orientações abaixo.

    Componente ImagemView

    A primeira seção do item da lista de previsão climática é uma figura representando o ícone da condição climática. Adicione um componente ImageView, escolha a imagem padrão "avatars" para usar apenas como referência na criação do desenho da UI. O ícone da previsão será baixado durante o uso do app portanto não precisamos configurar uma imagem inicial para o item.

    Configure a imagem com as seguintes propriedades:

    • id: conditionImageView
    • layout:width e height: 56dp – defina um novo recurso de dimensão image_side_lenght
    • contentDescription: use o recurso strings weather_condition_image
    • scaleType: fitCenter – o ícone se encaixará dentro dos limites do elemento ImageView e será centralizado horizontalmente e verticalmente

    Agora configure o posicionamento do elemento para ficar no topo e a esquerda da tela. Use as restrições do constraint layout e margem de 8dp. Como resultado a imagem ficará como:


    Note que estamos desenhando apenas um item da lista e não uma tela inteira, então a pré-visualização da UI pode ser redimensionada para melhor apresentação dos elementos, o que é o caso da figura acima.

    Componente TextView das condições climáticas

    A segunda parte do item é dividida em uma TextView mostrando as informações da condição climático e três TextViews menores ilustrando a temperatura e humidade. Vamos configurar o elemento maior primeiro.

    Adicione uma TextView e configure as propriedades:

    • id: dayTextView
    • textAppearance @style/TextAppearance.AppCompat.Large

    Configure o posicionamento do elemento para ficar no topo, a esquerda da ImageView e a direita da margem da tela. Use as restrições do constraint layout e margem de 8dp. Modifique também a propriedade layout:width para 0dp (match constraint) . Como resultado a imagem ficará como:


    Componente TextView das temperaturas e umidade

    Agora para finalizar adicione três elementos TextView com os seguintes ids lowTextView, highTextView e humidityTextView.

    Organize os elementos da seguinte forma:

    • humidityTextView: alinhado ao elemento TextView no topo e a margem a direita da tela. Espaçamento 8dp.
    • highTextView: alinhado ao elemento TextView no topo e ao elemento humidityTextView a direita. Espaçamento 8dp.
    • lowTextView: alinhado ao elemento TextView no topo e ao elemento highTextView a direita. Espaçamento 8dp.

    Ajustes finais

    Certifique-se que as propriedades do ConstraintLayout estão corretas para exibir itens de uma lista.

    • layout_width: match_parente
    • layout_height: wrap_content

    Como resultado final você deve obter a imagem abaixo:


    6. Lógica da Aplicação

    Neste projeto, utilizaremos as seguintes classes:

    • Weather: Esta classe representa os dados climáticos de um determinado dia. O fragmento principal será responsável por converter os dados climáticos de um formato JSON para um objeto Weather.
    • WeatherAdapter: Adaptador personalizado para controlar os dados apresentados na RecyclerView.
    • MainActivity: A atividade principal do aplicativo vai coordenar o funcionamento do aplicativo,
    • FirstFragment: Neste fragamento estará concentrada a lógica da aplicação necessária para interagir com o serviço web da previsão climática OpenWeatherMap, bem como o processamento da resposta em formato JSON.

    Cada uma dessas classes desempenha um papel específico e essencial no funcionamento geral do aplicativo, contribuindo para uma organização clara e eficiente do código. Os próximos capítulos tratarão de cada classe individualmente.

    6.1. Classe Weather

    A classe Weather encapsula as variáveis que representam os diversos elementos relacionados às condições climáticas de um determinado dia. Para realizar tarefas essenciais, como a conversão do carimbo de data/hora para o nome do dia correspondente e a formatação de números em strings apropriadas, a classe faz uso das funcionalidades fornecidas pelos pacotes java.text e java.util.

    Para incorporar essa funcionalidade em seu projeto, crie um novo arquivo Kotlin no diretório principal do projeto, nomeando-o Weather

    Construtor

    No Kotlin, a declaração do construtor de uma classe pode ser realizada de maneira direta no corpo da própria declaração da classe. Dessa forma, é possível criar um construtor de maneira simples, como exemplificado abaixo:

    class Weather(var dayOfWeek: String,
    var minTemp: String,
    var maxTemp: String,
    var humidity: String,
    var description: String,
    var iconURL: String) {}

    Este trecho de código fornece um construtor padrão que aceita os valores da previsão e os atribui às variáveis correspondentes. Vale notar que esse processo é notavelmente simplificado no Kotlin, eliminando a necessidade de configurar manualmente as variáveis ou declará-las individualmente. Além disso, o acesso às variáveis pode ser feito sem a necessidade de implementar métodos getters/setters. Isso significa que podemos realizar operações de forma direta, como exemplificado abaixo:

    var previsao: Weather = Weather(...)
    previsao.description = "Dia Ensolaro"
    var descricao = previsao.description

    Dessa maneira, o Kotlin oferece uma sintaxe concisa e eficiente para lidar com a inicialização e manipulação de objetos, contribuindo para um código mais legível e de fácil manutenção.

    Padrão de projeto Factory

    Neste projeto, é necessário realizar algumas conversões durante a criação dos dados de previsão do tempo. Por exemplo, o carimbo de data deve ser convertido em um dia da semana antes de ser armazenado na variável dayOfWeek, e os valores de temperatura também precisam ser formatados para incluir o símbolo "ºC" no final. Para alcançar esses objetivos, optaremos por seguir um padrão de projeto conhecido como Factory.

    O padrão de projeto Factory (ou Fábrica) é um padrão de criação que fornece uma interface para criar instâncias de uma classe, permitindo que a classe delegue o processo de instanciação para suas subclasses. O objetivo principal é abstrair o processo de criação de objetos, tornando-o mais flexível e adaptável às mudanças.

    Adicione o seguinte código a classe Weather.

    // Factory method - para criar um novo objeto weather
    companion object{
    fun create(timeStamp: Long, minTemp: Double, maxTemp: Double,
    humidity: Double, description: String, iconName: String): Weather {
    val numberFormat: NumberFormat = NumberFormat.getInstance()
    NumberFormat.getInstance().setMaximumFractionDigits(0)
    var dayOfWeek = convertTimeStampToDay(timeStamp)
    var minTemp = numberFormat.format(minTemp) + " \u00b0C"
    var maxTemp = numberFormat.format(maxTemp) + " \u00b0C"
    var humidity = NumberFormat.getPercentInstance().format(humidity / 100.0)
    var description = description
    var iconURL = "http://openweathermap.org/img/w/$iconName.png"
    return Weather(dayOfWeek,minTemp,maxTemp,humidity,description,iconURL)
    }
    private fun convertTimeStampToDay(timeStamp: Long): String {
    // Cria um objeto Calendar com os dados atuais
    val calendar = Calendar.getInstance()
    // Configura a hora
    calendar.timeInMillis = timeStamp * 1000
    // obtem o fuso horário do dispositivo
    val tz: TimeZone = TimeZone.getDefault()
    // ajusta a hora com o fuso horário obtido
    calendar.add(
    Calendar.MILLISECOND, tz.getOffset(
    calendar.timeInMillis
    )
    )
    // SimpleDataFormat retorna o nome do dia
    val dataFormatter = SimpleDateFormat("EEEE")
    return dataFormatter.format(calendar.time)
    }
    }

    O código apresenta uma aplicação do padrão de projeto Factory Method aplicado na criação de objetos do tipo Weather.  O método create desempenha o papel fundamental, sendo encarregado da instanciação de objetos da classe Weather. Nesse contexto, ele recebe parâmetros que encapsulam as informações essenciais para a criação de uma nova instância de Weather.

    No código utilizamos o objeto NumberFormat para formatar os valores das temperaturas em ºC sem vírgulas e a umidade em porcentagem (%). O caractere especial º é representado pelo código UTF-8 \u00b0. A variável iconURL faz uso de uma template padrão para criar a string correspondente. Essa string pré-definida corresponde ao endereço web no qual a OpenWeatherMap disponibiliza os ícones utilizados para representar diversas condições climáticas, como nuvens com chuva, dia ensolarado, entre outras. O método convertTimeStampToDay é usado para descobrir o nome do dia semana referente ao carimbo de data e será comentado logo a seguir.

    Método convertTimeStampToDay

    O método privado convertTimeStampToDay converte um carimbo de data em um nome de dia da semana. Esse método auxiliar desempenha a função de interpretar um valor longo, que representa o número de segundos decorridos desde 1º de janeiro de 1970, GMT (a convenção padrão para representação de tempo em sistemas Linux e, por conseguinte, Android).

    Para realizar essa conversão, utilizamos um objeto Calendar. Configuramos o calendário com a localização atual do dispositivo, utilizando o método getInstance, para obter datas no idioma correspondente. Além disso, definimos o fuso horário por meio da variável tz. Informamos ao calendário a data desejada por meio do método add e, posteriormente, empregamos a classe SimpleDateFormat para extrair apenas o nome do dia da semana, indicado pelo parâmetro “EEEE”.


    6.2. Classe WeatherListAdapter

    A classe WeatherListAdapter utiliza herança da classe Adapter de RecyclerView e é utilizada para controlar uma lista de dados de previsão climática para ser apresentada no componente RecyclerView do fragmento principal do aplicativo. Cada item na RecyclerView utiliza um layout personalizado, conforme definido no arquivo list_item.xml, para apresentar de forma estilizada as informações meteorológicas na tela.

    Dessa forma, é necessário correlacionar os valores de cada objeto Weather aos elementos visuais da interface gráfica, que incluem a representação gráfica do ícone, o texto referente ao dia, as temperaturas mínima e máxima, e a umidade. Esse mapeamento é implementado através da abordagem ViewHolder que será explicada neste capítulo.

    Padrão ViewHolder

    O padrão ViewHolder é um padrão de projeto utilizado para otimizar o desempenho de exibições em listas, como as utilizadas em RecyclerViews em aplicativos Android. Ele é particularmente valioso quando se lida com grandes conjuntos de dados, como em listas extensas.

    Em Kotlin, esse padrão é frequentemente aplicado em conjunto com o RecyclerView. A ideia principal é manter uma referência direta aos elementos visuais de um item da lista para evitar a necessidade de chamadas repetitivas de findViewById, o que pode ser caro em termos de desempenho.

    No contexto do Kotlin e do Android, o padrão ViewHolder envolve a criação de uma classe interna (ou aninhada) chamada ViewHolder dentro do adaptador da lista (geralmente uma subclasse de RecyclerView.Adapter). Essa classe ViewHolder contém referências aos elementos visuais individuais de um item na lista, como TextViews, ImageViews, etc.

    Para implementar um adaptador utilizando o padrão ViewHolder é necessário:

    • Criar uma classe ViewHolder para manter referências aos elementos visuais (textView e imageView) dentro de cada item da lista.
    • Criar um onCreateViewHolder responsável por inflar a visão do item e criar uma instância da ViewHolder.
    • Criar um onBindViewHolder onde os dados são associados aos elementos visuais do item da lista.
    • Criar um método getItemCount fornece o número total de itens na lista.

    Essa abordagem reduz a necessidade de chamadas findViewById repetitivas, melhorando significativamente o desempenho, especialmente em listas grandes. Vamos implementar cada um dos passos a seguir.

    Configuração Inicial

    Crie uma nova classe WeatherListAdapter e faça a extensão da classe RecyclerView.Adapter:

    package br.edu.ifsc.fulano.weatherapp

    import androidx.recyclerview.widget.RecyclerView.Adapter

    class WeatherListAdapter() : Adapter<>() { }

    Observe que o compilador emite uma reclamação no momento em que estendemos o Adapter. Isso ocorre porque ele requer que passemos uma classe que estenda um RecyclerView.ViewHolder por meio de generics, ou seja, o ViewHolder que precisamos criar para o Adapter de um RecyclerView.

    Portanto, procederemos à criação da classe ViewHolder, que irá herdar da classe RecyclerView.ViewHolderDado que a classe ViewHolder é necessária exclusivamente para o nosso WeatherListAdapter, é bastante pertinente transformá-la em uma classe aninhada (Nested class):

    package br.edu.ifsc.fulano.weatherapp

    import androidx.recyclerview.widget.RecyclerView
    import androidx.recyclerview.widget.RecyclerView.Adapter

    class WeatherListAdapter() : Adapter<>() {

    class ViewHolder : RecyclerView.ViewHolder() {

    }
    }

    Agora as duas classes forem criadas mas ainda há erros de compilação. Para terminar a configuração inicial do adaptador primeiro devemos fazer com que o RecyclerView.Adapter receba o ViewHolder via generics:

    package br.edu.ifsc.fulano.weatherapp

    import androidx.recyclerview.widget.RecyclerView
    import androidx.recyclerview.widget.RecyclerView.Adapter

    class WeatherListAdapter() : Adapter<WeatherListAdapter.ViewHolder>() {

    class ViewHolder : RecyclerView.ViewHolder() {

    }
    }

    Nesse contexto, tanto o adaptador quanto o ViewHolder estão utilizando suas heranças corretamente, no entanto, ainda ocorrem erros devido à implementação estar incompleta, tanto do adaptador quanto do ViewHolder. Concluiremos a configuração de cada um deles nas seções subsequentes.

    Classe ViewHolder

    Essencialmente, a classe ViewHolder é uma estrutura que mantém referências aos elementos visuais contidos em cada item da lista, permitindo um acesso eficiente durante a rolagem da lista. Dentro desta classe, você normalmente incluiria propriedades que representam os elementos visuais específicos do item, como TextView, ImageView, etc. 

    Dessa forma, configuramos o ViewHolder para receber uma View no seu construtor. Lembre que no kotlin o código é simplificado e fazemos tudo em apenas uma linha.

    class ViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) {
    }

    O construtor da classe recebe um parâmetro itemView do tipo View. Este parâmetro representa a visão individual de um item em uma lista, geralmente inflado a partir de um layout XML. No caso deste projeto, itemView será a instanciação de cada elemento na lista gerando com o layout list_item criado anteriormente.

    Dessa forma, vamos completar a classe para guardar as referência de cada elemento da UI em uma variável adequada:

    class ViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) {
    var conditionImageView = itemView.findViewById(R.id.conditionImageView) as ImageView
    var dayTextView: TextView = itemView.findViewById(R.id.dayTextView) as TextView
    var lowTextView: TextView = itemView.findViewById(R.id.lowTextView) as TextView
    var highTextView: TextView = itemView.findViewById(R.id.highTextView) as TextView
    var humidityTextView: TextView = itemView.findViewById(R.id.humidityTextView) as TextView
    }

    Propriedade WeatherList

    Neste projeto vamos utilizar uma MutableList<Weather> para armazenar uma lista de dados da previsão do tempo. Esses dados da previsão climática serão gerenciados e manipulados pelo adaptador para exibir cada item na interface do usuário. 

    A lista de dados será passada como argumento para o construtor da classe e armazenada em uma propriedade chamada WeatherList. Adicionalmente, vamos incluir também um argumento context necessário para a criação do layout de cada item da lista.

    Assim, o código da classe deve ficar como:

    class WeatherListAdapter(private val weatherList: MutableList<Weather>,
    private val context: Context?
    ) : Adapter<WeatherListAdapter.ViewHolder>() {

    ...
    }

    Método getItemCount

    O adaptador precisa ter conhecimento sobre a quantidade de itens na lista de dados para gerenciar quais deles exibir na tela. Embora o método getItemCount seja simples de implementar, é crucial para o correto funcionamento do adaptador. Sua implementação é a seguinte:

    class WeatherListAdapter(private val weatherList: MutableList<Weather>,
    private val context: Context?
    ) : Adapter<WeatherListAdapter.ViewHolder>() {

    ...

    override fun getItemCount(): Int {
    return weatherList.size
    }
    }

    Método onCreateViewHolder

    O onCreateViewHolder é um método fundamental em um adaptador do RecyclerView em Android. Ele é responsável por criar novas instâncias de ViewHolder quando o RecyclerView precisa de uma.

    Ao criar um adaptador para o RecyclerView, você precisa fornecer uma implementação para onCreateViewHolder. Este método é chamado pelo RecyclerView quando ele necessita criar um novo ViewHolder para representar um item da lista.

    No caso do projeto da previsão do tempo, vamos criar cada elemento da lista baseado no arquivo de layout list_item. A implementação fica como:

    package ...

    class WeatherListAdapter(private val weatherList: MutableList<Weather>,
    private val context: Context?
    ) : Adapter<WeatherListAdapter.ViewHolder>() {


    ...

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
    val view = LayoutInflater.from(context).inflate(R.layout.list_item, parent,false)
    return ViewHolder(view)
    }

    }

    Explicando cada parte:

    • parent: ViewGroup: O ViewGroup ao qual a nova visualização será anexada após ser criada. O RecyclerView usa isso para definir os parâmetros de layout, entre outras coisas.
    • viewType: Int: O tipo de vista do novo ViewHolder. Pode ser útil quando você tem diferentes tipos de itens em sua lista, usando múltiplos layouts.
    • LayoutInflater.from(parent.context).inflate(R.layout.list_item, parent, false): Infla (cria) uma nova instância da visão do item a partir do seu layout XML. O terceiro argumento, false, indica que a nova visão não deve ser anexada imediatamente ao parent, pois o RecyclerView cuidará disso.
    • return ViewHolder(view): Retorna uma nova instância do seu ViewHolder criado, que normalmente é associado a um item específico na lista.

    Método onBindViewHolder

    O método onBindViewHolder é outro componente crucial em um adaptador do RecyclerView em Android. Ele é responsável por associar os dados de um determinado item da lista aos elementos visuais do correspondente ViewHolder.

    Para o nosso projeto, a implementação fica como:

    package ...


    class WeatherListAdapter(private val weatherList: MutableList<Weather>,
    private val context: Context?
    ) : Adapter<WeatherListAdapter.ViewHolder>() {

    private val bitmaps: MutableMap<String, Bitmap> = HashMap()

    ...

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
    val weather = weatherList[position]
    //Verifica se a imagem do ícone já foi adicionada anteriormente
    if (bitmaps.containsKey(weather.iconURL)){
    //se sim
    holder.conditionImageView.setImageBitmap(
    bitmaps.get(weather.iconURL)
    )
    }
    //senão iniciar uma tarefa de download
    else {
    if (context != null) {
    Glide.with(context).asBitmap().load(weather.iconURL).into(object : CustomTarget<Bitmap>(){
    override fun onResourceReady(resource: Bitmap, transition: Transition<in Bitmap>?) {
    bitmaps.put(weather.iconURL, resource)
    holder.conditionImageView.setImageBitmap(resource)
    }
    override fun onLoadCleared(placeholder: Drawable?) {}
    })
    }
    }

    //Associa cada elemento da UI ao seu respectivo valor da previsão do tempo atual
    holder.dayTextView.setText(context?.getString(R.string.day_description, weather.dayOfWeek, weather.description))
    holder.lowTextView.setText(context?.getString(R.string.low_temp, weather.minTemp))
    holder.highTextView.setText(context?.getString(R.string.high_temp, weather.maxTemp))
    holder.humidityTextView.setText(context?.getString(R.string.humidity, weather.humidity))
    }
    }

    O método onBindViewHolder é executado individualmente para cada item apresentado na UI. Para ler os dados corretamente, utiliza a variável position para descobrir qual item da lista deve ser mostrado naquele referido item do RecyclerView.

    Para exibir os ícones da previsão climática, é necessário realizar o download da imagem e associá-la ao elemento ImageView do item. No entanto, os dados de previsão climática podem se repetir, como uma semana de dias ensolarados. Para evitar downloads repetidos de imagens entre outros benefícios vamos utilizar a biblioteca Glide, que será discutida em breve.

    Para os demais itens, foi adotada uma abordagem de template de strings para gerar uma string pré-formatada usando os recursos de strings do projeto. Por exemplo, a temperatura é representada como "23 ºC", mas incluímos o texto "Mínima: 23 ºC" na frente. Observa-se também que, para acessar os recursos do projeto, foi necessário utilizar a variável context recebida durante a construção do adaptador.

    Biblioteca Glide

    Glide é uma biblioteca de carregamento de imagens para Android que simplifica o processo de carregar e exibir imagens em um aplicativo Android. Ela foi projetada para ser eficiente em termos de consumo de memória e de largura de banda, oferecendo uma experiência de carregamento de imagens rápida e suave.

    Principais recursos do Glide:

    • Carregamento de Imagens Eficiente: Glide utiliza técnicas avançadas de carregamento e cache para garantir que as imagens sejam carregadas rapidamente e de forma eficiente, evitando o consumo excessivo de recursos do dispositivo.
    • Cache Inteligente: Glide possui um sistema de cache eficiente que armazena em cache automaticamente as imagens carregadas. Isso reduz a necessidade de baixar a mesma imagem repetidamente, melhorando o desempenho e economizando largura de banda.
    • Suporte a GIFs e Vídeos: Além de imagens estáticas, o Glide oferece suporte nativo para GIFs animados e vídeos, permitindo uma experiência de carregamento de mídia mais abrangente.
    • Integração com Componentes do Android: Glide é integrado com componentes do Android, como ImageView, tornando sua implementação mais fácil e intuitiva. Pode ser facilmente integrado com o RecyclerView para carregar imagens em listas.
    • Transformações de Imagem: Glide permite aplicar transformações às imagens carregadas, como redimensionamento, rotação, corte, entre outros.
    • Manipulação de Erros: Oferece maneiras de lidar com erros durante o carregamento de imagens, permitindo a exibição de imagens de fallback ou mensagens de erro.
    • Suporte a Customização: Glide é altamente configurável, permitindo ajustes e personalizações de acordo com as necessidades específicas do desenvolvedor.

    Para integrar o Glide no projeto Android,  adiciona a dependência no arquivo build.gradle do módulo:

    ...

    dependencies {
    ...

    implementation("com.github.bumptech.glide:glide:4.16.0")

    }

    Essa dependência pode ser ajustada de acordo com a versão desejada. Além disso, ao utilizar o Glide, é necessário cuidar das permissões de internet no manifesto do aplicativo, pois a biblioteca fará solicitações de rede para carregar as imagens, a menos que elas estejam armazenadas localmente.

     

    6.3. Classe FirstFragment

    A classe MainActivity define a interface do usuário do aplicativo, a lógica para interagir com a OpenWeatherMap API e processar sua resposta JSON. Vamos criar uma classe interna GetWeatherTask, também usando herança da classe AsyncTask. para fazer as solicitações ao web service em uma thread separada.

    • weatherList – representa a lista de objetos Weather contendo as previsões climáticas
    • weatherArrayAdapter – vai ser usado para vincular os dados da weatherList a ListView da interface gráfica
    • weatherRecyclerView – vai referenciar a RecyclerView da interface gráfica.

    O método onCreate configura a interface gráfica do usuário inflando o layout criado no arquivo activity_main.xml e vinculando os dados da weatherList à weatherRecyclerView usando o weatherArrayAdapter instânciado.

    Também configuramos o FAB para disparar uma nova GetWeatherTask caso a caixa de texto não esteja em branco. Se estiver em branco, usamos uma Snackbar para mostrar uma mensagem informativa.

    Os próximos dois métodos auxiliares servem para, respectivamente, esconder o teclado virtual quando o usuário toca no FAB e preparar uma URL para envio ao web service

    No método dismissKeyboard usamos a programação para ocultar o teclado virtual. Fazemos isso usando a classe InputMethodManger para obter acesso ao serviço de entrada do tipo INPUT_METHOD_SERVICE. Uma vez que tenha acesso a esse serviço do Android, escodemos o teclado invocando o método hideSoftInputFromWindow.

    No método createURL, juntamos os recursos strings do aplicativo (baseURL e apiKey) para preparar uma string contendo a requisição ao web service da OpenWeatherMap. Nessa url, colocamos o nome da cidade a ser pesquisada, o padrão de unidades “metric” (ºC) e o idioma português.

    A classe interna GetWeatherTasl usa herança da classe AsyncTask. Ela faz a solicitação ao web service e processa a resposta JSON com o método convertJSONtoArrayList.

    A solicitação ao web service é realizada dentro do método doInBackground. Ela usa uma classe HttpURLConnection para fazer a conexão ao url informado e recebe os dados com um objeto InputStream. Como os dados retornados serão um objeto JSON, e, portanto, dados do tipo String, usamos um objeto BufferedReader para fazer a leitura da stream linha por linha e um objeto StringBuilder para concatenar cada linha lida. Ao final, podemos criar um JSONObject com os dados lidas simplesmente invocando o seu construtor e passando a string lida.

    Caso ocorra algum erro durante a conexão, seja por falha serviço, queda da internet, ou outro motivo, a comunicação disparará uma exceção. Caso isso aconteça, mostramos ao usuário usando uma Snackbar uma mensagem informativa.

    Após o recebimento dos dados de resposta da API, o método onPostExecute é disparado e inicia o processamento dos dados recebidos. O método convertJSONtoArrayList prepara a lista com os novos dados lidos. Notificamos o weatherArrayAdapter que os dados foram atualizados, assim ele atualiza as views nas tela, com o método notifyDataSetChanged. E, por fim, fazemos com que a rolagem da tela volte para topo usando o método smoothScrollToPosition(0).

    O método convertJSONtoArrayList recebe o JSON retornado pela API e processa a leitura dos dados. Primeiramente, acessamos o campo “list” do JSON que representa a previsão do tempo de 5 dias a cada 3 horas. Como esse campo retorna uma lista de outros objetos JSON devemos usar a classe JSONArray. Agora, percorremos cada elemento da lista e acessos os campos referentes a temperatura mínima e máxima, umidade, descrição do tempo e dia da semana para criar um novo objeto Weather e armazenar na weatherList.

    Como queremos fazer a previsão do tempo de 5 dias e não a cada 3 horas, fazemos o laço de repetição andar com passo não de 1 em 1, mas sim de 8 em 8. A API normalmente retorna 40 previsões, mas se pularmos de 8 em 8, conseguiremos obter as condições climáticas de cinco dias.

    Você pode conferir o código final do fragmento abaixo:

    package br.edu.ifsc.fulano.weatherapp

    import android.content.Context
    import android.os.AsyncTask
    import android.os.Bundle
    import android.util.Log
    import android.view.LayoutInflater
    import android.view.View
    import android.view.ViewGroup
    import android.view.inputmethod.InputMethodManager
    import android.widget.FrameLayout
    import androidx.fragment.app.Fragment
    import androidx.recyclerview.widget.LinearLayoutManager
    import androidx.recyclerview.widget.RecyclerView
    import br.edu.ifsc.fulano.weatherapp.databinding.FragmentFirstBinding
    import com.google.android.material.snackbar.Snackbar
    import org.json.JSONArray
    import org.json.JSONException
    import org.json.JSONObject
    import java.io.BufferedReader
    import java.io.IOException
    import java.io.InputStreamReader
    import java.io.UnsupportedEncodingException
    import java.net.HttpURLConnection
    import java.net.MalformedURLException
    import java.net.URL
    import java.net.URLEncoder


    class FirstFragment : Fragment() {
    // Lista de objeto Weather que representam a previsão do tempo
    private val weatherList: MutableList<Weather> = ArrayList()
    // ArrayAdapter para vincular objetos Weather a uma ListView
    private var weatherAdapter: WeatherListAdapter? = null
    // Lista que exebi as informações climáticas
    private var weatherRecyclerView: RecyclerView? = null
    // Container para receber as snackBar
    private var frameLayout: FrameLayout? = null

    // Estratégia de controle da UI usando binding automatico da UI
    private var _binding: FragmentFirstBinding? = null

    // This property is only valid between onCreateView and
    // onDestroyView.
    private val binding get() = _binding!!

    override fun onCreateView(
    inflater: LayoutInflater, container: ViewGroup?,
    savedInstanceState: Bundle?
    ): View? {

    _binding = FragmentFirstBinding.inflate(inflater, container, false)

    // cria o adaptador para a lista da previsão do tempo
    weatherAdapter = WeatherListAdapter(weatherList, context)
    // acessa a RecyclerView e ajusta a configuração inicial
    weatherRecyclerView = binding.weatherRecyclerView
    weatherRecyclerView?.layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
    weatherRecyclerView?.adapter = weatherAdapter

    val fab = binding.fab
    fab.setOnClickListener {
    // obtem texto de locationInputText e cria a URL do webservice
    val locationEditText = binding.locationEditText
    val url: URL? = createURL(locationEditText.text.toString())
    // oculta o teclado antes de fazer o pedido a API
    if (url != null) {
    dismissKeyboard(locationEditText)
    val getLocalWeatherTask = GetWeatherTask()
    getLocalWeatherTask.execute(url)
    } else {
    Snackbar.make(
    binding.frameLayout,
    R.string.invalid_url, Snackbar.LENGTH_LONG
    ).show()
    }
    }
    return binding.root
    }

    // remove o teclado via programação quando usuário clicar no FAB
    private fun dismissKeyboard(view: View) {
    val imm = context?.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
    imm.hideSoftInputFromWindow(view.windowToken, 0)
    }

    // cria a URL do web service de OpenWeatherMap.org usando "city"
    private fun createURL(city: String): URL? {
    // acesso os recursos strings para pegar os valores
    val apiKey = getString(R.string.api_key)
    val baseUrl = getString(R.string.web_service_url)
    try {
    // cria a URL para a cidade e solicita as unidades
    // internacionais (Celcius)
    val urlString = (baseUrl + URLEncoder.encode(city, "UTF-8")
    + "&units=metric&lang=pt&APPID=" + apiKey)
    Log.d("WAPP", urlString)
    return URL(urlString)
    } catch (e: MalformedURLException) {
    e.printStackTrace()
    } catch (e: UnsupportedEncodingException) {
    e.printStackTrace()
    }
    // só chega aqui se a URL foi mal formada
    return null
    }

    private inner class GetWeatherTask :
    AsyncTask<URL?, Void?, JSONObject?>() {
    override fun doInBackground(vararg urls: URL?): JSONObject? {
    var connection: HttpURLConnection? = null
    try {
    connection = urls[0]?.openConnection() as HttpURLConnection
    val response = connection.responseCode
    if (response == HttpURLConnection.HTTP_OK) {
    // Inicialize o StringBuilder que armazenará os dados lidos
    val builder = StringBuilder()
    // prepara um objeto fazer a leitura dos dados
    val reader = BufferedReader(
    InputStreamReader(
    connection!!.inputStream
    )
    )
    // le os dados linha por linha e salva na StringBuilder
    var line: String?
    while (reader.readLine().also { line = it } != null) {
    builder.append(line)
    }
    // retorna um JSON com os dados lidos
    return JSONObject(builder.toString())
    } else {
    Snackbar.make(
    binding.frameLayout,
    R.string.connect_error, Snackbar.LENGTH_LONG
    ).show()
    }
    } catch (e: IOException) {
    e.printStackTrace()
    } catch (e: JSONException) {
    e.printStackTrace()
    } finally {
    connection!!.disconnect()
    }
    return null
    }

    override fun onPostExecute(jsonObject: JSONObject?) {
    // limpa os dados antigos da lista
    weatherList.clear()
    // preenche a lista com os dados no objeto JSON
    convertJSONtoArrayList(jsonObject)
    // avisa ao adaptador para atualizar a UI
    weatherAdapter?.notifyDataSetChanged()
    // rola para o topo
    weatherRecyclerView?.smoothScrollToPosition(0)
    }

    // método para converter os do JSON em uma ArrayList
    private fun convertJSONtoArrayList(forecast: JSONObject?) {
    // obtem uma lista do tipo JSONArray com os dados da previsão
    var list: JSONArray? = null
    try {
    list = forecast!!.getJSONArray("list")
    // converte cada elemento da lista em um objeto Weather
    // como a API retorna a previsão a cada 3h, vamos pular 24h,
    // para o próximo dia (8*3 = 24)
    var i = 0
    while (i < list.length()) {
    // obtem o valor inteiro do dia
    val day = list.getJSONObject(i)
    // obtem o campo 'main', ele possui os valores:
    // temp, temp_min, temp_max, pressure, sea_level, grnd_level,
    // humidity e pressure
    val main = day.getJSONObject("main")
    // obtem o campo 'weather', ele possui os valores:
    // id, main, description, icon
    val weather = day.getJSONArray("weather").getJSONObject(0)
    // adiciona novo objeto Weather a weatherList com os dados lidos
    weatherList.add(
    Weather.create(
    day.getLong("dt"), // timestamp data/hora
    main.getDouble("temp_min"), // temperatura minima
    main.getDouble("temp_max"), // temperatura máxima
    main.getDouble("humidity"), // porcentagem de umidade
    weather.getString("description"), // condições climáticas
    weather.getString("icon") // nome do icone
    )
    )
    i = i + 8
    }
    } catch (e: JSONException) {
    e.printStackTrace()
    }
    }

    }

    override fun onDestroyView() {
    super.onDestroyView()
    _binding = null
    }
    }

    6.4. Classe MainActivity

    Configure a MainActivity para servir apenas como uma inicialização da aplicação.

    package br.edu.ifsc.fulano.weatherapp

    import android.os.Bundle
    import com.google.android.material.snackbar.Snackbar
    import androidx.appcompat.app.AppCompatActivity
    import androidx.navigation.findNavController
    import androidx.navigation.ui.AppBarConfiguration
    import androidx.navigation.ui.navigateUp
    import androidx.navigation.ui.setupActionBarWithNavController
    import android.view.Menu
    import android.view.MenuItem
    import br.edu.ifsc.fulano.weatherapp.databinding.ActivityMainBinding

    class MainActivity : AppCompatActivity() {

    private lateinit var appBarConfiguration: AppBarConfiguration
    private lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    binding = ActivityMainBinding.inflate(layoutInflater)
    setContentView(binding.root)

    setSupportActionBar(binding.toolbar)

    val navController = findNavController(R.id.nav_host_fragment_content_main)
    appBarConfiguration = AppBarConfiguration(navController.graph)
    setupActionBarWithNavController(navController, appBarConfiguration)
    }

    override fun onSupportNavigateUp(): Boolean {
    val navController = findNavController(R.id.nav_host_fragment_content_main)
    return navController.navigateUp(appBarConfiguration)
    || super.onSupportNavigateUp()
    }
    }

    7. Finalizando

    Neste projeto, adquirimos conhecimento sobre três recursos fundamentais no desenvolvimento de aplicativos Android: o uso de web services, a implementação de RecyclerView e a utilização de AsyncTask.

    Para efetuar solicitações a um web service, exploramos a classe HttpURLConnection. Através de objetos URL, estabelecemos a comunicação com o serviço e recebemos a resposta em uma stream de entrada (InputStream). Demonstrei o processo de receber esses dados e convertê-los em objetos JSONObject.

    Lidar com API's demanda compreender o funcionamento de suas solicitações e o formato de resposta JSON. Com essas informações, construímos a lógica da aplicação, como no caso do WeatherApp, onde realizamos solicitações à API da OpenWeatherMap, passando parâmetros como nome da cidade, condições climáticas, padrão de medida e idioma português.

    A API retorna uma lista contendo a previsão de 5 dias a cada 3 horas da localização informada. Processamos esses dados e exibimos na tela a previsão do tempo para os próximos cinco dias na cidade indicada.

    A utilização do RecyclerView permitiu-nos apresentar uma lista de itens na interface do aplicativo. Este componente, por sua vez, depende de uma classe ArrayAdapter para preencher os itens da lista. No WeatherApp, criamos uma subclasse de ArrayAdapter para preencher os itens da lista com uma visualização personalizada. Dessa forma, os dados da previsão climática são apresentados na tela através de um ícone, descrição do dia, temperaturas máxima e mínima, e umidade.

    Por fim, abordamos o uso de AsyncTask para realizar tarefas em paralelo com a thread principal do aplicativo, evitando assim a aparição de caixas de diálogo 'Activity Not Responding (ANR)', que surgem quando o Android identifica que a interface gráfica não está respondendo."