Livro 4 - Projeto da Previsão do Tempo

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

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();
}