6. Lógica da Aplicação

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.