Imprimir o livro todoImprimir o livro todo

Livro 4 - Projeto da Previsão do Tempo

Site: Moodle - IFSC
Curso: 2022-1 - FIC - Programação para Dispositivos Móveis - Prof. Bruno Calegaro
Livro: Livro 4 - Projeto da Previsão do Tempo
Impresso por: Usuário visitante
Data: sexta, 18 Out 2024, 11:31

1. Apresentação

Prezados alunos,

Neste livro vamos finalizar os conteúdos da disciplina com a criação de aplicativo para mostrar a previsão do tempo. Para a construção do aplicativo vamos aprender como se comunicar com uma API externa ao aplicativo e como fazer o processamento de dados de uma resposta do tipo JSON (Javascript Object Notation). Também vamos aprender como lidar com um novo componente gráfico chamado SnackBar.

O aplicativo WeatherApp vai usar serviços gratuitos da OpenWeatherMap API para obter os dados da previsão do tempo. Vamos usar uma conta gratuita e pedir a previsão do tempo de 5 dias para uma cidade específica. O aplicativo vai receber os dados climáticos no formato JSON (Javascript Object Notation) e iniciar o processamento para a exibição dos dados em formato de lista. Os dados climáticos serão exibidos em uma ListView.

Neste aplicativo, vamos usar um layout personalizado para cada view pertencente a lista.  Isto é diferente do Projeto 3 pois anteriormente usamos um layout padrão capaz de exibir apenas um texto (string) onde usamos para mostrar o nome de um contato. Aqui, vamos personalizar o layout para cada item da lista exibir:

  • Um ícone de condição climática
  • Dia da semana e descrição textual da condição climática
  • Temperatura máxima e mínima (em ºC)
  • Umidade (em %)

Esses itens fazem parte da resposta da API gratuita do OpenWeatherMap. Mais dados podem ser obtidas com uma conta paga. Para mais informações, consulte:

https://openweathermap.org/price

Bons estudos!

2. Recursos Envolvidos

Nesse projeto usaremos os seguintes recursos na construção do aplicativo WeatherApp:

  • Web Services 
    • Um web service é um tipo de servidor hospedado na rede (internet) que recebe pedidos de clientes (por exemplo, o aplicativo WeatherApp), os processa e retorna uma resposta pela rede.
    • Através desse serviço, um aplicativo pode acessar dados sob demanda em vez de armazená-los diretamente no dispositivo ou fazer acesso a um banco de dados externo. Da mesma forma, um aplicativo que não tenha poder de processamento para efetuar cálculos específicos poderia usar um web servisse para usufruir dos recursos superiores de outro sistema.

  • Web Service REST
    • REST (Representation State Transfer) se refere ao estilo arquitetônico para implementar web services – também podem ser chamados de web services RESTful. Muitos dos web services mais populares da atualidade, gratuitos e pagos, são RESTful.
    • Essa arquitetura faz normalmente o uso do protocolo HTTP (HyperText Transfer Protocol) usado pelos navegadores web. Assim, cada método de um web service RESTful é identificado por uma URL exclusiva. Quando o servidor recebe uma solicitação, ele sabe imediatamente qual operação executar.
    • Quais operações podem ser feitas, bem como os parâmetros que podem ser especificados, estão documentos na API (Aplication Programming Interface) de cada web service. Algumas API´s populares: Google Maps, Facebook, Twitter, Youtube, OpenWeatherMap e LinkdIn.
    • Para fazer o uso do web service frequentemente é exigido uma chave de API. Essa chave é usada para identificar o cliente (aplicativo ou usuário) e serve para confirmar a permissão de uso ao serviço e também monitorar o seu uso. Normalmente, chaves gratuitas possuem limitações como 50 acessos por segundo e podem ser usadas para aplicativos pequenos como no caso do WeatherApp. Se seu aplicativo fosse para distribuição comercial, provavelmente os números de acessos cresceria e você teria que pagar por uma chave para continuar usando o serviço.
  • Web Service da OpenWeatherMap.org
    • Os serviços da OpenWeatherMap API utilizados neste projeto usarão uma chave de API gratuita disponibilizada neste documento. Caso você queria criar uma conta pessoal e usar uma chave própria, realize o registro no site oficial da API:

    • Para obter mais informações sobre os planos do serviço e seu custo, acesso o link:

    • Neste aplicativo iremos usar a API “Call 5 day / 3 hour forecast data” para obter os dados da previsão do tempo de uma cidade específica. Basicamente, precisamos usar a URL: api.openweathermap.org/data/2.5/forecast?q= informando o nome da cidade e campos opcionais (especificamente, números de dias, padrão de medidas e idioma). Para mais informações sobre a API acesso o link:

    • Para mais informações sobre os serviços disponibilizados pela OpenWeatherMap acesso o link a seguir. Note que alguns serviços só estarão disponíveis para chaves pagas:

    • Para obter mais informações sobre os termos de serviço acesse o primeiro link a seguir. Ademais, a OpenWeatherMap usa uma licença Creative Commons pública para serviços e pode ser acessada no segundo link abaixo.

  • JSON
    • JSON (JavaScript Object Notation) é um formato de representação de dados baseado em texto, utilizado para representar objetos em JavaScript, como coleções de pares nome/valor. Devido a facilidade de criar, ler e analisar objetos no formato JSON, diversos serviços web optam por usar essa representação ao invés do padrão XML.

