6. Lógica da Aplicação

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
}
}