{

   nome: “Fulano”

   idade: 23

   disciplinas: [ “Português”, “Matemática”, “Química”]

   endereco: {

      cidade: Jaraguá do Sul

      cep: 98392-239

   }

}

  

    • Cada objeto JSON é representado como uma lista de nomes e valores de propriedades contidas em chaves, como exemplificado acima. Cada nome da propriedade é uma String e o valor de cada chave pode ser um número, texto, array (lista de valores) ou outro objeto JSON. No caso de arrays, usamos os colchetes para identificar a lista de valores, e no caso dos JSON usamos novamente as chaves.
    • Alguns web services, como no caso da OpenWeatherMap, oferecem a opção de resposta tanto em JSON quanto XML. Neste projeto optaremos pela primeira opção. Dessa forma, uma solicitação a API deve retornar algo do tipo:

{

    "cod": "200",

    "message": 0.0047,

    "cnt": 2,

    "list": [

        {

            "dt": 1525370400,

            "main": {

                "temp": 300.95,

                "temp_min": 297.369,

                "temp_max": 300.95,

                "pressure": 968.34,

                "sea_level": 1029.33,

                "grnd_level": 968.34,

                "humidity": 84,

                "temp_kf": 3.58

            },

            "weather": [

                {

                    "id": 500,

                    "main": "Rain",

                    "description": "light rain",

                    "icon": "10d"

                }

            ],

            "clouds": {

                "all": 24

            },

            "wind": {

                "speed": 0.41,

                "deg": 21.5013

            },

            "rain": {

                "3h": 0.24

            },

            "sys": {

                "pod": "d"

            },

            "dt_txt": "2018-05-03 18:00:00"

        },

        {

            "dt": 1525381200,

            "main": {

                "temp": 296.36,

                "temp_min": 293.975,

                "temp_max": 296.36,

                "pressure": 968.72,

                "sea_level": 1029.83,

                "grnd_level": 968.72,

                "humidity": 97,

                "temp_kf": 2.39

            },

            "weather": [

                {

                    "id": 500,

                    "main": "Rain",

                    "description": "light rain",

                    "icon": "10n"

                }

            ],

            "clouds": {

                "all": 20

            },

            "wind": {

                "speed": 1.01,

                "deg": 12.0102

            },

            "rain": {

                "3h": 0.165

            },

            "sys": {

                "pod": "n"

            },

            "dt_txt": "2018-05-03 21:00:00"

        }

    ],

    "city": {

        "id": 3460102,

        "name": "Jaraguá do Sul",

        "coord": {

            "lat": -26.4898,

            "lon": -49.0779

        },

        "country": "BR",

        "population": 130130

    }

}

    • No objeto JSON retornado acima, há muitas propriedades. Neste projeto iremos usar apenas a propriedade “list”. Essa propriedade retorna uma lista com a previsão a cada 3h para até 5 dias. Enfim, cada elemento do array “list” possui os campos:

      • dt” – um inteiro long, contém o carimbo de data/hora (timestamp) representando o número de segundos desde 1º de janeiro de 1970, GMT. Usamos essa informação para obter o nome do dia.

      • "main” – um objeto JSON contendo as propriedades double das temperaturas mínima (“temp_min”) e máxima (“temp_max”). Também vamos usar a umidade (“humidity”) representada como um inteiro.

      • weather” – um objeto JSON contendo as informações da condição climática. Vamos usar a descrição da condição (“description”) e o nome do ícone que as representa (“icon”)

  • Pacote org.json
    • Vamos usar as seguintes classes do pacote org.json para processar os dados JSON recebidos pelo aplicativo.
    • JSONObject – Um dos construtores dessa classe converte uma String, escrita nos padrões JSON, em um JSONObject. Esse objeto contém um Map<String, Object> para mapear as propriedades aos respectivos valores. Os valores associados a cada campo podem ser do tipo int, long, double, boolean, String, JSONObject e JSONArray. Através dos métodos get e set desse objeto, conseguimos fazer acesso as propriedades.

    • JSONArray – essa classe representa um array de JSON e fornece métodos para acessas seus elementos. A propriedade “list” na resposta da OpenWeatherMap API será manipulada como um JSONArray.

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

    • Para ativar serviço web vamos precisar fazer uma solicitação HTTP para a API. Essa tarefa é alcançada no Java com a classe HttpUrlConnection, presente no pacote java.net. Através de uma String representando uma URL, vamos criar um objeto URL para abrir uma nova HttpUrlConnection. Essa conexão fará a solicitação HTTP e receberá como resposta um objeto JSON. A leitura dos dados de resposta é feita com o uso de uma classe InputStream que recebe linha por linha, string por string, o conteúdo do objeto JSON. Vamos converter os dados lidos em um JSONObject para processamento.

  • AsyncTask
    • A comunicação a um serviço externo (leitura de arquivos, banco de dados, serviço web) pode ser uma operação longa ou até mesmo bloquear a execução do aplicativo. Portanto, essas operações devem ser realizadas fora da thread principal do aplicativo. Isso ajuda a manter a velocidade de resposta do aplicativo e evita caixas de diálogo Activity Not Responding (ANR), que aparecem quando o Android detecta que a interface gráfica não está respondendo.

    • Para executar tarefas em paralelo o Android disponibiliza a classe AsyncTask (pacote android.os), a qual executa operações de longa duração em uma thread separada e transmite os resultados para a thread principal. Neste aplicativo, vamos usar duas subclasses de AsyncTask: uma para ativar o serviço web e fazer as solicitações HTTP; e outra para baixar uma imagem de condição climática.
  • ListView, ArrayAdapter e Padrão ViewHolder
    • Este aplicativo exibe os dados climáticos em um componente ListView. Para preencher essa lista vamos precisar criar uma subclasse de ArrayAdapter, cuja finalidade é preencher o elemento ListView utilizando dados de um objeto ArrayList.
    • Quando o aplicativo adicionar os dados climáticos ao ArrayList, vamos chamar o método notifyDataSetChanged de ArrayAdapter para indicar que os dados mudaram. Então, o adaptador notificará o componente ListView para que atualize sua lista de itens exibidos. Isso é conhecido como vinculação de dados (data biding).
    • Cada item adicionado a lista envolve a execução do processo de criar novos objetos dinamicamente. Para listas grandes, nos quais o usuário rola rapidamente, a quantidade de itens gera uma sobrecarga que pode impedir uma rolagem suave. Logo, para reduzir essa sobrecarga, quando os itens do componente ListView rolam para fora da tela, vamos fazer a reutilização desses itens de lista para os novos que estão entrando na tela. Para isso, usamos o padrão ViewHolder, no qual criamos uma classe (normalmente chamada ViewHolder) para conter variáveis de instância para as views que exibem os dados dos itens na ListView.

    • Para cada item da lista, adicionamos também um novo objeto ViewHolder a view através do método setTag. Assim, quando um novo item está a ponto de rolar para fora da tela, o elemento ListView verifica se existe uma view reutilizável. Se não tiver, cria um novo item. Mas se tiver, ele usa o método getTag para obter o objeto ViewHolder e substituir o valor dos seus campos.
  • FloatingActionButton

    • Os botões de ação flutuantes, mais comumente chamados de FAB (FloatingActionButton), foram introduzidos pelo Material Design e são botões que “flutuam”, isto é, possuem elevação maior que o restante dos elementos da interface gráfica do aplicativo. A partir do Android 6.0, esse componente passou a ser disponibilizado pela Android Design Support Library

    • É comum usar esse botão para uma ação única, mas importante para o aplicativo. Alguns exemplos de seu uso: enviar um email (Gmail) , adicionar um contato (Projeto 3) e, no caso da WeatherApp, submeter a busca das condições climáticas de uma cidade.

    • FloatingActionButton é uma subclasse de ImageView, portanto, é possível usar esse botão para exibir uma imagem. Dessa forma, podemos adicionar ícones do Material Design e usar nos FAB’s.

    • As diretrizes do Material Design sugerem posicionar esses botões a pelo menos 16dp das margens do celular e a pelo menos 24dp das margens em um tablet. Naturalmente, o Android Studio configura esses valores padrões aos botões do projeto, mas nada impede de modifica-los.

  • TextInputLayout

    • Os componentes EditText são usados para criar caixas de texto de modo que o usuário possa digitar algum conteúdo. Para ajudar o usuário entender a finalidade da caixa de texto, podemos especificar a propriedade hint (dica) para esse elemento. Isso mostrará uma mensagem dentro da caixa de texto que desaparecerá assim que o usuário começar a digitar o texto. Por causa disso, pode acontecer do usuário esquecer da finalidade do elemento EditText, uma vez que a dica não aparecerá novamente. Para evitar isso, usamos TextInputLayout (pacote android.support.design.widget) da Android Design Support Library.

    • Em um TextInputLayout, quando o elemento EditText recebe foco, o TextInputLayout anima o texto da dica, mudando seu tamanho original para um menor e exibe-o acima da caixa de texto. Desse modo, o usuário pode digitar os dados e ver a dica ao mesmo tempo.

  • Snackbar
    • Snackbar (pacote android.support.design.widget) é um componente Material Design conceitualmente semelhante a um Toast. Além de aparecer na tela por um limite de tempo especificado, os componentes Snackbar também são interativos. Os usuários podem passar o dedo rapidamente por eles para removê-los. Um componente Snackbar também tem uma ação associada para executar quando o usuário toca nele. Nesse aplicativo, vamos usar um componentes Snackbar para exibir mensagens informativas.

3. Criando o projeto

Vamos inicializar o Projeto 4 - WeatherApp de maneira diferente dos projetos anteriores. Por tanto, siga corretamente as instruções a seguir.

  • Abra o Android Studio e clique na opção Start a new Android Studio Project na tela de abertura ou, caso já exista um projeto em aberto, clique em File > New > New Project
  • Na primeira janela escolha a template Basic Activity

  • Na próxima janela digite o nome do projeto como WeatherApp  e modique o local do projeto se achar necessário. Selecione a linguagem de programação Java e API 23. Clique em Finish.

4. Configurações iniciais

A template Basic Activity gera dois fragmentos acoplados a uma atividade principal e um layout básico possuindo um botão de transição entre telas, um texto e um FAB. Na sua construção padrão são criados os seguintes arquivos:

  • Uma classe MainActivity, FirstFragment e SecondFragment
  • Arquivos de layout:
    • activity_main.xml
    • content_main.xml
    • fragment_first.xml
    • fragment_second.xml
  • Um arquivo de navegação nav_graph.xml

Neste aplicativo vamos apenas utilizar uma única tela portanto não é necessário a navegação entre dois fragmentos, vamos nos focar apenas no necessário e remover os componentes não utilizados.

Para etapa inicial de configuração do projeto vamos alterar o tema do aplicativo modificando o arquivo themes.xml. Queremos modificar a propriedade colorSecondary para mudar a tonalidade da cor de realce. Lembre-se que essa cor é aplicada no FloatActionButton, assim para gerar um melhor contraste com o ícone que iremos adicionar, vamos definir essa cor como um azul mais claro (#448AFF). Se desejar você pode também mudar a cor primária (propriedade colorPrimary) e modificar a cor da app bar do aplicativo.

4.1. Recurso strings.xml

Para o aplicativo vamos precisar dos seguintes recursos strings:

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

Na primeira linha da tabela vemos a definição da chave de API a nível de código. Você pode definir uma chave de acesso de sua conta pessoal do OpenWeatherApp ou usar a disponibilizada na tabela. Na segunda linha vemos uma boa prática de programação para definir um recurso para o endereço da API de modo que a programação nunca faça acesso direto a URL mas haja por intermeio desta variável. Isso possibilita a migração da API no futuro de modo que não impacte a programação em si. Nas próximas linhas existem alguns mensagens de erros que serão mostradas na Snackbar em caso de falhas na comunicação com a API, essas mensagens também são definidas em forma de recurso para facilitar a internacionalização do aplicativo. Por fim, fazemos o uso de strings formatadas com a notação %s, %1$s e %2$s. Essa estratégia é adotada para a geração de mensagens dinâmicas para as temperaturas e umidade de modo que apenas os valores sejam substituídos na hora da conversão.

4.2. Configurando as permissões no AndroidManifest.xml

O aplicativo WeatherApp precisa de permissão de acesso à Internet para poder se comunicar com serviço web. A partir da versão Android 6.0, a permissão de internet é automaticamente concedida ao aplicativo, pois o acesso à Internet é considerado fundamental nos aplicativos atuais. De acordo com o Google, a permissão da internet, e muitas outras, não apresenta “grande risco à privacidade ou à segurança do usuário” e se enquadram na categoria PROTECTION_NORMAL. Portanto, elas são automaticamente concedidas no momento de instalação. Contudo, apesar de não ser necessário verificar no aplicativo se existe permissão, a mesma deve ser solicitada no arquivo de manisfesto AndroidManifest.xml.
Dessa forma, abra o arquivo AndroidManifest.xml e adicione a linha “<uses-permission .../>”. O arquivo deve ficar algo como:

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

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

<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.WeatherApp">
<activity
android:name=".MainActivity"
android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.WeatherApp.NoActionBar">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

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

</manifest>

4.3. Adicionando um ícone

Neste projeto vamos precisar adicionar apenas um ícone do Material Design. Portanto, abra a janela Vector Asset do Android Studio, como mostrado nos projetos anteriores, e adicione o ícone “done”.

Modifique o nome do ícone para ic_done_24 e a cor de preenchimento para branco (#FFFFFF).

Clique em Next e depois Finish.

5. Desenhando a tela principal do aplicativo e um layout personalizada para item da ListView

Para construir a interface gráfica do dispositivo vamos precisar configurar o FAB na  MainActivity e adicionar uma ListView ao fragmento principal. 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 weather_list_item.xml que ficará encarregado do design dos dados exibidos para cada item.

Para construir a interface gráfica do dispositivo vamos precisar de apenas da MainActivity. 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 do arquivo activity_main.xml

Primeiramente vamos configurar o arquivo activity_main.xml.

  • Selecione o elemento CoordinatorLayout e configure sua propriedade id para coordinatorLayout
    • Isso é importante pois vamos usar esse ID para identificar onde será posicionado o componente SnackBar
  • Selecione o elemento FloatingActionButton e:
    • Modifique sua propriedade layout_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 (ic_done_24dp).
    • A fim de mover o botão a direita da caixa de texto, defina um novo recurso de dimensão fab_margin_top com o valor 60dp e use na propriedade layout_margin_tup.

Como resultado final o arquivo deve ficar como:

<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">

<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/Theme.WeatherApp.AppBarOverlay">

<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:popupTheme="@style/Theme.WeatherApp.PopupOverlay" />

</com.google.android.material.appbar.AppBarLayout>

<include layout="@layout/content_main" />

<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="top|end"
android:layout_marginTop="@dimen/fab_margin_top"
android:layout_marginEnd="@dimen/fab_margin"
android:layout_marginBottom="16dp"
app:srcCompat="@drawable/ic_done_24" />

</androidx.coordinatorlayout.widget.CoordinatorLayout>

5.2. Layout do arquivo fragment_first.xml

Abra o fragment_first.xml e execute os passos a seguir:

  • Remova o elemento TextView e Button criado automaticamente pelo Android Studio
  • Abra o arquivo em modo texto e modifique layout para LinearLayout. Adicione a propriedade orientation a esse elemento (você também pode usar a janela de desenho para isso) como:
    • android:orientation="vertical"
  • De volta a janela de desenho, insira um TextInputLayout. Para encontrar esse componente é mais fácil usar a ferramenta de busca sobre a paleta. Automaticamente será incluído junto ao novo elemento um TextInputEditText. Configure a propriedades do layout adicionado 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 ListView e configure suas propriedades:

    • id: weatherListView

    • layout_height: 0dp
    • layout_weight: 1

O arquivo fragment_first.xml deve ficar como:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".FirstFragment" >

<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">

<com.google.android.material.textfield.TextInputEditText
android:id="@+id/locationEditText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/hint_text"
android:singleLine="true" />
</com.google.android.material.textfield.TextInputLayout>

<ListView
android:id="@+id/weatherListView"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
</LinearLayout>

E resultado é a seguinte tela:

5.3. Layout weather_list_item.xml

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

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

  • Clique com o botão direito do mouse na pasta layout e selecione New > Layout resource file
  • Na janela New Resource File digite o nome weather_list_item. Certifique-se que no campo root element está especificado LinearLayout e clique em OK.

  • Abra o arquivo criado e selecione o LinearLayout. Mude sua orientação para horizontal.

Com o arquivo criado vamos desenhar agora uma ImageView e um GridLayout. Siga os passos abaixo:

1. Adicionando um elemento ImageView para exibir um ícone de condição climática

    • Arreste um elemento ImageView para o LinearLayout e selecione a imagem padrão avatars apenas para fazermos uma visualização estética de como vai ficar o layout.
    • Configure as propriedades do componente como:
      • id: conditionImageView
      • layout:width: 50dp – defina um novo recurso de dimensão image_side_lenght
      • layout:height: match_parent – a altura do elemento ImageView dependerá da altura do item da ListView
      • contentDescription: use o recurso string weather_condition_image
      • scaleType: fitCenter – o ícone se encaixará dentro dos limites do elemento ImageView e será centralizado horizontalmente e verticalmente

2. Adição do GridLayout para exibir os componentes TextView

  • Adicione um GridLayout ao lado da imagem e configura suas propriedades
    • colummCount: 3
    • rowCount: 2
  • Arraste um elemento TextView para a primeira linha e configure suas propriedades:Adicione novamente três elementos TextView e configure seus id para lowTextView, highTextView e humidityTextView, respectivamente. Para cada um dos elementos configure a propriedade layout:row como 1 e layout:columnWeight como 1. Coloque a propriedade layout:column como 0, 1 e 2 também respectivamente.
    • id: dayTextView
    • textAppearence: @android:style/TextAppearence.Material.Large
    • layout_row: 0
    • layout_column: 0
    • layout_columnSpan: 3
    • layout_width: match_parent
  • Não é preciso alterar os valores textuais de cada TextView pois elas serão preenchidas por programação durante o uso do aplicativo.

As configurações finais do layout devem ficar como:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">


<ImageView
android:id="@+id/conditionImageView"
android:layout_width="50dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:contentDescription="@string/weather_condition_image"
android:scaleType="fitCenter"
tools:srcCompat="@tools:sample/avatars" />


<androidx.gridlayout.widget.GridLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="1"
app:columnCount="3"
app:rowCount="2">


<TextView
android:id="@+id/dayTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="TextView"
android:textAppearance="@android:style/TextAppearance.Material.Large"
app:layout_column="0"
app:layout_columnSpan="3"
app:layout_row="0" />


<TextView
android:id="@+id/lowTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="TextView"
app:layout_column="0"
app:layout_columnWeight="1"
app:layout_row="1" />


<TextView
android:id="@+id/highTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="TextView"
app:layout_column="1"
app:layout_columnWeight="1"
app:layout_row="1" />


<TextView
android:id="@+id/humidityTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="TextView"
app:layout_column="2"
app:layout_columnWeight="1"
app:layout_row="1" />


</androidx.gridlayout.widget.GridLayout>
</LinearLayout>

E o resultado é a seguinte tela: (Note que para ver a visualização assim é preciso apenas redimensionar a tela no Android Studio para ficar menor)

6. Adicionando a Lógica da Aplicação

Neste projeto faremos o uso de três classes:

  • Weather: representa os dados climáticos de um dia. A classe MainActivity converterá esses dados climáticos JSON em um ArrayList<Weather>;
  • WeatherArrayAdapter: defini uma subclasse de ArrayAdapter personalizada para vincular o ArrayList<Weather> ao elemento ListView.
  • MainActivity define a interface gráfica do aplicativo e a logica para interagir com o serviço web da previsão climática OpenWeatherMap e o processamento da resposta JSON.

6.1. Classe Weather

A classe Weather possui como variáveis os valores usados para representar as condições climáticas de um dia. Ela usará classes dos pacotes java.text e java.util para converter o carimbo de data/hora no nome do dia e também para formatar os números lidos em strings adequadas.

Adicione uma nova classe ao projeto e configura suas importações e variáveis como:

import java.text.NumberFormat;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.TimeZone;

public class Weather {
public final String dayOfWeek;
public final String minTemp;
public final String maxTemp;
public final String humidity;
public final String description;
public final String iconURL;

6.1.1 Construtor

O construtor da classe inicializa as variáveis de instancia da classe.

public Weather(long timeStamp, double minTemp, double maxTemp, double himidity, String description, String iconName){
// NumberFormat para formatar temperaturas em double
// arredondadas para inteiros
NumberFormat numberFormat = NumberFormat.getInstance();
numberFormat.setMaximumFractionDigits(0);
this.dayOfWeek = convertTimeStampToDay(timeStamp);
this.minTemp = numberFormat.format(minTemp) + " \u00b0C";
this.maxTemp = numberFormat.format(maxTemp) + " \u00b0C";
this.humidity = NumberFormat.getPercentInstance().format( himidity/100.0);
this.description = description;
this.iconURL = "https://openweathermap.org/img/w/" + iconName + ".png";
}

O método convertTimeStampToDay será implementado logo a seguir. O objeto NumberFormat é usado para formatar o valor lido para as temperaturas em ºC sem vírgulas e a umidade para porcentagem (%). O caractere especial º é representado pelo código UTF-8 “\u00b0’. A variável iconURL usa uma template padrão para a criação da string. Essa string pré-definida é o endereço web onde a OpenWeatherMap disponibiliza os ícones usados para representar as condições climáticas. Por exemplo, uma imagem de nuvens com chuva, dia ensolarado, etc.

6.1.2 Método convertTimeStampToDay

O método convertTimeStampToDay serve para fazer a leitura de valor long, representando o número de segundos decorridos desde 1º de janeiro de 1970, GMT. A maneira padrão de representar tempo em sistemas Linux e, portanto, Android. 

private static String convertTimeStampToDay(long timeStamp) {
// Cria um objeto Calendar com os dados atuais
Calendar calendar = Calendar.getInstance();
// Configura a hora
calendar.setTimeInMillis(timeStamp * 1000);
// obtem o fuso horário do dispositivo
TimeZone tz = TimeZone.getDefault(); // ajusta a hora com o fuso horário obtido
calendar.add(Calendar.MILLISECOND, tz.getOffset(calendar.getTimeInMillis()));
// SimpleDataFormat retorna o nome do dia
SimpleDateFormat dataFormatter = new SimpleDateFormat("EEEE");
return dataFormatter.format(calendar.getTime());
}

Para fazer a conversão usamos um objeto Calendar. Assimilamos ao calendário a localização atual (método getInstance) para retornar as datas no idioma da localização atual do dispositivo e configurarmos o fuso horário para uma variável tz. Dizemos ao calendário em que dias estamos (método add) e usamos a classe SimpleDataFormat para obter apenas o nome do dia (parâmetro “EEEE”) onde se encontra o calendário.

6.2. Classe WeatherArrayAdapter

A classe WeatherArrayAdapter usa herança da classe ArrayAdapter para vincular um ArrayList<Weather> ao elemento ListView da tela principal do aplicativo. Os itens da lista usam um layout personalizado para exibir os dados climáticos na tela como desenhado no arquivo weather_list_item.xml. Assim, devemos mapear os valores de cada objeto Weather aos componentes da interface gráfica que representam a imagem do ícone, o texto representando o dia, as temperaturas mínima e máxima, e a umidade. Esse mapeamento é programado sobrescrevendo o método getView de ArrayAdapter.

Adicione uma nova classe ao projeto, adiciona a herança da classe ArrayAdapter<Weather> e configure suas importações e variáveis como:

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.AsyncTask;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ImageView;
import android.widget.TextView;

import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class WeatherArrayAdapter extends ArrayAdapter<Weather> {
private static class ViewHolder {
ImageView conditionImageView;
TextView dayTextView;
TextView lowTextView;
TextView highTextView;
TextView humidityTextView;
}

private Map<String, Bitmap> bitmaps = new HashMap<>();

A classe interna ViewHolder define variáveis de instancia que a classe WeatherArrayAdapter acessa diretamente ao manipular objetos ViewHolder. Quando um item da lista for criado, vamos associar a ele um novo objeto ViewHolder. Se for possível reutilizar um item da lista, vamos obter o objeto ViewHolder vinculado esse item e atualizar seus campos.

A variável bitmaps será utilizada para armazenar os ícones baixados durante o uso do aplicativo. A cada ícone será assimilado um valor string para sua identificação (seu nome) e um valor bitmap para armazenar a imagem em si.

6.2.1 Construtor

O construtor da classe inicializa com a chamada do construtor da classe pai (método super).

public WeatherArrayAdapter(Context context, List<Weather> forecast) {
super(context, -1, forecast);
}

O primeiro argumento é contexto atual da aplicação, o segundo (-1) significa que o ArrayAdapter vai utilizar uma view personalizada e o terceiro representa a lista de dados a serem exibidos.

6.2.2 Método getView de ArrayAdapter

O método getView é chamado para obter a view que exibe os dados de um item de ListView. Sobrescrever esse método permite você a mapear dados para um item personalizado da lista. O método recebe como argumento a posição do item, a view que representa o item e o pai do item. Manipulando convertView, você pode personalizar o conteúdo do item na lista.

public View getView(int position, View convertView, ViewGroup parent) {
// obtém objeto Weather para esta posição de ListView especificada
Weather day = getItem(position);
// objeto que referencia as views do item da lista
ViewHolder viewHolder;
// verifica se há ViewHolder reutilizável de um item
// de ListView que rolou para fora da tela
// caso contrário, cria um novo ViewHolder
if (convertView == null) {
// não pode reciclar, então cria um novo
viewHolder = new ViewHolder();
LayoutInflater inflater = LayoutInflater.from(getContext());
convertView = inflater.inflate(R.layout.weather_list_item, parent, false);
viewHolder.conditionImageView = (ImageView) convertView.findViewById(R.id.conditionImageView);
viewHolder.dayTextView = (TextView) convertView.findViewById(R.id.dayTextView);
viewHolder.lowTextView = (TextView) convertView.findViewById(R.id.lowTextView);
viewHolder.highTextView = (TextView) convertView.findViewById(R.id.highTextView);
viewHolder.humidityTextView = (TextView) convertView.findViewById(R.id.humidityTextView);
convertView.setTag(viewHolder);
} else {
// pode reusuar
viewHolder = (ViewHolder) convertView.getTag();
}
// se o icone da condição climatica já foi baixado, o utiliza
// caso contrario, baixo o icone em uma thread separada
if (bitmaps.containsKey(day.iconURL)) {
viewHolder.conditionImageView.setImageBitmap(bitmaps.get(day.iconURL));
} else { // baixa e exibe a imagem de condição climática

new LoadImageTask(viewHolder.conditionImageView).execute(day.iconURL);
}
// obtém outros dados do objeto Weather e coloca nas Views
Context context = getContext();
// para carregar os recursos string
viewHolder.dayTextView.setText(context.getString(R.string.day_description, day.dayOfWeek, day.description));
viewHolder.lowTextView.setText(context.getString(R.string.low_temp, day.minTemp));
viewHolder.highTextView.setText(context.getString(R.string.high_temp, day.maxTemp));
viewHolder.humidityTextView.setText(context.getString(R.string.humidity, day.humidity));
return convertView;
}

O método getItem é usado para obter o objeto Weather na posição informada. Através desse objeto vamos conseguir fazer a leitura dos dados que devem ser exibidos na view personalizada. Antes de fazer isso, verificamos se convertView existe. Se a lista for pequena ou ainda estar sendo preenchida, esse valor será nulo. Logo, devemos inflar uma nova view e criar um objeto ViewHolder para armazenar as referências aos elementos da view criada. Adicionamos esse objeto usando o método setTag. Caso algum item role para fora da tela, convertView não será nulo e portanto uma view pode ser reutilizadas. Dessa forma, usamos o método getTag para obter um objeto ViewHolder, no qual será usado para atualizar os dados exibidos na interface.

Independente de qual forma serão adicionados os dados exibidos na tela, usamos o objeto ViewHolder para colocar as strings pré-definidas nos recursos do projeto com os valores do objeto Weather em questão. No caso do ícone para imagem, primeiro verificamos se o item já foi baixado, ou seja, existe uma chave com seu nome na variável bitmaps. Caso contrário, disparamos uma LoadImageTask para fazer o download da imagem em paralelo. Essa classe precisa do nome da imagem a ser baixada e da ImageView (parâmetro do método execute) os dados devem ser atualizados depois do download.

6.2.3 Classe interna LoadImageTask

A classe interna LoadImageTask usa herança da classe AsyncTask. Ela define como baixar uma imagem de um endereço URL e retorna essa imagem para a thread principal do aplicativo exibir o ícone baixado.

private class LoadImageTask extends AsyncTask<String, Void, Bitmap> {
private ImageView imageView;

// armazena ImageView na qual configura o Bitmap baixado
public LoadImageTask(ImageView imageView) {
this.imageView = imageView;
}

// carrega a imagem
@Override
protected Bitmap doInBackground(String... strings) {
Bitmap bitmap = null;
HttpURLConnection connection = null;
try {
// cria a URL para a imagem
URL url = new URL(strings[0]);
// abre uma HttpURLConnection, obtém seu InputStream
// e baixa a imagem
connection = (HttpURLConnection) url.openConnection();
InputStream inputStream = connection.getInputStream();
bitmap = BitmapFactory.decodeStream(inputStream);
// coloca em cache para uso posterior
bitmaps.put(strings[0], bitmap);
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return bitmap;
}

// configura a imagem da condição climática no item da lista
@Override
protected void onPostExecute(Bitmap bitmap) {
imageView.setImageBitmap(bitmap);
}
}

A classe AsyncTask<?,?,?> é um tipo genérico que exige três parâmetros de tipo:

  • O primeiro é o tipo de parâmetro, de comprimento variável, para o método doInBackground. Esse método é responsável por realizar a operação da thread, nesse caso, fazer o download da imagem.
  • O segundo é o tipo de parâmetro, de comprimento variável, para o método onProgressUpdate. Neste projeto não usaremos esse método, mas você poderia o usar para criar na tela do aplicativo uma barra mostrando a atualização da execução da tarefa, por exemplo, já foi baixado 30% de um arquivo.
  • O terceiro é o tipo de parâmetro, de comprimento variável, para o método onPostExecute. Esse método é executado na thread principal do aplicativo e permite que a ImageView seja atualizada com os dados baixados.

O download da imagem representando o ícone da previsão climática é realizado dentro do método doInBackground. Para fazer o download da imagem, fazemos o uso da classe HttlUrlConnection. Essa classe, abre uma conexão (stream) através de uma URL a um servidor web na internet. Uma vez que essa conexão seja realizada com sucesso (isto é, nenhuma exceção foi disparada), iniciamos o download do arquivo utilizando um objeto InputStream para receber os dados e um objeto BitmapFactory para usar esses dados e criar uma imagem bitmap. Essa imagem então é armazenada junto com uma chave (seu nome) a variável bitmaps.

Após o download da imagem, o método doInBackground termina, disparando logo na sequência a execução do método onPostExecute. Esse método então, apenas atualiza ImageView especificada com a imagem baixada.
se encontra o calendário.

6.3. Classe MainActivity

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 serviço web em uma thread separada. 

Abra o arquivo MainActivity.java e configura suas importações e variáveis para:


import android.content.Context;
import android.os.AsyncTask;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.inputmethod.InputMethodManager;
import android.widget.ListView;

import androidx.appcompat.app.AppCompatActivity;
import androidx.navigation.NavController;
import androidx.navigation.Navigation;
import androidx.navigation.ui.AppBarConfiguration;
import androidx.navigation.ui.NavigationUI;

import com.google.android.material.snackbar.Snackbar;
import com.google.android.material.textfield.TextInputEditText;

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;
import java.util.ArrayList;
import java.util.List;

import br.com.bcalegaro.weatherapp.databinding.ActivityMainBinding;


public class MainActivity extends AppCompatActivity {

private AppBarConfiguration appBarConfiguration;
private ActivityMainBinding binding;

// Lista de objeto Weather que representam a previsão do tempo private
List<Weather> weatherList = new ArrayList<>();

// ArrayAdapter para vincular objetos Weather a uma ListView private
WeatherArrayAdapter weatherArrayAdapter;
// Lista que exebi as informações climáticas
private ListView weatherListView;

Vamos precisar de três variáveis nesta atividade:

  • 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
  • weatherListView – vai referenciar a ListView da interface gráfica.

6.3.1 Método onCreate

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 à weatherListView usando o weatherArrayAdapter instanciado.

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

binding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());

setSupportActionBar(binding.toolbar);

// cria ArrayAdapter para vincular weatherList a weatherListView
weatherArrayAdapter = new WeatherArrayAdapter(this, weatherList);
weatherListView = (ListView) findViewById(R.id.weatherListView);
weatherListView.setAdapter(weatherArrayAdapter);

NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment_content_main);
appBarConfiguration = new AppBarConfiguration.Builder(navController.getGraph()).build();
NavigationUI.setupActionBarWithNavController(this, navController, appBarConfiguration);

binding.fab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
// obtem texto de locationInputText e cria a URL do webservice
TextInputEditText locationEditText = (TextInputEditText) findViewById(R.id.locationEditText);
URL url = createURL(locationEditText.getText().toString());
// oculta o teclado e inicia uma GetWeatherTask
// para o download de dados climáticos de
// OpenWeatherMap.org em uma thread separada
if (url != null) {
dismissKeyboard(locationEditText);
GetWeatherTask getLocalWeatherTask = new GetWeatherTask();
getLocalWeatherTask.execute(url);
} else {
Snackbar.make(view, R.string.invalid_url, Snackbar.LENGTH_LONG).show();
}
}
});
}

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.

6.3.2 Método dismissKeyboard e createURL de ArrayAdapter

Esses 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 serviço web

// remove o teclado via programação quando usuário clicar no FAB
private void dismissKeyboard(View view) {
InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(view.getWindowToken(), 0);
}

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

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 serviço web OpenWeatherMap. Nessa URL, colocamos o nome da cidade a ser pesquisada, o padrão de unidades “metric” (ºC) e o idioma português.

6.3.3 Classe interna GetWeatherTask

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

private class GetWeatherTask extends AsyncTask<URL, Void, JSONObject> {
@Override
protected JSONObject doInBackground(URL... urls) {
HttpURLConnection connection = null;
try {
connection = (HttpURLConnection) urls[0].openConnection();
int response = connection.getResponseCode();

if (response == HttpURLConnection.HTTP_OK) {
// Inicialize o StringBuilder que armazenará os dados lidos
StringBuilder builder = new StringBuilder();
// prepara um objeto fazer a leitura dos dados
BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
// le os dados linha por linha e salva na StringBuilder
String line;
while ((line = reader.readLine()) != null) {
builder.append(line);
}
// retorna um JSON com os dados lidos
return new JSONObject(builder.toString());
} else {
Snackbar.make(findViewById(R.id.coordinatorLayout), R.string.connect_error, Snackbar.LENGTH_LONG).show();
}
} catch (IOException e) {
e.printStackTrace();
} catch (JSONException e) {
e.printStackTrace();
} finally {
connection.disconnect();
}
return null;
}

@Override
protected void onPostExecute(JSONObject jsonObject) {
// preenche weatherList novamente
convertJSONtoArrayList(jsonObject);
// vincula a ListView novamene
weatherArrayAdapter.notifyDataSetChanged();
// rola para o topo
weatherListView.smoothScrollToPosition(0);
}

// método para converter os do JSON em uma ArrayList
private void convertJSONtoArrayList(JSONObject forecast) {
// limpa a lista antiga
weatherList.clear();
// obtem uma lista do tipo JSONArray com os dados da previsão
JSONArray list = 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)
for (int i = 0; i < list.length(); i = i + 8) {
// obtem o valor inteiro do dia
JSONObject 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
JSONObject main = day.getJSONObject("main");
// obtem o campo 'weather', ele possui os valores:
// id, main, description, icon
JSONObject weather = day.getJSONArray("weather").getJSONObject(0);
// adiciona novo objeto Weather a weatherList com os dados lidos
weatherList.add(new Weather(
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
));
}
} catch (JSONException e) {
e.printStackTrace();
}
}
}

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.

6.3.4 Métodos padrões da template BasicActivity

O restante dos métodos a seguir são métodos padrões criados durante a criação da template Basic Activity e devem ser deixados sem alteração. De todo modo segue o código restante:

@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.menu_main, menu);
return true;
}

@Override
public boolean onOptionsItemSelected(MenuItem item) {
// Handle action bar item clicks here. The action bar will
// automatically handle clicks on the Home/Up button, so long
// as you specify a parent activity in AndroidManifest.xml.
int id = item.getItemId();

//noinspection SimplifiableIfStatement
if (id == R.id.action_settings) {
return true;
}

return super.onOptionsItemSelected(item);
}

@Override
public boolean onSupportNavigateUp() {
NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment_content_main);
return NavigationUI.navigateUp(navController, appBarConfiguration)
|| super.onSupportNavigateUp();
}

6.4. Classe FirstFragment

Como alteramos o layout do primeiro fragmento precisamos adequar o código para não fazer referência a componentes inesxistentes. Portanto, simplesmente abra o arquivo FirstFragment.java e remove o código referenciando ao buttonFirst.  O resultado final deve ficar como:

import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.navigation.fragment.NavHostFragment;

import br.com.bcalegaro.weatherapp.databinding.FragmentFirstBinding;

public class FirstFragment extends Fragment {

private FragmentFirstBinding binding;

@Override
public View onCreateView(
LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState
) {

binding = FragmentFirstBinding.inflate(inflater, container, false);
return binding.getRoot();

}

public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
}

@Override
public void onDestroyView() {
super.onDestroyView();
binding = null;
}

}

7. Considerações Finais

Neste projeto aprendemos três recursos importantes no desenvolvimento de aplicativos Android. O uso de serviços web, utilização de listas personalizadasAsyncTask.

Para fazer solicitações a um serviço web devemos usar a classe HttpURLConnection. Através de objetos URL abrimos a comunicação com o serviço e recebemos a resposta em uma stream de entrada (InputStream). Vimos como receber esses dados e transformá-los em objetos JSONObject.

A parte crucial em lidar com API’s é entender como funcionam suas solicitações e qual o formato de resposta do JSON. Com essas informações, podemos construir a lógica da aplicação para ler os dados e processá-los. No caso WeatherApp. fazemos solicitações a API da OpenWeatherMap passando como parâmetros o nome da cidade a ser pesquisada as condições climáticas, o padrão de medida e o idioma português.

A API retorna uma lista contendo a previsão de 5 dias de 3 em 3 horas da localização informada. Processamos esses dados e mostramos na tela a previsão do tempo de cinco dias da cidade informada.

Usamos ListView para mostrar uma lista de itens na interface gráfica do aplicativo. Esse componente depende de uma classe ArrayAdapter para preencher os itens da lista. No aplicativo WeatherApp, criamos uma subclasse de ArrayAdapter para preencher os itens da lista com uma view personalizada. Dessa forma, os dados da previsão climática são mostrados tela segundo o desenho de um ícone, a descrição do dia, o valor das temperaturas máxima e mínima, e umidade.

Por fim, vimos como usar AsyncTask para realizar tarefas em paralelo com a thread principal do aplicativo. Dessa forma, evitamos de aparecer caixas de diálogo Activity Not Responding (ANR), que aparecem quando o Android detecta que a interface gráfica não está respondendo.